From 80b293c8caa78bb38f013c2037c671feaee8b977 Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Mon, 19 Oct 2026 15:17:17 -0500 Subject: [PATCH 1/2] ci(docs-dispatch): authenticate as the dispatchbot GitHub App The DOCS_DISPATCH_TOKEN PAT is retired. Each dispatch now mints a short-lived installation token from the DISPATCHBOT_APP_ID / DISPATCHBOT_PRIVATE_KEY org secrets, scoped to its one target repo, gated on the same condition as the dispatch it feeds. Change detection is unchanged. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs-dispatch.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs-dispatch.yml b/.github/workflows/docs-dispatch.yml index 05cabae..4f8acdb 100644 --- a/.github/workflows/docs-dispatch.yml +++ b/.github/workflows/docs-dispatch.yml @@ -8,8 +8,11 @@ name: Docs dispatch # tango-rust is a `coloc-source` repo: its docs/ folder is the authoritative # source for the Rust SDK pages on docs.makegov.com. # -# Required secrets: -# DOCS_DISPATCH_TOKEN — GitHub token with contents:write on makegov/docs. +# Required secrets — both are ORG secrets with `visibility: selected`, so this repo has to be on their selected list or every dispatch fails auth: +# DISPATCHBOT_APP_ID — the makegov-dispatchbot App. +# DISPATCHBOT_PRIVATE_KEY — its private key. +# +# The App mints a short-lived installation token scoped to makegov/docs, the one repo this dispatches to. # # Required variables (optional): # DOCS_TARGET_REPO — override the dispatch target (default: makegov/docs). @@ -67,10 +70,21 @@ jobs: echo "has_external=$has_external" } >> "$GITHUB_OUTPUT" + - name: Mint an installation token for the docs composer + if: steps.changes.outputs.has_external == 'true' + id: docs_composer + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.DISPATCHBOT_APP_ID }} + private-key: ${{ secrets.DISPATCHBOT_PRIVATE_KEY }} + owner: makegov + # Scoped to the single repo this step dispatches to, so a leaked token can't reach the rest of the org. A repository_dispatch needs Contents:write on the receiving repo. + repositories: docs + - name: Dispatch to docs composer (makegov/docs) if: steps.changes.outputs.has_external == 'true' env: - GH_TOKEN: ${{ secrets.DOCS_DISPATCH_TOKEN }} + GH_TOKEN: ${{ steps.docs_composer.outputs.token }} TARGET: ${{ vars.DOCS_TARGET_REPO || 'makegov/docs' }} SOURCE_REPO: ${{ github.repository }} SOURCE_REF: ${{ github.sha }} From b812e8c089507031c1157620a15b46daa3ebc5ce Mon Sep 17 00:00:00 2001 From: "V. David Zvenyach" Date: Mon, 19 Oct 2026 15:21:04 -0500 Subject: [PATCH 2/2] ci(docs-dispatch): scope the App token to the resolved dispatch target The token was hardcoded to the default composer repo, so a target-repo variable override would dispatch with a token that cannot reach it. A resolve step now derives owner and repo from the same target the dispatch uses. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs-dispatch.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs-dispatch.yml b/.github/workflows/docs-dispatch.yml index 4f8acdb..56caccf 100644 --- a/.github/workflows/docs-dispatch.yml +++ b/.github/workflows/docs-dispatch.yml @@ -12,7 +12,7 @@ name: Docs dispatch # DISPATCHBOT_APP_ID — the makegov-dispatchbot App. # DISPATCHBOT_PRIVATE_KEY — its private key. # -# The App mints a short-lived installation token scoped to makegov/docs, the one repo this dispatches to. +# The App mints a short-lived installation token scoped to the one repo this dispatches to (makegov/docs unless DOCS_TARGET_REPO overrides it). # # Required variables (optional): # DOCS_TARGET_REPO — override the dispatch target (default: makegov/docs). @@ -70,6 +70,24 @@ jobs: echo "has_external=$has_external" } >> "$GITHUB_OUTPUT" + - name: Resolve the docs composer target + if: steps.changes.outputs.has_external == 'true' + id: docs_target + env: + TARGET: ${{ vars.DOCS_TARGET_REPO || 'makegov/docs' }} + run: | + set -euo pipefail + # The token below is scoped to whatever repo the dispatch hits, so a `DOCS_TARGET_REPO` override still authenticates. + if ! [[ "$TARGET" =~ ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$ ]]; then + echo "::error::DOCS_TARGET_REPO must be owner/repo, got '$TARGET'" + exit 1 + fi + { + echo "target=$TARGET" + echo "owner=${TARGET%%/*}" + echo "repo=${TARGET#*/}" + } >> "$GITHUB_OUTPUT" + - name: Mint an installation token for the docs composer if: steps.changes.outputs.has_external == 'true' id: docs_composer @@ -77,15 +95,15 @@ jobs: with: app-id: ${{ secrets.DISPATCHBOT_APP_ID }} private-key: ${{ secrets.DISPATCHBOT_PRIVATE_KEY }} - owner: makegov + owner: ${{ steps.docs_target.outputs.owner }} # Scoped to the single repo this step dispatches to, so a leaked token can't reach the rest of the org. A repository_dispatch needs Contents:write on the receiving repo. - repositories: docs + repositories: ${{ steps.docs_target.outputs.repo }} - name: Dispatch to docs composer (makegov/docs) if: steps.changes.outputs.has_external == 'true' env: GH_TOKEN: ${{ steps.docs_composer.outputs.token }} - TARGET: ${{ vars.DOCS_TARGET_REPO || 'makegov/docs' }} + TARGET: ${{ steps.docs_target.outputs.target }} SOURCE_REPO: ${{ github.repository }} SOURCE_REF: ${{ github.sha }} CHANGED_PATHS: ${{ steps.changes.outputs.external }}