From 919ad6a7e0a96e10cc94ca17562817ca2c14cfe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Guilhermo=20de=20Barros=20Val=C3=A9rio?= Date: Wed, 14 Oct 2026 23:57:17 +0200 Subject: [PATCH] bot-token: set the git identity with env vars, not GIT_CONFIG_GLOBAL GIT_CONFIG_GLOBAL captured actions/checkout's temporary global credential include, so a later submodule fetch sent Authorization twice. The action now exports GIT_AUTHOR_* and GIT_COMMITTER_* to GITHUB_ENV. mints-scoped checks a real commit; a new job checks out marola with submodules and fetches each. Closes #121 Tested: devkit-quality.sh rc=0; actionlint on bot-token-test.yml clean; local repro of the include.path write and commit identity; live jobs run in CI only Co-Authored-By: Claude --- .github/actions/bot-token/action.yml | 12 ++++----- .github/workflows/bot-token-test.yml | 39 +++++++++++++++++++++++++--- CHANGELOG.md | 6 +++++ docs/4-reference_workflows.md | 4 ++- 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/.github/actions/bot-token/action.yml b/.github/actions/bot-token/action.yml index bd2d115..ed468cc 100644 --- a/.github/actions/bot-token/action.yml +++ b/.github/actions/bot-token/action.yml @@ -67,7 +67,8 @@ runs: permission-statuses: ${{ inputs.permission-statuses }} permission-issues: ${{ inputs.permission-issues }} - # GIT_CONFIG_GLOBAL, not `git config --global`, which would outlive the job on a self-hosted runner. + # Identity through env vars: GIT_CONFIG_GLOBAL captured actions/checkout's temporary global + # include and doubled Authorization on submodule fetches (#121). # The commit email needs the bot user's id, which is not the App id (create-github-app-token's README). - id: who shell: bash @@ -78,9 +79,8 @@ runs: set -euo pipefail uid="$(gh api "users/${SLUG}%5Bbot%5D" --jq .id)" name="${SLUG}[bot]" email="${uid}+${SLUG}[bot]@users.noreply.github.com" - cfg="$RUNNER_TEMP/bot-token.gitconfig" - if [ -f ~/.gitconfig ]; then cp ~/.gitconfig "$cfg"; else : >"$cfg"; fi - git config --file "$cfg" user.name "$name" - git config --file "$cfg" user.email "$email" - echo "GIT_CONFIG_GLOBAL=$cfg" >>"$GITHUB_ENV" + { + echo "GIT_AUTHOR_NAME=$name"; echo "GIT_AUTHOR_EMAIL=$email" + echo "GIT_COMMITTER_NAME=$name"; echo "GIT_COMMITTER_EMAIL=$email" + } >>"$GITHUB_ENV" { echo "name=$name"; echo "email=$email"; } >>"$GITHUB_OUTPUT" diff --git a/.github/workflows/bot-token-test.yml b/.github/workflows/bot-token-test.yml index c6784e4..391aa07 100644 --- a/.github/workflows/bot-token-test.yml +++ b/.github/workflows/bot-token-test.yml @@ -101,6 +101,39 @@ jobs: fi grep -Eq '403|denied' push-site.log || { echo "::error::push to marola-site failed, but not with 403/denied"; tail -n 5 push-site.log; exit 1; } echo "dry-run push to marola-site: refused ($(tail -n 1 push-site.log))" - [[ "$(git config --global user.email)" =~ ^[0-9]+\+${SLUG}\[bot\]@users\.noreply\.github\.com$ ]] || { echo "::error::git email is $(git config --global user.email)"; exit 1; } - [ "$(git config --global user.email)" = "$EMAIL" ] - [ "$(git config --global user.name)" = "${SLUG}[bot]" ] + git commit --allow-empty -qm probe + [[ "$EMAIL" =~ ^[0-9]+\+${SLUG}\[bot\]@users\.noreply\.github\.com$ ]] || { echo "::error::bot email is $EMAIL"; exit 1; } + got="$(git log -1 --format=%an/%ae/%cn/%ce)" + want="${SLUG}[bot]/${EMAIL}/${SLUG}[bot]/${EMAIL}" + [ "$got" = "$want" ] || { echo "::error::commit identity is $got, want $want"; exit 1; } + + checkout-submodules: + if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - id: bot + uses: ./.github/actions/bot-token + with: + app-id: ${{ vars.PR_BOT_APP_ID }} + private-key: ${{ secrets.PR_BOT_KEY }} + repositories: marola,marola-site,marola-corpus,marola-ml,marola-app,marola-oods,marola-skills,marola-agents,marola-research + permission-contents: read + - uses: actions/checkout@v7 + with: + repository: marola-dev/marola + submodules: true + token: ${{ steps.bot.outputs.token }} + path: umbrella + - name: no credential include persists in the global git config + run: | + if [ -n "${GIT_CONFIG_GLOBAL:-}" ] || git config --global --get include.path >/dev/null; then + echo "::error::a credential include persists in the global git config (#121)"; exit 1 + fi + - name: every submodule fetches + run: git -C umbrella submodule foreach --recursive git fetch -q origin diff --git a/CHANGELOG.md b/CHANGELOG.md index 10ef48b..6c10fb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Each release moves `plugin.json`'s `version`, the flake package version and ever together. A consumer adopts one by bumping its flake input, its workflow `@v…`/`devkit-ref` and its marketplace `ref` at once. +## v0.11.1 — 2026-10-15 + +- `bot-token` sets the bot's git identity with `GIT_AUTHOR_*`/`GIT_COMMITTER_*` variables instead of + `GIT_CONFIG_GLOBAL`, which captured `actions/checkout`'s temporary global credential include and made + a later submodule fetch send `Authorization` twice (400 Duplicate header) (#121). + ## v0.11.0 — 2026-10-14 - `bump-consumers` runs as `prepare` (nix, no key) and `publish` (marola-merge-bot, checked-in scripts diff --git a/docs/4-reference_workflows.md b/docs/4-reference_workflows.md index 8f2829f..b9555d0 100644 --- a/docs/4-reference_workflows.md +++ b/docs/4-reference_workflows.md @@ -429,7 +429,9 @@ newline-separated list of repo names; an empty list fails the step, because the otherwise cover every repo the App is installed on, and so does a call with no `permission-*` input set (`permission-contents`, `-pull-requests`, `-workflows`, `-checks`, `-statuses`, `-issues`). Outputs: `token`, `app-slug`, `installation-id`, `user-name` and `user-email`. The git identity is -job-scoped, through `GIT_CONFIG_GLOBAL` rather than `git config --global`. A job that pushes to +job-scoped, set through `GIT_AUTHOR_*` and `GIT_COMMITTER_*` variables in `GITHUB_ENV`, not git +config: they outrank a later `git config user.*` in the same job, and `actions/checkout` with +`submodules: true` keeps working (#121). A job that pushes to the repo it checked out needs `actions/checkout` with `persist-credentials: false`; otherwise checkout's `GITHUB_TOKEN` extraheader is used for the push and no CI starts. `release.py --consumer` moves its `@v…` pin with the other devkit pins.