diff --git a/.github/workflows/build-pdf.yml b/.github/workflows/build-pdf.yml index b7aadef..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 @@ -321,11 +327,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' && env.HAS_APP == 'true' + uses: actions/create-github-app-token@v3 + with: + 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' 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..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 @@ -42,7 +48,9 @@ jobs: GHTOKEN: ${{ secrets.GHTOKEN }} run: | set -euo pipefail - if [[ -z "$GHTOKEN" ]]; then + if [[ "$HAS_APP" == "true" ]]; then + 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 # from pushing any change under .github/workflows/, which this @@ -50,6 +58,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 (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 @@ -212,11 +222,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' && env.HAS_APP == 'true' + uses: actions/create-github-app-token@v3 + with: + 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' 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 359759f..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 @@ -292,10 +298,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: env.HAS_APP == 'true' + uses: actions/create-github-app-token@v3 + with: + 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 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 bfb8bde..37a302a 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 + (`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 push. The `riscv` organization ruleset requires signed commits and a passing DCO check on the default branch, so `version-bot.yml`'s pre-tag push of the diff --git a/MIGRATION.md b/MIGRATION.md index 3a96e9c..51d3f7a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -684,6 +684,13 @@ 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 `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*. - [ ] 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..fdbc229 100644 --- a/README.adoc +++ b/README.adoc @@ -249,10 +249,33 @@ 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`. + +* *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