From 28d4fd2c3544d6e233fdd859491901b7f9bd1028 Mon Sep 17 00:00:00 2001 From: Bill Traynor Date: Mon, 19 Oct 2026 05:40:10 -0400 Subject: [PATCH 1/2] Sign bot commits via a GitHub App token Under the riscv org's ruleset, main requires verified signatures as well as DCO. The three workflows that open PRs (template-sync, the version-bot milestone PR, the build-pdf site stamp PR) push with GHTOKEN, a personal access token, and create-pull-request cannot sign with a PAT. Every bot PR was blocked with "Commits must have verified signatures" while DCO was green (riscv-performance-event-sampling#90). When a GitHub App is configured (vars.BOT_APP_CLIENT_ID, secrets.BOT_APP_PRIVATE_KEY), mint its token with actions/create-github-app-token@v3 and pass sign-commits: true, so GitHub signs the commit as the App's bot. Probot DCO skips bot-authored commits. Without the App, behavior is unchanged: fall back to GHTOKEN. Document the setup in README.adoc, add a MIGRATION.md Step 16 check, and warn in template-sync when commits will be unsigned. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Bill Traynor --- .github/workflows/build-pdf.yml | 12 +++++++++++- .github/workflows/template-sync.yml | 24 ++++++++++++++++++++++-- .github/workflows/version-bot.yml | 12 +++++++++++- CHANGELOG.md | 8 ++++++++ MIGRATION.md | 6 ++++++ README.adoc | 24 +++++++++++++++++++++--- 6 files changed, 79 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index b7aadef..c30fbad 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -321,11 +321,21 @@ jobs: fi echo "changed=true" >> "$GITHUB_OUTPUT" + # Signed commits -- see the matching step in template-sync.yml. + - name: Mint GitHub App token + id: app-token + if: steps.stamp.outputs.changed == 'true' && vars.BOT_APP_CLIENT_ID != '' + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.BOT_APP_CLIENT_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + - name: Open site version stamp PR if: steps.stamp.outputs.changed == 'true' uses: peter-evans/create-pull-request@v8 with: - token: ${{ secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token || secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + sign-commits: ${{ steps.app-token.outputs.token != '' }} base: main # Signed off as the dispatching user, for the DCO check -- see the # matching note in version-bot.yml's milestone PR step. diff --git a/.github/workflows/template-sync.yml b/.github/workflows/template-sync.yml index 565e3fa..30b7d3f 100644 --- a/.github/workflows/template-sync.yml +++ b/.github/workflows/template-sync.yml @@ -40,9 +40,12 @@ jobs: - name: Check for bot token env: GHTOKEN: ${{ secrets.GHTOKEN }} + HAS_APP: ${{ vars.BOT_APP_CLIENT_ID != '' }} run: | set -euo pipefail - if [[ -z "$GHTOKEN" ]]; then + if [[ "$HAS_APP" == "true" ]]; then + echo "Using the GitHub App token (BOT_APP_CLIENT_ID); commits will be signed." + elif [[ -z "$GHTOKEN" ]]; then # Unlike milestone-pr in version-bot.yml, this is not just a # "checks stall" warning: GITHUB_TOKEN is hard-blocked by GitHub # from pushing any change under .github/workflows/, which this @@ -50,6 +53,8 @@ jobs: # `workflow` scope) the create-pull-request step below will fail # outright. echo "::warning::GHTOKEN is not set. This workflow updates files under .github/workflows/, and GITHUB_TOKEN cannot push those -- the pull request step below will fail. Set a GHTOKEN repository secret (a PAT with the 'workflow' scope). See README.adoc, 'Recommended repository secret'." + else + echo "::warning::No GitHub App configured (BOT_APP_CLIENT_ID), so the sync commit will be pushed with GHTOKEN and will not be signed. If main requires signed commits, the PR cannot merge until a maintainer re-signs it. See README.adoc, 'Recommended repository secret'." fi - name: Add template remote @@ -212,11 +217,26 @@ jobs: EOF git add .template-version + # See README.adoc, 'Recommended repository secret'. create-pull-request + # signs commits (sign-commits) only with a bot token -- with a PAT such as + # GHTOKEN it pushes them unsigned, which a "require signed commits" rule + # on main blocks. An App token lets GitHub sign them as the App's bot. + - name: Mint GitHub App token + id: app-token + if: steps.refs.outputs.skip == 'false' && vars.BOT_APP_CLIENT_ID != '' + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.BOT_APP_CLIENT_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + - name: Create pull request if: steps.refs.outputs.skip == 'false' uses: peter-evans/create-pull-request@v8 with: - token: ${{ secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token || secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + # With an App token the commit is authored by the App's bot, which the + # DCO check skips; the trailer below then only records who ran it. + sign-commits: ${{ steps.app-token.outputs.token != '' }} # Same reason as milestone-pr in version-bot.yml: the DCO check that # RISC-V spec repos run requires a Signed-off-by matching the # commit's author. create-pull-request authors the commit as the user diff --git a/.github/workflows/version-bot.yml b/.github/workflows/version-bot.yml index 90ad26b..bc23e6a 100644 --- a/.github/workflows/version-bot.yml +++ b/.github/workflows/version-bot.yml @@ -276,10 +276,20 @@ jobs: set -euo pipefail ./scripts/update-spec-state.sh "${{ needs.tag-version.outputs.next_version }}" "$(date +%Y-%m-%d)" + # Signed commits -- see the matching step in template-sync.yml. + - name: Mint GitHub App token + id: app-token + if: vars.BOT_APP_CLIENT_ID != '' + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ vars.BOT_APP_CLIENT_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + - name: Create milestone review PR uses: peter-evans/create-pull-request@v8 with: - token: ${{ secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token || secrets.GHTOKEN || secrets.GITHUB_TOKEN }} + sign-commits: ${{ steps.app-token.outputs.token != '' }} # The DCO check that RISC-V spec repos run requires a Signed-off-by # matching the commit's author. create-pull-request authors the commit # as the user who dispatched this workflow and adds no trailer, so the diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b20ee5..479bbae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 **_NOTE:_** PROJECTS BUILT USING THE TEMPLATE SHOULD UPDATE THE BELOW SECTIONS AS-NEEDED. ## [Unreleased] +- Sign bot commits so their PRs can merge where `main` requires signed commits. + `template-sync.yml`, `version-bot.yml` (milestone PR) and `build-pdf.yml` + (site version stamp PR) pushed with `GHTOKEN`, a personal access token, which + `create-pull-request` cannot sign, so under the `riscv` organization's ruleset + every bot PR was blocked even with DCO passing. When a GitHub App is configured + (`BOT_APP_CLIENT_ID` variable, `BOT_APP_PRIVATE_KEY` secret), each workflow + now mints its token and passes `sign-commits: true`; otherwise it falls back + to `GHTOKEN` as before. See README.adoc, "Signed commits: use a GitHub App". - Stop `template-sync.yml` from overwriting `GOVERNANCE.md`. The template's copy is a placeholder every task group rewrites, and `UPGRADING.md` already said to treat an amended copy as a shared file, but the workflow copied it diff --git a/MIGRATION.md b/MIGRATION.md index 04d0d8e..4e8cf20 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -676,6 +676,12 @@ earlier. secrets count). The workflow writes files under `.github/workflows/`, which `GITHUB_TOKEN` cannot push regardless of the *Allow GitHub Actions to create and approve pull requests* setting. +- [ ] If `main` requires signed commits (the `riscv` organization's ruleset + does), confirm the `BOT_APP_CLIENT_ID` variable and `BOT_APP_PRIVATE_KEY` + secret are visible and the App is installed on the repository. Without + them the sync commit is pushed with `GHTOKEN` unsigned, and the PR is + blocked until someone re-signs it. See README.adoc, *Signed commits: use a + GitHub App*. - [ ] Run it once from *Actions → Template Sync → Run workflow*, rather than waiting for the Monday schedule. - [ ] While you are in *Actions*, run **Repository Setup** too. It applies the diff --git a/README.adoc b/README.adoc index 703be9c..6d9ffbb 100644 --- a/README.adoc +++ b/README.adoc @@ -249,10 +249,28 @@ PDF build, `pre-commit`, Vale, and the Antora content-source validation -- is created in the `action_required` state and waits for a maintainer to press "Approve and run". The PR itself is still correct; only its checks stall. -`GHTOKEN` may be either: +`GHTOKEN` is a fine-grained personal access token scoped to the repository with `Contents: read and write`, `Pull requests: read and write`, and `Workflows: read and write`. It is tied to one person's account and expires. -* A fine-grained personal access token scoped to the repository with `Contents: read and write`, `Pull requests: read and write`, and `Workflows: read and write`. Simplest to set up; tied to one person's account and expires. -* A GitHub App installation token minted per run with `actions/create-github-app-token`. Preferred for repositories with more than one maintainer, since it is not tied to an individual. +==== Signed commits: use a GitHub App + +Commits pushed with `GHTOKEN` are *not signed*. If `main` requires signed +commits (the `riscv` organization's ruleset does), every bot PR -- template +sync, milestone, site version stamp -- is blocked with "Commits must have +verified signatures" even though DCO passes, until a maintainer re-signs the +commit by hand. `create-pull-request` signs commits only with a bot token, never +with a personal access token. + +To have GitHub sign them, configure a GitHub App. The three workflows that open +PRs mint a token for it per run with `actions/create-github-app-token` and pass +`sign-commits: true`; when it is not configured they fall back to `GHTOKEN`. + +* *Variable* `BOT_APP_CLIENT_ID`: the App's client ID. +* *Secret* `BOT_APP_PRIVATE_KEY`: a private key generated for the App. +* App permissions: `Contents`, `Pull requests` and `Workflows`, all read and write. Install it on each repository. + +Both can be set at the organization level. Commits are then authored by the +App's bot, which the DCO check skips. An App is also not tied to one person's +account, and its tokens don't need rotating by hand. === Manual builds From ceea3b8249b9ab5f1c5fe7639105e5e8923a1aff Mon Sep 17 00:00:00 2001 From: Bill Traynor Date: Mon, 19 Oct 2026 10:04:51 -0400 Subject: [PATCH 2/2] Read the App credentials from the TEMPLATE_SYNC_* secrets The riscv organization already carries the App's client ID and private key as the secrets TEMPLATE_SYNC_APP_CLIENT_ID and TEMPLATE_SYNC_PRIVATE_KEY, so read those rather than asking an org admin to re-create them as BOT_APP_CLIENT_ID and BOT_APP_PRIVATE_KEY. The client ID moves from a variable to a secret, which forces one structural change: GitHub's context availability table permits `secrets` in `env`, `with` and `run`, but not in `jobs..steps.if`, so the mint step can no longer test the credential directly. It is allowed in `jobs..env`, so each of the three jobs now hoists the test into a job-level `HAS_APP` and the gate reads `env.HAS_APP`. Observed on riscv/riscv-high-assurance-cryptography: a dispatch of the previous version skipped "Mint GitHub App token" because vars.BOT_APP_CLIENT_ID was empty, and the sync commit came back verified=false, reason=unsigned. Co-Authored-By: Claude Opus 5 Signed-off-by: Bill Traynor --- .github/workflows/build-pdf.yml | 12 +++++++++--- .github/workflows/template-sync.yml | 17 +++++++++++------ .github/workflows/version-bot.yml | 12 +++++++++--- CHANGELOG.md | 2 +- MIGRATION.md | 5 +++-- README.adoc | 9 +++++++-- 6 files changed, 40 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index c30fbad..609ac38 100644 --- a/.github/workflows/build-pdf.yml +++ b/.github/workflows/build-pdf.yml @@ -269,6 +269,12 @@ jobs: permissions: contents: write pull-requests: write + # `secrets` cannot be read from a step's `if:` -- GitHub's context + # availability table allows it in `env`, `with` and `run`, but not in + # `jobs..steps.if`. It IS allowed in `jobs..env`, so hoist the + # "is an App configured?" test to the job and let the gate read `env`. + env: + HAS_APP: ${{ secrets.TEMPLATE_SYNC_APP_CLIENT_ID != '' }} steps: - name: Checkout main uses: actions/checkout@v7 @@ -324,11 +330,11 @@ jobs: # Signed commits -- see the matching step in template-sync.yml. - name: Mint GitHub App token id: app-token - if: steps.stamp.outputs.changed == 'true' && vars.BOT_APP_CLIENT_ID != '' + if: steps.stamp.outputs.changed == 'true' && env.HAS_APP == 'true' uses: actions/create-github-app-token@v3 with: - client-id: ${{ vars.BOT_APP_CLIENT_ID }} - private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + client-id: ${{ secrets.TEMPLATE_SYNC_APP_CLIENT_ID }} + private-key: ${{ secrets.TEMPLATE_SYNC_PRIVATE_KEY }} - name: Open site version stamp PR if: steps.stamp.outputs.changed == 'true' diff --git a/.github/workflows/template-sync.yml b/.github/workflows/template-sync.yml index 30b7d3f..f8a6f2a 100644 --- a/.github/workflows/template-sync.yml +++ b/.github/workflows/template-sync.yml @@ -27,6 +27,12 @@ jobs: sync: runs-on: ubuntu-latest if: github.repository != 'riscv/docs-spec-template' + # `secrets` cannot be read from a step's `if:` -- GitHub's context + # availability table allows it in `env`, `with` and `run`, but not in + # `jobs..steps.if`. It IS allowed in `jobs..env`, so hoist the + # "is an App configured?" test to the job and let the gate read `env`. + env: + HAS_APP: ${{ secrets.TEMPLATE_SYNC_APP_CLIENT_ID != '' }} steps: - name: Checkout repository uses: actions/checkout@v7 @@ -40,11 +46,10 @@ jobs: - name: Check for bot token env: GHTOKEN: ${{ secrets.GHTOKEN }} - HAS_APP: ${{ vars.BOT_APP_CLIENT_ID != '' }} run: | set -euo pipefail if [[ "$HAS_APP" == "true" ]]; then - echo "Using the GitHub App token (BOT_APP_CLIENT_ID); commits will be signed." + echo "Using the GitHub App token (TEMPLATE_SYNC_APP_CLIENT_ID); commits will be signed." elif [[ -z "$GHTOKEN" ]]; then # Unlike milestone-pr in version-bot.yml, this is not just a # "checks stall" warning: GITHUB_TOKEN is hard-blocked by GitHub @@ -54,7 +59,7 @@ jobs: # outright. echo "::warning::GHTOKEN is not set. This workflow updates files under .github/workflows/, and GITHUB_TOKEN cannot push those -- the pull request step below will fail. Set a GHTOKEN repository secret (a PAT with the 'workflow' scope). See README.adoc, 'Recommended repository secret'." else - echo "::warning::No GitHub App configured (BOT_APP_CLIENT_ID), so the sync commit will be pushed with GHTOKEN and will not be signed. If main requires signed commits, the PR cannot merge until a maintainer re-signs it. See README.adoc, 'Recommended repository secret'." + echo "::warning::No GitHub App configured (TEMPLATE_SYNC_APP_CLIENT_ID), so the sync commit will be pushed with GHTOKEN and will not be signed. If main requires signed commits, the PR cannot merge until a maintainer re-signs it. See README.adoc, 'Recommended repository secret'." fi - name: Add template remote @@ -223,11 +228,11 @@ jobs: # on main blocks. An App token lets GitHub sign them as the App's bot. - name: Mint GitHub App token id: app-token - if: steps.refs.outputs.skip == 'false' && vars.BOT_APP_CLIENT_ID != '' + if: steps.refs.outputs.skip == 'false' && env.HAS_APP == 'true' uses: actions/create-github-app-token@v3 with: - client-id: ${{ vars.BOT_APP_CLIENT_ID }} - private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + client-id: ${{ secrets.TEMPLATE_SYNC_APP_CLIENT_ID }} + private-key: ${{ secrets.TEMPLATE_SYNC_PRIVATE_KEY }} - name: Create pull request if: steps.refs.outputs.skip == 'false' diff --git a/.github/workflows/version-bot.yml b/.github/workflows/version-bot.yml index 122cf1a..3ca7ec3 100644 --- a/.github/workflows/version-bot.yml +++ b/.github/workflows/version-bot.yml @@ -277,6 +277,12 @@ jobs: # meaningless for a doc-mode repo, which has no milestones. See # scripts/release-info.sh's .docmode / "mode" output above. if: needs.tag-version.outputs.milestone_transition == 'true' && needs.tag-version.outputs.mode == 'spec' + # `secrets` cannot be read from a step's `if:` -- GitHub's context + # availability table allows it in `env`, `with` and `run`, but not in + # `jobs..steps.if`. It IS allowed in `jobs..env`, so hoist the + # "is an App configured?" test to the job and let the gate read `env`. + env: + HAS_APP: ${{ secrets.TEMPLATE_SYNC_APP_CLIENT_ID != '' }} steps: - name: Checkout repository @@ -295,11 +301,11 @@ jobs: # Signed commits -- see the matching step in template-sync.yml. - name: Mint GitHub App token id: app-token - if: vars.BOT_APP_CLIENT_ID != '' + if: env.HAS_APP == 'true' uses: actions/create-github-app-token@v3 with: - client-id: ${{ vars.BOT_APP_CLIENT_ID }} - private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + client-id: ${{ secrets.TEMPLATE_SYNC_APP_CLIENT_ID }} + private-key: ${{ secrets.TEMPLATE_SYNC_PRIVATE_KEY }} - name: Create milestone review PR uses: peter-evans/create-pull-request@v8 diff --git a/CHANGELOG.md b/CHANGELOG.md index d27d3ac..37a302a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (site version stamp PR) pushed with `GHTOKEN`, a personal access token, which `create-pull-request` cannot sign, so under the `riscv` organization's ruleset every bot PR was blocked even with DCO passing. When a GitHub App is configured - (`BOT_APP_CLIENT_ID` variable, `BOT_APP_PRIVATE_KEY` secret), each workflow + (`TEMPLATE_SYNC_APP_CLIENT_ID` and `TEMPLATE_SYNC_PRIVATE_KEY` secrets), each workflow now mints its token and passes `sign-commits: true`; otherwise it falls back to `GHTOKEN` as before. See README.adoc, "Signed commits: use a GitHub App". - Keep the version stamp on the release tag when `main` refuses the bot's diff --git a/MIGRATION.md b/MIGRATION.md index c6948b4..51d3f7a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -685,8 +685,9 @@ earlier. `GITHUB_TOKEN` cannot push regardless of the *Allow GitHub Actions to create and approve pull requests* setting. - [ ] If `main` requires signed commits (the `riscv` organization's ruleset - does), confirm the `BOT_APP_CLIENT_ID` variable and `BOT_APP_PRIVATE_KEY` - secret are visible and the App is installed on the repository. Without + does), confirm the `TEMPLATE_SYNC_APP_CLIENT_ID` and + `TEMPLATE_SYNC_PRIVATE_KEY` secrets are visible and the App is installed + on the repository. Without them the sync commit is pushed with `GHTOKEN` unsigned, and the PR is blocked until someone re-signs it. See README.adoc, *Signed commits: use a GitHub App*. diff --git a/README.adoc b/README.adoc index 6d9ffbb..fdbc229 100644 --- a/README.adoc +++ b/README.adoc @@ -264,14 +264,19 @@ To have GitHub sign them, configure a GitHub App. The three workflows that open PRs mint a token for it per run with `actions/create-github-app-token` and pass `sign-commits: true`; when it is not configured they fall back to `GHTOKEN`. -* *Variable* `BOT_APP_CLIENT_ID`: the App's client ID. -* *Secret* `BOT_APP_PRIVATE_KEY`: a private key generated for the App. +* *Secret* `TEMPLATE_SYNC_APP_CLIENT_ID`: the App's client ID. +* *Secret* `TEMPLATE_SYNC_PRIVATE_KEY`: a private key generated for the App. * App permissions: `Contents`, `Pull requests` and `Workflows`, all read and write. Install it on each repository. Both can be set at the organization level. Commits are then authored by the App's bot, which the DCO check skips. An App is also not tied to one person's account, and its tokens don't need rotating by hand. +The client ID is not sensitive, but it is stored as a secret rather than a +variable so that both values live in one place. Because `secrets` cannot be +read from a step's `if:`, each job hoists the "is an App configured?" test into +a job-level `env` (`HAS_APP`) that the gate reads instead. + === Manual builds `workflow_dispatch` for `.github/workflows/build-pdf.yml` is a *preview* build: it