From 50759b0ee57ce93930a3dcddde3523840b2aa9e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:51:45 +0000 Subject: [PATCH 1/5] Initial plan From db07f685126fe393d94e82c418b8c9309f8eedd5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:56:09 +0000 Subject: [PATCH 2/5] chore: outline plan for safe outputs parser fix Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/smoke-copilot-aoai-entra.lock.yml | 4 ++-- .github/workflows/smoke-copilot.lock.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/smoke-copilot-aoai-entra.lock.yml b/.github/workflows/smoke-copilot-aoai-entra.lock.yml index 39b8d8a865a..06cce6097a1 100644 --- a/.github/workflows/smoke-copilot-aoai-entra.lock.yml +++ b/.github/workflows/smoke-copilot-aoai-entra.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"aedaddef3f058e3390275c268927dc411726ff550ae0c1130dbfaf30cbba86cb","body_hash":"2889d48bbc10acf2afe7b2e91d801eca31de7eba05b2775c1e506adad86eb7e4","strict":true,"agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.65"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"aedaddef3f058e3390275c268927dc411726ff550ae0c1130dbfaf30cbba86cb","body_hash":"2889d48bbc10acf2afe7b2e91d801eca31de7eba05b2775c1e506adad86eb7e4","agent_id":"copilot","agent_model":"o4-mini-aw","engine_versions":{"copilot":"1.0.65"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","FOUNDRY_OPENAI_ENDPOINT","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.10","digest":"sha256:e47878fa4953f5b4d38b4ec12c155aa12ab9befea299ea2d21a8b104de8bcbc8","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.10@sha256:e47878fa4953f5b4d38b4ec12c155aa12ab9befea299ea2d21a8b104de8bcbc8"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.10","digest":"sha256:4bd2598466928efbd360fd6575b68c6b420a7ec3b7c1be20844c560a0dd2878e","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.10@sha256:4bd2598466928efbd360fd6575b68c6b420a7ec3b7c1be20844c560a0dd2878e"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.10"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.10","digest":"sha256:4d7a79482c47f2390f9fa87663cd9cb728bfb2380d9a9610479fa234c906ea98","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.10@sha256:4d7a79482c47f2390f9fa87663cd9cb728bfb2380d9a9610479fa234c906ea98"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.30","digest":"sha256:4d0101d8740c99b755181d19dc0067ac7eb40433d1c354fd715358bee4a296c1","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.30@sha256:4d0101d8740c99b755181d19dc0067ac7eb40433d1c354fd715358bee4a296c1"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.4.0","digest":"sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036","pinned_image":"ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -173,7 +173,7 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_FRONTMATTER_EMOJI: "🧪" - GH_AW_COMPILED_STRICT: "true" + GH_AW_COMPILED_STRICT: "false" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 7ce9d0e6094..c75c01038bf 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6669d3fc0bdfdd7a9e54e10d8c1e41c04dc17e1d162f2e639dabdb1255ee8a78","body_hash":"812899a64607d2d204003410dba1febf488c85700602e8702b89dd7db3609096","strict":true,"agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.65"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6669d3fc0bdfdd7a9e54e10d8c1e41c04dc17e1d162f2e639dabdb1255ee8a78","body_hash":"812899a64607d2d204003410dba1febf488c85700602e8702b89dd7db3609096","agent_id":"copilot","agent_model":"gpt-5.4","engine_versions":{"copilot":"1.0.65"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7.0.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"docker/build-push-action","sha":"f9f3042f7e2789586610d6e8b85c8f03e5195baf","version":"v7.2.0"},{"repo":"docker/setup-buildx-action","sha":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5","version":"v4.1.0"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.10","digest":"sha256:e47878fa4953f5b4d38b4ec12c155aa12ab9befea299ea2d21a8b104de8bcbc8","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.10@sha256:e47878fa4953f5b4d38b4ec12c155aa12ab9befea299ea2d21a8b104de8bcbc8"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.10","digest":"sha256:4bd2598466928efbd360fd6575b68c6b420a7ec3b7c1be20844c560a0dd2878e","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.10@sha256:4bd2598466928efbd360fd6575b68c6b420a7ec3b7c1be20844c560a0dd2878e"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.10"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.10","digest":"sha256:4d7a79482c47f2390f9fa87663cd9cb728bfb2380d9a9610479fa234c906ea98","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.10@sha256:4d7a79482c47f2390f9fa87663cd9cb728bfb2380d9a9610479fa234c906ea98"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.30","digest":"sha256:4d0101d8740c99b755181d19dc0067ac7eb40433d1c354fd715358bee4a296c1","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.30@sha256:4d0101d8740c99b755181d19dc0067ac7eb40433d1c354fd715358bee4a296c1"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.4.0","digest":"sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036","pinned_image":"ghcr.io/github/github-mcp-server:v1.4.0@sha256:2afb26356481d1a350e14544a6e160f7f7ec1561a1ea309b823665abf0309036"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -173,7 +173,7 @@ jobs: GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_INFO_FRONTMATTER_EMOJI: "🧪" - GH_AW_COMPILED_STRICT: "true" + GH_AW_COMPILED_STRICT: "false" GH_AW_INFO_MODEL_COSTS: '{"providers":{"anthropic":{"models":{"my-custom-claude":{"cost":{"cache_read":"3e-07","cache_write":"3.75e-06","input":"3e-06","output":"1.5e-05"}}}}}}' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: From f2d5998d0a593ed18b2cc9e0c2acfd218bdfc8c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:03:41 +0000 Subject: [PATCH 3/5] fix: align bundle safe-output file detection Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/push_to_pull_request_branch.cjs | 36 ++++++++++- .../js/push_to_pull_request_branch.test.cjs | 64 +++++++++++++++++++ 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index 9708860df9f..e40ad3f9cea 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -208,9 +208,9 @@ async function main(config = {}) { core.warning(`Bundle file path was provided but file is not present on disk: ${bundleFilePath}; falling back to patch transport`); } - // Always require a patch file for policy enforcement. Bundle is used for apply-time - // transport, but allowed-files/protected-files checks must run on patch content - // (see validation block below that calls checkFileProtection on patchContent). + // Always require a patch file. The patch remains the preview/debug artifact and + // the first-pass validation input; bundle transport adds an authoritative + // pre-apply git diff check later after the bundle ref has been fetched. if (!hasPatchFile) { const msg = "No patch file found - cannot push without changes"; @@ -789,6 +789,36 @@ async function main(config = {}) { } core.info(`Fetched bundle to ${bundleRef}`); + // SECURITY: Use git's own diff against the fetched bundle ref as the + // authoritative pre-apply file set for bundle transport. This keeps + // bundle pre-check and post-apply verification aligned even when the + // patch artifact under-detects files (for example, merge-resolution + // content preserved only by the bundle transport). + { + const bundleDiffResult = await exec.getExecOutput("git", ["diff", "--name-only", "--no-renames", `${rangeBaseRef}..${bundleRef}`], baseGitOpts); + const bundleFiles = bundleDiffResult.stdout + .split("\n") + .map(f => f.trim()) + .filter(Boolean); + if (bundleFiles.length > 0) { + core.info(`Pre-apply bundle verification: ${bundleFiles.length} file(s) detected from bundle transport`); + const bundleProtection = checkFileProtectionPostApply(bundleFiles, config); + if (bundleProtection.action === "deny") { + const filesStr = bundleProtection.files.join(", "); + const msg = + bundleProtection.source === "post-apply" + ? `Cannot push to pull request branch: bundle modifies files outside the allowed-files list (${filesStr}). Add the files to the allowed-files configuration field or remove them from the bundle.` + : `Cannot push to pull request branch: bundle modifies protected files (${filesStr}). Add them to the allowed-files configuration field or set protected-files: fallback-to-issue to create a review issue instead.`; + core.error(msg); + return { success: false, error: msg }; + } + if (bundleProtection.action === "fallback") { + core.warning(`Protected file protection triggered (fallback-to-issue): ${bundleProtection.files.join(", ")}. Will create review issue instead of pushing.`); + return await createProtectedFilesFallbackIssue(bundleProtection.files); + } + } + } + // Point the checked-out branch at the bundle tip directly. In shallow // checkouts, merge --ff-only can fail to discover the ancestry even // when the bundle tip is based on the current branch tip and the diff --git a/actions/setup/js/push_to_pull_request_branch.test.cjs b/actions/setup/js/push_to_pull_request_branch.test.cjs index 96ce10b7cef..0561a9f2b21 100644 --- a/actions/setup/js/push_to_pull_request_branch.test.cjs +++ b/actions/setup/js/push_to_pull_request_branch.test.cjs @@ -1532,6 +1532,70 @@ index 0000000..abc1234 } }); + it("should use authoritative bundle file detection before apply and match post-apply verification", async () => { + const bundlePath = canonicalBundlePath("feature-branch"); + const patchPath = createPatchFile( + "feature-branch", + `From abc123 Mon Sep 17 00:00:00 2001 +From: Test Author +Date: Mon, 1 Jan 2024 00:00:00 +0000 +Subject: [PATCH] Test commit + +diff --git a/.changeset/patch-fix.md b/.changeset/patch-fix.md +new file mode 100644 +index 0000000..abc1234 +--- /dev/null ++++ b/.changeset/patch-fix.md +@@ -0,0 +1 @@ ++content +-- +2.34.1 +` + ); + fs.writeFileSync(bundlePath, "bundle content"); + + const pushSignedCommitsModule = require("./push_signed_commits.cjs"); + const pushSignedSpy = vi.spyOn(pushSignedCommitsModule, "pushSignedCommits").mockResolvedValue("bundle-tip"); + + try { + const actualFiles = [".changeset/patch-fix.md", "pkg/workflow/pi_byok_env_passthrough_integration_test.go", "pkg/workflow/pi_engine.go", "pkg/workflow/pi_engine_test.go"]; + mockExec.getExecOutput.mockImplementation((cmd, args, options) => { + if (cmd === "git" && args[0] === "ls-remote") { + return Promise.resolve({ exitCode: 0, stdout: "remote-head\trefs/heads/feature-branch\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "HEAD") { + return Promise.resolve({ exitCode: 0, stdout: "remote-head\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-shallow-repository") { + return Promise.resolve({ exitCode: 0, stdout: "false\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode) { + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + } + if (cmd === "git" && args[0] === "diff" && args[1] === "--name-only" && args[2] === "--no-renames") { + return Promise.resolve({ exitCode: 0, stdout: `${actualFiles.join("\n")}\n`, stderr: "" }); + } + if (cmd === "git" && args[0] === "rev-list") { + return Promise.resolve({ exitCode: 0, stdout: "2\n", stderr: "" }); + } + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + }); + + const module = await loadModule(); + const handler = await module.main({ allowed_files: [".changeset/**", "pkg/workflow/**"] }); + const result = await handler({ branch: "feature-branch", diff_size: 5 * 1024 }, {}); + + expect(result.success).toBe(true); + expect(mockCore.info).toHaveBeenCalledWith("Pre-apply bundle verification: 4 file(s) detected from bundle transport"); + + const diffCalls = mockExec.getExecOutput.mock.calls.filter(([, args]) => Array.isArray(args) && args[0] === "diff" && args[1] === "--name-only" && args[2] === "--no-renames"); + expect(diffCalls.map(([, args]) => args[3])).toContain("remote-head..refs/bundles/push-feature-branch"); + expect(diffCalls.map(([, args]) => args[3])).toContain("remote-head..HEAD"); + } finally { + pushSignedSpy.mockRestore(); + } + }); + it("should use sanitized branch name (not agent-supplied message.branch) in bundle fetch refspec", async () => { // The agent may supply a message.branch value; the bundle fetch must use the // sanitized branchName from the GitHub API — never the raw agent input. From 6cc5cdc6f1d1a4b05e09fccf637b2b2b36083b58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:29:02 +0000 Subject: [PATCH 4/5] Refactor bundle pre-check helper Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../setup/js/push_to_pull_request_branch.cjs | 25 ++- ...o_pull_request_branch.integration.test.cjs | 142 ++++++++++++++++++ 2 files changed, 161 insertions(+), 6 deletions(-) create mode 100644 actions/setup/js/push_to_pull_request_branch.integration.test.cjs diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index e40ad3f9cea..4d9ebcea59a 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -102,6 +102,23 @@ function parsePositiveInteger(value) { return Number.isInteger(parsed) && parsed > 0 ? parsed : null; } +/** + * Uses git as the source of truth for the files modified by a fetched bundle ref. + * + * @param {{ getExecOutput: (command: string, args?: string[], options?: any) => Promise<{ stdout: string }> }} exec + * @param {Record} gitOptions + * @param {string} rangeBaseRef + * @param {string} bundleRef + * @returns {Promise} + */ +async function getBundlePreApplyFiles(exec, gitOptions, rangeBaseRef, bundleRef) { + const bundleDiffResult = await exec.getExecOutput("git", ["diff", "--name-only", "--no-renames", `${rangeBaseRef}..${bundleRef}`], gitOptions); + return bundleDiffResult.stdout + .split("\n") + .map(f => f.trim()) + .filter(Boolean); +} + /** * Main handler factory for push_to_pull_request_branch * Returns a message handler function that processes individual push_to_pull_request_branch messages @@ -795,11 +812,7 @@ async function main(config = {}) { // patch artifact under-detects files (for example, merge-resolution // content preserved only by the bundle transport). { - const bundleDiffResult = await exec.getExecOutput("git", ["diff", "--name-only", "--no-renames", `${rangeBaseRef}..${bundleRef}`], baseGitOpts); - const bundleFiles = bundleDiffResult.stdout - .split("\n") - .map(f => f.trim()) - .filter(Boolean); + const bundleFiles = await getBundlePreApplyFiles(exec, baseGitOpts, rangeBaseRef, bundleRef); if (bundleFiles.length > 0) { core.info(`Pre-apply bundle verification: ${bundleFiles.length} file(s) detected from bundle transport`); const bundleProtection = checkFileProtectionPostApply(bundleFiles, config); @@ -1330,4 +1343,4 @@ async function main(config = {}) { }; } -module.exports = { main, HANDLER_TYPE }; +module.exports = { main, HANDLER_TYPE, getBundlePreApplyFiles }; diff --git a/actions/setup/js/push_to_pull_request_branch.integration.test.cjs b/actions/setup/js/push_to_pull_request_branch.integration.test.cjs new file mode 100644 index 00000000000..5181ab8238e --- /dev/null +++ b/actions/setup/js/push_to_pull_request_branch.integration.test.cjs @@ -0,0 +1,142 @@ +import { describe, it, expect, afterEach, vi } from "vitest"; +import { createRequire } from "module"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import { spawnSync } from "child_process"; + +const require = createRequire(import.meta.url); +const { getBundlePreApplyFiles } = require("./push_to_pull_request_branch.cjs"); + +global.core = { + debug: vi.fn(), + error: vi.fn(), + info: vi.fn(), + warning: vi.fn(), +}; + +function execGit(args, options = {}) { + const result = spawnSync("git", args, { + encoding: "utf8", + ...options, + }); + if (result.error) { + throw result.error; + } + if (result.status !== 0 && !options.allowFailure) { + throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`); + } + return result; +} + +function createRepo(prefix) { + const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); + execGit(["init"], { cwd: repoDir }); + execGit(["config", "user.name", "Test User"], { cwd: repoDir }); + execGit(["config", "user.email", "test@example.com"], { cwd: repoDir }); + return repoDir; +} + +function writeRepoFile(repoDir, relativePath, content) { + const fullPath = path.join(repoDir, relativePath); + fs.mkdirSync(path.dirname(fullPath), { recursive: true }); + fs.writeFileSync(fullPath, content); +} + +function createExecApi(cwd) { + return { + async getExecOutput(command, args = [], options = {}) { + if (command !== "git") { + throw new Error(`unexpected command: ${command}`); + } + const result = execGit(args, { cwd, allowFailure: true }); + if (result.status !== 0 && !options.ignoreReturnCode) { + throw new Error(result.stderr || result.stdout); + } + return { exitCode: result.status, stdout: result.stdout, stderr: result.stderr }; + }, + }; +} + +function fetchBaseCommit(targetRepo, sourceRepo, baseSha, branchName) { + execGit(["remote", "add", "origin", sourceRepo], { cwd: targetRepo }); + execGit(["fetch", "origin", baseSha], { cwd: targetRepo }); + execGit(["checkout", "-b", branchName, "FETCH_HEAD"], { cwd: targetRepo }); +} + +describe("push_to_pull_request_branch bundle integration", () => { + const tempDirs = []; + + afterEach(() => { + for (const tempDir of tempDirs.splice(0)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + vi.clearAllMocks(); + }); + + it("lists files from a fetched bundle before applying it", async () => { + const branchName = "autoloop/simple-bundle"; + const sourceRepo = createRepo("push-pr-bundle-source-"); + const targetRepo = createRepo("push-pr-bundle-target-"); + tempDirs.push(sourceRepo, targetRepo); + + writeRepoFile(sourceRepo, "README.md", "base\n"); + execGit(["add", "README.md"], { cwd: sourceRepo }); + execGit(["commit", "-m", "base"], { cwd: sourceRepo }); + execGit(["branch", "-M", "main"], { cwd: sourceRepo }); + const baseSha = execGit(["rev-parse", "HEAD"], { cwd: sourceRepo }).stdout.trim(); + + execGit(["checkout", "-b", branchName], { cwd: sourceRepo }); + writeRepoFile(sourceRepo, ".changeset/fix.md", "patch\n"); + writeRepoFile(sourceRepo, "docs/guide.md", "guide\n"); + execGit(["add", ".changeset/fix.md", "docs/guide.md"], { cwd: sourceRepo }); + execGit(["commit", "-m", "bundle change"], { cwd: sourceRepo }); + + const bundlePath = path.join(sourceRepo, "bundle.bundle"); + execGit(["bundle", "create", bundlePath, `refs/heads/${branchName}`], { cwd: sourceRepo }); + + fetchBaseCommit(targetRepo, sourceRepo, baseSha, branchName); + const bundleRef = "refs/bundles/test-simple-bundle"; + execGit(["fetch", bundlePath, `refs/heads/${branchName}:${bundleRef}`], { cwd: targetRepo }); + + const actualFiles = await getBundlePreApplyFiles(createExecApi(targetRepo), {}, baseSha, bundleRef); + + expect(actualFiles.sort()).toEqual([".changeset/fix.md", "docs/guide.md"]); + }); + + it("includes files introduced through merge-commit bundle history", async () => { + const branchName = "autoloop/merge-bundle"; + const sourceRepo = createRepo("push-pr-merge-source-"); + const targetRepo = createRepo("push-pr-merge-target-"); + tempDirs.push(sourceRepo, targetRepo); + + writeRepoFile(sourceRepo, "README.md", "base\n"); + execGit(["add", "README.md"], { cwd: sourceRepo }); + execGit(["commit", "-m", "base"], { cwd: sourceRepo }); + execGit(["branch", "-M", "main"], { cwd: sourceRepo }); + const baseSha = execGit(["rev-parse", "HEAD"], { cwd: sourceRepo }).stdout.trim(); + + execGit(["checkout", "-b", "feature"], { cwd: sourceRepo }); + writeRepoFile(sourceRepo, "feature.txt", "feature branch change\n"); + execGit(["add", "feature.txt"], { cwd: sourceRepo }); + execGit(["commit", "-m", "feature commit"], { cwd: sourceRepo }); + + execGit(["checkout", "main"], { cwd: sourceRepo }); + writeRepoFile(sourceRepo, "main.txt", "main branch change\n"); + execGit(["add", "main.txt"], { cwd: sourceRepo }); + execGit(["commit", "-m", "main commit"], { cwd: sourceRepo }); + execGit(["merge", "--no-ff", "feature", "-m", "merge feature"], { cwd: sourceRepo }); + execGit(["checkout", "-b", branchName], { cwd: sourceRepo }); + + const bundlePath = path.join(sourceRepo, "merge.bundle"); + execGit(["bundle", "create", bundlePath, `refs/heads/${branchName}`], { cwd: sourceRepo }); + + fetchBaseCommit(targetRepo, sourceRepo, baseSha, branchName); + const bundleRef = "refs/bundles/test-merge-bundle"; + execGit(["fetch", bundlePath, `refs/heads/${branchName}:${bundleRef}`], { cwd: targetRepo }); + + const actualFiles = await getBundlePreApplyFiles(createExecApi(targetRepo), {}, baseSha, bundleRef); + + expect(actualFiles.sort()).toEqual(["feature.txt", "main.txt"]); + }); +}); From ec2512b73979b1c6668fe70fe613a5b764830544 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 19:21:09 +0000 Subject: [PATCH 5/5] chore: merge main and recompile workflows Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/mcp.json | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/.github/mcp.json b/.github/mcp.json index 341a8b87588..3d0d72b2af5 100644 --- a/.github/mcp.json +++ b/.github/mcp.json @@ -3,18 +3,8 @@ "github-agentic-workflows": { "type": "local", "command": "gh", - "args": [ - "aw", - "mcp-server" - ], - "tools": [ - "compile", - "audit", - "logs", - "inspect", - "status", - "audit-diff" - ] + "args": ["aw", "mcp-server"], + "tools": ["compile", "audit", "logs", "inspect", "status", "audit-diff"] } } -} \ No newline at end of file +}