Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/build-msix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ name: Build CmdPal MSIX Bundle

on:
workflow_dispatch:
pull_request:
branches: [master]
paths:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include central dependency files in PR validation

When a PR changes only Directory.Packages.props, global.json, or nuget.config, none of these path filters match, so the new MSIX validation workflow does not run. Because this repository centrally declares every NuGet version in Directory.Packages.props, dependency-only PRs— including the updates configured in .github/dependabot.yml—receive no compile or packaging validation. Add the root build inputs to this paths list.

Useful? React with 👍 / 👎.

- 'DefinitionExtension/**'
- 'CmdPal-Definition.sln'
- '.github/workflows/build-msix.yml'
- '.github/workflows/deploy.yml'
- '.github/workflows/winget-publish.yml'
- 'winget/**'

permissions:
contents: read
Expand Down
107 changes: 81 additions & 26 deletions .github/workflows/winget-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind packaging to the release that triggered the run

When an older release workflow is rerun after a newer release exists, or two release runs overlap, this branch ignores the triggering workflow_run and queries the repository-wide releases/latest endpoint. The job then downloads and hashes the newer release rather than packaging the release whose workflow just completed. Carry the release tag from the upstream run, for example in a run artifact, instead of resolving it globally.

Useful? React with 👍 / 👎.

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
Comment thread
ruslanlap marked this conversation as resolved.
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
Expand Down
111 changes: 51 additions & 60 deletions winget/SUBMIT.md
Original file line number Diff line number Diff line change
@@ -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-<tag>` 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-<tag>` artifact.
4. Copy the four files into:
`manifests/r/ruslanlap/DefinitionForCommandPalette/<PackageVersion>/`
in the `microsoft/winget-pkgs` fork.
5. Validate with:
```powershell
winget validate --manifest <manifest-directory>
```
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 <path>`.
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.
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.
Loading