From 2532d983ca7df832dd62ab9078dc67f8688970eb Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 12:54:40 +0200 Subject: [PATCH 1/6] fix(release): let release-please read its config and manifest The action was given `release-type: rust` together with `config-file` and `manifest-file`. When `release-type` is set, release-please-action v5 ignores both files, so the bump policy, the changelog sections and the pull request header in release-please-config.json never applied. Dropping the input turns the config on, so `"separate-pull-requests": false` goes in the same change. With one named package, that setting makes release-please open its pull request from a branch without a component, and the merged pull request is then never tagged ("PR component: undefined does not match configured component"). The default for one package is what the v0.4.1 release pull request already used (release-please--branches--main--components--grape). The manifest already holds 0.4.1, the latest tag, and `include-component-in-tag: false` keeps tags as vX.Y.Z. --- .github/workflows/release.yml | 8 ++++---- release-please-config.json | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84416fe..b9bea7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,13 +41,13 @@ jobs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} steps: + # No `release-type` input, on purpose: when it is set, the action + # ignores both files below, and with them the changelog sections, the + # bump policy and the version in the manifest. The release type lives in + # release-please-config.json. - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 id: release with: - release-type: rust - # Named explicitly rather than left to the action's defaults: the - # config is in manifest mode, and a silent fallback to simple mode - # would version the repo differently without saying so. config-file: release-please-config.json manifest-file: .release-please-manifest.json diff --git a/release-please-config.json b/release-please-config.json index 6870e0e..a1f41ec 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -48,6 +48,5 @@ "bump-patch-for-minor-pre-major": false } }, - "separate-pull-requests": false, "pull-request-header": "The next release, assembled from the commits since the last one. Merging this tags it, and the tag is what builds and publishes." } From 55f29613725c6330317f486809ad34d090141eed Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 13:07:59 +0200 Subject: [PATCH 2/6] build(windows): embed a version resource in grape.exe SignPath signs only a Windows executable whose version resource names the project (ProductName) and carries its version (ProductVersion). grape.exe had no version resource at all. build.rs now writes one through winresource when the target is Windows: ProductName and FileDescription "Grape", FileVersion and ProductVersion from CARGO_PKG_VERSION, which release-please bumps. The check is on CARGO_CFG_TARGET_OS rather than cfg!(windows), since a build script runs on the host. winresource is the only new build dependency, without its default TOML feature, which build.rs does not use. Other targets are unchanged: the build script does nothing for them. --- Cargo.lock | 10 ++++++++++ Cargo.toml | 7 +++++++ build.rs | 20 ++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 build.rs diff --git a/Cargo.lock b/Cargo.lock index f9d0ae0..9b41d73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1852,6 +1852,7 @@ dependencies = [ "tray-icon", "unicode-normalization", "windows 0.62.2", + "winresource", "zbus", ] @@ -6730,6 +6731,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +dependencies = [ + "version_check", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/Cargo.toml b/Cargo.toml index 63642ea..7e2bfd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,13 @@ zbus = "5.19.0" [target.'cfg(target_os = "windows")'.dependencies] windows = { version = "0.62.2", features = ["Foundation", "Media", "Storage", "Storage_Streams", "Win32_Foundation", "Win32_System_WinRT"] } +# The Windows version resource, written by build.rs. Not target-specific: a +# build dependency's cfg is the host's, and a Linux host building for Windows +# needs it too. No default features: they only add a TOML parser for reading +# [package.metadata.winresource], which build.rs does not use. +[build-dependencies] +winresource = { version = "0.1.31", default-features = false } + # mockall, pretty_assertions, tokio-test and serial_test were declared here but # referenced nowhere in src/ or tests/. serial_test 4.0.1 also carried a 1.93.1 # rust-version, which would have put the test floor three releases above the diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..d794798 --- /dev/null +++ b/build.rs @@ -0,0 +1,20 @@ +// Windows version resource. SignPath signs only an .exe whose ProductName is +// the project name and whose ProductVersion is set, and Windows shows +// FileDescription as the program's name in Task Manager and file dialogs. +// +// Decided on the target, not with cfg!(windows): a build script runs on the +// host, so cfg! would describe the machine building Grape, not the one it is +// built for. +fn main() { + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + // FileVersion and ProductVersion default to CARGO_PKG_VERSION, which + // release-please bumps, so the release version is always the one in + // the resource. Only the names need setting: they default to the + // lowercase package name. + winresource::WindowsResource::new() + .set("ProductName", "Grape") + .set("FileDescription", "Grape") + .compile() + .expect("failed to compile the Windows version resource"); + } +} From 50792a648bd3b59a34e97e345ea6990d9eaf928a Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 13:09:39 +0200 Subject: [PATCH 3/6] ci(release): sign and publish through the shared Colony workflow Every Windows release now goes through Project-Colony-Resources/.github/workflows/sign-and-publish.yml, pinned by commit, in the same run as the build legs: preflight, Authenticode through SignPath once a signpath-project-slug is set, the ed25519 .sig, .meta and .meta.sig over the final bytes, upload to the draft, download again, verify, publish. Authenticode rewrites the PE, so signing with the organisation key inside each build leg, as before, would describe bytes SignPath then replaces. The build legs now only build, check and upload artifacts, and never see a key. Kept from the old workflow: the Linux apt list, the colony.json validation, the --version smoke test and the x86_64 check on the cross-compiled macOS binary. Asset names are unchanged. Also: - a Windows step after the smoke test fails the leg if grape-windows.exe does not carry ProductName "Grape" and the tag's version as ProductVersion, which SignPath requires; - no build cache in the release legs (SignPath forbids reusing unverified build outputs), GitHub-hosted runners only; - permissions are per job, from an empty default; - the checkout pins the tag and does not persist credentials; - a workflow_dispatch recovery path finishes a draft release whose run failed, started from the tag. signpath-project-slug stays commented out until SignPath has approved the project. The two secrets are passed by name, not inherited. --- .github/workflows/release.yml | 221 ++++++++++++++++------------------ 1 file changed, 103 insertions(+), 118 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9bea7d..fa8155f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,29 +1,42 @@ -# Colony Rust program — release workflow -# -# Copy to your repo as .github/workflows/release.yml and replace grape -# with your binary name (lowercase, as it appears in Cargo.toml). +# Grape - release workflow # +# Built from templates/sign-and-publish-caller.yml in Project-Colony-Resources. # Assets follow the Colony naming convention, so Colony auto-detects which -# platforms you ship and your colony.json only needs a name and a category: +# platforms Grape ships: # # grape-linux grape-windows.exe # grape-macos grape-macos-x86 # -# See design/releases.md in Project-Colony-Resources for the full contract. +# The build legs only build, check and upload artifacts; they never see a key. +# Authenticode (SignPath, once approved), the ed25519 .sig/.meta/.meta.sig, +# verification and publishing all happen in +# .github/workflows/sign-and-publish.yml of Project-Colony-Resources, in this +# same run. See design/releases.md there, section "Shared signing workflow". # -# Action refs are pinned to commit SHAs on purpose: this workflow handles the -# organisation's release signing key, and whoever can move a floating tag can -# read that secret. +# Action refs are pinned to commit SHAs on purpose: the called workflow handles +# the organisation's release signing key, and whoever can move a floating tag +# can read that secret. name: Release on: push: branches: [main] - -permissions: - contents: write - pull-requests: write + # Recovery path: finish an existing DRAFT release whose run failed. + # release-please emits `release_created` once per release, so "Re-run all + # jobs" on the original run makes it report nothing to do and every job + # below skip. Start the dispatch from the tag ("Use workflow from: v0.4.1", + # or `gh workflow run release.yml --ref v0.4.1 -f tag=v0.4.1`): with SignPath + # on, sign-and-publish refuses a run whose commit is not the tag's, since + # SignPath records the run's commit as the source of what it signs. + workflow_dispatch: + inputs: + tag: + description: "Existing draft release to build, sign and publish (e.g. v0.4.1). Run from that same tag." + required: true + type: string + +permissions: {} concurrency: group: release-${{ github.ref }} @@ -36,7 +49,11 @@ jobs: # Assembles a release PR from the conventional commits since the last release. # Merging that PR is what creates the tag; everything below runs only then. release-please: + if: github.event_name == 'push' runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write outputs: release_created: ${{ steps.release.outputs.release_created }} tag_name: ${{ steps.release.outputs.tag_name }} @@ -53,27 +70,33 @@ jobs: # release-please PUBLISHES the release - and moves /releases/latest - # before a single binary exists. For the whole build window every Colony - # in the field would show an update badge whose every click fails: a - # missing asset first, then a fail-closed signature refusal once the - # binary is up but its .sig is not. Held as a draft here and published by - # the `publish` job once everything is verified present. + # in the field would show an update badge whose every click fails. Held + # as a draft here; sign-and-publish refuses anything else and publishes + # it last, once every file is signed and verified on the release. - name: Hold the release as a draft until it is complete if: ${{ steps.release.outputs.release_created }} env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag_name }} # `-R` is not optional: this job has no `actions/checkout`, so `gh` has # no git remote to infer the repository from and dies with "fatal: not - # a git repository". That is exactly what happened on v0.3.0 — this step - # failed, `build` and `publish` were skipped, and the tag was published - # with no assets at all. Colony carries the same warning after paying - # for it with an empty v0.10.0. - run: gh release edit "${{ steps.release.outputs.tag_name }}" -R "${{ github.repository }}" --draft=true + # a git repository". That is exactly what happened on v0.3.0: this step + # failed, the build was skipped, and the tag was published with no + # assets at all. + run: gh release edit "$TAG" -R "$GITHUB_REPOSITORY" --draft=true build: name: build ${{ matrix.asset }} needs: release-please - if: ${{ needs.release-please.outputs.release_created }} + # Runs on a merge that created a release, or on a recovery dispatch (when + # release-please is skipped). NOT `always()`: a FAILED release-please must + # stop the run at an empty draft. + if: ${{ !cancelled() && !failure() && (needs.release-please.outputs.release_created || github.event_name == 'workflow_dispatch') }} + # GitHub-hosted runners only: SignPath rejects a signing request if any + # job leading to it ran on a self-hosted runner. runs-on: ${{ matrix.os }} + permissions: + contents: read strategy: fail-fast: false matrix: @@ -94,15 +117,19 @@ jobs: asset: "grape-macos-x86" steps: + # The tag, not the branch: a recovery dispatch must rebuild the tagged + # commit, since the .meta sidecar binds these bytes to that version. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ needs.release-please.outputs.tag_name || inputs.tag }} + persist-credentials: false - uses: dtolnay/rust-toolchain@89b12181fb390509a0842a86cc55eeb8eb928c1d # stable branch with: targets: ${{ matrix.target }} - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - key: ${{ matrix.target }} + # No build cache here: SignPath forbids release builds that reuse + # outputs of earlier, unverified builds. # alsa-sys is the only crate needing a system development package on # Linux; it resolves libasound through pkg-config. aws-lc-sys also links @@ -131,15 +158,10 @@ jobs: # A manifest can PARSE perfectly and still leave the program listed in # Colony with no Download button, because none of its assets match a # convention. `colony validate-manifest` given the asset names catches - # exactly that. Linux only: one check per release is enough. - # - # Deliberately ordered BEFORE the signing step: this downloads and runs a - # binary, and the signing key must never have touched this runner's disk - # while a fetched executable is running on it. + # exactly that. Linux only: one check per release is enough. No key is + # on this runner: signing happens in sign-and-publish. - name: Validate colony.json if: runner.os == 'Linux' - env: - GH_TOKEN: ${{ github.token }} run: | curl -fsSL -o colony-validator \ https://github.com/Project-Colony/Colony/releases/latest/download/colony-linux @@ -159,6 +181,24 @@ jobs: chmod +x "${{ matrix.asset }}" || true "./${{ matrix.asset }}" --version + # SignPath signs only an .exe whose version resource names the project + # and carries its version (build.rs writes it). A build that lost either + # would be refused at signing time, hours later; this fails it here. + - name: Check the Windows version resource + if: runner.os == 'Windows' + shell: pwsh + env: + ASSET: ${{ matrix.asset }} + TAG: ${{ needs.release-please.outputs.tag_name || inputs.tag }} + run: | + $info = (Get-Item $env:ASSET).VersionInfo + $version = $env:TAG -replace '^v', '' + "ProductName='$($info.ProductName)' ProductVersion='$($info.ProductVersion)' FileVersion='$($info.FileVersion)'" + if ($info.ProductName -cne 'Grape' -or $info.ProductVersion -cne $version) { + "::error::$env:ASSET has ProductName '$($info.ProductName)' and ProductVersion '$($info.ProductVersion)', expected 'Grape' and '$version'" + exit 1 + } + # The Intel macOS artefact is cross-compiled on an arm64 runner and # cannot be executed there, so assert its architecture instead - that # being the leg with the fewest users, where a regression survives @@ -170,90 +210,35 @@ jobs: file "${{ matrix.asset }}" | grep -q 'x86_64' \ || { echo "::error::${{ matrix.asset }} is not an x86_64 binary"; exit 1; } - # Signing is opt-in. Delete this step and the next unless colony.json sets - # "signed": true — a program that advertises signatures but ships none - # fails closed and cannot be installed at all. - # - # Requires the COLONY_SIGNING_KEY_PEM organisation secret (the ed25519 - # private key, PEM). The key never lives in a repository. - # - # `shell: bash` rather than skipping Windows: the Windows runner has bash - # and openssl, and an unsigned .exe in a repo that declares - # "signed": true is refused at install time — so excluding it did not - # produce an unsigned-but-working asset, it produced an uninstallable one. - - name: Sign the artifact - shell: bash - env: - SIGNING_KEY: ${{ secrets.COLONY_SIGNING_KEY_PEM }} - run: | - if [ -z "$SIGNING_KEY" ]; then - echo "::error::COLONY_SIGNING_KEY_PEM is not set but signing is enabled" - exit 1 - fi - keyfile="$(mktemp)" - trap 'rm -f "$keyfile"' EXIT - printf '%s' "$SIGNING_KEY" > "$keyfile" - COLONY_SIGNING_KEY="$keyfile" \ - COLONY_RELEASE_VERSION="${{ needs.release-please.outputs.tag_name }}" \ - ./scripts/sign-release.sh "${{ matrix.asset }}" - - - name: Upload to the GitHub release - uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 + # The file at the artifact root, under its release name. The name + # prefix is what `artifact-pattern` below matches. + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - # Without this the uploader PUBLISHES the release it is uploading to, - # because `draft` defaults to false and the action updates the release - # rather than only adding files. The first platform to finish - # therefore undoes the draft-hold, and the remaining platforms upload - # into a release that is already at /releases/latest. v0.3.1 went out - # that way: published with Linux and macOS while Windows was still - # compiling. `publish` is what makes it visible, once it has verified - # every asset is present. - draft: true - tag_name: ${{ needs.release-please.outputs.tag_name }} - files: | - ${{ matrix.asset }} - ${{ matrix.asset }}.sig - ${{ matrix.asset }}.meta - ${{ matrix.asset }}.meta.sig - # true: a signature that silently failed to upload is exactly the - # state that makes the release uninstallable, so it must fail here - # rather than at the user's machine. - fail_on_unmatched_files: true - - # Nothing is visible to users until every asset and every signature is really - # on the release. Chained as `needs:` rather than `on: release published`, - # because a release created by release-please with the default GITHUB_TOKEN - # does not fire release events - a workflow written that way never runs, and - # never says so. - publish: - name: Publish the release + name: build-${{ matrix.asset }} + path: ${{ matrix.asset }} + if-no-files-found: error + retention-days: 1 + + # Authenticode (once SignPath is on), .sig/.meta/.meta.sig over the final + # bytes, upload to the draft, download again, verify, publish. Chained as + # `needs:` in this same run: SignPath accepts only an artifact uploaded by + # the run that requests the signature. + sign-and-publish: needs: [release-please, build] - if: ${{ needs.release-please.outputs.release_created }} - runs-on: ubuntu-latest - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ needs.release-please.outputs.tag_name }} - # Single source of truth for the asset list. Repeating it per step is how - # a platform silently ends up unsigned. - ASSETS: "grape-linux grape-windows.exe grape-macos grape-macos-x86" - # Set to false if colony.json does not declare "signed": true. - SIGNED: "true" - steps: - - name: Verify the release carries everything - run: | - gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' | sort > /tmp/published - for a in $ASSETS; do - required="$a" - if [ "$SIGNED" = "true" ]; then - required="$a $a.sig $a.meta $a.meta.sig" - fi - for f in $required; do - grep -qx "$f" /tmp/published \ - || { echo "::error::$f is missing from $TAG - Colony would refuse this release"; exit 1; } - done - done - echo "release verified" - - - name: Make the release visible - # Same reason as the draft step above: no checkout in this job either. - run: gh release edit "$TAG" -R "$GITHUB_REPOSITORY" --draft=false + if: ${{ !cancelled() && needs.build.result == 'success' }} + # The ceiling for every job in the called workflow. + permissions: + actions: read + contents: write + uses: Project-Colony/Project-Colony-Resources/.github/workflows/sign-and-publish.yml@619460ff4dc0049f129955f0988d368427b9eedb # main + with: + tag: ${{ needs.release-please.outputs.tag_name || inputs.tag }} + assets: "grape-linux grape-windows.exe grape-macos grape-macos-x86" + artifact-pattern: build-* + # Uncomment once SignPath has approved this project and its project + # exists in the Project-Colony SignPath organisation: + # signpath-project-slug: "grape" + # Named, not `inherit`: the called workflow gets these two and nothing else. + secrets: + COLONY_SIGNING_KEY_PEM: ${{ secrets.COLONY_SIGNING_KEY_PEM }} + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} From 0a03504557680f802359e5f5736d6313640f59d7 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 13:09:39 +0200 Subject: [PATCH 4/6] ci: check the Windows version resource on pull requests The Windows test leg now reads target\debug\grape.exe's version resource and fails unless ProductName is "Grape" and ProductVersion is the version in Cargo.toml. The release workflow runs the same check on the release build; this one proves on every pull request that build.rs still writes it. --- .github/workflows/ci.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0341a4a..550469e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,6 +67,23 @@ jobs: - name: Build run: cargo build --all-targets + # Same check as the release workflow's Windows leg: SignPath signs only + # an .exe whose version resource names the project and carries its + # version, and build.rs is what writes it. Checked here so a pull + # request that breaks it fails now rather than at the next release. + - name: Check the Windows version resource + if: runner.os == 'Windows' + shell: pwsh + run: | + $version = (cargo metadata --no-deps --format-version 1 | ConvertFrom-Json).packages | + Where-Object name -eq 'grape' | ForEach-Object version + $info = (Get-Item target\debug\grape.exe).VersionInfo + "ProductName='$($info.ProductName)' ProductVersion='$($info.ProductVersion)' FileVersion='$($info.FileVersion)'" + if ($info.ProductName -cne 'Grape' -or $info.ProductVersion -cne $version) { + "::error::grape.exe has ProductName '$($info.ProductName)' and ProductVersion '$($info.ProductVersion)', expected 'Grape' and '$version'" + exit 1 + } + - name: Test run: cargo test # 19 of the player tests need a real audio device and are #[ignore]d, so From 8e9a408933c22c11d279657aaa449bc00a8511b6 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 13:10:04 +0200 Subject: [PATCH 5/6] docs: add the code signing policy and the privacy policy SignPath Foundation requires a "Code signing policy" section on the home page, and the shared release workflow links every release to README#code-signing-policy. The section carries the SignPath attribution, says honestly that Windows builds ship without Authenticode until the project is accepted (every asset keeps its ed25519 signature, which Colony verifies), and lists the team roles. The privacy policy is written from the code, not from the feature list: the only network call is Last.fm album.getInfo in src/library/metadata/online.rs, made only once the user has put an API key into preferences.json, and it sends the key, the album's artist and title and a Grape/ user agent. Notifications, MPRIS, the Windows media controls and the tray are local IPC. --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index a15f87b..cb498b4 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,39 @@ Everything is in [docs/](docs/); start at the index, | [docs/internals/packaging.md](docs/internals/packaging.md) | how a release is built and signed | | [docs/project/roadmap.md](docs/project/roadmap.md) | what is done and what is next | +## Code signing policy + +Free code signing provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org). + +Windows builds are Authenticode-signed this way once the SignPath Foundation +has accepted the project; until then they ship without Authenticode. Every +release asset, on every platform, is always signed with the Project-Colony +ed25519 release key, and Colony verifies that signature before installing. + +Team roles and members: + +- Committers and reviewers: [MotherSphere](https://github.com/MotherSphere) +- Approvers: [MotherSphere](https://github.com/MotherSphere) + +### Privacy policy + +Grape contacts one networked service, Last.fm, and only after you have turned +it on yourself. Nothing else ever leaves your machine: there is no telemetry, +no analytics, no account and no update check. + +- **What turns it on.** Putting your own Last.fm API key into + `preferences.json` (`metadata_api_key`). The key is empty by default and + there is no field for it in Preferences; with no key, Grape makes no network + request at all. +- **When.** With a key set, Grape asks Last.fm's `album.getInfo` + (`https://ws.audioscrobbler.com/2.0/`) for the album you select, directly or + through its artist, genre or folder, unless it already holds an answer for + that album younger than the cache lifetime (24 hours by default), and again + when you press **Enrich** on an album. +- **What is sent.** Your API key, the album's artist and title, and a + `Grape/` user agent. Like any HTTPS request, it also shows Last.fm + your IP address. Last.fm's answer (genre and year) is cached on your machine. + ## License [GPL-3.0-or-later](LICENSE) © 2026 MotherSphere From d2afc57cf453074e669013502495444fa1a203ad Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 13:10:35 +0200 Subject: [PATCH 6/6] docs(packaging): describe the shared sign-and-publish flow The page still described a `sign` job that no longer existed and a config that the action ignored. It now says what the release workflow does: build legs that upload artifacts and never see a key, the Windows version resource check, then the shared workflow that Authenticode-signs (once SignPath is on), ed25519-signs the final bytes, verifies them on the draft and publishes. It also gives the recovery command for a draft whose run failed. --- docs/internals/packaging.md | 53 ++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/docs/internals/packaging.md b/docs/internals/packaging.md index a665f66..e847a22 100644 --- a/docs/internals/packaging.md +++ b/docs/internals/packaging.md @@ -1,8 +1,8 @@ # How a release is made -One workflow, `.github/workflows/release.yml`, with three jobs that run in -order: decide, build, sign. It is also the only workflow in the repository — -there is no test or lint job. +`.github/workflows/release.yml` runs three jobs in order: decide, build, then +sign and publish. The last one is the shared workflow from +Project-Colony-Resources. Pull requests are checked separately, by `ci.yml`. ## 1. release-please decides @@ -16,9 +16,10 @@ does not. `release_created` is false on every other push, and both later jobs are gated on it. Configuration lives in `release-please-config.json` (`release-type: rust`, -`bump-minor-pre-major: false`) and the current version in -`.release-please-manifest.json`. Neither `CHANGELOG.md` nor the version in -`Cargo.toml` should ever be edited by hand. +`bump-minor-pre-major: true`) and the current version in +`.release-please-manifest.json`. The action is given those two files and no +`release-type` input: with one, it ignores both files. Neither `CHANGELOG.md` +nor the version in `Cargo.toml` should ever be edited by hand. ## 2. Four targets are built @@ -36,30 +37,38 @@ vendored sources with `cc`, `wayland-sys` dlopens at runtime, and D-Bus is spoken by pure-Rust `zbus` — none of them need an apt package. The GTK 3 stack left the tree when the Linux tray moved to `ksni`. -Each job builds `--release` for its target, copies the binary out under the -asset name, and uploads it to the GitHub release. +Each job builds `--release` for its target with no build cache, copies the +binary out under the asset name, runs it with `--version` (the Intel macOS +binary, which the arm64 runner cannot run, gets an architecture check instead), +and uploads it as a workflow artifact. Nothing reaches the release from a build job, and no build +job sees a key. The Windows job also checks that `grape-windows.exe` carries +the version resource `build.rs` writes: ProductName `Grape` and the release +version as ProductVersion, which SignPath requires before it signs. That build is also the only automated check Windows and macOS ever get: those targets typecheck at release time and are never tested. See [contributing.md](contributing.md#what-the-tests-do-not-cover). -## 3. Everything is signed +## 3. Everything is signed, then published -The `sign` job downloads the release assets, strips the metadata companions -(`.sig`, `.sha256`, `.txt`, `.yml`, `.json`, `.asc`), and signs each remaining -file with the Project-Colony ed25519 key from `secrets.COLONY_SIGNING_KEY_PEM`. -Each signature is verified against the derived public key immediately after -being produced, then uploaded as `.sig`. +The last job calls `sign-and-publish.yml` from Project-Colony-Resources, pinned +by commit, in the same run. It checks the release is still a draft, sends +`grape-windows.exe` to SignPath for Authenticode once a `signpath-project-slug` +is set (it is not yet: SignPath has not accepted the project), then signs every +final file with the Project-Colony ed25519 key from +`secrets.COLONY_SIGNING_KEY_PEM`: `.sig`, `.meta` and +`.meta.sig`. It uploads them to the draft, downloads them again, +verifies them, and only then publishes the release. -Two deliberate details: +The order matters: Authenticode rewrites the `.exe`, so an ed25519 signature +made before it would describe bytes users never download. A missing secret, a +failed check or a refused signing request leaves the release a draft. -- **A missing secret fails the job.** The step checks for an empty key and - exits 1 rather than continuing, so a release cannot quietly ship unsigned. -- **`gh release upload` passes `-R "${{ github.repository }}"`.** The job has no - `actions/checkout`, so there is no git repository for `gh` to infer a target - from. Without `-R` it dies with "not a git repository" *after* the assets are - already signed — which is how a release once shipped unsigned while the job - meant to prevent exactly that reported its failure too late to stop it. +If a run fails after the tag exists, finish the draft from the tag: + +```bash +gh workflow run release.yml -R Project-Colony/Grape --ref vX.Y.Z -f tag=vX.Y.Z +``` ## 4. Colony picks it up