diff --git a/.github/workflows/daily-arxiv-researcher.lock.yml b/.github/workflows/daily-arxiv-researcher.lock.yml index bbc80366943..1039b589fa7 100644 --- a/.github/workflows/daily-arxiv-researcher.lock.yml +++ b/.github/workflows/daily-arxiv-researcher.lock.yml @@ -919,7 +919,7 @@ jobs: touch /tmp/gh-aw/agent-step-summary.md (umask 177 && touch /tmp/gh-aw/agent-stdio.log) # shellcheck disable=SC2016 - printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","anthropic.com","api.anthropic.com","api.github.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","cdn.playwright.dev","codeload.github.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","files.pythonhosted.org","ghcr.io","github-cloud.githubusercontent.com","github-cloud.s3.amazonaws.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","lfs.github.com","objects.githubusercontent.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","playwright.download.prss.microsoft.com","ppa.launchpad.net","pypi.org","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","sentry.io","statsig.anthropic.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"],"isolation":true,"topologyAttach":["awmg-mcpg"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxCacheMisses":5,"maxAiCredits":250,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.5","gpt-5.6","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"auto":["copilot/auto","large"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex","kimi"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"fable":["copilot/*fable*","anthropic/*fable*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-3.6-flash":["copilot/gemini-3.6*flash*","google/gemini-3.6*flash*","gemini/gemini-3.6*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-omni":["copilot/gemini-omni*","google/gemini-omni*","gemini/gemini-omni*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.1":["copilot/gpt-5.1*","openai/gpt-5.1*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"gpt-5.6":["copilot/gpt-5.6*","openai/gpt-5.6*"],"grok":["copilot/*grok*","openai/*grok*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"image-generation":["copilot/gpt-image*","openai/gpt-image*","openai/chatgpt-image*","copilot/gemini-*image*","google/gemini-*image*","gemini/gemini-*image*","google/imagen*"],"kimi":["copilot/kimi*","openai/kimi*"],"kiwi":["copilot/kiwi*","openai/kiwi*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"lyria":["google/lyria*","gemini/lyria*","copilot/lyria*"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mai-code-1-flash-picker":["copilot/MAI-Code-1-Flash-picker*","copilot/mai-code-1-flash-picker*","openai/MAI-Code-1-Flash-picker*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"raptor-mini":["copilot/raptor*","openai/raptor*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-5*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*","anthropic/*sonnet-5*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"veo":["google/veo*","gemini/veo*"],"vision":["copilot/gemini-*image*","google/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},"logging":{"proxyLogsDir":"/tmp/gh-aw/sandbox/firewall/logs","auditDir":"/tmp/gh-aw/sandbox/firewall/audit"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","anthropic.com","api.anthropic.com","api.github.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","cdn.playwright.dev","codeload.github.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","files.pythonhosted.org","ghcr.io","github-cloud.githubusercontent.com","github-cloud.s3.amazonaws.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","lfs.github.com","objects.githubusercontent.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","playwright.download.prss.microsoft.com","ppa.launchpad.net","pypi.org","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","sentry.io","statsig.anthropic.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"],"isolation":true,"topologyAttach":["awmg-mcpg"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxCacheMisses":5,"maxAiCredits":250,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.5","gpt-5.6","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"auto":["copilot/auto","large"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex","kimi"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"detection":["small"],"evals":["small"],"fable":["copilot/*fable*","anthropic/*fable*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","google/nano-banana*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-3.6-flash":["copilot/gemini-3.6*flash*","google/gemini-3.6*flash*","gemini/gemini-3.6*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-omni":["copilot/gemini-omni*","google/gemini-omni*","gemini/gemini-omni*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.1":["copilot/gpt-5.1*","openai/gpt-5.1*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"gpt-5.6":["copilot/gpt-5.6*","openai/gpt-5.6*"],"grok":["copilot/*grok*","openai/*grok*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"image-generation":["copilot/gpt-image*","openai/gpt-image*","openai/chatgpt-image*","copilot/gemini-*image*","google/gemini-*image*","gemini/gemini-*image*","google/imagen*"],"kimi":["copilot/kimi*","openai/kimi*"],"kiwi":["copilot/kiwi*","openai/kiwi*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"lyria":["google/lyria*","gemini/lyria*","copilot/lyria*"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mai-code-1-flash-picker":["copilot/MAI-Code-1-Flash-picker*","copilot/mai-code-1-flash-picker*","openai/MAI-Code-1-Flash-picker*"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"nano-banana":["copilot/nano-banana*","google/nano-banana*","gemini/nano-banana*"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"raptor-mini":["copilot/raptor*","openai/raptor*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"small-agent":["haiku","gpt-5-mini","gemini-flash"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-5*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*","anthropic/*sonnet-5*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"veo":["google/veo*","gemini/veo*"],"vision":["copilot/gemini-*image*","google/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},"logging":{"proxyLogsDir":"/tmp/gh-aw/sandbox/firewall/logs","auditDir":"/tmp/gh-aw/sandbox/firewall/audit"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -1592,7 +1592,7 @@ jobs: touch /tmp/gh-aw/agent-step-summary.md (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"anthropic.com\",\"api.anthropic.com\",\"api.github.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"cdn.playwright.dev\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"files.pythonhosted.org\",\"ghcr.io\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"playwright.download.prss.microsoft.com\",\"ppa.launchpad.net\",\"pypi.org\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"sentry.io\",\"statsig.anthropic.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"anthropic.com\",\"api.anthropic.com\",\"api.github.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"cdn.playwright.dev\",\"codeload.github.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"files.pythonhosted.org\",\"ghcr.io\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"playwright.download.prss.microsoft.com\",\"ppa.launchpad.net\",\"pypi.org\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"sentry.io\",\"statsig.anthropic.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -1613,9 +1613,10 @@ jobs: fi # shellcheck disable=SC1003,SC2016,SC2086 awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --tty --env-all --exclude-env ANTHROPIC_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/claude_harness.cjs claude --print --no-chrome --allowed-tools '\''Bash,BashOutput,Edit(/tmp/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit(/tmp/*),NotebookRead,Read,Read(/tmp/*),Task,TodoWrite,Write(/tmp/*)'\'' --debug-file /tmp/gh-aw/threat-detection/detection.log --verbose --permission-mode acceptEdits --output-format stream-json --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt${GH_AW_MODEL_DETECTION_CLAUDE:+ --model "$GH_AW_MODEL_DETECTION_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/claude_harness.cjs claude --print --no-chrome --allowed-tools '\''Bash,BashOutput,Edit(/tmp/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit(/tmp/*),NotebookRead,Read,Read(/tmp/*),Task,TodoWrite,Write(/tmp/*)'\'' --debug-file /tmp/gh-aw/threat-detection/detection.log --verbose --permission-mode acceptEdits --output-format stream-json --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + ANTHROPIC_MODEL: detection BASH_DEFAULT_TIMEOUT_MS: 60000 BASH_MAX_TIMEOUT_MS: 60000 CLAUDE_CODE_DISABLE_FAST_MODE: 1 @@ -1624,7 +1625,6 @@ jobs: DISABLE_TELEMETRY: 1 GH_AW_LLM_PROVIDER: anthropic GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} - GH_AW_MODEL_DETECTION_CLAUDE: ${{ vars.GH_AW_MODEL_DETECTION_CLAUDE || vars.GH_AW_DEFAULT_MODEL_CLAUDE || 'claude-sonnet-5' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_VERSION: dev diff --git a/.github/workflows/smoke-checkout-pr-dispatch.lock.yml b/.github/workflows/smoke-checkout-pr-dispatch.lock.yml index bb61f3eb769..794e252976c 100644 --- a/.github/workflows/smoke-checkout-pr-dispatch.lock.yml +++ b/.github/workflows/smoke-checkout-pr-dispatch.lock.yml @@ -895,7 +895,7 @@ jobs: export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.grafana.net\",\"*.sentry.io\",\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.grafana.net\",\"*.sentry.io\",\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"crl.geotrust.com\",\"crl.globalsign.com\",\"crl.identrust.com\",\"crl.sectigo.com\",\"crl.thawte.com\",\"crl.usertrust.com\",\"crl.verisign.com\",\"crl3.digicert.com\",\"crl4.digicert.com\",\"crls.ssl.com\",\"github.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"registry.npmjs.org\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"telemetry.enterprise.githubcopilot.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -1547,7 +1547,7 @@ jobs: export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -1574,7 +1574,7 @@ jobs: COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + COPILOT_MODEL: detection GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 10546c66099..379294592d9 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -416,7 +416,10 @@ async function rewriteBundleBranchAsSingleCommit(baseBranch, execApi, bundleFile } core.warning(`Rewriting bundled commits to a single linear commit for signed push compatibility (base: ${baseRef})`); - const newHead = await linearizeRangeAsCommit(baseRef, commitHeadline, execApi, { excludedFiles: options.excludedFiles }); + const newHead = await linearizeRangeAsCommit(baseRef, commitHeadline, execApi, { + excludedFiles: options.excludedFiles, + rebaseOnto: fallbackBaseRef, + }); core.info(`Bundle rewrite completed (new HEAD: ${newHead})`); } diff --git a/actions/setup/js/create_pull_request_bundle_integration.test.cjs b/actions/setup/js/create_pull_request_bundle_integration.test.cjs index 5a12a14b97e..25844684514 100644 --- a/actions/setup/js/create_pull_request_bundle_integration.test.cjs +++ b/actions/setup/js/create_pull_request_bundle_integration.test.cjs @@ -5,14 +5,27 @@ * bundle handling for checked-out target branches. */ -import { describe, it, expect, afterEach, vi } from "vitest"; +import { describe, it, expect, beforeAll, afterEach, vi } from "vitest"; import { createRequire } from "module"; +import { fileURLToPath } from "url"; import fs from "fs"; import os from "os"; import path from "path"; import { spawnSync } from "child_process"; const require = createRequire(import.meta.url); +const promptsSourceDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../md"); + +/** + * create_pull_request.cjs reads the disclosure-header prompt template at module + * load time. Ensure the file is present before the first `require` so the module + * can be loaded successfully in any environment (local dev or CI). + */ +function ensureDisclosureHeaderPrompt() { + const promptsDir = path.join(process.env.RUNNER_TEMP || os.tmpdir(), "gh-aw", "prompts"); + fs.mkdirSync(promptsDir, { recursive: true }); + fs.copyFileSync(path.join(promptsSourceDir, "safe_outputs_disclosure_header.md"), path.join(promptsDir, "safe_outputs_disclosure_header.md")); +} global.core = { debug: vi.fn(), @@ -77,6 +90,10 @@ function createExecApi(cwd, onExec) { describe("create_pull_request bundle integration", () => { const tempDirs = []; + beforeAll(() => { + ensureDisclosureHeaderPrompt(); + }); + afterEach(() => { for (const tempDir of tempDirs.splice(0)) { fs.rmSync(tempDir, { recursive: true, force: true }); @@ -529,18 +546,21 @@ describe("create_pull_request bundle integration", () => { // 8. Verify the synthesized commit. // // a) "agent-file.txt" must be in the working tree (agent's actual change was preserved). - // b) "base-drift.txt" must NOT be in the working tree — the rewrite was anchored at A - // (which predates the drift), so the feature branch tree never included it. + // b) "base-drift.txt" must NOT appear in `git diff origin/main..HEAD` (the PR diff). + // After rebase-onto B the file is present in the working tree (inherited from origin/main), + // but must not appear as added or deleted relative to origin/main — the PR diff is clean. // c) The diff HEAD^..HEAD must show "agent-file.txt" as added. - // d) "base-drift.txt" must NOT appear in the diff at all — not deleted, not added. - // (With the naive soft-reset to origin/main=B, it would appear as "D base-drift.txt".) + // d) "base-drift.txt" must NOT appear in HEAD^..HEAD — not deleted, not added. + // (With a naive soft-reset to origin/main=B, it would appear as "D base-drift.txt".) // e) HEAD must have exactly one parent (linear, not a merge commit). const agentFilePresent = fs.existsSync(path.join(safeOutputsRepo, "agent-file.txt")); - const baseDriftInWorkingTree = fs.existsSync(path.join(safeOutputsRepo, "base-drift.txt")); expect(agentFilePresent).toBe(true); - // base-drift.txt belongs to origin/main (B), not to the feature branch anchored at A. - expect(baseDriftInWorkingTree).toBe(false); + // base-drift.txt must not appear in the PR diff (origin/main..HEAD). After rebase-onto B + // the file is present in the working tree (inherited from origin/main) but must not be + // listed as added or deleted relative to origin/main. + const prDiffStat = execGit(["diff", "--name-status", "origin/main", "HEAD"], { cwd: safeOutputsRepo }).stdout; + expect(prDiffStat).not.toMatch(/base-drift\.txt/); // The HEAD commit (linearized) should show "agent-file.txt" as added. const headStat = execGit(["show", "--stat", "HEAD"], { cwd: safeOutputsRepo }).stdout; @@ -557,4 +577,948 @@ describe("create_pull_request bundle integration", () => { const parentShas = parentLine.split(/\s+/).filter(Boolean); expect(parentShas).toHaveLength(1); }); + + it("rewriteBundleBranchAsSingleCommit squashes multiple linear commits into one", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // The signed-push path requires a single-commit head. An agent may produce + // several linear commits (no merge topology). Verify that + // rewriteBundleBranchAsSingleCommit collapses all of them into one commit + // that contains every file change from the full range, and that the result + // has exactly one parent rooted on origin/main. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/multi-commit-squash"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-squash-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-squash-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-squash-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote and seed main. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Agent creates the feature branch with three separate commits. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "file-a.txt"), "content a\n"); + execGit(["add", "file-a.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: add file-a"], { cwd: agentRepo }); + + fs.writeFileSync(path.join(agentRepo, "file-b.txt"), "content b\n"); + execGit(["add", "file-b.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: add file-b"], { cwd: agentRepo }); + + fs.writeFileSync(path.join(agentRepo, "file-c.txt"), "content c\n"); + execGit(["add", "file-c.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: add file-c"], { cwd: agentRepo }); + + // Confirm three commits beyond main before bundling. + const commitCount = Number(execGit(["rev-list", "--count", `${agentBaseCommit}..HEAD`], { cwd: agentRepo }).stdout.trim()); + expect(commitCount).toBe(3); + + // 3. Bundle the feature branch (prereq = agentBaseCommit). + const bundlePath = path.join(agentRepo, "multi-commit.bundle"); + execGit(["bundle", "create", bundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // 4. Safe-outputs runner: fresh clone of origin/main, apply bundle. + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 5. Rewrite the three commits to one. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath); + + // 6. HEAD must have exactly one parent. + const parentLine = execGit(["log", "-1", "--format=%P", "HEAD"], { cwd: safeOutputsRepo }).stdout.trim(); + const parentShas = parentLine.split(/\s+/).filter(Boolean); + expect(parentShas).toHaveLength(1); + + // 7. All three files must be present in the working tree. + expect(fs.existsSync(path.join(safeOutputsRepo, "file-a.txt"))).toBe(true); + expect(fs.existsSync(path.join(safeOutputsRepo, "file-b.txt"))).toBe(true); + expect(fs.existsSync(path.join(safeOutputsRepo, "file-c.txt"))).toBe(true); + + // 8. The diff origin/main..HEAD must contain exactly those three files. + const diffNames = execGit(["diff", "--name-only", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim().split("\n").filter(Boolean).sort(); + expect(diffNames).toEqual(["file-a.txt", "file-b.txt", "file-c.txt"]); + + // 9. Exactly one commit beyond origin/main. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + }); + + it("rewriteBundleBranchAsSingleCommit excludes specified files from the linearized commit", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // When the caller specifies excludedFiles (e.g. secrets or build artifacts), + // the rewritten single commit must not contain those files. This verifies + // that excludedFiles are forwarded through rewriteBundleBranchAsSingleCommit + // → linearizeRangeAsCommit and are unstaged before the squash commit is made. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/excluded-files-rewrite"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-excl-rewrite-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-excl-rewrite-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-excl-rewrite-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote and seed main. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Agent adds both a kept file and a file that should be excluded. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "kept.txt"), "agent change\n"); + fs.writeFileSync(path.join(agentRepo, "secret.txt"), "sensitive data\n"); + execGit(["add", "kept.txt", "secret.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: add kept and secret files"], { cwd: agentRepo }); + + // 3. Bundle the feature branch. + const bundlePath = path.join(agentRepo, "excluded-files.bundle"); + execGit(["bundle", "create", bundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // 4. Safe-outputs runner: fresh clone, apply bundle. + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 5. Rewrite with secret.txt excluded. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath, { + excludedFiles: ["secret.txt"], + }); + + // 6. The diff origin/main..HEAD must contain only kept.txt. + const diffNames = execGit(["diff", "--name-only", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim().split("\n").filter(Boolean).sort(); + expect(diffNames).toEqual(["kept.txt"]); + + // 7. secret.txt must not appear in the commit diff at all (not added, not deleted). + const commitDiff = execGit(["diff", "--name-status", "HEAD^", "HEAD"], { cwd: safeOutputsRepo }).stdout; + expect(commitDiff).not.toMatch(/secret\.txt/); + + // 8. Exactly one linearized commit beyond origin/main. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + }); + + it("rewriteBundleBranchAsSingleCommit falls back to origin/main when bundle declares no prerequisites (self-contained bundle)", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // Some agents bundle with `git bundle create refs/heads/` + // without a `..` range exclusion. This produces a self-contained + // bundle that includes all reachable history — meaning the bundle has NO + // recorded prerequisites (getBundlePrerequisites returns []). + // + // rewriteBundleBranchAsSingleCommit must take the "prereqs.length === 0" + // fallback branch and use origin/ as the linearization base, still + // producing a single correct commit. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/self-contained-bundle"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-no-prereq-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-no-prereq-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-no-prereq-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote and seed main. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + + // 2. Agent creates feature branch and adds a file. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "agent-file.txt"), "agent change\n"); + execGit(["add", "agent-file.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: agent adds a file"], { cwd: agentRepo }); + + // 3. Create a SELF-CONTAINED bundle (no range exclusion → no prerequisites). + // The bundle includes the full reachable history and records no prereqs. + const bundlePath = path.join(agentRepo, "self-contained.bundle"); + execGit(["bundle", "create", bundlePath, `refs/heads/${branchName}`], { cwd: agentRepo }); + + // Confirm the bundle has no prerequisites (git reports "complete history"). + const verifyOut = execGit(["bundle", "verify", bundlePath], { cwd: agentRepo, allowFailure: true }); + const verifyText = `${verifyOut.stdout || ""}\n${verifyOut.stderr || ""}`; + expect(verifyText).toMatch(/complete history/i); + + // 4. Safe-outputs runner: fresh clone of origin/main. + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 5. Rewrite using the self-contained bundle; function should log + // "Bundle declares no prerequisites; falling back to origin/main". + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath); + + // 6. Exactly one commit beyond origin/main. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + + // 7. The diff origin/main..HEAD must contain only agent-file.txt. + const diffNames = execGit(["diff", "--name-only", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim().split("\n").filter(Boolean).sort(); + expect(diffNames).toEqual(["agent-file.txt"]); + + // 8. Exactly one parent (linearized, not a merge commit). + const parentLine = execGit(["log", "-1", "--format=%P", "HEAD"], { cwd: safeOutputsRepo }).stdout.trim(); + const parentShas = parentLine.split(/\s+/).filter(Boolean); + expect(parentShas).toHaveLength(1); + }); + + it("rewriteBundleBranchAsSingleCommit falls back to origin/main when bundle has multiple prerequisites", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // When the bundle range includes a commit whose parent is NOT reachable + // from the range exclusion commit, the bundle records TWO prerequisites: + // + // ROOT ─── MAIN (main branch, pushed to origin as agent base) + // └────── INDEPENDENT (side branch, never on main) + // + // Feature: MAIN → FEAT(agent-work.txt) → MERGE(FEAT, INDEPENDENT) + // Bundle: MAIN..refs/heads/feature + // • Included: FEAT, INDEPENDENT, MERGE + // • Prerequisites: MAIN (parent of FEAT) + ROOT (parent of INDEPENDENT) + // + // rewriteBundleBranchAsSingleCommit falls back to origin/main when + // prereqs.length > 1 and must still produce a single correct commit. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/multi-prereq-rewrite"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-multi-prereq-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-multi-prereq-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-multi-prereq-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote. ROOT is the initial commit; MAIN is the + // second commit on main. The agent branches from MAIN. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + const rootCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + fs.writeFileSync(path.join(agentRepo, "setup.txt"), "project setup\n"); + execGit(["add", "setup.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "chore: project setup"], { cwd: agentRepo }); + execGit(["push", "origin", "main"], { cwd: agentRepo }); + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Create an INDEPENDENT branch off ROOT (not on main's history). + // This branch is never pushed to origin. + execGit(["checkout", "-b", "side-branch", rootCommit], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "side.txt"), "side branch content\n"); + execGit(["add", "side.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: add side.txt from independent branch"], { cwd: agentRepo }); + + // 3. Agent creates feature branch from MAIN and makes a commit. + execGit(["checkout", "-b", branchName, agentBaseCommit], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "agent-work.txt"), "agent work\n"); + execGit(["add", "agent-work.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: agent work"], { cwd: agentRepo }); + + // 4. Agent merges the independent side branch into the feature branch. + execGit(["merge", "--no-ff", "side-branch", "-m", "feat: merge side branch"], { cwd: agentRepo }); + + // 5. Create the bundle with range MAIN..feature. + // FEAT and INDEPENDENT are included (reachable from feature but not from MAIN). + // Prerequisites: MAIN (parent of FEAT, not included) + + // ROOT (parent of INDEPENDENT, not included) = TWO prereqs. + const bundlePath = path.join(agentRepo, "multi-prereq.bundle"); + execGit(["bundle", "create", bundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // Confirm the bundle records both prereq SHAs in git bundle verify output. + const verifyOut = execGit(["bundle", "verify", bundlePath], { cwd: agentRepo, allowFailure: true }); + const verifyText = `${verifyOut.stdout || ""}\n${verifyOut.stderr || ""}`; + expect(verifyText).toMatch(new RegExp(rootCommit.slice(0, 8), "i")); + expect(verifyText).toMatch(new RegExp(agentBaseCommit.slice(0, 8), "i")); + + // 6. Safe-outputs runner: fresh clone of origin/main (has ROOT and MAIN, + // satisfying both bundle prerequisites). + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 7. Rewrite: 2 prereqs detected → falls back to origin/main (= MAIN). + // All changes relative to origin/main are captured in one squash commit. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath); + + // 8. Exactly one commit beyond origin/main. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + + // 9. The diff origin/main..HEAD covers both the agent's file and the merged-in + // side file (all changes relative to origin/main = MAIN). + const diffNames = execGit(["diff", "--name-only", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim().split("\n").filter(Boolean).sort(); + expect(diffNames).toContain("agent-work.txt"); + expect(diffNames).toContain("side.txt"); + + // 10. Exactly one parent (linearized, not a merge commit). + const parentLine = execGit(["log", "-1", "--format=%P", "HEAD"], { cwd: safeOutputsRepo }).stdout.trim(); + const parentShas = parentLine.split(/\s+/).filter(Boolean); + expect(parentShas).toHaveLength(1); + }); + + it("rewriteBundleBranchAsSingleCommit falls back to origin/main when bundle prerequisite SHA is not in local object store", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // rewriteBundleBranchAsSingleCommit verifies reachability of the bundle's + // prerequisite SHA via `git cat-file -e ^{commit}`. If the SHA is + // not in the local object store (e.g., in a shallow clone, or when the + // prereq comes from a repo whose history was never fetched), the function + // falls back to origin/main. + // + // To trigger this path deterministically, we pass a bundle file whose + // prerequisite commit is from an unrelated repository — a SHA that will + // never exist in the safe-outputs checkout — while the feature branch + // itself was correctly applied via a separate (full) bundle. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/prereq-inaccessible"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-inaccessible-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-inaccessible-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-inaccessible-so-")); + // Unrelated repo whose history never enters safeOutputsRepo. + const unrelatedRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-inaccessible-unrelated-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo, unrelatedRepo); + + // 1. Normal project setup. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Agent creates feature branch and adds a file. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "agent-file.txt"), "agent change\n"); + execGit(["add", "agent-file.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: agent adds a file"], { cwd: agentRepo }); + + // 3. Create the real bundle (with correct prereq) for applyBundleToBranch. + const realBundlePath = path.join(agentRepo, "real.bundle"); + execGit(["bundle", "create", realBundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // 4. Create an UNRELATED repo and bundle from it. + // This bundle's prerequisite SHA is from unrelatedRepo and will NEVER + // exist in safeOutputsRepo. + execGit(["init"], { cwd: unrelatedRepo }); + execGit(["config", "user.name", "Unrelated"], { cwd: unrelatedRepo }); + execGit(["config", "user.email", "unrelated@example.com"], { cwd: unrelatedRepo }); + fs.writeFileSync(path.join(unrelatedRepo, "base.txt"), "unrelated base\n"); + execGit(["add", "base.txt"], { cwd: unrelatedRepo }); + execGit(["commit", "-m", "unrelated base"], { cwd: unrelatedRepo }); + const unrelatedBase = execGit(["rev-parse", "HEAD"], { cwd: unrelatedRepo }).stdout.trim(); + execGit(["checkout", "-b", "feature-side"], { cwd: unrelatedRepo }); + fs.writeFileSync(path.join(unrelatedRepo, "side.txt"), "side\n"); + execGit(["add", "side.txt"], { cwd: unrelatedRepo }); + execGit(["commit", "-m", "side commit"], { cwd: unrelatedRepo }); + // Bundle with prereq = unrelatedBase (a SHA that will NOT be in safeOutputsRepo). + const fakeBundlePath = path.join(unrelatedRepo, "fake.bundle"); + execGit(["bundle", "create", fakeBundlePath, `${unrelatedBase}..refs/heads/feature-side`], { cwd: unrelatedRepo }); + + // Confirm the fake bundle's prereq is unrelatedBase. + const fakeVerify = execGit(["bundle", "verify", fakeBundlePath], { cwd: unrelatedRepo, allowFailure: true }); + const fakeVerifyText = `${fakeVerify.stdout || ""}\n${fakeVerify.stderr || ""}`; + expect(fakeVerifyText).toMatch(new RegExp(unrelatedBase.slice(0, 8), "i")); + + // 5. Safe-outputs runner: fresh clone, apply the REAL bundle. + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(realBundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 6. Confirm the unrelated prereq is not in safeOutputsRepo. + const catFileResult = execGit(["cat-file", "-e", `${unrelatedBase}^{commit}`], { cwd: safeOutputsRepo, allowFailure: true }); + expect(catFileResult.status).not.toBe(0); + + // 7. Rewrite using the FAKE bundle path (whose prereq is not accessible). + // The function must detect `cat-file -e` failure and fall back to origin/main. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), fakeBundlePath); + + // 8. The diff origin/main..HEAD must contain only agent-file.txt (correct result + // from the fallback-to-origin/main path). + const diffNames = execGit(["diff", "--name-only", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim().split("\n").filter(Boolean).sort(); + expect(diffNames).toEqual(["agent-file.txt"]); + + // 9. Exactly one linearized commit beyond origin/main. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + }); + + it("rewriteBundleBranchAsSingleCommit correctly preserves a file deletion", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // Existing tests cover file additions. This test verifies that when the + // agent DELETES a file that was present on the base branch, + // rewriteBundleBranchAsSingleCommit produces a linearized commit that also + // deletes the file — the deletion is not silently dropped. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/delete-file-rewrite"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-del-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-del-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-del-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote with a base commit that includes a file to delete. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + fs.writeFileSync(path.join(agentRepo, "to-delete.txt"), "this file will be removed\n"); + execGit(["add", "README.md", "to-delete.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Agent deletes to-delete.txt and adds a new file in the same commit. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + execGit(["rm", "to-delete.txt"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "replacement.txt"), "replacement content\n"); + execGit(["add", "replacement.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: remove to-delete.txt, add replacement.txt"], { cwd: agentRepo }); + + // 3. Bundle the feature branch. + const bundlePath = path.join(agentRepo, "delete-file.bundle"); + execGit(["bundle", "create", bundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // 4. Safe-outputs runner: fresh clone, apply bundle. + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 5. Rewrite. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath); + + // 6. to-delete.txt must be ABSENT from the working tree (it was deleted). + expect(fs.existsSync(path.join(safeOutputsRepo, "to-delete.txt"))).toBe(false); + + // 7. replacement.txt must be present. + expect(fs.existsSync(path.join(safeOutputsRepo, "replacement.txt"))).toBe(true); + + // 8. The PR diff (origin/main..HEAD) must show: + // - to-delete.txt as DELETED (D) + // - replacement.txt as ADDED (A) + const prDiffStatus = execGit(["diff", "--name-status", "origin/main", "HEAD"], { cwd: safeOutputsRepo }).stdout; + expect(prDiffStatus).toMatch(/^D\s+to-delete\.txt/m); + expect(prDiffStatus).toMatch(/^A\s+replacement\.txt/m); + + // 9. The commit diff (HEAD^..HEAD) must also show the deletion. + const commitDiffStatus = execGit(["diff", "--name-status", "HEAD^", "HEAD"], { cwd: safeOutputsRepo }).stdout; + expect(commitDiffStatus).toMatch(/^D\s+to-delete\.txt/m); + expect(commitDiffStatus).toMatch(/^A\s+replacement\.txt/m); + + // 10. Exactly one linearized commit. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + }); + + it("rewriteBundleBranchAsSingleCommit correctly preserves file modifications", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // Existing tests only cover new file additions. This test verifies that + // when the agent MODIFIES an existing file (changes its content), + // rewriteBundleBranchAsSingleCommit produces a linearized commit that + // reflects the final modified state — both the diff and the working tree + // must show the updated content. + // + // It also covers the case where the agent makes multiple sequential commits + // that each update the same file: only the final state matters. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/modify-file-rewrite"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-mod-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-mod-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-mod-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote with a base file to modify. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "config.txt"), "version: 1\n"); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "config.txt", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Agent modifies config.txt twice (tests that only the final state is captured) + // and appends to README.md. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + + fs.writeFileSync(path.join(agentRepo, "config.txt"), "version: 2\n"); + execGit(["add", "config.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "refactor: bump version to 2"], { cwd: agentRepo }); + + fs.writeFileSync(path.join(agentRepo, "config.txt"), "version: 3\nfeature-flag: true\n"); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n\nUpdated docs.\n"); + execGit(["add", "config.txt", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "refactor: final config and readme update"], { cwd: agentRepo }); + + // 3. Bundle the feature branch (two commits in range). + const bundlePath = path.join(agentRepo, "modify-file.bundle"); + execGit(["bundle", "create", bundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // Confirm two commits in range before squash. + const commitCount = Number(execGit(["rev-list", "--count", `${agentBaseCommit}..HEAD`], { cwd: agentRepo }).stdout.trim()); + expect(commitCount).toBe(2); + + // 4. Safe-outputs runner: fresh clone, apply bundle. + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 5. Rewrite two commits into one. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath); + + // 6. Working tree must reflect the final modified state. + expect(fs.readFileSync(path.join(safeOutputsRepo, "config.txt"), "utf8")).toBe("version: 3\nfeature-flag: true\n"); + expect(fs.readFileSync(path.join(safeOutputsRepo, "README.md"), "utf8")).toBe("# Project\n\nUpdated docs.\n"); + + // 7. The PR diff must show config.txt and README.md as modified (M). + const prDiffStatus = execGit(["diff", "--name-status", "origin/main", "HEAD"], { cwd: safeOutputsRepo }).stdout; + expect(prDiffStatus).toMatch(/^M\s+README\.md/m); + expect(prDiffStatus).toMatch(/^M\s+config\.txt/m); + + // 8. Exactly one linearized commit beyond origin/main. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + + // 9. Exactly one parent. + const parentLine = execGit(["log", "-1", "--format=%P", "HEAD"], { cwd: safeOutputsRepo }).stdout.trim(); + const parentShas = parentLine.split(/\s+/).filter(Boolean); + expect(parentShas).toHaveLength(1); + }); + + it("rewriteBundleBranchAsSingleCommit excludes a modified existing file (unstages modification, not deletion)", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // The existing excluded-files test only covers newly-added files. When the + // excluded file already exists on the base branch and the agent modifies it, + // `git reset HEAD -- ` must restore the index to the base version + // rather than removing the file entirely. This test verifies that: + // + // 1. The modified content is NOT committed (file stays at base version). + // 2. The non-excluded new file IS committed. + // 3. The excluded file is absent from both the PR diff and the commit diff. + // 4. The working tree still reflects the base version of the excluded file + // (the rebase onto the current base preserves it correctly). + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/exclude-modified-file"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-excl-mod-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-excl-mod-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-excl-mod-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote with a base that contains the file to be modified. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "config.txt"), "version: 1\n"); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "config.txt", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Agent modifies config.txt (should be excluded) and adds a new file (kept). + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "config.txt"), "version: 2\nsecret-token: abc123\n"); + fs.writeFileSync(path.join(agentRepo, "new-feature.txt"), "new feature\n"); + execGit(["add", "config.txt", "new-feature.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: update config and add feature"], { cwd: agentRepo }); + + // 3. Bundle the feature branch. + const bundlePath = path.join(agentRepo, "excl-mod.bundle"); + execGit(["bundle", "create", bundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // 4. Safe-outputs runner: fresh clone, apply bundle. + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 5. Rewrite with config.txt excluded. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath, { + excludedFiles: ["config.txt"], + }); + + // 6. config.txt must be at its BASE version (v1, not v2) in the working tree. + // The excluded modification must not be committed. + expect(fs.readFileSync(path.join(safeOutputsRepo, "config.txt"), "utf8")).toBe("version: 1\n"); + + // 7. new-feature.txt must be present (non-excluded file was kept). + expect(fs.existsSync(path.join(safeOutputsRepo, "new-feature.txt"))).toBe(true); + + // 8. PR diff must show only new-feature.txt as added; config.txt must be absent. + const prDiffStatus = execGit(["diff", "--name-status", "origin/main", "HEAD"], { cwd: safeOutputsRepo }).stdout; + expect(prDiffStatus).toMatch(/^A\s+new-feature\.txt/m); + expect(prDiffStatus).not.toMatch(/config\.txt/); + + // 9. Commit diff (HEAD^..HEAD) must not mention config.txt. + const commitDiff = execGit(["diff", "--name-status", "HEAD^", "HEAD"], { cwd: safeOutputsRepo }).stdout; + expect(commitDiff).not.toMatch(/config\.txt/); + + // 10. Exactly one linearized commit. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + }); + + it("rewriteBundleBranchAsSingleCommit handles excluded files together with base drift", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // Tests excludedFiles and base-drift correction independently already exist, + // but their COMBINATION is untested. This is a realistic production scenario: + // + // 1. Agent runs while main is at A, adds kept.txt + secret.txt. + // 2. Main advances to B (drift.txt added) before the runner processes it. + // 3. rewriteBundleBranchAsSingleCommit is called with secret.txt excluded. + // + // The linearized commit must contain ONLY kept.txt: + // • secret.txt excluded (excluded file feature). + // • drift.txt absent from PR diff (base-drift feature: prereq A, not B). + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/excl-and-drift"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-excl-drift-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-excl-drift-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-excl-drift-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote at commit A. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Agent adds kept.txt and secret.txt while main is still at A. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "kept.txt"), "agent change\n"); + fs.writeFileSync(path.join(agentRepo, "secret.txt"), "sensitive data\n"); + execGit(["add", "kept.txt", "secret.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: add kept and secret files"], { cwd: agentRepo }); + + // 3. Bundle while main is STILL at A (before drift). + const bundlePath = path.join(agentRepo, "excl-drift.bundle"); + execGit(["bundle", "create", bundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // 4. AFTER bundling: base drifts to B (drift.txt added). + execGit(["checkout", "main"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "drift.txt"), "base drift\n"); + execGit(["add", "drift.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "chore: base drift"], { cwd: agentRepo }); + execGit(["push", "origin", "main"], { cwd: agentRepo }); + + // 5. Safe-outputs runner clones updated origin/main (B includes drift.txt). + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + const newOriginMainSha = execGit(["rev-parse", "origin/main"], { cwd: safeOutputsRepo }).stdout.trim(); + expect(newOriginMainSha).not.toBe(agentBaseCommit); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 6. Rewrite with secret.txt excluded. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath, { + excludedFiles: ["secret.txt"], + }); + + // 7. kept.txt must be present (agent's non-excluded change preserved). + expect(fs.existsSync(path.join(safeOutputsRepo, "kept.txt"))).toBe(true); + + // 8. PR diff (origin/main..HEAD) must contain only kept.txt. + // - secret.txt excluded → not in diff. + // - drift.txt already on origin/main → not in diff (base-drift correction). + const prDiffNames = execGit(["diff", "--name-only", "origin/main", "HEAD"], { cwd: safeOutputsRepo }).stdout.trim().split("\n").filter(Boolean).sort(); + expect(prDiffNames).toEqual(["kept.txt"]); + + // 9. Exactly one linearized commit. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + }); + + it("rewriteBundleBranchAsSingleCommit falls back to origin/main when no bundleFilePath is supplied", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // When rewriteBundleBranchAsSingleCommit is called without a bundle file + // path (undefined/null), it must skip prerequisite extraction entirely and + // fall back to origin/ as the linearization base. This is the + // code path exercised when the signed-push rewrite is invoked outside the + // bundle flow (e.g. after a direct branch push). Verify that: + // + // • The function succeeds and produces a single linearized commit. + // • The PR diff contains only the agent's changes. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/no-bundle-path"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-no-bundle-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-no-bundle-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-no-bundle-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote and seed main. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + + // 2. Agent creates feature branch with two commits. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "alpha.txt"), "alpha\n"); + execGit(["add", "alpha.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: add alpha"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "beta.txt"), "beta\n"); + execGit(["add", "beta.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: add beta"], { cwd: agentRepo }); + + // 3. Safe-outputs runner: push the branch directly (simulate a non-bundle path). + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + // Copy the commits to the runner without using a bundle. + execGit(["fetch", agentRepo, `${branchName}:${branchName}`], { cwd: safeOutputsRepo }); + execGit(["checkout", branchName], { cwd: safeOutputsRepo }); + + const { rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + + // 4. Call without bundleFilePath (undefined) — must fall back to origin/main. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), undefined); + + // 5. Exactly one commit beyond origin/main. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + + // 6. PR diff must contain both agent files and nothing else. + const diffNames = execGit(["diff", "--name-only", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim().split("\n").filter(Boolean).sort(); + expect(diffNames).toEqual(["alpha.txt", "beta.txt"]); + + // 7. Exactly one parent (linearized, not a merge commit). + const parentLine = execGit(["log", "-1", "--format=%P", "HEAD"], { cwd: safeOutputsRepo }).stdout.trim(); + const parentShas = parentLine.split(/\s+/).filter(Boolean); + expect(parentShas).toHaveLength(1); + }); + + it("rewriteBundleBranchAsSingleCommit preserves a file rename", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // File additions and deletions are tested separately. A rename is a logical + // combination (delete + add the same content), but git tracks it explicitly + // with rename detection. Verify that after linearization: + // + // • The original filename is gone from the working tree and the PR diff + // shows it as deleted (or as a rename). + // • The new filename is present with the correct content. + // • No spurious extra changes appear in the PR diff. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/rename-file"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-rename-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-rename-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-rename-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote with a file that will be renamed. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "old-name.txt"), "file content\n"); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "old-name.txt", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Agent renames old-name.txt → new-name.txt. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + execGit(["mv", "old-name.txt", "new-name.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "refactor: rename old-name.txt to new-name.txt"], { cwd: agentRepo }); + + // 3. Bundle the feature branch. + const bundlePath = path.join(agentRepo, "rename.bundle"); + execGit(["bundle", "create", bundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // 4. Safe-outputs runner: fresh clone, apply bundle. + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 5. Rewrite. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath); + + // 6. old-name.txt must be gone; new-name.txt must be present with correct content. + expect(fs.existsSync(path.join(safeOutputsRepo, "old-name.txt"))).toBe(false); + expect(fs.existsSync(path.join(safeOutputsRepo, "new-name.txt"))).toBe(true); + expect(fs.readFileSync(path.join(safeOutputsRepo, "new-name.txt"), "utf8")).toBe("file content\n"); + + // 7. PR diff must show old-name.txt removed and new-name.txt added + // (rename detection may show D/A or R depending on similarity threshold). + const prDiffStatus = execGit(["diff", "--name-status", "origin/main", "HEAD"], { cwd: safeOutputsRepo }).stdout; + expect(prDiffStatus).toMatch(/old-name\.txt/); + expect(prDiffStatus).toMatch(/new-name\.txt/); + + // 8. README.md must NOT appear in the PR diff (unmodified base file). + expect(prDiffStatus).not.toMatch(/README\.md/); + + // 9. Exactly one linearized commit. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + }); + + it("rewriteBundleBranchAsSingleCommit uses the latest commit subject as the linearized commit message", async () => { + // ─── Why this test exists ──────────────────────────────────────────────── + // + // rewriteBundleBranchAsSingleCommit reads the HEAD commit subject with + // `git log -1 --format=%s` and uses it as the message for the synthesized + // single commit. When the agent has multiple commits, the LATEST subject + // must be used (not the first, not a default fallback). This verifies that + // the commit message forwarding works end-to-end in a real repository. + // ───────────────────────────────────────────────────────────────────────── + + const branchName = "feature/commit-message-preservation"; + + const bareRemote = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-msg-bare-")); + const agentRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-msg-agent-")); + const safeOutputsRepo = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-msg-so-")); + tempDirs.push(bareRemote, agentRepo, safeOutputsRepo); + + // 1. Initialize bare remote and seed main. + execGit(["init", "--bare", "-b", "main"], { cwd: bareRemote }); + execGit(["clone", bareRemote, "."], { cwd: agentRepo }); + execGit(["config", "user.name", "Agent"], { cwd: agentRepo }); + execGit(["config", "user.email", "agent@example.com"], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "README.md"), "# Project\n"); + execGit(["add", "README.md"], { cwd: agentRepo }); + execGit(["commit", "-m", "Initial commit"], { cwd: agentRepo }); + execGit(["branch", "-M", "main"], { cwd: agentRepo }); + execGit(["push", "-u", "origin", "main"], { cwd: agentRepo }); + const agentBaseCommit = execGit(["rev-parse", "HEAD"], { cwd: agentRepo }).stdout.trim(); + + // 2. Agent makes two commits; the second has the subject that should be used. + execGit(["checkout", "-b", branchName], { cwd: agentRepo }); + fs.writeFileSync(path.join(agentRepo, "step1.txt"), "step 1\n"); + execGit(["add", "step1.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "wip: intermediate step"], { cwd: agentRepo }); + + fs.writeFileSync(path.join(agentRepo, "step2.txt"), "step 2\n"); + execGit(["add", "step2.txt"], { cwd: agentRepo }); + execGit(["commit", "-m", "feat: implement feature X"], { cwd: agentRepo }); + + // 3. Bundle the feature branch. + const bundlePath = path.join(agentRepo, "msg.bundle"); + execGit(["bundle", "create", bundlePath, `${agentBaseCommit}..refs/heads/${branchName}`], { cwd: agentRepo }); + + // 4. Safe-outputs runner: fresh clone, apply bundle. + execGit(["clone", bareRemote, "."], { cwd: safeOutputsRepo }); + execGit(["config", "user.name", "Runner"], { cwd: safeOutputsRepo }); + execGit(["config", "user.email", "runner@example.com"], { cwd: safeOutputsRepo }); + + const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); + await applyBundleToBranch(bundlePath, branchName, `refs/heads/${branchName}`, createExecApi(safeOutputsRepo), "main"); + + // 5. Rewrite. + await rewriteBundleBranchAsSingleCommit("main", createExecApi(safeOutputsRepo), bundlePath); + + // 6. The linearized commit message must match the LATEST agent commit subject. + const commitSubject = execGit(["log", "-1", "--format=%s", "HEAD"], { cwd: safeOutputsRepo }).stdout.trim(); + expect(commitSubject).toBe("feat: implement feature X"); + + // 7. Both files from the agent's commits must be in the PR diff. + const diffNames = execGit(["diff", "--name-only", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim().split("\n").filter(Boolean).sort(); + expect(diffNames).toEqual(["step1.txt", "step2.txt"]); + + // 8. Exactly one linearized commit. + const linearCommitCount = Number(execGit(["rev-list", "--count", "origin/main..HEAD"], { cwd: safeOutputsRepo }).stdout.trim()); + expect(linearCommitCount).toBe(1); + }); }); diff --git a/actions/setup/js/create_pull_request_validation_push_parity.test.cjs b/actions/setup/js/create_pull_request_validation_push_parity.test.cjs index b6733ff8350..45ff5f5d2bc 100644 --- a/actions/setup/js/create_pull_request_validation_push_parity.test.cjs +++ b/actions/setup/js/create_pull_request_validation_push_parity.test.cjs @@ -18,9 +18,9 @@ * * Both regression tests assert: file_set(patch) == file_set(pushed_commit). * - * The non-rewrite regression now passes with the filtered-bundle fix in place. - * The merge-commit rewrite regression remains `it.fails(...)` until the rewrite - * path is updated to preserve parity across base-branch drift as well. + * Both regressions now pass: filtered bundle synthesis keeps excluded files out + * of the pushed commit, and the merge-commit rewrite path linearizes against + * the bundle prerequisite so base-branch drift is not absorbed into the result. */ import { describe, it, expect, beforeAll, afterEach, vi } from "vitest"; @@ -297,11 +297,10 @@ describe("create_pull_request – validation/push file-set parity", () => { * taken by `rewriteBundleBranchAsSingleCommit` inside `create_pull_request` * when signed push refuses merge-commit topology). * - * When base drift exists, the rewrite path still synthesizes a commit whose - * file set does not match the validated patch. Keep this as `it.fails(...)` - * until the rewrite/base-drift fix lands. + * When base drift exists, the rewritten commit must still match the validated + * patch file set rather than reverting or absorbing unrelated base changes. */ - it.fails("merge-commit rewrite path: rewritten commit file set matches validated patch", async () => { + it("merge-commit rewrite path: rewritten commit file set matches validated patch", async () => { const { generateGitPatch } = require("./generate_git_patch.cjs"); const { generateGitBundle } = require("./generate_git_bundle.cjs"); const { applyBundleToBranch, rewriteBundleBranchAsSingleCommit } = require("./create_pull_request.cjs"); @@ -390,8 +389,6 @@ describe("create_pull_request – validation/push file-set parity", () => { }); // REGRESSION ASSERTION: the rewritten commit must contain the same files as the patch. - // Today this still fails under base drift, which is why the test is marked - // `it.fails(...)` until the remaining rewrite-path fix lands. const fromPush = fileListFromPushedCommit(safeOutputsRepo, "origin/main"); expect(fromPush, "rewritten commit should match patch file set after rewrite under base drift").toEqual(fromPatch); }); diff --git a/actions/setup/js/git_helpers.cjs b/actions/setup/js/git_helpers.cjs index bfe3db58595..c98cd55318e 100644 --- a/actions/setup/js/git_helpers.cjs +++ b/actions/setup/js/git_helpers.cjs @@ -712,6 +712,9 @@ async function backfillCommitObjects(execApi, commitShas, options = {}) { * invocation (e.g. `["--allow-empty", "--no-verify"]`). * @param {string[]} [opts.excludedFiles] - Paths that should be removed from the staged rewrite * before creating the linearized commit. + * @param {string} [opts.rebaseOnto] - Optional ref to replay the synthesized commit onto after + * it has been linearized relative to `baseRef`. Use this when `baseRef` captures the agent's + * actual change base but the resulting single commit must sit on a newer branch tip. * @param {number} [opts.maxCommits] - Override the implausibility threshold (default * `SHALLOW_RANGE_MAX_COMMITS`). Set to `Infinity` to disable the shallow guard. * @returns {Promise} The new HEAD SHA after the rewrite. @@ -719,7 +722,7 @@ async function backfillCommitObjects(execApi, commitShas, options = {}) { * shallow checkout produces an implausible commit range. */ async function linearizeRangeAsCommit(baseRef, commitMessage, execApi, opts = {}) { - const { gitOpts, commitFlags = [], excludedFiles = [], maxCommits = SHALLOW_RANGE_MAX_COMMITS } = opts; + const { gitOpts, commitFlags = [], excludedFiles = [], rebaseOnto, maxCommits = SHALLOW_RANGE_MAX_COMMITS } = opts; // Spread gitOpts into exec calls only when it is explicitly provided — passing // `undefined` as a third argument changes the arity seen by mocks in tests. const execArgs = gitOpts !== undefined ? [gitOpts] : []; @@ -762,12 +765,30 @@ async function linearizeRangeAsCommit(baseRef, commitMessage, execApi, opts = {} throw new Error("Could not resolve current HEAD before linearizing range"); } + // Track whether a `git rebase` call was started so the catch block can distinguish + // "rebase in progress" (needs --abort) from "pre-rebase failure" (needs reset only). + let rebaseStarted = false; try { await execApi.exec("git", ["reset", "--soft", baseRef], ...execArgs); if (Array.isArray(excludedFiles) && excludedFiles.length > 0) { const { stdout: excludedStagedOut } = await execApi.getExecOutput("git", ["diff", "--cached", "--name-only", "--", ...excludedFiles], ...execArgs); if (excludedStagedOut.trim()) { - await execApi.exec("git", ["checkout", "HEAD", "--", ...excludedFiles], ...execArgs); + // Use `git reset HEAD -- ` rather than `git checkout HEAD -- `. + // For newly-added excluded files (not present in HEAD), `checkout` fails with + // "pathspec did not match any file(s) known to git". `reset HEAD --` handles + // both cases: removes new files from the index and restores modified files to + // the HEAD version, without touching the working tree. + await execApi.exec("git", ["reset", "HEAD", "--", ...excludedFiles], ...execArgs); + // For excluded files that were modifications (not new additions), the working tree + // still has the agent's version while the index was just restored to HEAD. This + // creates an unstaged change that would cause `git rebase --onto` to fail. + // Detect any such unstaged changes among the excluded files and restore them from + // the index so the working tree stays in sync before the commit and rebase steps. + const { stdout: modifiedExcludedOut } = await execApi.getExecOutput("git", ["diff", "--name-only", "--", ...excludedFiles], ...execArgs); + const modifiedExcluded = modifiedExcludedOut.trim().split("\n").filter(Boolean); + if (modifiedExcluded.length > 0) { + await execApi.exec("git", ["checkout", "--", ...modifiedExcluded], ...execArgs); + } } } const { stdout: stagedFilesOut } = await execApi.getExecOutput("git", ["diff", "--cached", "--name-only"], ...execArgs); @@ -775,10 +796,31 @@ async function linearizeRangeAsCommit(baseRef, commitMessage, execApi, opts = {} throw new Error(`No staged changes found after soft reset to ${baseRef}. ` + `The commit range may contain only no-op or empty commits. ` + `Ensure your commits contain actual file changes before pushing.`); } await execApi.exec("git", ["commit", ...commitFlags, "-m", commitMessage], ...execArgs); + if (typeof rebaseOnto === "string" && rebaseOnto.trim() && rebaseOnto.trim() !== baseRef.trim()) { + rebaseStarted = true; + await execApi.exec("git", ["rebase", "--onto", rebaseOnto.trim(), baseRef, "HEAD"], ...execArgs); + rebaseStarted = false; + // Guard: if the rebase silently dropped the commit (became empty relative to rebaseOnto), + // the agent's changes are lost. Detect and fail loudly rather than pushing an empty diff. + const { stdout: diffOut } = await execApi.getExecOutput("git", ["diff", "--name-only", rebaseOnto.trim(), "HEAD"], ...execArgs); + if (!diffOut.trim()) { + throw new Error(`Rebase onto ${rebaseOnto} produced no changes; the synthesized commit was dropped as empty`); + } + } const { stdout: newHeadOut } = await execApi.getExecOutput("git", ["rev-parse", "HEAD"], ...execArgs); return newHeadOut.trim(); } catch (rewriteError) { try { + if (rebaseStarted) { + // A rebase was in progress when the error occurred; abort it to restore the repo to its + // pre-rebase state before the hard reset below finishes the rollback. + try { + await execApi.exec("git", ["rebase", "--abort"], ...execArgs); + } catch (abortError) { + // --abort failed while a rebase was genuinely in progress — repo may be in a dirty state. + core.error(`linearizeRangeAsCommit: rebase --abort also failed: ${getErrorMessage(abortError)}`); + } + } await execApi.exec("git", ["reset", "--hard", originalHead], ...execArgs); core.warning(`linearizeRangeAsCommit: rewrite failed; restored original HEAD ${originalHead}`); } catch (restoreError) { diff --git a/actions/setup/js/git_helpers.test.cjs b/actions/setup/js/git_helpers.test.cjs index c6060e3d6e9..0fc3b6cb56d 100644 --- a/actions/setup/js/git_helpers.test.cjs +++ b/actions/setup/js/git_helpers.test.cjs @@ -1620,6 +1620,90 @@ describe("git_helpers.cjs - integration (real git repo)", () => { expect(fs.existsSync(path.join(repoDir, "r.txt"))).toBe(true); }); + it("can replay the synthesized commit onto a newer origin/main tip without reverting base drift", async () => { + const { linearizeRangeAsCommit } = requireLocal("./git_helpers.cjs"); + + const originalBaseSha = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoDir, encoding: "utf8" }).stdout.trim(); + const originalBranch = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoDir, encoding: "utf8" }).stdout.trim(); + + try { + execSync("git checkout -b feature-drift", { cwd: repoDir, stdio: "pipe" }); + addCommit(repoDir, "agent.txt", "agent\n", "add agent change"); + + const collaboratorDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-helpers-collab-")); + try { + execSync(`git clone ${remoteDir} ${collaboratorDir}`, { stdio: "pipe" }); + execSync('git config user.email "test@example.com"', { cwd: collaboratorDir, stdio: "pipe" }); + execSync('git config user.name "Test User"', { cwd: collaboratorDir, stdio: "pipe" }); + addCommit(collaboratorDir, "drift.txt", "drift\n", "base drift"); + execSync("git push origin main", { cwd: collaboratorDir, stdio: "pipe" }); + } finally { + fs.rmSync(collaboratorDir, { recursive: true, force: true }); + } + + execSync("git fetch origin main:refs/remotes/origin/main", { cwd: repoDir, stdio: "pipe" }); + + await linearizeRangeAsCommit(originalBaseSha, "Squash agent change", makeRealExecApi(repoDir), { + gitOpts: { cwd: repoDir }, + rebaseOnto: "origin/main", + }); + + const diffNames = spawnSync("git", ["diff", "--name-only", "origin/main..HEAD"], { cwd: repoDir, encoding: "utf8" }).stdout.trim().split("\n").filter(Boolean); + expect(diffNames).toEqual(["agent.txt"]); + expect(fs.readFileSync(path.join(repoDir, "drift.txt"), "utf8")).toBe("drift\n"); + + const parentSha = spawnSync("git", ["rev-parse", "HEAD^"], { cwd: repoDir, encoding: "utf8" }).stdout.trim(); + const currentOriginMain = spawnSync("git", ["rev-parse", "origin/main"], { cwd: repoDir, encoding: "utf8" }).stdout.trim(); + expect(parentSha).toBe(currentOriginMain); + } finally { + execSync(`git checkout ${originalBranch}`, { cwd: repoDir, stdio: "pipe" }); + } + }); + + it("aborts cleanly and throws when rebase --onto encounters a conflict", async () => { + const { linearizeRangeAsCommit } = requireLocal("./git_helpers.cjs"); + + // Setup: main has conflict.txt at a known base. + addCommit(repoDir, "conflict.txt", "original\n", "add conflict.txt"); + execSync("git push origin main", { cwd: repoDir, stdio: "pipe" }); + const originalBaseSha = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoDir, encoding: "utf8" }).stdout.trim(); + + // Agent branch: modifies the same file. + execSync("git checkout -b feature-conflict", { cwd: repoDir, stdio: "pipe" }); + addCommit(repoDir, "conflict.txt", "agent-change\n", "agent modifies conflict.txt"); + const agentHeadSha = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoDir, encoding: "utf8" }).stdout.trim(); + + // Collaborator pushes a conflicting change to main (same file, incompatible content). + const collaboratorDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-helpers-conflict-collab-")); + try { + execSync(`git clone ${remoteDir} ${collaboratorDir}`, { stdio: "pipe" }); + execSync('git config user.email "test@example.com"', { cwd: collaboratorDir, stdio: "pipe" }); + execSync('git config user.name "Test User"', { cwd: collaboratorDir, stdio: "pipe" }); + addCommit(collaboratorDir, "conflict.txt", "base-change\n", "base drift modifies conflict.txt"); + execSync("git push origin main", { cwd: collaboratorDir, stdio: "pipe" }); + } finally { + fs.rmSync(collaboratorDir, { recursive: true, force: true }); + } + + execSync("git fetch origin main:refs/remotes/origin/main", { cwd: repoDir, stdio: "pipe" }); + + // Rebase will conflict: the squashed diff touches the same file as origin/main. + await expect( + linearizeRangeAsCommit(originalBaseSha, "Squash agent change", makeRealExecApi(repoDir), { + gitOpts: { cwd: repoDir }, + rebaseOnto: "origin/main", + }) + ).rejects.toThrow(/Failed to linearize/); + + // Repo must be in a clean state — no rebase in progress after the abort. + const rebaseHeadExists = fs.existsSync(path.join(repoDir, ".git", "REBASE_HEAD")); + expect(rebaseHeadExists).toBe(false); + + // HEAD must be restored to the pre-linearize state (agent's commit, not the squash). + const headAfter = spawnSync("git", ["rev-parse", "HEAD"], { cwd: repoDir, encoding: "utf8" }).stdout.trim(); + expect(headAfter).toBe(agentHeadSha); + }); + it("throws before any git state mutation for a shallow+implausible range", async () => { const shallowDir = fs.mkdtempSync(path.join(os.tmpdir(), "gh-aw-helpers-shallow-lin-")); try {