diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 010bce4..467a266 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,14 +24,17 @@ # published stable installer, so the job runs once the WINGET_PACKAGE_ID variable # is set (docs/RELEASING.md) and fails if the token is missing from then on. The # tokens live in the `release` environment, which only v* tags can use: -# DEPS_PAT fine-grained token: contents + pull requests (write) on this -# repository, so the packaging PR runs the required checks +# RELEASE_APP_PRIVATE_KEY private key of the release GitHub App (contents + +# pull requests write on this repository only); the job mints a +# short-lived token from it, so the packaging PR's commit is +# signed by GitHub and the PR runs the required checks # CHOCO_API_KEY chocolatey.org API key # WINGET_TOKEN fine-grained token that can fork and open pull requests on # public repositories (microsoft/winget-pkgs) # NUGET_API_KEY nuget.org key with push rights for gclo.Engine (NUGET_PUBLISH) -# plus the repository variable WINGET_PACKAGE_ID (KofTwentyTwo.gclo, set after the -# one-time manual `wingetcreate new`). +# plus the repository variables RELEASE_APP_CLIENT_ID (the release App's client id) +# and WINGET_PACKAGE_ID (KofTwentyTwo.gclo, set after the one-time manual +# `wingetcreate new`). name: Release on: @@ -136,13 +139,14 @@ jobs: - name: Require the publishing secrets shell: pwsh env: - DEPS_PAT: ${{ secrets.DEPS_PAT }} + RELEASE_APP_CLIENT_ID: ${{ vars.RELEASE_APP_CLIENT_ID }} + RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }} run: | - $missing = @('DEPS_PAT', 'CHOCO_API_KEY') | Where-Object { [string]::IsNullOrEmpty((Get-Item -Path "env:$_" -ErrorAction SilentlyContinue).Value) } + $missing = @('RELEASE_APP_CLIENT_ID', 'RELEASE_APP_PRIVATE_KEY', 'CHOCO_API_KEY') | Where-Object { [string]::IsNullOrEmpty((Get-Item -Path "env:$_" -ErrorAction SilentlyContinue).Value) } if ($missing) { - Write-Output "::error::Missing in the release environment: $($missing -join ', '). Add them (docs/RELEASING.md) and re-run this job." + Write-Output "::error::Missing in the release environment (or repository variables): $($missing -join ', '). Add them (docs/RELEASING.md) and re-run this job." exit 1 } @@ -173,19 +177,28 @@ jobs: CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }} run: choco push (Get-ChildItem artifacts/chocolatey/*.nupkg | Select-Object -First 1).FullName --source https://push.chocolatey.org/ --api-key $env:CHOCO_API_KEY + # protect-main requires signed commits and a DCO sign-off. The action only + # signs with a bot token (a PAT is explicitly unsupported), so the PR is + # opened with a short-lived token of the release GitHub App: GitHub signs + # the commit, its author is the App's [bot] user (exempt from the DCO + # check), and the PR still triggers the required checks (#81, #85). + - name: Mint a release App token + id: app + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.RELEASE_APP_CLIENT_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + permission-contents: write + permission-pull-requests: write + - name: Open a PR with the updated package manifests uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: - token: ${{ secrets.DEPS_PAT }} + token: ${{ steps.app.outputs.token }} + sign-commits: true base: main branch: packaging/v${{ needs.release.outputs.version }} delete-branch: true - # The pr / dco gate wants a Signed-off-by whose email matches the commit - # author, so author and committer are the same identity (the actor who - # pushed the tag) and the action signs off as that committer (#81). - author: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com> - committer: ${{ github.actor }} <${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com> - signoff: true commit-message: 'build(packaging): point the Scoop and Chocolatey manifests at v${{ needs.release.outputs.version }}' title: 'build(packaging): point the Scoop and Chocolatey manifests at v${{ needs.release.outputs.version }}' body: | diff --git a/docs/RELEASING.md b/docs/RELEASING.md index d81d175..3c93e25 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -65,12 +65,20 @@ its first submission cannot be automated; see below. | Job | Runs for | Needs | Publishes | | --- | -------- | ----- | --------- | | `release` (shared workflow) | every tag | nothing beyond `GITHUB_TOKEN` | the GitHub Release: Setup, portable, full/delta packages, feed, CLI zip, SBOMs, `SHA256SUMS`, provenance and SBOM attestations | -| `packages` | stable only | `DEPS_PAT` (fine-grained token: contents and pull requests, write, this repository, ≤ 90 days), `CHOCO_API_KEY` (chocolatey.org) | the Chocolatey package pushed to chocolatey.org, and a `build(packaging): …` pull request against `main` with `bucket/gclo.json` and `packaging/chocolatey` pointed at the release | +| `packages` | stable only | `RELEASE_APP_PRIVATE_KEY` (private key of the release GitHub App, see below) plus the repository variable `RELEASE_APP_CLIENT_ID`, `CHOCO_API_KEY` (chocolatey.org) | the Chocolatey package pushed to chocolatey.org, and a `build(packaging): …` pull request against `main` with `bucket/gclo.json` and `packaging/chocolatey` pointed at the release | | `winget` | stable only, once the repository variable `WINGET_PACKAGE_ID` is set (`KofTwentyTwo.gclo`, after the one-time manual submission below) | `WINGET_TOKEN` (fine-grained token that can fork and open pull requests on public repositories, ≤ 90 days); missing → the job fails | a manifest update PR on [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs) | | `nuget` | only when the repository variable `NUGET_PUBLISH` is `true` | `NUGET_API_KEY` (nuget.org, push rights for `gclo.Engine`) | `gclo.Engine` on nuget.org | -Why `DEPS_PAT`: a pull request opened with the default `GITHUB_TOKEN` triggers no -status checks, so the packaging PR could never satisfy `main`'s required checks. +Why a GitHub App: `main` requires signed commits, a DCO sign-off, and the full set +of status checks. A pull request opened with the default `GITHUB_TOKEN` triggers +no checks, and the action that opens the PR cannot sign commits with a personal +access token. With a short-lived token minted from the App, GitHub signs the +commit, its author is the App's `[bot]` user (exempt from the DCO check), and the +checks run. Create the App once (the same pattern the standards repository uses): +GitHub App installed **only on this repository**, permissions Contents and Pull +requests **write**, no ruleset bypass. Store its client id as the Actions variable +`RELEASE_APP_CLIENT_ID` and its private key (`.pem`) as `RELEASE_APP_PRIVATE_KEY` +in the `release` environment; never as a repository secret or a committed file. ## First winget submission (one-time, manual) diff --git a/docs/adr/0003-repository-as-scoop-bucket.md b/docs/adr/0003-repository-as-scoop-bucket.md index d6e5f50..dabf4e4 100644 --- a/docs/adr/0003-repository-as-scoop-bucket.md +++ b/docs/adr/0003-repository-as-scoop-bucket.md @@ -47,7 +47,9 @@ and hash; `PackagingManifestTests` keeps both manifests valid and in agreement. - Stable releases are consumable via `scoop bucket add gclo ` with no external account; prereleases never touch package managers. - The generated manifest pull request targets `main` and goes through the normal - gates (with `DEPS_PAT` it also gets status checks). + gates: it is opened with a short-lived release GitHub App token, so its commit + is signed by GitHub, exempt from the DCO check as a bot, and runs the required + status checks (#85). - Security: a new distribution channel; both channels verify the asset hash (threat model T2), and the hash they pin is the one `SHA256SUMS` attests. - Follow-up: `CHOCO_API_KEY` and the first Chocolatey moderation (#7). diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index b7689e7..3ead58b 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -111,3 +111,4 @@ flowchart LR | --- | --- | --- | --- | | 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | Initial model, covering the path-recovery, WSL clone, vault scoping, package-manager, and SBOM changes of 1.0.1 | | 2026-10-09 | v1.0.1 | James Maes (@KofTwentyTwo) | T11 updated: releases build in the shared standards workflow; publishing tokens moved to the `release` environment (#76) | +| 2026-10-10 | v1.0.2 | James Maes (@KofTwentyTwo) | T11: the packaging PR is opened with a short-lived release GitHub App token (contents + pull requests on this repository, no bypass) instead of a long-lived PAT; the commit is GitHub-signed (#85) |