diff --git a/.github/workflows/build-msix.yml b/.github/workflows/build-msix.yml index 6f5b570..d5866b0 100644 --- a/.github/workflows/build-msix.yml +++ b/.github/workflows/build-msix.yml @@ -2,6 +2,15 @@ name: Build CmdPal MSIX Bundle on: workflow_dispatch: + pull_request: + branches: [master] + paths: + - 'DefinitionExtension/**' + - 'CmdPal-Definition.sln' + - '.github/workflows/build-msix.yml' + - '.github/workflows/deploy.yml' + - '.github/workflows/winget-publish.yml' + - 'winget/**' permissions: contents: read diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/winget-publish.yml index 36bc418..775d6fd 100644 --- a/.github/workflows/winget-publish.yml +++ b/.github/workflows/winget-publish.yml @@ -37,7 +37,9 @@ jobs: runs-on: windows-latest if: > github.event_name == 'workflow_dispatch' || - github.event.workflow_run.event == 'release' + (github.event_name == 'workflow_run' && + github.event.workflow_run.event == 'release' && + github.event.workflow_run.conclusion == 'success') steps: - name: Checkout code @@ -48,49 +50,102 @@ jobs: shell: bash env: GH_TOKEN: ${{ github.token }} + MANUAL_RELEASE_TAG: ${{ github.event.inputs.release_tag }} run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - TAG="${{ github.event.inputs.release_tag }}" + TAG="$MANUAL_RELEASE_TAG" else TAG=$(gh api repos/${{ github.repository }}/releases/latest --jq '.tag_name') fi + + if [[ ! "$TAG" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then + echo "::error::Invalid release tag: $TAG" + exit 1 + fi + TAG_VERSION="${TAG#v}" - echo "TAG=$TAG" >> $GITHUB_OUTPUT - echo "TAG_VERSION=$TAG_VERSION" >> $GITHUB_OUTPUT + echo "TAG=$TAG" >> "$GITHUB_OUTPUT" + echo "TAG_VERSION=$TAG_VERSION" >> "$GITHUB_OUTPUT" echo "Resolved tag: $TAG (version $TAG_VERSION)" - - name: Verify release asset exists - shell: bash + - name: Download release bundle + shell: pwsh + env: + RELEASE_TAG: ${{ steps.get_release.outputs.TAG }} run: | - TAG="${{ steps.get_release.outputs.TAG }}" - URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/DefinitionForCommandPalette.msixbundle" - STATUS=$(curl -o /dev/null -s -w "%{http_code}" -L "$URL") - if [ "$STATUS" != "200" ] && [ "$STATUS" != "302" ]; then - echo "::error::Release asset not found (HTTP $STATUS): $URL" - exit 1 - fi - echo "✓ MSIX bundle asset verified for $TAG" + $tag = $env:RELEASE_TAG + $url = "https://github.com/${{ github.repository }}/releases/download/$tag/DefinitionForCommandPalette.msixbundle" + Invoke-WebRequest -Uri $url -OutFile DefinitionForCommandPalette.msixbundle + if (-not (Test-Path DefinitionForCommandPalette.msixbundle)) { + throw "Release asset download failed: $url" + } - - name: Package winget manifests + - name: Generate current winget manifests shell: pwsh + env: + RELEASE_TAG: ${{ steps.get_release.outputs.TAG }} + RELEASE_TAG_VERSION: ${{ steps.get_release.outputs.TAG_VERSION }} run: | - $tag = "${{ steps.get_release.outputs.TAG }}" - $tagVersion = "${{ steps.get_release.outputs.TAG_VERSION }}" + $tag = $env:RELEASE_TAG + $tagVersion = $env:RELEASE_TAG_VERSION + $parts = @($tagVersion.Split('.')) + while ($parts.Count -lt 4) { $parts += '0' } + $packageVersion = ($parts[0..3] -join '.') + $url = "https://github.com/${{ github.repository }}/releases/download/$tag/DefinitionForCommandPalette.msixbundle" + $installerHash = (Get-FileHash DefinitionForCommandPalette.msixbundle -Algorithm SHA256).Hash - if (-not (Test-Path winget)) { - Write-Host "::error::No winget/ directory at repo root — cannot package manifests." - exit 1 + $extractDir = Join-Path $env:RUNNER_TEMP "bundle-signature" + if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force } + [System.IO.Compression.ZipFile]::ExtractToDirectory( + (Resolve-Path DefinitionForCommandPalette.msixbundle), + $extractDir + ) + $sigFile = Join-Path $extractDir "AppxSignature.p7x" + if (-not (Test-Path $sigFile)) { + throw "AppxSignature.p7x not found. Refusing to package an unsigned MSIX bundle for WinGet." } + $signatureHash = (Get-FileHash $sigFile -Algorithm SHA256).Hash - $manifestFiles = Get-ChildItem -Path winget -Recurse -File - if ($manifestFiles.Count -eq 0) { - Write-Host "::error::No manifest files under winget/." - exit 1 + $stage = "winget-current" + New-Item -ItemType Directory -Force -Path $stage | Out-Null + + $files = @( + "ruslanlap.DefinitionForCommandPalette.yaml", + "ruslanlap.DefinitionForCommandPalette.installer.yaml", + "ruslanlap.DefinitionForCommandPalette.locale.en-US.yaml", + "ruslanlap.DefinitionForCommandPalette.locale.fr-FR.yaml" + ) + + foreach ($name in $files) { + $src = Join-Path "winget" $name + if (-not (Test-Path $src)) { throw "Missing template manifest: $src" } + $text = Get-Content $src -Raw + $text = $text -replace '(?m)^PackageVersion:\s*.*$', "PackageVersion: $packageVersion" + $text = $text -replace 'winget-manifest\.([^.]+)\.[0-9.]+\.schema\.json', 'winget-manifest.$1.1.12.0.schema.json' + $text = $text -replace '(?m)^ManifestVersion:\s*.*$', 'ManifestVersion: 1.12.0' + + if ($name -like '*.installer.yaml') { + $text = $text -replace '(?m)^\s*InstallerUrl:\s*.*$', " InstallerUrl: $url" + $text = $text -replace '(?m)^\s*InstallerSha256:\s*.*$', " InstallerSha256: $installerHash" + $text = [regex]::Replace($text, '(?m)^\s*SignatureSha256:\s*.*\r?\n?', '') + $needle = " InstallerSha256: $installerHash" + $replacement = " InstallerSha256: $installerHash`r`n SignatureSha256: $signatureHash" + $text = $text.Replace($needle, $replacement) + if ($text -notmatch '(?m)^MinimumOSVersion:') { + $text = $text -replace '(?m)^InstallerType:\s*msix\s*$', "MinimumOSVersion: 10.0.19041.0`r`nInstallerType: msix" + } + } + + Set-Content -Path (Join-Path $stage $name) -Value $text -NoNewline } + Write-Host "PackageVersion: $packageVersion" + Write-Host "InstallerSha256: $installerHash" + Write-Host "SignatureSha256: $signatureHash" + $zip = "winget-manifests-$tag.zip" - Compress-Archive -Path winget/* -DestinationPath $zip -Force - Write-Host "Packaged $($manifestFiles.Count) file(s) into $zip" + Compress-Archive -Path "$stage/*" -DestinationPath $zip -Force + Write-Host "Packaged exactly 4 current-version manifests into $zip" - name: Upload winget manifests artifact uses: actions/upload-artifact@v4 diff --git a/winget/SUBMIT.md b/winget/SUBMIT.md index c981c21..03267b9 100644 --- a/winget/SUBMIT.md +++ b/winget/SUBMIT.md @@ -1,64 +1,55 @@ # Submitting to winget-pkgs -This repository ships the winget manifests under `winget/`. The CI workflow -[`.github/workflows/winget-publish.yml`](../.github/workflows/winget-publish.yml) -packages them into an artifact on every Microsoft Store release, but it does -**not** open the upstream PR. Submitting the manifests to -[`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs) is a -manual step that takes about five minutes. - -## Why the workflow doesn't submit - -- `wingetcreate new` requires Sharprompt interactive input. It cannot run - in CI without a TTY and fails with - `Sharprompt requires an interactive environment`. -- `wingetcreate update` requires the package to already exist in - `microsoft/winget-pkgs`. This package does not. -- The winget-pkgs GitHub App is the recommended path, but configuring it is - a separate project. - -## When you need to do this - -Every time you bump `PackageVersion` in `winget/*.yaml`. The CI artifact is -the source of truth — pull it after the Store publish completes. - -## Procedure - -1. Wait for the `Publish CmdPal Extension to Microsoft Store` workflow run - to complete successfully. -2. Download the `winget-manifests-` artifact from the matching - `Publish to WinGet` run. -3. Fork [`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs) - if you have not already. -4. Create a branch named after the package and version: +The repository keeps four manifest templates under `winget/`. The +`.github/workflows/winget-publish.yml` workflow turns those templates into a +clean, current-version manifest set after a signed GitHub release is available. + +## Signing requirement + +The distributed `.msixbundle` must be signed by a certificate chain trusted by +Windows. The current release workflow uses SignPath Foundation via `SIGNPATH_API_TOKEN` +plus the SignPath repository variables. + +A self-signed certificate is not sufficient for winget validation and produces +`0x800B0109 / CERT_E_UNTRUSTEDROOT`. For MSIX, the manifest `Publisher` +must also match the subject of the code-signing certificate, so do not add a +generic PFX fallback without handling package identity explicitly. + +## What CI now generates + +For the requested release tag, `Publish to WinGet`: + +1. Downloads `DefinitionForCommandPalette.msixbundle` from the GitHub Release. +2. Computes the bundle `InstallerSha256`. +3. Extracts `AppxSignature.p7x` and computes `SignatureSha256`. +4. Normalizes the release tag to a four-part `PackageVersion`. +5. Updates the manifest schema/version to 1.12.0. +6. Packages exactly these four current-version files: + - `ruslanlap.DefinitionForCommandPalette.yaml` + - `ruslanlap.DefinitionForCommandPalette.installer.yaml` + - `ruslanlap.DefinitionForCommandPalette.locale.en-US.yaml` + - `ruslanlap.DefinitionForCommandPalette.locale.fr-FR.yaml` + +Historical folders under `winget/` are intentionally excluded from the +artifact so a submission cannot accidentally contain multiple package versions. + +## Submission procedure + +1. Ensure the release workflow completed and the GitHub Release contains the + trusted-signed `DefinitionForCommandPalette.msixbundle`. +2. Run `Publish to WinGet` for that release tag. +3. Download the `winget-manifests-` artifact. +4. Copy the four files into: + `manifests/r/ruslanlap/DefinitionForCommandPalette//` + in the `microsoft/winget-pkgs` fork. +5. Validate with: + ```powershell + winget validate --manifest ``` - git checkout -b ruslanlap/DefinitionForCommandPalette-1.0.4 - ``` -5. Copy the manifests into the canonical path: - ``` - cp winget-manifests-1.0.4.zip /tmp/ - cd /tmp && unzip winget-manifests-1.0.4.zip -d manifests - mkdir -p upstream/manifests/r/ruslanlap/DefinitionForCommandPalette/1.0.4.0 - cp -r manifests/* upstream/manifests/r/ruslanlap/DefinitionForCommandPalette/1.0.4.0/ - ``` -6. Update `PackageVersion`, `InstallerUrl`, and `InstallerSha256` in the - manifests to match the new release. The current values point at v1.0.0 - and need to be bumped before each submission. -7. Validate locally with `winget validate --manifest `. -8. Commit, push, open a PR upstream. -9. Watch the validation checks. Common expected labels for this package - type (MSIX bundle with no primary executable): - - `Validation-Executable-Error` - - `Validation-No-Executables` - - These are routine for MSIX-only packages. Comment on the PR explaining - that the install path is `Plugins/DefinitionForCommandPalette` under the - PowerToys Run installation directory, and link the relevant section of - `deploy.yml` if the moderator asks. -10. Wait for a moderator to merge. Do not ping more than twice. - -## Why hand-write instead of using wingetcreate +6. Commit, push, and open/update the upstream PR. +7. Wait for the winget validation pipeline and reviewer approval. -Hand-written manifests are easier to review, easier to bump across -versions, and easier to keep in sync with the rest of this repo's release -process. The win is small for new packages but compounds across versions. \ No newline at end of file +For the current v1.0.4 submission, upstream PR #431811 has already confirmed +that the only remaining validation blocker is the untrusted certificate chain. +Once a trusted-signed bundle replaces the release asset, regenerate the +manifests and update that PR with the new hashes.