Skip to content

Extra empty commit for CI triggering is pushed as github-actions[bot], so GH_AW_CI_TRIGGER_TOKEN / github-token-for-extra-empty-commit does not trigger CI #44073

Description

@sg650

Extra empty commit for CI triggering is pushed as github-actions[bot], so GH_AW_CI_TRIGGER_TOKEN / github-token-for-extra-empty-commit does not trigger CI

Summary

The "extra empty commit" CI-trigger mechanism — configured via the GH_AW_CI_TRIGGER_TOKEN magic secret or github-token-for-extra-empty-commit on create-pull-request / push-to-pull-request-branch — does not actually trigger CI in the default configuration. The empty commit is pushed authenticated as the persisted checkout credential (github-actions[bot] / the GITHUB_TOKEN) rather than the configured PAT/App, so per GitHub's event-cascade rule the resulting push / pull_request (synchronize) event does not start CI workflow runs.

  • Where the repo requires approval for github-actions[bot], the runs are created but stuck at conclusion: action_required ("N workflows awaiting approval") until a maintainer manually approves.
  • Otherwise no downstream runs are created at all.

The push logs Extra empty commit pushed ... successfully, so the failure is silent.

This contradicts docs/src/content/docs/reference/triggering-ci.mdx, which presents GH_AW_CI_TRIGGER_TOKEN as "the easiest way to fix this problem" and states the empty commit "will trigger push and pull_request events normally" — with no mention of the persisted-credential caveat.

Analysis (root cause)

  1. The safe_outputs job checks out with actions/checkout using persist-credentials: true and the checkout token ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} — which resolves to GITHUB_TOKEN (identity github-actions[bot]) unless a master-token override is configured. persist-credentials: true writes http.<serverUrl>/.extraheader (an Authorization: basic <checkout-token> header) into .git/config, which authenticates every request to <serverUrl>.

  2. actions/setup/js/extra_empty_commit.cjs authenticates the trigger push only by embedding the token in the remote URL:

    const remoteUrl = `https://x-access-token:${token}@${serverHostStripped}/${repoOwner}/${repoName}.git`;
    await exec.exec("git", ["remote", "add", "ci-trigger", remoteUrl]);
    // ...
    await exec.exec("git", ["push", "ci-trigger", branchName]);

    It never removes or overrides the persisted http.<serverUrl>/.extraheader. Because the push targets the same host, git still applies that persisted extraheader; its Authorization header (the checkout token) takes precedence over the URL-embedded credential, so the push authenticates as github-actions[bot] and the PAT is effectively ignored.

  3. The codebase already documents and handles this exact hazard in actions/setup/js/dynamic_checkout.cjs via checkoutHasPersistedExtraheader(), noting: "git treats the key as multi-valued and would send two Authorization headers, which GitHub rejects with 'Duplicate header: Authorization'". extra_empty_commit.cjs lacks the equivalent handling, so it silently pushes with the wrong identity instead.

How to verify

For a PR created by such a workflow, inspect the workflow run(s) for the empty-commit head SHA:

GET /repos/{owner}/{repo}/actions/runs?head_sha={empty_commit_sha}

Observed: actor and triggering_actor are github-actions[bot] (not the PAT/App user), and the github-actions check-suite for that SHA has conclusion: action_required. For contrast, a PR opened in the same repo by a human, or by a bot App that pushes under its own identity (e.g. a dependency-update bot), runs CI without the gate — confirming the deciding factor is the pushing identity, not the repo's fork/approval policy.

Implementation Plan

  1. Fix actions/setup/js/extra_empty_commit.cjs so the trigger push authenticates as GH_AW_CI_TRIGGER_TOKEN, not the persisted checkout credential:

    • Before git push ci-trigger, override the persisted auth to a single value carrying the trigger token (using --replace-all so any existing/multi-valued header is collapsed to one, avoiding the duplicate-Authorization 400 described in dynamic_checkout.cjs):

      const serverUrl = process.env.GITHUB_SERVER_URL || "https://github.com";
      const tokenB64 = Buffer.from(`x-access-token:${token}`).toString("base64");
      await exec.exec("git", [
        "config", "--replace-all",
        `http.${serverUrl}/.extraheader`,
        `Authorization: basic ${tokenB64}`,
      ]);
    • With the extraheader now carrying the trigger token, use a clean remote URL without embedded credentials (${serverUrl}/${repoOwner}/${repoName}.git) to avoid mixing credential sources.

    • Preserve the existing behavior: cycle-prevention check, cross-repo guard, newCommitCount === 1 guard, fetch/reset sync-to-remote, and temp-remote cleanup.

    • On cleanup (success and failure paths), restore or unset the extraheader override you added, so the trigger token does not leak into later steps of the job.

  2. Extract a shared helper (preferred, follows the focused-utilities pattern):

    • Move checkoutHasPersistedExtraheader() and add an overridePersistedExtraheader(serverUrl, token) helper into a shared module (e.g. new actions/setup/js/git_auth_helpers.cjs), and have both dynamic_checkout.cjs and extra_empty_commit.cjs consume it, so the extraheader handling lives in one place.
  3. Tests — actions/setup/js/extra_empty_commit.test.cjs:

    • New case: with GH_AW_CI_TRIGGER_TOKEN set, assert that a git config --replace-all http.<serverUrl>/.extraheader "Authorization: basic <base64('x-access-token:'+token)>" call is issued before git push ci-trigger, and that the push uses a credential-free remote URL. (Use the existing mockExec.exec call-sequence assertion style.)
    • Regression case: ensure the no-op/skip paths (no token, cross-repo target, newCommitCount !== 1, cycle limit reached) are unchanged.
  4. Docs — docs/src/content/docs/reference/triggering-ci.mdx:

    • Confirm the "will trigger push and pull_request events normally" claim now holds regardless of the checkout token.
    • Add a short troubleshooting note: to confirm the trigger worked, verify the empty-commit run's actor is the PAT/App identity (not github-actions[bot]).
  5. Changeset: add a patch changeset under .changeset/ describing the fix (extra-empty-commit trigger push now authenticates as the CI-trigger token even when the checkout persisted GITHUB_TOKEN credentials).

  6. Follow guidelines: run make agent-finish (build, test, recompile, format, lint) and make recompile so bundled action / lock outputs regenerate.

Suggested labels

bug, documentation

Environment

  • Component: actions/setup/js/extra_empty_commit.cjs (extra-empty-commit safe-output path)
  • Config: create-pull-request with the default persist-credentials: true checkout and GITHUB_TOKEN (no master github-token override)
  • Same-repo (non-fork) PR branch
  • Source referenced from main at time of filing

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions