diff --git a/.github/workflows/_selftest.yml b/.github/workflows/_selftest.yml index e9c9bd8..c10c6df 100644 --- a/.github/workflows/_selftest.yml +++ b/.github/workflows/_selftest.yml @@ -1487,6 +1487,34 @@ jobs: fi echo "claude.yml's SENSITIVE_PATHS mirror matches claude-code-action@${sha}." + # gha#442: SUBMODULES_TOKEN authenticates a cross-owner SUBMODULE fetch + # (via the checkout-submodules composite's `submodules-token:` input) and + # must never gate the caller's own repo checkout --- a consumer that sets + # it correctly for its own submodule is exactly the consumer whose main + # checkout then fails, since the token has no reason to be able to read the + # caller's repo. The anchor is `^\s*token:`, not a bare `token:` substring + # match, so this does not also flag `submodules-token:` (which legitimately + # carries the secret) --- a trimmed line starting with "submodules-token:" + # never matches a pattern requiring "token:" immediately after the leading + # whitespace. + lint-checkout-tokens: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Assert no workflow passes SUBMODULES_TOKEN to a top-level checkout's token input + shell: bash + run: | + set -euo pipefail + if grep -rnE '^\s*token:\s*\$\{\{[^}]*SUBMODULES_TOKEN' .github/workflows/*.yml; then + echo "::error::A workflow above passes SUBMODULES_TOKEN to a top-level actions/checkout 'token:' input (see gha#442) --- it authenticates a cross-owner submodule fetch, not the caller's own repo. Use the checkout-submodules composite's 'submodules-token:' input instead." + exit 1 + fi + echo "No workflow passes SUBMODULES_TOKEN to a top-level checkout token: input." + # Covers the open-failure-issue action. report-failure.yml is its only # caller so far; check-links.yml's migration onto it is deferred to gha#327, # since a @v2 reference to a not-yet-released action fails at job-prepare diff --git a/.github/workflows/antigravity-code-review.yml b/.github/workflows/antigravity-code-review.yml index 605034b..561996c 100644 --- a/.github/workflows/antigravity-code-review.yml +++ b/.github/workflows/antigravity-code-review.yml @@ -102,13 +102,24 @@ jobs: echo "blocked=false" >> "$GITHUB_OUTPUT" fi + # The caller's own repo is checked out with the runner's default + # github.token, never SUBMODULES_TOKEN --- that secret authenticates a + # CROSS-OWNER submodule fetch and has no reason to (and, when a + # consumer sets it correctly for its own submodule, frequently cannot) + # read the caller's own repo (gha#442). Submodule auth is handled below + # by the shared checkout-submodules composite, the same one + # claude-code-review.yml already uses. - name: Checkout repository if: success() && steps.dispatch-guard.outputs.blocked != 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - submodules: ${{ inputs.checkout-submodules }} - token: ${{ secrets.SUBMODULES_TOKEN || github.token }} + + - name: Checkout submodules + if: success() && steps.dispatch-guard.outputs.blocked != 'true' && inputs.checkout-submodules + uses: Morrison-Lab/gha/.github/actions/checkout-submodules@v2 + with: + submodules-token: ${{ secrets.SUBMODULES_TOKEN }} - name: Run Antigravity Action if: success() && steps.dispatch-guard.outputs.blocked != 'true' diff --git a/.github/workflows/gemini-code-review.yml b/.github/workflows/gemini-code-review.yml index a663dce..b28f949 100644 --- a/.github/workflows/gemini-code-review.yml +++ b/.github/workflows/gemini-code-review.yml @@ -162,12 +162,22 @@ jobs: # the guard step is skipped entirely, so its output is the empty string; # there the job `if:` has already applied the equivalent checks from the # event payload. + # The caller's own repo is checked out with the runner's default + # github.token, never SUBMODULES_TOKEN --- that secret authenticates a + # CROSS-OWNER submodule fetch and has no reason to (and, when a + # consumer sets it correctly for its own submodule, frequently cannot) + # read the caller's own repo (gha#442). Submodule auth is handled below + # by the shared checkout-submodules composite, the same one + # claude-code-review.yml already uses. - name: Checkout repository if: success() && steps.dispatch-guard.outputs.blocked != 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Checkout submodules + if: success() && steps.dispatch-guard.outputs.blocked != 'true' && inputs.checkout-submodules + uses: Morrison-Lab/gha/.github/actions/checkout-submodules@v2 with: - submodules: ${{ inputs.checkout-submodules }} - token: ${{ secrets.SUBMODULES_TOKEN || github.token }} + submodules-token: ${{ secrets.SUBMODULES_TOKEN }} # Pinned to a release SHA per README's "Pinning third-party actions". # run-gemini-cli publishes v0 / v0.1.x and has no v1 tag, so a `@v1` ref diff --git a/.github/workflows/gemini.yml b/.github/workflows/gemini.yml index 7109c5a..e75eeec 100644 --- a/.github/workflows/gemini.yml +++ b/.github/workflows/gemini.yml @@ -278,12 +278,22 @@ jobs: echo "thread_line=This is an unattended run in $REPO with no associated issue or pull request, so there is no thread to read or reply to." >> "$GITHUB_OUTPUT" fi + # The caller's own repo is checked out with the runner's default + # github.token, never SUBMODULES_TOKEN --- that secret authenticates a + # CROSS-OWNER submodule fetch and has no reason to (and, when a + # consumer sets it correctly for its own submodule, frequently cannot) + # read the caller's own repo (gha#442). Submodule auth is handled below + # by the shared checkout-submodules composite, the same one claude.yml + # already uses. - name: Checkout repository if: steps.gate.outputs.skip != 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Checkout submodules + if: steps.gate.outputs.skip != 'true' && inputs.checkout-submodules + uses: Morrison-Lab/gha/.github/actions/checkout-submodules@v2 with: - submodules: ${{ inputs.checkout-submodules }} - token: ${{ secrets.SUBMODULES_TOKEN || github.token }} + submodules-token: ${{ secrets.SUBMODULES_TOKEN }} # Passed via env rather than inlined into the script, so a caller's value # is a shell word list and never interpreted as script. diff --git a/changelog.d/checkout-token-conflation-cross-owner-submodule.fixed.md b/changelog.d/checkout-token-conflation-cross-owner-submodule.fixed.md new file mode 100644 index 0000000..552ed5d --- /dev/null +++ b/changelog.d/checkout-token-conflation-cross-owner-submodule.fixed.md @@ -0,0 +1,22 @@ +- **`antigravity-code-review.yml`, `gemini-code-review.yml`, and `gemini.yml` + no longer fail to check out the CALLER's own repo when `SUBMODULES_TOKEN` + is set for a cross-owner submodule** (#442). + All three passed + `${{ secrets.SUBMODULES_TOKEN || github.token }}` as the top-level + `actions/checkout` `token:` input --- the same input `claude-code-review.yml` + leaves at the runner's default `github.token`. + `SUBMODULES_TOKEN` exists + precisely because a submodule lives under a different owner than the + consumer, so a token scoped to read the submodule has no reason to be able + to read the caller's own repo, and in `ucdavis/bcs` it could not: the main + checkout failed with `fatal: could not read Username for + 'https://github.com': terminal prompts disabled` on every run, so a repo + that configured `SUBMODULES_TOKEN` correctly for its submodule got a red + check and zero automated review from two of its three configured agents. + All three now check out the caller with the runner's own token and + authenticate the submodule fetch (when `checkout-submodules` is set) + through the shared `checkout-submodules` composite action, matching + `claude-code-review.yml`. + A new `lint-checkout-tokens` selftest job asserts + no workflow in this repo passes `SUBMODULES_TOKEN` to a top-level + `actions/checkout` `token:` input, so the conflation can't recur silently.