Repository navigation
Description
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.cjsand adds the in-code comment inpush_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. Thels-remotecall inherited that auth implicitly. Working as intended at the time. -
2026-05-11 11:41 UTC — PR Add codemod to enforce
persist-credentials: falseonactions/checkoutsteps #31478 ("Add codemod to enforcepersist-credentials: falseonactions/checkoutsteps") merges. The codemod mechanically rewrites everyactions/checkoutstep in compiled workflows topersist-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_memoryjob's checkout no longer leaves an extraheader in.git/config. Thels-remotecall at line 363, having nogitAuthEnv, 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
- Use any workflow with
safe-outputs.repo-memory:configured. - Compile with a
gh awversion that includes thepersist-credentials: falsecodemod from Add codemod to enforcepersist-credentials: falseonactions/checkoutsteps #31478 (any release after 2026-05-11). - Target a repository that has a ruleset requiring signed commits on the memory branch glob (e.g.
memory/*). - Trigger the workflow so the
push_repo_memoryjob runs. - Observe the
Failed to push changes after 4 attemptsfailure, with thecould not read Usernameline 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
- It restores the design intent of fix: repo-memory push uses GraphQL signed commits to satisfy "Require signed commits" rulesets #29330 (the in-code comment in
push_repo_memory.cjsis now accurate again). - It uses the credential mechanism the rest of the file already uses successfully.
- It does not re-introduce the disk-persistent extraheader that Add codemod to enforce
persist-credentials: falseonactions/checkoutsteps #31478 deliberately removed. - It does not weaken
persist-credentials: false— the env-var credential lives only for the singlegit ls-remotechild process, not in.git/config. - It fixes the root failure (
ls-remoteauth) so the downstream GraphQL signed-commit path that fix: repo-memory push uses GraphQL signed commits to satisfy "Require signed commits" rulesets #29330 set up can actually run, instead of always falling through to the unsigned-push fallback that rulesets reject.
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. Mockactions/checkoutenv state (no extraheader in.git/config), providegitAuthEnvtopushSignedCommits, mockexec.getExecOutputto assert that thels-remote origincall receives anenvargument containingGIT_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 agitAuthEnv-mergedenv, so a future contributor can't reintroduce the same omission for another network call.
Related
- Regression of #29301: orphan-branch first commit pushes unsigned, fails on "Require signed commits" rulesets #31489 — original "memory branch push broken on repos with signed-commits rulesets" report. The earlier discussion focused on the orphan-first-commit signing case; this issue is the distinct
ls-remote authcause that fails every run, not just the first one.
Activity
pelikhan commentedon May 18, 2026
@mason-tim which version of the compiler?
mason-tim commentedon May 18, 2026
gh aw compiler is v0.71.6
mason-tim commentedon May 18, 2026
Re-confirmed on the latest release v0.74.4 (published ~21 hours ago).
Source unchanged:
actions/setup/js/push_signed_commits.cjs:363still callsgit ls-remote originwith noenv: gitAuthEnv.Reproduced in a workflow whose target repo has a "Require signed commits" ruleset and uses
actions/checkoutwithpersist-credentials: false(the default emitted by the gh-aw compiler since #31478). Worth noting this is not the orphan-first-commit case from #31489 — thememory/<workflow>branch already exists and is checked out cleanly:The local commit is created against a valid parent. The GraphQL signed-push path then begins normally and runs
ls-remoteto confirm the remote head — that is where it fails:Because
ls-remotefailed with an auth error rather than a "missing ref" error, the GraphQL path bails to an unsignedgit push, which the signed-commits ruleset correctly rejects.We believe adding
env: { ...process.env, ...(gitAuthEnv || {}) }to thels-remotecall (mirroring the siblinggit pushinvocation on line 140) would resolve it. Happy to help confirm this locally or submit a PR if that proves correct.pelikhan commentedon May 18, 2026
/scout summarize this issue
github-actions commentedon May 18, 2026
🏕️ Lost in the wilderness! Scout failed to deliver outputs. Sending search party...
github-actions commentedon May 18, 2026
🔍 Scout Research Report
Triggered by
@pelikhanExecutive Summary
push_repo_memoryis broken on any repository with a Require signed commits ruleset, due to a regression introduced by the interaction of two previously-fine PRs:push_signed_commits.cjs, whosels-remote origincall at line 363 reads auth implicitly from.git/config'shttp.extraheader(written byactions/checkout).persist-credentials: falseonactions/checkoutsteps #31478 (merged 2026-05-11) added a codemod that rewrites everyactions/checkouttopersist-credentials: false, which stops checkout from writing that extraheader.Result:
ls-remoteruns with no auth →fatal: could not read Username for 'https://github.com'→ catch handler falls through to a plain unsignedgit push→ ruleset rejects it withGH013. Every run fails; pre-seeding the memory branch does not help.Verified Against the Code
actions/setup/js/push_signed_commits.cjs:140— thegit pushcall correctly passesenv: { ...process.env, ...(gitAuthEnv || {}) }.actions/setup/js/push_signed_commits.cjs:363— thegit ls-remote origincall passes only{ cwd }, with no env merging. Confirmed as the only network-touching git call in the file that omitsgitAuthEnv.persist-credentials: falseonactions/checkoutsteps #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:363from:to:
This mirrors the existing
pushBranchAndResolveHeadpattern at line 140 — same file, same function family, same credential mechanism.gitAuthEnvis ephemeral per-process config (env-var-basedGIT_CONFIG_*), so it does not re-introduce the disk-persistent extraheader that #31478 deliberately removed. Thepersist-credentials: falseposture is preserved.Recommended test coverage
In
actions/setup/js/push_signed_commits.test.cjs:persist-credentials: falsestate (no extraheader in.git/config), supplygitAuthEnvtopushSignedCommits, and assert the mockedexec.getExecOutput("git", ["ls-remote", ...], opts)is invoked with anopts.envcontainingGIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0.exec.getExecOutput("git", ...)call known to be network-touching (push,ls-remote, future fetches) and requires each to receive the mergedenv. This stops a future contributor reintroducing the same omission for a different network call.Recommendations
Suggested Next Steps
actions/setup/js/push_signed_commits.cjs:363.Require signed commitsruleset onmemory/*(the issue's reproduction recipe is concrete).ls-remotefailure 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.pelikhan commentedon May 20, 2026
I will turn on signing on our memory branches today and look go through this.