You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #29330 (which fixed #29301) routed push_repo_memory.cjs through pushSignedCommits so memory commits are server-signed via the GraphQL createCommitOnBranch mutation and pass Require signed commits rulesets.
PR #30463 then fixed a separate, real bug — orphan-branch first commits being silently dropped when ${baseRef}..HEAD resolves to zero shas — by adding an early-return guard in actions/setup/js/push_signed_commits.cjs. That guard does the right thing for unsigned environments, but it falls back to plain git push, which bypasses the signed-commit path:
// actions/setup/js/push_signed_commits.cjs (current main)asyncfunctionpushSignedCommits({ ...,baseRef, ... }){if(!baseRef){core.info(`pushSignedCommits: empty baseRef detected (orphan branch first push), using git push directly for branch ${branch}`);awaitexec.exec("git",["push","origin",branch],{
cwd,env: { ...process.env, ...(gitAuthEnv||{})},});
...
returnheadSha;}
...
}
Result: on any repo with a Require signed commits ruleset, the first push to a brand-new memory branch fails with GH013 — the exact failure mode #29301 was filed against. Because the branch is never created on origin, every subsequent run takes the same orphan-first-commit path and fails identically. Memory never persists; agents see "first baseline" indefinitely.
Reproduction
A repository with the org-level "Commits must have verified signatures" ruleset applied to all branches, running any workflow that uses safe-outputs.repo-memory for the first time on a fresh memory id (so the memory branch does not yet exist on origin), with gh-aw v0.71.6 (or v0.72.x — same code path).
Sanitized log excerpt from the Push repo-memory changes step:
Branch memory/<id> does not exist, creating orphan branch...
Cleaning working directory for orphan branch...
Created orphan branch: memory/<id>
...
Scan complete: Found 1 file(s) to copy
Copied: metrics-history.json (3750 bytes)
Changes detected, committing and pushing...
[memory/<id> (root-commit) <sha>] Update repo memory from workflow run <run-id>
1 file changed, 59 insertions(+)
Pushing changes to memory/<id> (attempt 1/4)...
pushSignedCommits: empty baseRef detected (orphan branch first push), using git push directly for branch memory/<id>
remote: error: GH013: Repository rule violations found for refs/heads/memory/<id>.
remote: - Commits must have verified signatures.
remote: Found 1 violation:
remote: <sha>
! [remote rejected] memory/<id> -> memory/<id> (push declined due to repository rule violations)
error: failed to push some refs to 'https://github.com/...'
##[warning]Push failed (attempt 1/4), retrying in 1000ms: The process '/usr/bin/git' failed with exit code 1
fatal: could not read Username for 'https://github.com': No such device or address
ls-remote on retry failed, keeping existing baseRef: The process '/usr/bin/git' failed with exit code 128
Pushing changes to memory/<id> (attempt 2/4)...
pushSignedCommits: empty baseRef detected (orphan branch first push), using git push directly for branch memory/<id>
... [same GH013 rejection × 4] ...
##[error]Failed to push changes after 4 attempts: The process '/usr/bin/git' failed with exit code 1
Analysis
Root cause
pushSignedCommits's orphan-branch first-push guard uses git push to create the branch, bypassing the GraphQL signed-commit path that was specifically introduced (#29330) to satisfy verified-signatures rulesets.
Design constraint to preserve
Memory branches are intentionally orphan — no shared ancestry with source. Any fix should preserve this:
git log memory/<id> should show only memory commits, not the source history.
A force-push or full reset of memory cannot affect source history.
Memory data cannot accidentally be merged into main (no merge base).
So a fix that re-parents memory branches under main's HEAD (the obvious workaround) regresses an architectural property and should be rejected.
Prior art ruled out
GraphQL createCommitOnBranch — expectedHeadOid is non-null in the schema, so the mutation cannot be called for the first commit on a branch that doesn't yet exist.
REST PUT /repos/{owner}/{repo}/contents/{path} — auto-signs when called by a GitHub App, but requires the target branch to already exist (returns 422 otherwise). Cannot seed an orphan branch.
REST POST /git/commits — accepts a caller-supplied signature field but does not auto-sign. The runner has no signing key.
git push -S / git commit -S — the runner has no GPG key and shipping one with the action is a non-starter.
Open question for the maintainers
Is there a documented REST/GraphQL combination that produces a server-signed root commit on a brand-new orphan branch? If not, the design may need a different shape — for example:
(a) A one-time API-only seed step: REST create a tree (POST /git/trees), then a low-level commit (POST /git/commits with no parents), then the ref (POST /git/refs), accepting that the seed commit lands as Unverified and relying on a ruleset bypass for the seed only. Subsequent commits on the now-existing branch take the GraphQL signed path.
(b) A non-destructive "anchor" first commit on main: POST /git/refs to point memory/<id> at main's current HEAD, then immediately use GraphQL createCommitOnBranch to overwrite the tree to memory contents. The branch is no longer truly orphan (has main as ancestor), but it's signed end-to-end. (Likely rejected — regresses the orphan property.)
(c) A new server-side signing path — coordinate with the GraphQL team to expose createCommitOnBranch with expectedHeadOid: null for the brand-new-branch case. Out of scope for this repo, but the right long-term fix.
(d) Something the maintainers know about that I don't — please advise.
I'd rather not prescribe an implementation in this issue without knowing which of these (or another option) the team prefers, given that #30463 was closed less than a week ago and clearly considered the constraints carefully.
Implementation plan (once direction is agreed)
Files affected
actions/setup/js/push_signed_commits.cjs — replace the unsigned git push in the if (!baseRef) branch with the chosen signed seed strategy.
actions/setup/js/push_repo_memory.cjs — only if the chosen strategy needs a different baseRef value or pre-checkout flow.
actions/setup/js/push_signed_commits.test.cjs — new tests:
Orphan-branch first push lands as a Verified commit (mock GraphQL/REST responses, assert the chosen endpoints are called and unsigned git push is not).
Orphan-branch first push retries correctly when the chosen endpoint returns a transient error.
Existing non-orphan signed-push tests continue to pass unchanged.
actions/setup/js/push_repo_memory.test.cjs — end-to-end test covering "fresh memory id, ruleset-protected repo, first push succeeds".
docs/src/content/docs/reference/repo-memory.md — note that memory commits, including the first commit on a new memory branch, are pushed as Verified and satisfy Require signed commits rulesets.
Acceptance criteria
On a repo with Require signed commits enforced for all branches, the first memory push of a fresh memory id succeeds and the resulting commit shows the Verified badge in the GitHub UI.
Memory branches retain orphan property (no shared ancestry with main) — git merge-base memory/<id> main returns no merge base.
Summary
PR #29330 (which fixed #29301) routed
push_repo_memory.cjsthroughpushSignedCommitsso memory commits are server-signed via the GraphQLcreateCommitOnBranchmutation and passRequire signed commitsrulesets.PR #30463 then fixed a separate, real bug — orphan-branch first commits being silently dropped when
${baseRef}..HEADresolves to zero shas — by adding an early-return guard inactions/setup/js/push_signed_commits.cjs. That guard does the right thing for unsigned environments, but it falls back to plaingit push, which bypasses the signed-commit path:Result: on any repo with a
Require signed commitsruleset, the first push to a brand-new memory branch fails withGH013— the exact failure mode #29301 was filed against. Because the branch is never created on origin, every subsequent run takes the same orphan-first-commit path and fails identically. Memory never persists; agents see "first baseline" indefinitely.Reproduction
A repository with the org-level "Commits must have verified signatures" ruleset applied to all branches, running any workflow that uses
safe-outputs.repo-memoryfor the first time on a fresh memory id (so the memory branch does not yet exist on origin), withgh-aw v0.71.6(or v0.72.x — same code path).Sanitized log excerpt from the
Push repo-memory changesstep:Analysis
Root cause
pushSignedCommits's orphan-branch first-push guard usesgit pushto create the branch, bypassing the GraphQL signed-commit path that was specifically introduced (#29330) to satisfy verified-signatures rulesets.Design constraint to preserve
Memory branches are intentionally orphan — no shared ancestry with source. Any fix should preserve this:
git log memory/<id>should show only memory commits, not the source history.main(no merge base).So a fix that re-parents memory branches under
main's HEAD (the obvious workaround) regresses an architectural property and should be rejected.Prior art ruled out
createCommitOnBranch—expectedHeadOidis non-null in the schema, so the mutation cannot be called for the first commit on a branch that doesn't yet exist.PUT /repos/{owner}/{repo}/contents/{path}— auto-signs when called by a GitHub App, but requires the targetbranchto already exist (returns 422 otherwise). Cannot seed an orphan branch.POST /git/commits— accepts a caller-suppliedsignaturefield but does not auto-sign. The runner has no signing key.git push -S/git commit -S— the runner has no GPG key and shipping one with the action is a non-starter.Open question for the maintainers
Is there a documented REST/GraphQL combination that produces a server-signed root commit on a brand-new orphan branch? If not, the design may need a different shape — for example:
(a) A one-time API-only seed step: REST create a tree (
POST /git/trees), then a low-level commit (POST /git/commitswith no parents), then the ref (POST /git/refs), accepting that the seed commit lands as Unverified and relying on a ruleset bypass for the seed only. Subsequent commits on the now-existing branch take the GraphQL signed path.(b) A non-destructive "anchor" first commit on
main:POST /git/refsto pointmemory/<id>atmain's current HEAD, then immediately use GraphQLcreateCommitOnBranchto overwrite the tree to memory contents. The branch is no longer truly orphan (has main as ancestor), but it's signed end-to-end. (Likely rejected — regresses the orphan property.)(c) A new server-side signing path — coordinate with the GraphQL team to expose
createCommitOnBranchwithexpectedHeadOid: nullfor the brand-new-branch case. Out of scope for this repo, but the right long-term fix.(d) Something the maintainers know about that I don't — please advise.
I'd rather not prescribe an implementation in this issue without knowing which of these (or another option) the team prefers, given that #30463 was closed less than a week ago and clearly considered the constraints carefully.
Implementation plan (once direction is agreed)
Files affected
actions/setup/js/push_signed_commits.cjs— replace the unsignedgit pushin theif (!baseRef)branch with the chosen signed seed strategy.actions/setup/js/push_repo_memory.cjs— only if the chosen strategy needs a different baseRef value or pre-checkout flow.actions/setup/js/push_signed_commits.test.cjs— new tests:git pushis not).actions/setup/js/push_repo_memory.test.cjs— end-to-end test covering "fresh memory id, ruleset-protected repo, first push succeeds".docs/src/content/docs/reference/repo-memory.md— note that memory commits, including the first commit on a new memory branch, are pushed as Verified and satisfyRequire signed commitsrulesets.Acceptance criteria
Require signed commitsenforced for all branches, the first memory push of a fresh memory id succeeds and the resulting commit shows the Verified badge in the GitHub UI.main) —git merge-base memory/<id> mainreturns no merge base.References
repo-memorypush fails with "Commits must have verified signatures" —push_repo_memory.cjsshould usepushSignedCommits#29301 — original report of unsigned-commit failure onRequire signed commitsrulesetspushSignedCommitsactions/setup/js/push_signed_commits.cjslines 134–149 (current main)actions/setup/js/push_repo_memory.cjslines 160–210 (current main)Happy to take a stab at the PR once a direction is agreed.