diff --git a/actions/setup/js/push_signed_commits.cjs b/actions/setup/js/push_signed_commits.cjs index b87caa43b5b..31643b5d842 100644 --- a/actions/setup/js/push_signed_commits.cjs +++ b/actions/setup/js/push_signed_commits.cjs @@ -184,9 +184,23 @@ async function pushSignedCommits({ githubClient, owner, repo, branch, baseRef, c // so skip it entirely and fall directly through to git push. if (!baseRef) { core.info(`pushSignedCommits: empty baseRef detected (orphan branch first push), using git push directly for branch ${branch}`); - const headSha = await pushBranchAndResolveHead({ branch, cwd, gitAuthEnv }); - core.info(`pushSignedCommits: git push completed for orphan branch, HEAD=${headSha}`); - return headSha; + try { + const headSha = await pushBranchAndResolveHead({ branch, cwd, gitAuthEnv }); + core.info(`pushSignedCommits: git push completed for orphan branch, HEAD=${headSha}`); + return headSha; + } catch (pushErr) { + const pushErrMsg = pushErr instanceof Error ? pushErr.message : String(pushErr); + throw new Error( + `pushSignedCommits: failed to push orphan branch '${branch}' (first commit). ` + + `If the repository requires signed commits, the branch must be seeded manually with a signed commit before this workflow can push to it. ` + + `Run the following commands locally (requires a GPG key configured with Git):\n\n` + + ` git switch --orphan ${branch}\n` + + ` git commit --allow-empty -S -m "Initialize ${branch}"\n` + + ` git push origin ${branch}\n\n` + + `Original error: ${pushErrMsg}`, + { cause: pushErr } + ); + } } // Collect the commits introduced (oldest-first) using topological order to ensure diff --git a/actions/setup/js/push_signed_commits.test.cjs b/actions/setup/js/push_signed_commits.test.cjs index 3faf33a376b..f07d65bd27b 100644 --- a/actions/setup/js/push_signed_commits.test.cjs +++ b/actions/setup/js/push_signed_commits.test.cjs @@ -645,6 +645,49 @@ describe("push_signed_commits integration tests", () => { // Return value should be the HEAD SHA. expect(result).toBe(expectedSha); }); + + it("should throw with manual seeding instructions when orphan-branch git push fails", async () => { + // Simulate a repo where "Require signed commits" is enforced. The orphan-branch + // first push uses git push directly, which will be rejected by the remote with + // GH013. We simulate this by using a bare repo that refuses the push via a + // pre-receive hook. + execGit(["checkout", "--orphan", "experiments/signed-required"], { cwd: workDir }); + execGit(["read-tree", "--empty"], { cwd: workDir }); + fs.writeFileSync(path.join(workDir, "state.json"), JSON.stringify({ runs: 1 })); + execGit(["add", "state.json"], { cwd: workDir }); + execGit(["commit", "-m", "Initial experiment state"], { cwd: workDir }); + + // Install a pre-receive hook in the bare repo that mimics GH013 by rejecting all pushes. + const hooksDir = path.join(bareDir, "hooks"); + fs.mkdirSync(hooksDir, { recursive: true }); + const hookPath = path.join(hooksDir, "pre-receive"); + fs.writeFileSync(hookPath, "#!/bin/sh\necho 'remote: error: GH013: Repository rule violations found.' >&2\necho 'remote: - Commits must have verified signatures.' >&2\nexit 1\n"); + fs.chmodSync(hookPath, "0755"); + + // Use the real exec so git push actually runs and hits the hook. + global.exec = makeRealExec(workDir); + const githubClient = makeMockGithubClient(); + + let thrownErr; + try { + await pushSignedCommits({ + githubClient, + owner: "test-owner", + repo: "test-repo", + branch: "experiments/signed-required", + baseRef: "", + cwd: workDir, + }); + } catch (err) { + thrownErr = err; + } + expect(thrownErr).toBeDefined(); + expect(thrownErr.message).toContain("failed to push orphan branch"); + expect(thrownErr.message).toContain("git switch --orphan experiments/signed-required"); + expect(thrownErr.message).toContain("git commit --allow-empty -S"); + expect(thrownErr.message).toContain("git push origin experiments/signed-required"); + expect(thrownErr.message).toContain("signed commits"); + }); }); // ──────────────────────────────────────────────────────