8000 push_repo_memory broken on signed-commit rulesets: ls-remote missing gitAuthEnv (regression from #31478) · Issue #33084 · github/gh-aw · GitHub
[go: up one dir, main page]

Skip to content

push_repo_memory broken on signed-commit rulesets: ls-remote missing gitAuthEnv (regression from #31478) #33084

Closed

Description

@mason-tim

Summary

actions/setup/js/push_signed_commits.cjs makes a network-touching git ls-remote origin call (line 363) without passing the gitAuthEnv credentials the same function already accepts and uses for its sibling git push call (line 140). Since gh aw fix's codemod started rewriting every actions/checkout to persist-credentials: false (PR #31478, merged 2026-05-11), .git/config no longer contains an http.extraheader token. The ls-remote call therefore runs without any credentials, git prompts for a username, fails with fatal: could not read Username for 'https://github.com', and the catch handler falls back to a plain unsigned git push — which is rejected by Require signed commits rulesets with GH013.

Net result: every push_repo_memory run is broken on any repository with a "Require signed commits" ruleset, regardless of whether the target memory branch already exists. The previously-suggested "manually seed the orphan branch" workaround does not help, because the failure happens before any branch-existence check matters.

This is the same root cause noted in #31489. Filing as a dedicated issue with a concrete one-line fix so it can be tracked and merged independently of the orphan-branch discussion.

Symptoms

In the push_repo_memory job log, four failed retry cycles each show the same two lines:

fatal: could not read Username for 'https://github.com': No such device or address
...
remote: error: GH013: Repository rule violations found for refs/heads/memory/<id>.
 ! [remote rejected] memory/<id> -> memory/<id> (push declined due to repository rule violations)
##[error]Failed to push changes after 4 attempts: The process '/usr/bin/git' failed with exit code 1

The first line is the real failure (the ls-remote auth prompt). The second is a downstream consequence: when ls-remote fails, the catch handler falls through to a plain unsigned git push (the fallback path), and the ruleset correctly rejects that.

Root Cause

actions/setup/js/push_signed_commits.cjs has two network-touching git calls:

Line 137-141 — pushBranchAndResolveHead for the fallback git push:

async function pushBranchAndResolveHead({ branch, cwd, gitAuthEnv }) {
  ...
  await exec.getExecOutput("git", ["push", ...], {
    cwd,
    env: { ...process.env, ...(gitAuthEnv || {}) },   // ← auth provided
  });

Line 363 — ls-remote to read the remote branch HEAD OID:

const { stdout: oidOut } = await exec.getExecOutput(
  "git",
  ["ls-remote", "origin", `refs/heads/${branch}`],
  { cwd },                                            // ← no auth provided
);

Every other git call in the file is a local operation (rev-parse, rev-list, diff-tree, log) and correctly doesn't need credentials. Line 363 is the only network call without auth — a clear inconsistency with line 140.

The caller (actions/setup/js/push_repo_memory.cjs) constructs gitAuthEnv via getGitAuthEnv(token) in git_helpers.cjs, which produces three env vars:

{
  GIT_CONFIG_COUNT: "1",
  GIT_CONFIG_KEY_0: `http.${serverUrl}/.extraheader`,
  GIT_CONFIG_VALUE_0: `Authorization: basic <base64 of x-access-token:TOKEN>`,
}

When passed to a child git process, these are interpreted as ephemeral config — the equivalent of the http.extraheader line actions/checkout used to write into .git/config, but scoped to a single process instead of persisted to disk. Line 140 already proves this channel works.

Regression Timeline

  • 2026-04-30 — PR fix: repo-memory push uses GraphQL signed commits to satisfy "Require signed commits" rulesets #29330 ("fix: repo-memory push uses GraphQL signed commits to satisfy 'Require signed commits' rulesets") introduces push_signed_commits.cjs and adds the in-code comment in push_repo_memory.cjs:

    pushSignedCommits authenticates via the git extraheader set by actions/checkout (and the gitAuthEnv fallback for the git-push path).

    So the design explicitly relied on actions/checkout's default behaviour of writing the extraheader to .git/config. The ls-remote call inherited that auth implicitly. Working as intended at the time.

  • 2026-05-11 11:41 UTC — PR Add codemod to enforce persist-credentials: false on actions/checkout steps #31478 ("Add codemod to enforce persist-credentials: false on actions/checkout steps") merges. The codemod mechanically rewrites every actions/checkout step in compiled workflows to persist-credentials: false — the explicit toggle that stops checkout from writing the extraheader. The premise fix: repo-memory push uses GraphQL signed commits to satisfy "Require signed commits" rulesets #29330's design rested on is silently removed.

  • 2026-05-11 onward — anyone who recompiles a workflow picks up the codemod. Their push_repo_memory job's checkout no longer leaves an extraheader in .git/config. The ls-remote call at line 363, having no gitAuthEnv, has no auth source at all → fatal: could not read Username → catch handler → unsigned push → GH013.

So a true regression: #29330 alone was fine, #31478 alone is fine, but the pair together breaks push_repo_memory for any repo with a signed-commits ruleset.

Reproduction

  1. Use any workflow with safe-outputs.repo-memory: configured.
  2. Compile with a gh aw version that includes the persist-credentials: false codemod from Add codemod to enforce persist-credentials: false on actions/checkout steps #31478 (any release after 2026-05-11).
  3. Target a repository that has a ruleset requiring signed commits on the memory branch glob (e.g. memory/*).
  4. Trigger the workflow so the push_repo_memory job runs.
  5. Observe the Failed to push changes after 4 attempts failure, with the could not read Username line as the first symptom in each retry.

Pre-seeding the memory branch (e.g. creating memory/<id> via the GitHub web UI to bypass the orphan-first-commit signing problem) does NOT mask this bug — the ls-remote failure happens regardless of whether the branch exists.

Proposed Fix (one line)

actions/setup/js/push_signed_commits.cjs, line 363 — change:

const { stdout: oidOut } = await exec.getExecOutput(
  "git",
  ["ls-remote", "origin", `refs/heads/${branch}`],
  { cwd },
);

to:

const { stdout: oidOut } = await exec.getExecOutput(
  "git",
  ["ls-remote", "origin", `refs/heads/${branch}`],
  { cwd, env: { ...process.env, ...(gitAuthEnv || {}) } },
);

This is structurally identical to what pushBranchAndResolveHead does at line 140 (same function family, same file). No new auth mechanism, no change to the security posture of persist-credentials: false — gitAuthEnv is per-process ephemeral config, not persisted to disk.

Why This Is a Fix, Not a Workaround

Test Coverage to Add

actions/setup/js/push_signed_commits.test.cjs:

  • New test: GraphQL signed-commit path succeeds when invoked from a checkout configured with persist-credentials: false. Mock actions/checkout env state (no extraheader in .git/config), provide gitAuthEnv to pushSignedCommits, mock exec.getExecOutput to assert that the ls-remote origin call receives an env argument containing GIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0.

  • Regression guard: a parameterised test that walks every network-touching exec.getExecOutput("git", …) call in the file and asserts each is invoked with a gitAuthEnv-merged env, so a future contributor can't reintroduce the same omission for another network call.

Related

Activity

  1. pelikhan commented on May 18, 2026

    @pelikhan
    Collaborator

    @mason-tim which version of the compiler?

  2. locked and limited conversation to collaborators on May 18, 2026
  3. unlocked this conversation on May 18, 2026
  4. mason-tim commented on May 18, 2026

    @mason-tim
    Author

    @mason-tim which version of the compiler?

    gh aw compiler is v0.71.6

  5. locked and limited conversation to collaborators on May 18, 2026
  6. unlocked this conversation on May 18, 2026
  7. mason-tim commented on May 18, 2026

    @mason-tim
    Author

    Re-confirmed on the latest release v0.74.4 (published ~21 hours ago).

    Source unchanged: actions/setup/js/push_signed_commits.cjs:363 still calls git ls-remote origin with no env: gitAuthEnv.

    Reproduced in a workflow whose target repo has a "Require signed commits" ruleset and uses actions/checkout with persist-credentials: false (the default emitted by the gh-aw compiler since #31478). Worth noting this is not the orphan-first-commit case from #31489 — the memory/<workflow> branch already exists and is checked out cleanly:

    Checking out branch: memory/<workflow>...
    Switched to branch 'memory/<workflow>'
    Checked out existing branch: memory/<workflow>
    Captured baseRef for signed commit push: ef28218…
    ...
    [memory/<workflow> 97aee21] Update repo memory from workflow run …
     1 file changed, 26 insertions(+)
    

    The local commit is created against a valid parent. The GraphQL signed-push path then begins normally and runs ls-remote to confirm the remote head — that is where it fails:

    pushSignedCommits: processing commit 1/1 sha=97aee21…
    /usr/bin/git ls-remote origin refs/heads/memory/<workflow>
    fatal: could not read Username for 'https://github.com': No such device or address
    Warning: pushSignedCommits: GraphQL signed push failed, falling back to git push: ...
    /usr/bin/git push origin memory/<workflow>
    remote: error: GH013: Repository rule violations found for refs/heads/memory/<workflow>.
    remote:   Commits must have verified signatures.
    

    Because ls-remote failed with an auth error rather than a "missing ref" error, the GraphQL path bails to an unsigned git push, which the signed-commits ruleset correctly rejects.

    We believe adding env: { ...process.env, ...(gitAuthEnv || {}) } to the ls-remote call (mirroring the sibling git push invocation on line 140) would resolve it. Happy to help confirm this locally or submit a PR if that proves correct.

  8. locked and limited conversation to collaborators on May 18, 2026
  9. unlocked this conversation on May 18, 2026
  10. pelikhan commented on May 18, 2026

    @pelikhan
    Collaborator

    /scout summarize this issue

  11. github-actions commented on May 18, 2026

    @github-actions
    Contributor

    🏕️ Lost in the wilderness! Scout failed to deliver outputs. Sending search party...

  12. github-actions commented on May 18, 2026

    @github-actions
    Contributor

    🔍 Scout Research Report

    Triggered by @pelikhan

    Executive Summary

    push_repo_memory is broken on any repository with a Require signed commits ruleset, due to a regression introduced by the interaction of two previously-fine PRs:

    Result: ls-remote runs with no auth → fatal: could not read Username for 'https://github.com' → catch handler falls through to a plain unsigned git push → ruleset rejects it with GH013. Every run fails; pre-seeding the memory branch does not help.

    Verified Against the Code

    • actions/setup/js/push_signed_commits.cjs:140 — the git push call correctly passes env: { ...process.env, ...(gitAuthEnv || {}) }.
    • actions/setup/js/push_signed_commits.cjs:363 — the git ls-remote origin call passes only { cwd }, with no env merging. Confirmed as the only network-touching git call in the file that omits gitAuthEnv.
    • PR Add codemod to enforce persist-credentials: false on actions/checkout steps #31478 merge timestamp confirmed: 2026-05-11T11:41:17Z (matches the regression window in the issue).

    Proposed One-Line Fix

    Change push_signed_commits.cjs:363 from:

    ["ls-remote", "origin", `refs/heads/${branch}`], { cwd }

    to:

    ["ls-remote", "origin", `refs/heads/${branch}`], { cwd, env: { ...process.env, ...(gitAuthEnv || {}) } }

    This mirrors the existing pushBranchAndResolveHead pattern at line 140 — same file, same function family, same credential mechanism. gitAuthEnv is ephemeral per-process config (env-var-based GIT_CONFIG_*), so it does not re-introduce the disk-persistent extraheader that #31478 deliberately removed. The persist-credentials: false posture is preserved.

    Recommended test coverage

    In actions/setup/js/push_signed_commits.test.cjs:

    1. Positive case — mock the persist-credentials: false state (no extraheader in .git/config), supply gitAuthEnv to pushSignedCommits, and assert the mocked exec.getExecOutput("git", ["ls-remote", ...], opts) is invoked with an opts.env containing GIT_CONFIG_COUNT / GIT_CONFIG_KEY_0 / GIT_CONFIG_VALUE_0.
    2. Regression guard — parameterised assertion that walks every exec.getExecOutput("git", ...) call known to be network-touching (push, ls-remote, future fetches) and requires each to receive the merged env. This stops a future contributor reintroducing the same omission for a different network call.

    Recommendations

    Suggested Next Steps

    1. Open a PR with the one-line change to actions/setup/js/push_signed_commits.cjs:363.
    2. Add the targeted unit test and the regression-guard test described above.
    3. Verify end-to-end against a repo with a Require signed commits ruleset on memory/* (the issue's reproduction recipe is concrete).
    4. Once merged, this issue and the relevant portion of Regression of #29301: orphan-branch first commit pushes unsigned, fails on "Require signed commits" rulesets #31489 (the per-run ls-remote failure mode) can be closed; the orphan-first-commit discussion in Regression of #29301: orphan-branch first commit pushes unsigned, fails on "Require signed commits" rulesets #31489 remains separate.

    🔭 Intelligence gathered by Scout · ● 4.8M · ◷

  13. pelikhan commented on May 20, 2026

    @pelikhan
    Collaborator

    I will turn on signing on our memory branches today and look go through this.

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

    0