Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d3bbb43
Initial plan
Copilot Jul 16, 2026
0de1c72
Plan safe-output fork PR support
Copilot Jul 16, 2026
0bc27ba
Support fork-backed safe-output PRs
Copilot Jul 16, 2026
9046b95
Plan merge main and recompile
Copilot Jul 16, 2026
ffcfbcb
Merge remote-tracking branch 'origin/main' into copilot/support-fork-…
Copilot Jul 16, 2026
e0b8ea1
Merge branch 'main' into copilot/support-fork-backed-prs
pelikhan Jul 16, 2026
7933022
Merge branch 'main' into copilot/support-fork-backed-prs
pelikhan Jul 16, 2026
a03f810
Merge branch 'main' into copilot/support-fork-backed-prs
pelikhan Jul 16, 2026
d962f45
Merge branch 'main' into copilot/support-fork-backed-prs
pelikhan Jul 16, 2026
79cdf6c
fix: restore container pins stripped by update-all-packages commit
Copilot Jul 16, 2026
a978af0
Plan: address PR review feedback
Copilot Jul 16, 2026
12c9b07
fix: address review feedback on fork-backed safe-output PR support
Copilot Jul 16, 2026
8b8516a
fix: pass cwd to overridePersistedExtraheader in pushBranchAndResolve…
Copilot Jul 16, 2026
3944a01
Merge remote-tracking branch 'origin/main' into copilot/support-fork-…
Copilot Jul 16, 2026
01b81a0
chore: start head-github-app implementation
Copilot Jul 16, 2026
fd1fd7a
Add head-github-app support for fork-backed PR token minting
Copilot Jul 16, 2026
fcbbe14
Clarify headRepoNameFromSlug expression guard comment
Copilot Jul 16, 2026
407ecca
Merge remote-tracking branch 'origin/main' into copilot/support-fork-…
Copilot Jul 16, 2026
dc6575f
recompile: update lock files after merging main
Copilot Jul 16, 2026
b5a6b13
fix: reject push when head.repo is null regardless of head-repo confi…
Copilot Jul 16, 2026
131e70b
feat: add head-github-app to JSON schema for create-pull-request and …
Copilot Jul 16, 2026
918185a
Add fork-backed push integration tests to push_signed_commits.test.cjs
Copilot Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/cli-consistency-checker.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

134 changes: 106 additions & 28 deletions actions/setup/js/create_pull_request.cjs

Large diffs are not rendered by default.

83 changes: 53 additions & 30 deletions actions/setup/js/create_pull_request.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as path from "path";
import * as os from "os";

const require = createRequire(import.meta.url);
const promptsSourceDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md");

const { getPatchPathForBranch, getPatchPathForBranchInRepo } = require("./git_patch_utils.cjs");
const { getBundlePathForBranch, getBundlePathForBranchInRepo } = require("./generate_git_bundle.cjs");
Expand Down Expand Up @@ -41,8 +42,19 @@ function cleanupCanonicalTransports() {
createdTransportPaths.clear();
}

function copyPromptTemplate(promptsDir, templateName) {
fs.copyFileSync(path.join(promptsSourceDir, templateName), path.join(promptsDir, templateName));
}

function ensureDefaultDisclosureHeaderPrompt() {
const promptsDir = path.join(process.env.RUNNER_TEMP || os.tmpdir(), "gh-aw", "prompts");
fs.mkdirSync(promptsDir, { recursive: true });
copyPromptTemplate(promptsDir, "safe_outputs_disclosure_header.md");
}

beforeEach(() => {
cleanupCanonicalTransports();
ensureDefaultDisclosureHeaderPrompt();
});
afterEach(() => {
cleanupCanonicalTransports();
Expand Down Expand Up @@ -1674,12 +1686,10 @@ describe("create_pull_request - allowed-files strict allowlist", () => {
pushSignedSpy = vi.spyOn(pushSignedCommitsModule, "pushSignedCommits").mockResolvedValue("bundle-tip");
const promptsDir = path.join(tempDir, "prompts");
fs.mkdirSync(promptsDir, { recursive: true });
const requestReviewTemplateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/manifest_protection_request_review.md");
fs.copyFileSync(requestReviewTemplateSrc, path.join(promptsDir, "manifest_protection_request_review.md"));
const requestChangesTemplateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/manifest_protection_request_changes_review.md");
fs.copyFileSync(requestChangesTemplateSrc, path.join(promptsDir, "manifest_protection_request_changes_review.md"));
const threatWarningTemplateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/threat_warning_request_changes_review.md");
fs.copyFileSync(threatWarningTemplateSrc, path.join(promptsDir, "threat_warning_request_changes_review.md"));
copyPromptTemplate(promptsDir, "manifest_protection_request_review.md");
copyPromptTemplate(promptsDir, "manifest_protection_request_changes_review.md");
copyPromptTemplate(promptsDir, "threat_warning_request_changes_review.md");
copyPromptTemplate(promptsDir, "safe_outputs_disclosure_header.md");
process.env.GH_AW_PROMPTS_DIR = promptsDir;

// Clear module cache so globals are picked up fresh
Expand Down Expand Up @@ -1882,10 +1892,9 @@ ${diffs}
const patchPath = writePatch("feature/protected", createPatchWithFiles(".github/aw/instructions.md"));
const promptsDir = path.join(tempDir, "prompts");
fs.mkdirSync(promptsDir, { recursive: true });
const templateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/manifest_protection_create_pr_fallback.md");
fs.copyFileSync(templateSrc, path.join(promptsDir, "manifest_protection_create_pr_fallback.md"));
const pushFailedTemplateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/manifest_protection_push_failed_fallback.md");
fs.copyFileSync(pushFailedTemplateSrc, path.join(promptsDir, "manifest_protection_push_failed_fallback.md"));
copyPromptTemplate(promptsDir, "manifest_protection_create_pr_fallback.md");
copyPromptTemplate(promptsDir, "manifest_protection_push_failed_fallback.md");
copyPromptTemplate(promptsDir, "safe_outputs_disclosure_header.md");
process.env.GH_AW_PROMPTS_DIR = promptsDir;

global.github.rest.issues = {
Expand Down Expand Up @@ -1922,10 +1931,9 @@ ${diffs}
fs.writeFileSync(bundlePath, "bundle content");
const promptsDir = path.join(tempDir, "prompts");
fs.mkdirSync(promptsDir, { recursive: true });
const templateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/manifest_protection_create_pr_fallback.md");
fs.copyFileSync(templateSrc, path.join(promptsDir, "manifest_protection_create_pr_fallback.md"));
const pushFailedTemplateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/manifest_protection_push_failed_fallback.md");
fs.copyFileSync(pushFailedTemplateSrc, path.join(promptsDir, "manifest_protection_push_failed_fallback.md"));
copyPromptTemplate(promptsDir, "manifest_protection_create_pr_fallback.md");
copyPromptTemplate(promptsDir, "manifest_protection_push_failed_fallback.md");
copyPromptTemplate(promptsDir, "safe_outputs_disclosure_header.md");
process.env.GH_AW_PROMPTS_DIR = promptsDir;

global.github.rest.issues = {
Expand Down Expand Up @@ -2851,6 +2859,7 @@ describe("create_pull_request - patch apply fallback to original base commit", (
fs.mkdirSync(promptsDir, { recursive: true });
fs.writeFileSync(path.join(promptsDir, "manifest_protection_request_review.md"), "Protected files: {{files}}", "utf8");
fs.writeFileSync(path.join(promptsDir, "manifest_protection_request_changes_review.md"), "Protected files: {{files}}", "utf8");
copyPromptTemplate(promptsDir, "safe_outputs_disclosure_header.md");
process.env.GH_AW_PROMPTS_DIR = promptsDir;
global.exec = {
exec: vi.fn().mockImplementation((cmd, args) => {
Expand Down Expand Up @@ -3052,14 +3061,14 @@ describe("create_pull_request - patch apply fallback to original base commit", (
let renameCalled = false;
global.exec = {
exec: vi.fn().mockImplementation((cmd, args) => {
const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
const cmdStr = `${cmd} ${(args || []).join(" ")}`;
if (cmdStr.includes("git branch -m")) {
renameCalled = true;
}
return Promise.resolve(0);
}),
getExecOutput: vi.fn().mockImplementation((cmd, args) => {
const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
const cmdStr = `${cmd} ${(args || []).join(" ")}`;
if (cmdStr.includes("ls-remote --heads origin")) {
return Promise.resolve({ exitCode: 0, stdout: "abc123\trefs/heads/preserve-me\n", stderr: "" });
}
Expand Down Expand Up @@ -3092,7 +3101,7 @@ describe("create_pull_request - patch apply fallback to original base commit", (
global.exec = {
exec: vi.fn().mockResolvedValue(0),
getExecOutput: vi.fn().mockImplementation((cmd, args) => {
const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
const cmdStr = `${cmd} ${(args || []).join(" ")}`;
if (cmdStr.includes("ls-remote --heads origin")) {
return Promise.resolve({ exitCode: 0, stdout: "abc123\trefs/heads/preserve-me\n", stderr: "" });
}
Expand All @@ -3117,7 +3126,7 @@ describe("create_pull_request - patch apply fallback to original base commit", (
global.exec = {
exec: vi.fn().mockResolvedValue(0),
getExecOutput: vi.fn().mockImplementation((cmd, args) => {
const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
const cmdStr = `${cmd} ${(args || []).join(" ")}`;
if (cmdStr.includes("ls-remote --heads origin")) {
return Promise.resolve({ exitCode: 0, stdout: "abc123\trefs/heads/preserve-me\n", stderr: "" });
}
Expand All @@ -3142,7 +3151,7 @@ describe("create_pull_request - patch apply fallback to original base commit", (
let capturedRenamedBranch = null;
global.exec = {
exec: vi.fn().mockImplementation((cmd, args) => {
const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
const cmdStr = `${cmd} ${(args || []).join(" ")}`;
// Capture the new branch name from: git branch -m <old> <new>
const renameMatch = cmdStr.match(/git branch -m \S+ (\S+)/);
if (renameMatch) {
Expand All @@ -3151,7 +3160,7 @@ describe("create_pull_request - patch apply fallback to original base commit", (
return Promise.resolve(0);
}),
getExecOutput: vi.fn().mockImplementation((cmd, args) => {
const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
const cmdStr = `${cmd} ${(args || []).join(" ")}`;
if (cmdStr.includes("ls-remote --heads origin")) {
return Promise.resolve({ exitCode: 0, stdout: "abc123\trefs/heads/chaos/preserve-me\n", stderr: "" });
}
Expand Down Expand Up @@ -3196,14 +3205,14 @@ describe("create_pull_request - patch apply fallback to original base commit", (
let renameCalled = false;
global.exec = {
exec: vi.fn().mockImplementation((cmd, args) => {
const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
const cmdStr = `${cmd} ${(args || []).join(" ")}`;
if (cmdStr.includes("git branch -m")) {
renameCalled = true;
}
return Promise.resolve(0);
}),
getExecOutput: vi.fn().mockImplementation((cmd, args) => {
const cmdStr = typeof cmd === "string" ? cmd : `${cmd} ${(args || []).join(" ")}`;
const cmdStr = `${cmd} ${(args || []).join(" ")}`;
if (cmdStr.includes("ls-remote --heads origin")) {
return Promise.resolve({ exitCode: 0, stdout: "abc123\trefs/heads/some-branch\n", stderr: "" });
}
Expand Down Expand Up @@ -3393,12 +3402,10 @@ describe("create_pull_request - threat detection caution", () => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-threat-test-"));
const promptsDir = path.join(tempDir, "prompts");
fs.mkdirSync(promptsDir, { recursive: true });
const requestReviewTemplateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/manifest_protection_request_review.md");
fs.copyFileSync(requestReviewTemplateSrc, path.join(promptsDir, "manifest_protection_request_review.md"));
const requestChangesTemplateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/manifest_protection_request_changes_review.md");
fs.copyFileSync(requestChangesTemplateSrc, path.join(promptsDir, "manifest_protection_request_changes_review.md"));
const threatWarningTemplateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/threat_warning_request_changes_review.md");
fs.copyFileSync(threatWarningTemplateSrc, path.join(promptsDir, "threat_warning_request_changes_review.md"));
copyPromptTemplate(promptsDir, "manifest_protection_request_review.md");
copyPromptTemplate(promptsDir, "manifest_protection_request_changes_review.md");
copyPromptTemplate(promptsDir, "threat_warning_request_changes_review.md");
copyPromptTemplate(promptsDir, "safe_outputs_disclosure_header.md");
process.env.GH_AW_PROMPTS_DIR = promptsDir;

global.core = {
Expand Down Expand Up @@ -4122,6 +4129,22 @@ describe("create_pull_request - branch-prefix config", () => {
expect(branchArg).not.toContain("signed/");
});

it("should use an owner-qualified PR head when head-repo differs from target-repo", async () => {
const { main } = require("./create_pull_request.cjs");
const handler = await main({
allow_empty: true,
"head-repo": "fork-owner/test-repo",
allowed_repos: ["test-owner/test-repo", "fork-owner/test-repo"],
});

await handler({ title: "Test PR", body: "body", branch: "my-feature" }, {});

const createCall = global.github.rest.pulls.create.mock.calls[0][0];
expect(createCall.owner).toBe("test-owner");
expect(createCall.repo).toBe("test-repo");
expect(createCall.head).toMatch(/^fork-owner:my-feature(?:-[0-9a-f]+)?$/);
});

it("should normalize an invalid branch-prefix and emit a warning", async () => {
const { main } = require("./create_pull_request.cjs");
const handler = await main({ branch_prefix: "bad prefix: ", allow_empty: true });
Expand Down Expand Up @@ -4149,8 +4172,8 @@ describe("create_pull_request - E003 file-limit fallback-to-issue", () => {
// Set up prompts directory with the E003 template so getPromptPath resolves
const promptsDir = path.join(tempDir, "prompts");
fs.mkdirSync(promptsDir, { recursive: true });
const templateSrc = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md/e003_file_limit_fallback.md");
fs.copyFileSync(templateSrc, path.join(promptsDir, "e003_file_limit_fallback.md"));
copyPromptTemplate(promptsDir, "e003_file_limit_fallback.md");
copyPromptTemplate(promptsDir, "safe_outputs_disclosure_header.md");
process.env.GH_AW_PROMPTS_DIR = promptsDir;

global.core = {
Expand Down
5 changes: 3 additions & 2 deletions actions/setup/js/create_pull_request_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -223,11 +223,12 @@ function generatePatchPreview(patchContent) {
* @param {string} branchName
* @param {string} title
* @param {number} [fallbackIssueNumber]
* @param {string} [headRef]
* @returns {string}
*/
function buildManifestProtectionCreatePrUrl(githubServer, repoParts, baseBranch, branchName, title, fallbackIssueNumber) {
function buildManifestProtectionCreatePrUrl(githubServer, repoParts, baseBranch, branchName, title, fallbackIssueNumber, headRef) {
const encodedBase = encodePathSegments(baseBranch);
const encodedHead = encodePathSegments(branchName);
const encodedHead = encodePathSegments(headRef || branchName);
let createPrUrl = `${githubServer}/${repoParts.owner}/${repoParts.repo}/compare/${encodedBase}...${encodedHead}?expand=1&title=${encodeURIComponent(title)}`;
if (typeof fallbackIssueNumber === "number") {
createPrUrl += `&body=${encodeURIComponent(`Closes #${fallbackIssueNumber}`)}`;
Expand Down
Loading
Loading