-
Notifications
You must be signed in to change notification settings - Fork 306
Description
Summary
The pushSignedCommits feature introduced in PR #21576 (shipped in v0.61.1) does not work for create-pull-request safe outputs when the target branch is new (does not yet exist on the remote). The GraphQL createCommitOnBranch mutation requires a valid expectedHeadOid obtained via git ls-remote, which returns empty for a nonexistent branch. The code then falls back to an unsigned git push, which is rejected by repositories with "Require signed commits" rulesets (GH013).
This is a blocking issue for any repository that:
- Uses
gh-awagentic workflows withcreate-pull-requestsafe outputs - Has a "Require signed commits" ruleset enabled (org or repo level)
Environment
- gh-aw version: v0.62.4 (latest as of 2026-03-19)
- GitHub.com (not GHES)
- Repository ruleset: "Require signed commits" enabled on all branches
Steps to Reproduce
- Configure a repository with a "Require signed commits" branch ruleset
- Set up an agentic workflow that uses the
create-pull-requestsafe output - Trigger the workflow so the agent produces a patch for a new branch (branch does not already exist on the remote)
- Observe the safe output handler attempt to push
Actual Behavior
/usr/bin/git ls-remote --heads origin <new-branch-name>
/usr/bin/git rev-list --reverse origin/main..HEAD
<commit-sha>
pushSignedCommits: replaying 1 commit(s) via GraphQL createCommitOnBranch
/usr/bin/git ls-remote origin refs/heads/<new-branch-name>
Warning: pushSignedCommits: GraphQL signed push failed, falling back to git push: Could not resolve remote HEAD OID for branch <new-branch-name>
/usr/bin/git push origin <new-branch-name>
remote: error: GH013: Repository rule violations found for refs/heads/<new-branch-name>.
remote: - Commits must have verified signatures.
remote: Found 1 violation:
remote: <commit-sha>
error: failed to push some refs to 'https://github.com/<owner>/<repo>.git'
The workflow then falls back to creating a "fallback issue" instead of a pull request.
Expected Behavior
The create-pull-request flow should successfully push signed commits to a new branch and open a pull request, even when "Require signed commits" rulesets are active.
Root Cause Analysis
The issue is in push_signed_commits.cjs. The flow for replaying commits via GraphQL is:
1. git ls-remote origin refs/heads/<branch> → get expectedHeadOid
2. For each commit: call GraphQL createCommitOnBranch mutation
Step 1 fails when the branch is new because git ls-remote returns no OID for a nonexistent remote ref.
The push-to-pull-request-branch safe output does not have this problem because its branch already exists on the remote at the time of push.
Contrasting the Two Code Paths
| Safe Output | Branch State | git ls-remote Result |
Signed Push Works? |
|---|---|---|---|
push-to-pull-request-branch |
Already exists on remote | Returns valid OID | Yes |
create-pull-request |
New, local only | Returns empty | No — falls back to unsigned git push |
Proposed Fix
Before replaying commits via the GraphQL createCommitOnBranch mutation, the code should create the remote branch ref when it doesn't already exist. The GitHub REST API provides this capability:
Option A: Use the Git References API to Bootstrap the Branch (Recommended)
In push_signed_commits.cjs (or create_pull_request.cjs), before the GraphQL replay loop:
// In push_signed_commits.cjs, after getting an empty OID from ls-remote:
async function ensureRemoteBranchExists(octokit, owner, repo, branchName, baseSha) {
// Check if branch exists
try {
const { data } = await octokit.rest.git.getRef({
owner,
repo,
ref: `heads/${branchName}`,
});
return data.object.sha; // Branch exists, return its HEAD
} catch (e) {
if (e.status !== 404) throw e;
}
// Branch doesn't exist — create it pointing at the base branch HEAD
const { data } = await octokit.rest.git.createRef({
owner,
repo,
ref: `refs/heads/${branchName}`,
sha: baseSha, // SHA of the base branch (e.g., main) HEAD
});
return data.object.sha; // This becomes the expectedHeadOid for the first commit
}Then wire this into the signed push flow:
// Current code (simplified):
let headOid = await getRemoteHeadOid(branch); // returns null for new branch
if (!headOid) {
// CURRENT: throw/fallback to git push
// FIX: create the branch on the remote first
const baseSha = await getBaseBranchSha(); // SHA of the base branch (main/master)
headOid = await ensureRemoteBranchExists(octokit, owner, repo, branch, baseSha);
}
// Now replay commits via GraphQL createCommitOnBranch using headOid
for (const commit of commits) {
const result = await graphql(createCommitOnBranchMutation, {
branch: { repositoryNameWithOwner: `${owner}/${repo}`, branchName: branch },
expectedHeadOid: headOid,
// ... commit data
});
headOid = result.createCommitOnBranch.commit.oid; // Update for next commit
}Option B: Use GraphQL createRef Mutation
If the team prefers staying within GraphQL:
mutation CreateRef($repositoryId: ID!, $name: String!, $oid: GitObjectID!) {
createRef(input: {
repositoryId: $repositoryId
name: $name # "refs/heads/<branch-name>"
oid: $oid # base branch HEAD SHA
}) {
ref {
name
target { oid }
}
}
}This creates the branch on the remote with a valid commit (the base branch HEAD), providing the expectedHeadOid needed for the subsequent createCommitOnBranch calls.
Where to Apply the Fix
The change should go into push_signed_commits.cjs at the point where expectedHeadOid resolution fails. Specifically:
actions/setup/js/push_signed_commits.cjs
The fix should be applied before the fallback to git push, replacing the current error-catch behavior:
BEFORE (current):
ls-remote → empty → catch → "Could not resolve remote HEAD OID" → fallback to git push
AFTER (fixed):
ls-remote → empty → createRef (bootstrap branch from base) → get OID → replay via GraphQL
Related Issues and PRs
| Reference | Description |
|---|---|
| #21572 | Meta issue: "Signed commits" (closed) |
| #21576 | PR that added pushSignedCommits via GraphQL createCommitOnBranch (merged) |
| #21584 | Refactor: moved logic into shared push_signed_commits.cjs (merged) |
| #21562 | Enterprise blocker for create-pull-request + required_signatures (closed) |
Impact
This blocks all create-pull-request safe outputs in repositories with signed commit requirements. Since create-pull-request is the primary mechanism for agents to submit code changes as PRs, this effectively prevents agentic workflows from operating in repositories with standard security rulesets.
Affected safe outputs:
create-pull-request— broken (new branch)push-to-pull-request-branch— works (existing branch)