From d6faf60f1f4d3d56e8cd21bb9d3e9cfe411d8933 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 13:00:08 +0200 Subject: [PATCH 1/4] build: embed a Windows version resource in colony.exe SignPath signs only an executable whose ProductName is the project name and whose ProductVersion is set. build.rs now embeds ProductName and FileDescription "Colony" and ProductVersion and FileVersion from CARGO_PKG_VERSION, through winresource without default features, and only when the target is Windows. The Windows CI leg builds colony.exe and fails if either field is not what is expected. --- .github/workflows/ci.yml | 19 +++++++++++++++++++ Cargo.lock | 10 ++++++++++ Cargo.toml | 5 +++++ build.rs | 17 +++++++++++++++++ 4 files changed, 51 insertions(+) create mode 100644 build.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a642ce..5a1720c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,25 @@ jobs: - name: Test run: cargo test --verbose + # SignPath signs only an .exe whose ProductName is the project name and + # whose ProductVersion is set. build.rs embeds both; this proves it on + # every PR, and the release workflow checks the shipped file again. + # `-cne`, not `-ne`: PowerShell compares strings case-insensitively, and + # winresource's default ProductName is the lowercase package name. + - name: Check the Windows version resource + if: runner.os == 'Windows' + shell: pwsh + run: | + cargo build + if ($LASTEXITCODE) { exit $LASTEXITCODE } + $expected = (cargo metadata --no-deps --format-version 1 | ConvertFrom-Json).packages[0].version + $info = (Get-Item target\debug\colony.exe).VersionInfo + "ProductName='$($info.ProductName)' ProductVersion='$($info.ProductVersion)' FileVersion='$($info.FileVersion)'" + if ($info.ProductName -cne 'Colony' -or $info.ProductVersion -cne $expected) { + "::error::colony.exe has ProductName '$($info.ProductName)' and ProductVersion '$($info.ProductVersion)', expected 'Colony' and '$expected'" + exit 1 + } + # The declared MSRV was 1.80 for years and could never have built: 70 # transitive dependencies require more. Nothing checked it, because CI only # ever ran on stable. This leg is what stops it drifting again. diff --git a/Cargo.lock b/Cargo.lock index 3d7957e..dc6dbbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -563,6 +563,7 @@ dependencies = [ "toml", "tracing", "tracing-subscriber", + "winresource", "zip", ] @@ -5379,6 +5380,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.51.0" diff --git a/Cargo.toml b/Cargo.toml index c0fb05e..0b5e0c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,3 +66,8 @@ strip = false [dev-dependencies] tempfile = "3" + +# Embeds the Windows version resource (build.rs). Without default features it +# pulls in nothing but version_check, and it does nothing on other targets. +[build-dependencies] +winresource = { version = "0.1.31", default-features = false } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..e4c441b --- /dev/null +++ b/build.rs @@ -0,0 +1,17 @@ +// Windows version resource: SignPath signs only an .exe whose ProductName is +// the project name and whose ProductVersion is set, and the release workflow +// checks both on the built file. Decided on the TARGET, not with cfg!(windows): +// build scripts run on the host. +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") { + let version = env!("CARGO_PKG_VERSION"); + winresource::WindowsResource::new() + .set("ProductName", "Colony") + .set("FileDescription", "Colony") + .set("ProductVersion", version) + .set("FileVersion", version) + .compile() + .expect("could not embed the Windows version resource"); + } +} From 63082260f3b082f31cf3e8ccb71177ea71e39229 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 13:00:14 +0200 Subject: [PATCH 2/4] ci(release): sign and publish through the shared workflow The build legs now only build, smoke-test and upload each binary as an artifact. The new sign-and-publish job calls Project-Colony-Resources/.github/workflows/sign-and-publish.yml, pinned by commit, which checks the draft, Authenticode-signs colony-windows.exe through SignPath once signpath-project-slug is set, writes .sig, .meta and .meta.sig over the final bytes, uploads them to the draft, downloads and verifies them, and publishes. Asset names, the .meta format, the draft hold and the AUR job chained after signing are unchanged. Also: no build cache in the release legs (SignPath forbids it), a read-only token without persisted credentials for the build legs, per-job permissions, a check of the Windows version resource against the tag, and a corrected note on why the draft hold is a gh call. --- .github/workflows/release-please.yml | 234 +++++++++++---------------- 1 file changed, 93 insertions(+), 141 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 092febc..055ffc6 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -9,7 +9,11 @@ on: # downstream job skip, while the run goes green. That is how v0.10.0 ended up # tagged with an empty draft release and no way to finish it. # - # Dispatch with the tag to build, sign and publish an existing release. + # Dispatch with the tag to build, sign and publish an existing DRAFT release, + # and start it FROM that tag: `gh workflow run release-please.yml --ref + # v0.10.0 -f tag=v0.10.0`. Once SignPath is 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: @@ -17,9 +21,9 @@ on: required: true type: string -permissions: - contents: write - pull-requests: write +# Each job asks for what it needs. The build legs run every dependency's build +# script, so they only get to read. +permissions: {} env: CARGO_TERM_COLOR: always @@ -28,6 +32,9 @@ jobs: 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 }} @@ -45,11 +52,10 @@ jobs: # signature refusal once `build` had uploaded but `sign` had not. And if # any build leg failed, that state was permanent until someone noticed. # - # Held as a draft here rather than via a release-please config file: the - # config-file route means switching the action to manifest mode, which - # changes the outputs contract that the build, sign and aur jobs all - # depend on and that nine releases have proven. This is one gh call, and - # it narrows the window from minutes to the second between the two steps. + # Held as a draft by this one gh call, which narrows the window from + # minutes to the second between the two steps. release-please's own + # `draft` setting lives in a config file, and the action ignores config + # files whenever `release-type` is set, as it is here. - name: Hold the release as a draft until it is signed if: ${{ steps.release.outputs.release_created }} env: @@ -73,6 +79,10 @@ jobs: # is what a workflow_dispatch does. if: ${{ !cancelled() && !failure() && (needs.release-please.outputs.release_created || github.event_name == 'workflow_dispatch') }} runs-on: ubuntu-latest + permissions: + # Only reads. But a draft release is invisible to a token without push + # access, so with `contents: read` every draft looks like "no release". + contents: write outputs: tag: ${{ steps.pick.outputs.tag }} steps: @@ -88,12 +98,11 @@ jobs: tag="${FROM_RELEASE:-$FROM_DISPATCH}" [ -n "$tag" ] || { echo "::error::no tag to build"; exit 1; } - # Refuse to touch a release that is already visible to users. The - # uploader has overwrite_files on by default, so a dispatch against a - # published tag would DELETE and replace its live binaries and only - # then hit the draft assertion - leaving published assets whose .sig + # Refuse to touch a release that is already visible to users: + # replacing its live binaries would leave published assets whose .sig # and .meta describe bytes that no longer exist. Self-update is # fail-closed, so that breaks every install of that version. + # sign-and-publish checks this again; failing here saves the builds. if ! gh release view "$tag" -R "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft > /tmp/state 2>/dev/null; then echo "::error::no release found for $tag" exit 1 @@ -121,7 +130,11 @@ jobs: # condition was true and its direct dependency had succeeded. Naming any # status function suppresses the implicit one. if: ${{ !cancelled() && needs.target.result == 'success' }} + # 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: @@ -147,14 +160,14 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.target.outputs.tag }} + persist-credentials: false - uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable branch with: targets: ${{ matrix.target }} - - uses: Swatinem/rust-cache@f0d9c3887740aee45f6153b24b3a6b815192ec16 # v2 - with: - key: ${{ matrix.target }} + # No build cache here: SignPath forbids release builds that reuse + # outputs of earlier, unverified builds. - name: Install Linux dependencies if: runner.os == 'Linux' @@ -186,6 +199,25 @@ jobs: chmod +x "${{ matrix.asset }}" || true "./${{ matrix.asset }}" --version + # SignPath signs only an .exe whose ProductName is the project name and + # whose ProductVersion is set (build.rs embeds both). Checked against the + # tag, so it also catches a Cargo.toml version that disagrees with it. + # `-cne`: PowerShell's `-ne` ignores case. + - name: Check the Windows version resource + if: runner.os == 'Windows' + shell: pwsh + env: + ASSET: ${{ matrix.asset }} + TAG: ${{ needs.target.outputs.tag }} + run: | + $expected = $env:TAG -replace '^v', '' + $info = (Get-Item $env:ASSET).VersionInfo + "ProductName='$($info.ProductName)' ProductVersion='$($info.ProductVersion)' FileVersion='$($info.FileVersion)'" + if ($info.ProductName -cne 'Colony' -or $info.ProductVersion -cne $expected) { + "::error::$env:ASSET has ProductName '$($info.ProductName)' and ProductVersion '$($info.ProductVersion)', expected 'Colony' and '$expected'" + exit 1 + } + # The Intel macOS asset is cross-compiled on an arm64 runner and cannot # be executed there, so assert the architecture instead - which is the # regression that would otherwise survive longest, on the leg with the @@ -198,145 +230,65 @@ jobs: file "${{ matrix.asset }}" | grep -q 'x86_64' \ || { echo "::error::${{ matrix.asset }} is not an x86_64 binary"; exit 1; } - # Uploaded with `gh`, not softprops/action-gh-release. The action wants - # to PATCH the release to manage its draft flag, which returns 403 - # "Resource not accessible by integration" against this draft - and its - # default behaviour is to PUBLISH the release after uploading unless told - # otherwise, which is what put an empty v0.10.0 in front of users once - # already. `gh release upload` only ever adds assets: it cannot publish - # anything, so the draft is safe by construction rather than by flag. - # - # This is also exactly what the sign job below already does, and has done - # successfully since v0.9.2. - - name: Upload binary to GitHub Release - shell: bash - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ needs.target.outputs.tag }} - run: gh release upload "$TAG" -R "$GITHUB_REPOSITORY" "${{ matrix.asset }}" --clobber - - # Assert the hold actually survived, so a future action bump cannot - # silently reintroduce the above. Cheap, and it fails the leg rather than - # the release. - - name: The release must still be a draft - shell: bash - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ needs.target.outputs.tag }} - run: | - state=$(gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json isDraft --jq .isDraft) - [ "$state" = "true" ] || { - echo "::error::$TAG is no longer a draft after upload - it is public without signatures. Re-draft it NOW: gh release edit $TAG --draft=true" - exit 1 - } + # To sign-and-publish as an artifact, at its root and under its release + # name, and never to the release from here: Authenticode rewrites the + # .exe, so nothing may reach the release before every byte is final. + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: build-${{ matrix.asset }} + path: ${{ matrix.asset }} + if-no-files-found: error + retention-days: 1 # Self-update verification is fail-closed: a release without .sig assets # bricks "Download update" for every existing install (this happened on - # v0.7.0, which shipped unsigned because signing was a manual step). This job - # makes the signatures part of the release itself and FAILS the release if - # the signing secret is missing, so an unsigned release can never ship - # silently again. - sign: - name: Sign release assets + # v0.7.0, which shipped unsigned because signing was a manual step). The + # organisation's shared workflow, in this same run: Authenticode through + # SignPath for the .exe (once SignPath is on), then .sig, .meta and .meta.sig + # over the FINAL bytes with COLONY_SIGNING_KEY_PEM, upload to the draft, + # download again and verify, and only then publish. It fails the release if + # the key is missing, so an unsigned release can never ship silently. + # Pinned by SHA: it holds the release key. See design/releases.md, section + # "Shared signing workflow", in Project-Colony-Resources. + sign-and-publish: needs: [target, build] # Every dependency named explicitly. Guarding on `target` alone would let # this run after a FAILED build and try to sign assets that do not exist. if: ${{ !cancelled() && needs.target.result == 'success' && needs.build.result == 'success' }} - runs-on: ubuntu-latest - env: - # Single source of truth for the four launcher assets. Declared once so the - # download, sign, verify and post-upload steps cannot drift apart: a list - # repeated per step means adding a platform silently leaves it unsigned, - # which is exactly how v0.7.0 shipped. - ASSETS: colony-linux colony-windows.exe colony-macos colony-macos-x86 - GH_TOKEN: ${{ github.token }} - TAG: ${{ needs.target.outputs.tag }} - steps: - # Same tag as the build, so the signing script that runs is the one this - # release actually shipped with. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ needs.target.outputs.tag }} - - - name: Download release binaries - run: | - mkdir dist - for a in $ASSETS; do PATTERNS="$PATTERNS --pattern $a"; done - gh release download "$TAG" -R "$GITHUB_REPOSITORY" --dir dist $PATTERNS - for a in $ASSETS; do - [ -s "dist/$a" ] || { echo "::error::release asset $a is missing or empty"; exit 1; } - done - - - name: Sign - env: - KEY: ${{ secrets.COLONY_SIGNING_KEY_PEM }} - run: | - if [ -z "$KEY" ]; then - echo "::error::COLONY_SIGNING_KEY_PEM secret is not set - the release would ship unsigned and self-update is fail-closed. See docs/release-signing.md." - exit 1 - fi - printf '%s' "$KEY" > /tmp/colony-release.pem - chmod 600 /tmp/colony-release.pem - # The rm below is skipped when sign-release.sh exits non-zero under - # `bash -e`, leaving the private key on the runner's disk for the rest - # of the job. - trap 'rm -f /tmp/colony-release.pem' EXIT - for a in $ASSETS; do PATHS="$PATHS dist/$a"; done - COLONY_SIGNING_KEY=/tmp/colony-release.pem \ - COLONY_RELEASE_VERSION="$TAG" \ - ./scripts/sign-release.sh $PATHS - rm -f /tmp/colony-release.pem - - # Self-update is fail-closed on BOTH the signature and the signed metadata - # sidecar that binds it to a version, so a release missing either one is - # un-updatable. Check locally before uploading... - - name: Verify every asset got a signature and a signed sidecar - run: | - for a in $ASSETS; do - for required in "dist/$a.sig" "dist/$a.meta" "dist/$a.meta.sig"; do - [ -s "$required" ] || { echo "::error::missing or empty $required"; exit 1; } - done - grep -qx "version=$TAG" "dist/$a.meta" \ - || { echo "::error::dist/$a.meta does not bind version=$TAG"; exit 1; } - grep -qx "asset=$a" "dist/$a.meta" \ - || { echo "::error::dist/$a.meta does not bind asset=$a"; exit 1; } - done - echo "all assets carry .sig + .meta + .meta.sig, correctly bound" - - - name: Upload signatures and metadata - run: gh release upload "$TAG" -R "$GITHUB_REPOSITORY" dist/*.sig dist/*.meta --clobber - - # ...and again against the PUBLISHED release, because a local file proves - # nothing about what users can actually download. - - name: Verify the published release carries every asset - run: | - gh release view "$TAG" -R "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' | sort > /tmp/published - for a in $ASSETS; do - for required in "$a" "$a.sig" "$a.meta" "$a.meta.sig"; do - grep -qx "$required" /tmp/published \ - || { echo "::error::$required is not published on $TAG - self-update would refuse this release"; exit 1; } - done - done - echo "published release verified" - - # Only now does the release become visible. Until this step, /releases/ - # latest still points at the previous version, so no client is offered an - # update it cannot apply, and the README's download link never points at - # an empty release. - - name: Publish the release - run: gh release edit "$TAG" -R "$GITHUB_REPOSITORY" --draft=false + # 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.target.outputs.tag }} + # The four names self-update asks for (launcher_asset_name), each + # published with its .sig, .meta and .meta.sig. + assets: colony-linux colony-windows.exe colony-macos colony-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: "colony" + # 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 }} # Chained here (not on `release: published`) because release-please creates # the release with the default GITHUB_TOKEN, whose events do not trigger # other workflows - the standalone trigger never fired once in nine - # releases. Running after `sign` also guarantees the assets exist before + # releases. For the same reason the publish inside sign-and-publish, made + # with GITHUB_TOKEN, would not start it either. Running after + # sign-and-publish also guarantees the assets are signed and public before # the AUR job hashes them. aur: name: Publish colony-bin to AUR - needs: [target, sign] - # `sign` too: without it, a failed signing job would still bump the AUR - # package to a release that has no signatures. - if: ${{ !cancelled() && needs.target.result == 'success' && needs.sign.result == 'success' }} + needs: [target, sign-and-publish] + # sign-and-publish too: without it, a failed signing job would still bump + # the AUR package to a release that has no signatures. + if: ${{ !cancelled() && needs.target.result == 'success' && needs.sign-and-publish.result == 'success' }} + permissions: + contents: read uses: ./.github/workflows/aur-publish.yml with: tag: ${{ needs.target.outputs.tag }} From 393c378527a3da4ac66f7432edbcefadc436ff5f Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 13:00:19 +0200 Subject: [PATCH 3/4] docs: add the code signing policy and privacy policy The README gets the "Code signing policy" section SignPath Foundation asks for, with the team roles and a privacy policy that lists every server Colony contacts, when, and what it sends, as the code does it. The release signing guide and the workflow template pointer now describe the shared sign-and-publish workflow and the Authenticode step before ed25519 signing. --- .../colony-rust-release.yml.template | 8 ++-- README.md | 22 ++++++++++ docs/release-signing.md | 41 ++++++++++++++----- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/.github/workflows/colony-rust-release.yml.template b/.github/workflows/colony-rust-release.yml.template index 6c3b910..d2e4842 100644 --- a/.github/workflows/colony-rust-release.yml.template +++ b/.github/workflows/colony-rust-release.yml.template @@ -31,6 +31,8 @@ # key rotation, and how to adopt signatures in a program that already ships - # is in Project-Colony-Resources / design/releases.md. # -# Colony's own release pipeline (.github/workflows/release-please.yml) is the -# reference implementation and stays separate: it self-updates, so its -# signature verification is mandatory rather than opt-in. +# Colony's own release pipeline (.github/workflows/release-please.yml) signs and +# publishes through the shared workflow in Project-Colony-Resources, +# .github/workflows/sign-and-publish.yml (caller example: +# templates/sign-and-publish-caller.yml). Colony self-updates, so for it the +# signatures are mandatory rather than opt-in. diff --git a/README.md b/README.md index 75a5a71..ce9f4d7 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,28 @@ how these move up. | [Colony spec](docs/colony-spec.md) | Full `colony.json` manifest reference | | [Contributing](CONTRIBUTING.md) | How to add your app + how to contribute to Colony itself | +## 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 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 organisation's ed25519 key, and Colony verifies that signature before it applies a self-update (see [Release signing](docs/release-signing.md)). + +Team roles and members: + +- Committers and reviewers: [MotherSphere](https://github.com/MotherSphere) +- Approvers: [MotherSphere](https://github.com/MotherSphere) + +### Privacy policy + +Colony talks to GitHub and to no other server. It has no telemetry, no analytics and no crash reporting. + +- **At every start, automatically**, and again when you press Refresh: it reads the Project-Colony catalogue from the GitHub API (`api.github.com`): the organisation's repository list and, for each repository, its `colony.json`, README, licence, changelog and icon, plus its latest release when the manifest does not list its files. +- **Update checks, automatically unless turned off** (Settings, "Check automatically", on by default) or when you press "Check for updates": it asks the GitHub API for the release of every installed app and the latest release of Colony itself. These requests name the repositories of the apps you have installed, so GitHub can see which Colony apps you use. +- **When you install or update an app, or update Colony**: it looks the release up through the GitHub API, then downloads the release file and its `.sig`, `.meta` and `.meta.sig` from `github.com`, which serves them from GitHub's own download hosts. +- **When you sign in with GitHub** (optional): it uses GitHub's device flow (`github.com/login/device/code` and `github.com/login/oauth/access_token`, scope `read:user`), opens the verification page in your browser, and reads your user name from `api.github.com/user`. While you are signed in, your GitHub token is sent with the requests above, to GitHub only, to raise the API rate limit. + +Requests carry a `Colony-Launcher` user agent. Beyond that, they tell GitHub only which repositories and files are asked for (a download's file name shows your platform) and, while you are signed in, which account is asking; nothing from your settings or your files is sent. GitHub also sees what any web request shows it, such as your IP address, under the [GitHub General Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement). Links you click open in your web browser. The apps you install through Colony are separate programs with their own behaviour. + ## License [GPL-3.0-or-later](LICENSE) © 2026 Project Colony contributors diff --git a/docs/release-signing.md b/docs/release-signing.md index 5792248..e5c2023 100644 --- a/docs/release-signing.md +++ b/docs/release-signing.md @@ -42,8 +42,9 @@ download time, so the staging file cannot be swapped in between. Because verification is fail-closed, a release published without the `colony-.sig`, `.meta` and `.meta.sig` assets will make self-update -fail for users on that channel. The CI `sign` job checks all three exist for all -four platforms and fails the release otherwise. +fail for users on that channel. The release workflow's `sign-and-publish` job +checks all three exist and verify for all four platforms and fails the release +otherwise. ## Signing (CI or local) @@ -90,7 +91,9 @@ tells you which state you are in. **Still a draft, incomplete.** Nobody is affected: `/releases/latest` still points at the previous version and no client has been offered anything. Fix the -cause and use **"Re-run failed jobs"**. +cause and use **"Re-run failed jobs"**. The build artifacts are kept for one +day; after that, dispatch the workflow again from the tag itself: +`gh workflow run release-please.yml --ref -f tag=`. **Never "Re-run all jobs".** release-please re-runs against a `main` whose release already exists, emits an empty `release_created`, and every downstream @@ -113,14 +116,30 @@ then follow the manual signing procedure above and re-publish with Since the v0.7.0 incident (a release shipped unsigned because signing was a manual step, bricking self-update for every existing install), signing is a -mandatory job in the release workflow: `.github/workflows/release-please.yml` -(`sign` job) downloads the four built binaries, signs them with the -`COLONY_SIGNING_KEY_PEM` secret (the PEM contents), verifies that every asset -came out with a `.sig`, a `.meta` and a `.meta.sig`, and uploads them. The job -**fails the release** if the secret is missing or if any of those files is -missing or empty, so a release the launcher cannot verify can no longer ship -silently. The manual procedure above remains for re-signing an old release by -hand. +mandatory part of the release workflow, `.github/workflows/release-please.yml`. +Its build legs only build, smoke-test and upload each binary as a workflow +artifact. The `sign-and-publish` job then calls the organisation's shared +workflow, `.github/workflows/sign-and-publish.yml` in +[Project-Colony-Resources](https://github.com/Project-Colony/Project-Colony-Resources), +pinned by commit, which in that same run: + +1. checks the release is still a draft; +2. sends `colony-windows.exe` to SignPath for Authenticode, once SignPath is + turned on for this repository (`signpath-project-slug`), and waits for a + person to approve the request; +3. writes `.sig`, `.meta` and `.meta.sig` over the final bytes with the + `COLONY_SIGNING_KEY_PEM` secret (the PEM contents), in the same format as + `scripts/sign-release.sh`; +4. uploads everything to the draft, downloads it again and verifies every + signature and digest against what users will download; +5. publishes the release. + +The order is the point: Authenticode rewrites the `.exe`, so a `.sig` or +`.meta` computed before it would describe bytes that no longer exist, and the +launcher would refuse the update. The job **fails the release** if the secret is +missing or any file is missing, empty or does not verify, so a release the +launcher cannot verify can no longer ship silently. The manual procedure above +remains for re-signing an old release by hand. ## Key custody From 92f3d9f18a59c7cad610bb5dfa0bd8b72a75749f Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 8 Oct 2026 13:21:23 +0200 Subject: [PATCH 4/4] docs: list the release notes request in the privacy policy --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ce9f4d7..2afbba4 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,7 @@ Colony talks to GitHub and to no other server. It has no telemetry, no analytics - **At every start, automatically**, and again when you press Refresh: it reads the Project-Colony catalogue from the GitHub API (`api.github.com`): the organisation's repository list and, for each repository, its `colony.json`, README, licence, changelog and icon, plus its latest release when the manifest does not list its files. - **Update checks, automatically unless turned off** (Settings, "Check automatically", on by default) or when you press "Check for updates": it asks the GitHub API for the release of every installed app and the latest release of Colony itself. These requests name the repositories of the apps you have installed, so GitHub can see which Colony apps you use. +- **When you press "What's new in {version}"** on an app's page: it asks the GitHub API for that release, to show its notes. - **When you install or update an app, or update Colony**: it looks the release up through the GitHub API, then downloads the release file and its `.sig`, `.meta` and `.meta.sig` from `github.com`, which serves them from GitHub's own download hosts. - **When you sign in with GitHub** (optional): it uses GitHub's device flow (`github.com/login/device/code` and `github.com/login/oauth/access_token`, scope `read:user`), opens the verification page in your browser, and reads your user name from `api.github.com/user`. While you are signed in, your GitHub token is sent with the requests above, to GitHub only, to raise the API rate limit.