ci(release): open the packaging PR with a release GitHub App token #86

Merged
KofTwentyTwo merged 1 commits from ci/packaging-app-token into main 2026-10-10 17:24:58 +00:00
4 changed files with 42 additions and 18 deletions

No files matched your search

+27 -14
View File
@@ -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: |
+11 -3
View File
@@ -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)
+3 -1
View File
@@ -47,7 +47,9 @@ and hash; `PackagingManifestTests` keeps both manifests valid and in agreement.
- Stable releases are consumable via `scoop bucket add gclo <repo>` 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).
+1
View File
@@ -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) |