diff --git a/.github/workflows/ai-moderator.lock.yml b/.github/workflows/ai-moderator.lock.yml index 985b372a8f7..8fc5a91d703 100644 --- a/.github/workflows/ai-moderator.lock.yml +++ b/.github/workflows/ai-moderator.lock.yml @@ -1860,6 +1860,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "codex" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_EMOJI: "๐Ÿค–" diff --git a/.github/workflows/changeset.lock.yml b/.github/workflows/changeset.lock.yml index f3e6c70ec07..34d838e200a 100644 --- a/.github/workflows/changeset.lock.yml +++ b/.github/workflows/changeset.lock.yml @@ -2077,6 +2077,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "codex" GH_AW_ENGINE_MODEL: "gpt-5.4" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} diff --git a/.github/workflows/design-decision-gate.lock.yml b/.github/workflows/design-decision-gate.lock.yml index 21e5e28c548..db7c33dafa5 100644 --- a/.github/workflows/design-decision-gate.lock.yml +++ b/.github/workflows/design-decision-gate.lock.yml @@ -2175,6 +2175,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "claude" GH_AW_ENGINE_MODEL: "claude-sonnet-4-6" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ—๏ธ *ADR gate enforced by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ” [{workflow_name}]({run_url}) is checking for design decision records on this {event_type}...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed the design decision gate check.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during design decision gate check.\"}" diff --git a/.github/workflows/dev-hawk.lock.yml b/.github/workflows/dev-hawk.lock.yml index 0485d9f9a95..db95f658939 100644 --- a/.github/workflows/dev-hawk.lock.yml +++ b/.github/workflows/dev-hawk.lock.yml @@ -2056,6 +2056,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿฆ… *Observed from above by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿฆ… Dev Hawk circles the sky! [{workflow_name}]({run_url}) is monitoring this {event_type} from above...\",\"runSuccess\":\"๐Ÿฆ… Hawk eyes report! [{workflow_name}]({run_url}) has completed reconnaissance. Intel delivered! ๐ŸŽฏ\",\"runFailure\":\"๐Ÿฆ… Hawk down! [{workflow_name}]({run_url}) {status}. The skies grow quiet...\"}" diff --git a/.github/workflows/firewall-escape.lock.yml b/.github/workflows/firewall-escape.lock.yml index 9487be95d3c..0103860af2b 100644 --- a/.github/workflows/firewall-escape.lock.yml +++ b/.github/workflows/firewall-escape.lock.yml @@ -1867,6 +1867,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} diff --git a/.github/workflows/impeccable-skills-reviewer.lock.yml b/.github/workflows/impeccable-skills-reviewer.lock.yml index 16cfd16d4cb..671ada4e944 100644 --- a/.github/workflows/impeccable-skills-reviewer.lock.yml +++ b/.github/workflows/impeccable-skills-reviewer.lock.yml @@ -1745,6 +1745,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: "claude-sonnet-4.6" GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงต *Reviewed using Impeccable skills by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿงต [{workflow_name}]({run_url}) is reviewing this {event_type} using Impeccable skills...\",\"runSuccess\":\"๐Ÿงต [{workflow_name}]({run_url}) has completed the skills-based review. โœ…\",\"runFailure\":\"๐Ÿงต [{workflow_name}]({run_url}) {status} during the skills-based review.\"}" diff --git a/.github/workflows/mattpocock-skills-reviewer.lock.yml b/.github/workflows/mattpocock-skills-reviewer.lock.yml index 673104abfb2..d87d1f8b1de 100644 --- a/.github/workflows/mattpocock-skills-reviewer.lock.yml +++ b/.github/workflows/mattpocock-skills-reviewer.lock.yml @@ -1929,6 +1929,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: "claude-sonnet-4.6" GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿง  *Reviewed using Matt Pocock's skills by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿง  [{workflow_name}]({run_url}) is reviewing this {event_type} using Matt Pocock's engineering skills...\",\"runSuccess\":\"๐Ÿง  [{workflow_name}]({run_url}) has completed the skills-based review. โœ…\",\"runFailure\":\"๐Ÿง  [{workflow_name}]({run_url}) {status} during the skills-based review.\"}" diff --git a/.github/workflows/pr-code-quality-reviewer.lock.yml b/.github/workflows/pr-code-quality-reviewer.lock.yml index 16494735bf2..b8765dbe8d6 100644 --- a/.github/workflows/pr-code-quality-reviewer.lock.yml +++ b/.github/workflows/pr-code-quality-reviewer.lock.yml @@ -2143,6 +2143,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ”Ž *Code quality review by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”Ž [{workflow_name}]({run_url}) is reviewing code quality for this {event_type}...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed the code quality review.\",\"runFailure\":\"โš ๏ธ [{workflow_name}]({run_url}) {status} during code quality review.\"}" diff --git a/.github/workflows/pr-description-caveman.lock.yml b/.github/workflows/pr-description-caveman.lock.yml index 4d64566e66e..04d4357abeb 100644 --- a/.github/workflows/pr-description-caveman.lock.yml +++ b/.github/workflows/pr-description-caveman.lock.yml @@ -1643,6 +1643,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} diff --git a/.github/workflows/refiner.lock.yml b/.github/workflows/refiner.lock.yml index 4e9cffa3bff..7c54f074765 100644 --- a/.github/workflows/refiner.lock.yml +++ b/.github/workflows/refiner.lock.yml @@ -2075,6 +2075,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"runStarted\":\"๐Ÿ” Starting code refinement... [{workflow_name}]({run_url}) is analyzing PR #${{ github.event.pull_request.number }} for style alignment and security issues\",\"runSuccess\":\"โœ… Refinement complete! [{workflow_name}]({run_url}) has created a PR with improvements for PR #${{ github.event.pull_request.number }}\",\"runFailure\":\"โŒ Refinement failed! [{workflow_name}]({run_url}) {status} while processing PR #${{ github.event.pull_request.number }}\"}" diff --git a/.github/workflows/smoke-agent-all-merged.lock.yml b/.github/workflows/smoke-agent-all-merged.lock.yml index 30aa014230a..cdbcc4bd2b7 100644 --- a/.github/workflows/smoke-agent-all-merged.lock.yml +++ b/.github/workflows/smoke-agent-all-merged.lock.yml @@ -1769,6 +1769,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "claude" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿค– *Guard policy smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ” [{workflow_name}]({run_url}) testing guard policy: `repos=all, min-integrity=merged`...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed guard policy test.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status}. Check the logs for details.\"}" diff --git a/.github/workflows/smoke-agent-all-none.lock.yml b/.github/workflows/smoke-agent-all-none.lock.yml index cf1b1e67ea7..3b43785a144 100644 --- a/.github/workflows/smoke-agent-all-none.lock.yml +++ b/.github/workflows/smoke-agent-all-none.lock.yml @@ -1769,6 +1769,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "claude" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿค– *Guard policy smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ” [{workflow_name}]({run_url}) testing guard policy: `repos=all, min-integrity=none`...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed guard policy test.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status}. Check the logs for details.\"}" diff --git a/.github/workflows/smoke-agent-public-approved.lock.yml b/.github/workflows/smoke-agent-public-approved.lock.yml index bb4c1a59b43..74bab5e9f0a 100644 --- a/.github/workflows/smoke-agent-public-approved.lock.yml +++ b/.github/workflows/smoke-agent-public-approved.lock.yml @@ -1821,6 +1821,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "claude" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿค– *Smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿค– [{workflow_name}]({run_url}) is looking for a Smoke issue to assign...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed. Issue assigned to the agentic-workflows agent.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status}. Check the logs for details.\"}" diff --git a/.github/workflows/smoke-agent-public-none.lock.yml b/.github/workflows/smoke-agent-public-none.lock.yml index c2624578972..ca8376f0244 100644 --- a/.github/workflows/smoke-agent-public-none.lock.yml +++ b/.github/workflows/smoke-agent-public-none.lock.yml @@ -1769,6 +1769,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "claude" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿค– *Guard policy smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ” [{workflow_name}]({run_url}) testing guard policy: `repos=public, min-integrity=none`...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed guard policy test.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status}. Check the logs for details.\"}" diff --git a/.github/workflows/smoke-agent-scoped-approved.lock.yml b/.github/workflows/smoke-agent-scoped-approved.lock.yml index db04b271e9a..2df11c2444b 100644 --- a/.github/workflows/smoke-agent-scoped-approved.lock.yml +++ b/.github/workflows/smoke-agent-scoped-approved.lock.yml @@ -1776,6 +1776,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "claude" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿค– *Guard policy smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ” [{workflow_name}]({run_url}) testing guard policy: `repos=[github/gh-aw, github/*], min-integrity=approved`...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed guard policy test.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status}. Check the logs for details.\"}" diff --git a/.github/workflows/smoke-antigravity.lock.yml b/.github/workflows/smoke-antigravity.lock.yml index 91a8b3c06df..aaef7a060c6 100644 --- a/.github/workflows/smoke-antigravity.lock.yml +++ b/.github/workflows/smoke-antigravity.lock.yml @@ -1886,6 +1886,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "antigravity" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e โœจ *[{workflow_name}]({run_url}) โ€” Powered by Antigravity*{ai_credits_suffix}{history_link}\",\"runStarted\":\"โœจ Antigravity awakens... [{workflow_name}]({run_url}) begins its journey on this {event_type}...\",\"runSuccess\":\"๐Ÿš€ [{workflow_name}]({run_url}) **MISSION COMPLETE!** Antigravity has spoken. โœจ\",\"runFailure\":\"โš ๏ธ [{workflow_name}]({run_url}) {status}. Antigravity encountered unexpected challenges...\"}" diff --git a/.github/workflows/smoke-call-workflow.lock.yml b/.github/workflows/smoke-call-workflow.lock.yml index 3e0fe38be95..cc8c6e11d2d 100644 --- a/.github/workflows/smoke-call-workflow.lock.yml +++ b/.github/workflows/smoke-call-workflow.lock.yml @@ -1813,6 +1813,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "codex" GH_AW_ENGINE_MODEL: "gpt-5.4-mini" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} diff --git a/.github/workflows/smoke-ci.lock.yml b/.github/workflows/smoke-ci.lock.yml index 6497e9f9115..201788236e7 100644 --- a/.github/workflows/smoke-ci.lock.yml +++ b/.github/workflows/smoke-ci.lock.yml @@ -1738,6 +1738,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_EMOJI: "๐Ÿงช" diff --git a/.github/workflows/smoke-claude.lock.yml b/.github/workflows/smoke-claude.lock.yml index d6739a5587c..1d2d5f938d5 100644 --- a/.github/workflows/smoke-claude.lock.yml +++ b/.github/workflows/smoke-claude.lock.yml @@ -2665,6 +2665,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "claude" GH_AW_ENGINE_MODEL: "claude-sonnet-4-6" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ’ฅ *[THE END] โ€” Illustrated by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ’ฅ **WHOOSH!** [{workflow_name}]({run_url}) springs into action on this {event_type}! *[Panel 1 begins...]*\",\"runSuccess\":\"๐ŸŽฌ **THE END** โ€” [{workflow_name}]({run_url}) **MISSION: ACCOMPLISHED!** The hero saves the day! โœจ\",\"runFailure\":\"๐Ÿ’ซ **TO BE CONTINUED...** [{workflow_name}]({run_url}) {status}! Our hero faces unexpected challenges...\"}" diff --git a/.github/workflows/smoke-codex.lock.yml b/.github/workflows/smoke-codex.lock.yml index 046c677846b..e7361735842 100644 --- a/.github/workflows/smoke-codex.lock.yml +++ b/.github/workflows/smoke-codex.lock.yml @@ -2277,6 +2277,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "codex" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ”ฎ *The oracle has spoken through [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฎ The ancient spirits stir... [{workflow_name}]({run_url}) awakens to divine this {event_type}...\",\"runSuccess\":\"โœจ The prophecy is fulfilled... [{workflow_name}]({run_url}) has completed its mystical journey. The stars align. ๐ŸŒŸ\",\"runFailure\":\"๐ŸŒ‘ The shadows whisper... [{workflow_name}]({run_url}) {status}. The oracle requires further meditation...\"}" diff --git a/.github/workflows/smoke-copilot-arm.lock.yml b/.github/workflows/smoke-copilot-arm.lock.yml index f7cb0ee6635..8196f1a4fc1 100644 --- a/.github/workflows/smoke-copilot-arm.lock.yml +++ b/.github/workflows/smoke-copilot-arm.lock.yml @@ -2695,6 +2695,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ“ฐ *BREAKING: Report filed by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"๐Ÿ“ฐ BREAKING: [{workflow_name}]({run_url}) is now investigating this {event_type}. Sources say the story is developing...\",\"runSuccess\":\"๐Ÿ“ฐ VERDICT: [{workflow_name}]({run_url}) has concluded. All systems operational. This is a developing story. ๐ŸŽค\",\"runFailure\":\"๐Ÿ“ฐ DEVELOPING STORY: [{workflow_name}]({run_url}) reports {status}. Our correspondents are investigating the incident...\"}" diff --git a/.github/workflows/smoke-create-cross-repo-pr.lock.yml b/.github/workflows/smoke-create-cross-repo-pr.lock.yml index aca0b67c987..9546938f4f3 100644 --- a/.github/workflows/smoke-create-cross-repo-pr.lock.yml +++ b/.github/workflows/smoke-create-cross-repo-pr.lock.yml @@ -1821,6 +1821,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ”ฌ *Cross-repo smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is testing cross-repo PR creation in github/gh-aw-side-repo...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) successfully created a cross-repo PR in github/gh-aw-side-repo!\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) failed to create a cross-repo PR: {status}\"}" diff --git a/.github/workflows/smoke-gemini.lock.yml b/.github/workflows/smoke-gemini.lock.yml index e2a524b820c..8bc98d07aea 100644 --- a/.github/workflows/smoke-gemini.lock.yml +++ b/.github/workflows/smoke-gemini.lock.yml @@ -1967,6 +1967,7 @@ jobs: GH_AW_ENGINE_ID: "gemini" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "0.39.1" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e โœจ *[{workflow_name}]({run_url}) โ€” Powered by Gemini*{ai_credits_suffix}{history_link}\",\"runStarted\":\"โœจ Gemini awakens... [{workflow_name}]({run_url}) begins its journey on this {event_type}...\",\"runSuccess\":\"๐Ÿš€ [{workflow_name}]({run_url}) **MISSION COMPLETE!** Gemini has spoken. โœจ\",\"runFailure\":\"โš ๏ธ [{workflow_name}]({run_url}) {status}. Gemini encountered unexpected challenges...\"}" diff --git a/.github/workflows/smoke-multi-pr.lock.yml b/.github/workflows/smoke-multi-pr.lock.yml index 64d256d1707..7698ceff09e 100644 --- a/.github/workflows/smoke-multi-pr.lock.yml +++ b/.github/workflows/smoke-multi-pr.lock.yml @@ -1773,6 +1773,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Multi PR smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"๐Ÿงช [{workflow_name}]({run_url}) is now testing multiple PR creation...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) successfully created multiple PRs.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) failed to create multiple PRs. Check the logs.\"}" diff --git a/.github/workflows/smoke-opencode.lock.yml b/.github/workflows/smoke-opencode.lock.yml index c06f689e3a8..d8cbf5af3d3 100644 --- a/.github/workflows/smoke-opencode.lock.yml +++ b/.github/workflows/smoke-opencode.lock.yml @@ -1832,6 +1832,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "opencode" GH_AW_ENGINE_MODEL: "copilot/claude-sonnet-4.5" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ”ฅ *[{workflow_name}]({run_url}) โ€” Powered by OpenCode*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฅ OpenCode initializing... [{workflow_name}]({run_url}) begins on this {event_type}...\",\"runSuccess\":\"๐Ÿš€ [{workflow_name}]({run_url}) **MISSION COMPLETE!** OpenCode delivered. ๐Ÿ”ฅ\",\"runFailure\":\"โš ๏ธ [{workflow_name}]({run_url}) {status}. OpenCode encountered unexpected challenges...\"}" diff --git a/.github/workflows/smoke-pi.lock.yml b/.github/workflows/smoke-pi.lock.yml index 0cc62112b7a..69f7c46971f 100644 --- a/.github/workflows/smoke-pi.lock.yml +++ b/.github/workflows/smoke-pi.lock.yml @@ -1776,6 +1776,7 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "pi" GH_AW_ENGINE_MODEL: "copilot/gpt-5.4" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿฅง *[{workflow_name}]({run_url}) โ€” Powered by Pi*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿฅง Pi initializing... [{workflow_name}]({run_url}) begins on this {event_type}...\",\"runSuccess\":\"๐Ÿš€ [{workflow_name}]({run_url}) **MISSION COMPLETE!** Pi delivered. ๐Ÿฅง\",\"runFailure\":\"โš ๏ธ [{workflow_name}]({run_url}) {status}. Pi encountered unexpected challenges...\"}" diff --git a/.github/workflows/smoke-project.lock.yml b/.github/workflows/smoke-project.lock.yml index 376e56d34bb..db43de651cb 100644 --- a/.github/workflows/smoke-project.lock.yml +++ b/.github/workflows/smoke-project.lock.yml @@ -2099,6 +2099,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Project smoke test report by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"๐Ÿงช [{workflow_name}]({run_url}) is now testing project operations...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed successfully. All project operations validated.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) encountered failures. Check the logs for details.\"}" diff --git a/.github/workflows/smoke-temporary-id.lock.yml b/.github/workflows/smoke-temporary-id.lock.yml index 0269260310d..d2a13f4298d 100644 --- a/.github/workflows/smoke-temporary-id.lock.yml +++ b/.github/workflows/smoke-temporary-id.lock.yml @@ -1948,6 +1948,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Temporary ID smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"appendOnlyComments\":true,\"runStarted\":\"๐Ÿงช [{workflow_name}]({run_url}) is now testing temporary ID functionality...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed successfully. Temporary ID validation passed.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) encountered failures. Check the logs for details.\"}" diff --git a/.github/workflows/smoke-test-tools.lock.yml b/.github/workflows/smoke-test-tools.lock.yml index 955775fea8d..e115316157b 100644 --- a/.github/workflows/smoke-test-tools.lock.yml +++ b/.github/workflows/smoke-test-tools.lock.yml @@ -1797,6 +1797,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ”ง *Tool validation by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ง Starting tool validation... [{workflow_name}]({run_url}) is checking the agent container tools...\",\"runSuccess\":\"โœ… All tools validated successfully! [{workflow_name}]({run_url}) confirms agent container is ready.\",\"runFailure\":\"โŒ Tool validation failed! [{workflow_name}]({run_url}) detected missing tools: {status}\"}" diff --git a/.github/workflows/smoke-update-cross-repo-pr.lock.yml b/.github/workflows/smoke-update-cross-repo-pr.lock.yml index a3aceda055a..d60042cbff1 100644 --- a/.github/workflows/smoke-update-cross-repo-pr.lock.yml +++ b/.github/workflows/smoke-update-cross-repo-pr.lock.yml @@ -1855,6 +1855,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿ“œ *Cross-repo PR update smoke test by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ“œ [{workflow_name}]({run_url}) is adding the next Odyssey line to github/gh-aw-side-repo PR #1...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) successfully updated the cross-repo PR with a new Odyssey line!\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) failed to update the cross-repo PR: {status}\"}" diff --git a/.github/workflows/test-quality-sentinel.lock.yml b/.github/workflows/test-quality-sentinel.lock.yml index db81fc21217..bd2857776f0 100644 --- a/.github/workflows/test-quality-sentinel.lock.yml +++ b/.github/workflows/test-quality-sentinel.lock.yml @@ -1858,6 +1858,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: "${{ needs.activation.outputs.model_size }}" GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_SAFE_OUTPUT_MESSAGES: "{\"footer\":\"\\u003e ๐Ÿงช *Test quality analysis by [{workflow_name}]({run_url})*{ai_credits_suffix}{history_link}\",\"runStarted\":\"๐Ÿ”ฌ [{workflow_name}]({run_url}) is analyzing test quality on this {event_type}...\",\"runSuccess\":\"โœ… [{workflow_name}]({run_url}) completed test quality analysis.\",\"runFailure\":\"โŒ [{workflow_name}]({run_url}) {status} during test quality analysis.\"}" diff --git a/.github/workflows/visual-regression-checker.lock.yml b/.github/workflows/visual-regression-checker.lock.yml index 0493d086196..acfa7094797 100644 --- a/.github/workflows/visual-regression-checker.lock.yml +++ b/.github/workflows/visual-regression-checker.lock.yml @@ -1705,6 +1705,7 @@ jobs: GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_ENGINE_VERSION: "1.0.75" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_PROJECT_UTC: "-08:00" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} diff --git a/actions/setup/js/create_pr_review_comment.cjs b/actions/setup/js/create_pr_review_comment.cjs index e33a5f43cbb..41915f959e0 100644 --- a/actions/setup/js/create_pr_review_comment.cjs +++ b/actions/setup/js/create_pr_review_comment.cjs @@ -42,6 +42,7 @@ async function main(config = {}) { const requiredTitlePrefix = config.required_title_prefix || ""; if (requiredLabels.length > 0) core.info(`Required labels (all): ${requiredLabels.join(", ")}`); if (requiredTitlePrefix) core.info(`Required title prefix: ${requiredTitlePrefix}`); + let allowedMentionAliases = []; if (Array.isArray(config.allowedMentionAliases)) { allowedMentionAliases = config.allowedMentionAliases; @@ -73,6 +74,16 @@ async function main(config = {}) { logStagedPreviewInfo("PR review comments will be previewed without being submitted"); } + const pinnedCommitId = typeof config.commit_id === "string" ? config.commit_id.trim() : ""; + if (pinnedCommitId) { + core.info(`create_pull_request_review_comment: commit-id pinned to ${pinnedCommitId}`); + if (registry && typeof registry.setDefaultPinnedCommitId === "function") { + registry.setDefaultPinnedCommitId(pinnedCommitId); + } else if (legacyBuffer && typeof legacyBuffer.setPinnedCommitId === "function") { + legacyBuffer.setPinnedCommitId(pinnedCommitId); + } + } + // Extract triggering context for footer generation const triggeringIssueNumber = context.payload?.issue?.number && !context.payload?.issue?.pull_request ? context.payload.issue.number : undefined; const triggeringPRNumber = context.payload?.pull_request?.number || (context.payload?.issue?.pull_request ? context.payload.issue.number : undefined); diff --git a/actions/setup/js/create_pr_review_comment.test.cjs b/actions/setup/js/create_pr_review_comment.test.cjs index e18bb40d1df..694cba41c66 100644 --- a/actions/setup/js/create_pr_review_comment.test.cjs +++ b/actions/setup/js/create_pr_review_comment.test.cjs @@ -523,6 +523,55 @@ describe("create_pr_review_comment.cjs", () => { // Footer context is set on the buffer for review-level footer generation expect(buffer.getBufferedCount()).toBe(1); }); + + it("should use GH_AW_HEAD_SHA env var as commit_id when submitting review", async () => { + const previousHeadSHA = process.env.GH_AW_HEAD_SHA; + process.env.GH_AW_HEAD_SHA = "trigger-time-sha-xyz789"; + try { + // The buffer's submitReview uses GH_AW_HEAD_SHA automatically โ€” verify the + // review context is set correctly (no commitId field needed on context) + const handler = await createHandler(); + const message = { + type: "create_pull_request_review_comment", + path: "src/main.js", + line: 10, + body: "Review comment body", + }; + const result = await handler(message, {}); + + expect(result.success).toBe(true); + expect(result.buffered).toBe(true); + + const ctx = buffer.getReviewContext(); + expect(ctx).not.toBeNull(); + // No commitId on the context; the env var is used at submit time instead + expect(ctx.commitId).toBeUndefined(); + // The live PR head SHA should still be in pullRequest.head.sha + expect(ctx.pullRequest.head.sha).toBe("abc123def456"); + } finally { + if (previousHeadSHA !== undefined) { + process.env.GH_AW_HEAD_SHA = previousHeadSHA; + } else { + delete process.env.GH_AW_HEAD_SHA; + } + } + }); + + it("review context has no commitId field regardless of handler config", async () => { + const handler = await createHandler(); + const message = { + type: "create_pull_request_review_comment", + path: "src/main.js", + line: 10, + body: "Review comment without pinning", + }; + const result = await handler(message, {}); + + expect(result.success).toBe(true); + const ctx = buffer.getReviewContext(); + expect(ctx).not.toBeNull(); + expect(ctx.commitId).toBeUndefined(); + }); }); describe("create_pr_review_comment.cjs โ€” registry mode (multiple reviews)", () => { diff --git a/actions/setup/js/pr_review_buffer.cjs b/actions/setup/js/pr_review_buffer.cjs index 3a3e3cf2a51..56bd1a19fdb 100644 --- a/actions/setup/js/pr_review_buffer.cjs +++ b/actions/setup/js/pr_review_buffer.cjs @@ -112,6 +112,9 @@ function createReviewBuffer() { /** @type {boolean} When true, dismiss older same-workflow REQUEST_CHANGES reviews after posting a replacement review. */ let supersedeOlderReviews = false; + /** @type {string} When non-empty, pins the review to this commit SHA instead of the live PR head or GH_AW_HEAD_SHA. */ + let pinnedCommitId = ""; + /** * Best-effort execution-state capture. * When the installation token is out of quota, metadata collection should not @@ -245,6 +248,17 @@ function createReviewBuffer() { } } + /** + * Pin the review to a specific commit SHA, overriding GH_AW_HEAD_SHA and the live PR head. + * @param {string} commitId - The commit SHA to pin the review to + */ + function setPinnedCommitId(commitId) { + if (commitId && typeof commitId === "string") { + pinnedCommitId = commitId; + core.info(`PR review pinned to commit: ${commitId}`); + } + } + /** * Check if there are buffered comments to submit. * @returns {boolean} @@ -298,6 +312,20 @@ function createReviewBuffer() { return { success: false, error: "Pull request head SHA not available" }; } + // Use the head SHA captured at trigger time (GH_AW_HEAD_SHA, injected by the compiler) + // when available, falling back to the live PR head SHA. This pins the review to the + // commit the agent actually reviewed, preventing attribution drift when new commits are + // pushed during the run (most common under workflow_run triggers where the safe_outputs + // job runs after the agent job and pulls.get() may return a newer HEAD sha). + // A user-specified pinnedCommitId (from the commit-id config option) takes highest priority. + const awHeadSHA = process.env.GH_AW_HEAD_SHA || ""; + const resolvedCommitId = pinnedCommitId || awHeadSHA || pullRequest.head.sha; + if (pinnedCommitId && pinnedCommitId !== pullRequest.head.sha) { + core.info(`Using config-pinned commit SHA: ${pinnedCommitId} (PR head is now ${pullRequest.head.sha})`); + } else if (awHeadSHA && awHeadSHA !== pullRequest.head.sha) { + core.info(`Using trigger-time head SHA: ${awHeadSHA} (PR head is now ${pullRequest.head.sha})`); + } + // Determine review event and body let event = reviewMetadata ? reviewMetadata.event : "COMMENT"; let body = reviewMetadata ? reviewMetadata.body : ""; @@ -465,7 +493,7 @@ function createReviewBuffer() { owner: repoParts.owner, repo: repoParts.repo, pull_number: pullRequestNumber, - commit_id: pullRequest.head.sha, + commit_id: resolvedCommitId, event: event, }; @@ -774,6 +802,7 @@ function createReviewBuffer() { setIncludeFooter: setFooterMode, // Backward compatibility alias setStaged, setSupersedeOlderReviews, + setPinnedCommitId, hasBufferedComments, hasReviewMetadata, getBufferedCount, @@ -806,6 +835,8 @@ function createPrReviewBufferRegistry() { let defaultFooterContext = null; let defaultStaged = false; let defaultSupersedeOlderReviews = false; + /** @type {string} */ + let defaultPinnedCommitId = ""; /** * Get or create the buffer for the given (repo, prNumber) pair. @@ -831,6 +862,9 @@ function createPrReviewBufferRegistry() { if (defaultSupersedeOlderReviews) { buffer.setSupersedeOlderReviews(true); } + if (defaultPinnedCommitId) { + buffer.setPinnedCommitId(defaultPinnedCommitId); + } bufferMap.set(k, buffer); insertionOrder.push({ repo, prNumber, buffer }); core.info(`PR review registry: created buffer for ${repo}#${prNumber}`); @@ -874,6 +908,13 @@ function createPrReviewBufferRegistry() { defaultSupersedeOlderReviews = value === true; } + /** @param {string} value */ + function setDefaultPinnedCommitId(value) { + if (value && typeof value === "string") { + defaultPinnedCommitId = value; + } + } + return { getOrCreate, getAllEntries, @@ -882,6 +923,7 @@ function createPrReviewBufferRegistry() { setDefaultFooterContext, setDefaultStaged, setDefaultSupersedeOlderReviews, + setDefaultPinnedCommitId, }; } diff --git a/actions/setup/js/pr_review_buffer.test.cjs b/actions/setup/js/pr_review_buffer.test.cjs index 31079debc65..14099689aae 100644 --- a/actions/setup/js/pr_review_buffer.test.cjs +++ b/actions/setup/js/pr_review_buffer.test.cjs @@ -556,6 +556,71 @@ describe("pr_review_buffer (factory pattern)", () => { expect(callArgs.comments).toBeUndefined(); }); + it("should use GH_AW_HEAD_SHA from env instead of live PR head SHA", async () => { + const previousHeadSHA = process.env.GH_AW_HEAD_SHA; + process.env.GH_AW_HEAD_SHA = "original-reviewed-sha"; + try { + buffer.setReviewMetadata("Review with pinned commit", "COMMENT"); + buffer.setReviewContext({ + repo: "owner/repo", + repoParts: { owner: "owner", repo: "repo" }, + pullRequestNumber: 42, + pullRequest: { head: { sha: "live-head-sha-pushed-later" } }, + }); + + mockGithub.rest.pulls.createReview.mockResolvedValue({ + data: { + id: 700, + html_url: "https://github.com/owner/repo/pull/42#pullrequestreview-700", + }, + }); + + const result = await buffer.submitReview(); + + expect(result.success).toBe(true); + // The env var sha must be passed as commit_id, not the live head sha + const callArgs = mockGithub.rest.pulls.createReview.mock.calls[0][0]; + expect(callArgs.commit_id).toBe("original-reviewed-sha"); + } finally { + if (previousHeadSHA !== undefined) { + process.env.GH_AW_HEAD_SHA = previousHeadSHA; + } else { + delete process.env.GH_AW_HEAD_SHA; + } + } + }); + + it("should fall back to pullRequest.head.sha when GH_AW_HEAD_SHA is not set", async () => { + const previousHeadSHA = process.env.GH_AW_HEAD_SHA; + delete process.env.GH_AW_HEAD_SHA; + try { + buffer.setReviewMetadata("Regular review", "COMMENT"); + buffer.setReviewContext({ + repo: "owner/repo", + repoParts: { owner: "owner", repo: "repo" }, + pullRequestNumber: 42, + pullRequest: { head: { sha: "current-head-sha" } }, + }); + + mockGithub.rest.pulls.createReview.mockResolvedValue({ + data: { + id: 800, + html_url: "https://github.com/owner/repo/pull/42#pullrequestreview-800", + }, + }); + + const result = await buffer.submitReview(); + + expect(result.success).toBe(true); + const callArgs = mockGithub.rest.pulls.createReview.mock.calls[0][0]; + expect(callArgs.commit_id).toBe("current-head-sha"); + } finally { + if (previousHeadSHA !== undefined) { + process.env.GH_AW_HEAD_SHA = previousHeadSHA; + } + } + }); + it("should include multi-line comment fields with side fallback for start_side", async () => { buffer.addComment({ path: "src/index.js", diff --git a/actions/setup/js/submit_pr_review.cjs b/actions/setup/js/submit_pr_review.cjs index d9d8e8a01d8..d69ec229cd1 100644 --- a/actions/setup/js/submit_pr_review.cjs +++ b/actions/setup/js/submit_pr_review.cjs @@ -84,6 +84,16 @@ async function main(config = {}) { } } + const pinnedCommitId = typeof config.commit_id === "string" ? config.commit_id.trim() : ""; + if (pinnedCommitId) { + core.info(`submit_pull_request_review: commit-id pinned to ${pinnedCommitId}`); + if (registry) { + registry.setDefaultPinnedCommitId(pinnedCommitId); + } else if (legacyBuffer && typeof legacyBuffer.setPinnedCommitId === "function") { + legacyBuffer.setPinnedCommitId(pinnedCommitId); + } + } + let processedCount = 0; /** diff --git a/actions/setup/js/submit_pr_review.test.cjs b/actions/setup/js/submit_pr_review.test.cjs index 6b019debebb..82128e35268 100644 --- a/actions/setup/js/submit_pr_review.test.cjs +++ b/actions/setup/js/submit_pr_review.test.cjs @@ -877,3 +877,79 @@ describe("submit_pr_review multi-buffer (registry mode)", () => { expect(setSuperSpy).toHaveBeenCalledWith(true); }); }); + +describe("submit_pr_review: GH_AW_HEAD_SHA automatic commit pinning", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + delete global.context; + delete global.github; + }); + + it("review context has no commitId field (GH_AW_HEAD_SHA is used at submit time instead)", async () => { + global.context = { + eventName: "pull_request", + repo: { owner: "test-owner", repo: "test-repo" }, + payload: { + pull_request: { number: 42, head: { sha: "live-head-sha" } }, + }, + }; + global.github = { + rest: { pulls: { get: vi.fn() } }, + graphql: vi.fn(), + }; + + const localBuffer = createReviewBuffer(); + const { main } = require("./submit_pr_review.cjs"); + const localHandler = await main({ + max: 1, + _prReviewBuffer: localBuffer, + }); + + const result = await localHandler({ type: "submit_pull_request_review", body: "Review", event: "COMMENT" }, {}); + + expect(result.success).toBe(true); + const ctx = localBuffer.getReviewContext(); + expect(ctx).not.toBeNull(); + // No commitId on the context; GH_AW_HEAD_SHA is read at submitReview() time + expect(ctx.commitId).toBeUndefined(); + expect(ctx.pullRequest.head.sha).toBe("live-head-sha"); + expect(global.github.rest.pulls.get).not.toHaveBeenCalled(); + }); + + it("review context has no commitId in workflow_run scenario (GH_AW_HEAD_SHA used automatically)", async () => { + const fetchedPR = { number: 55, head: { sha: "live-head-after-push" } }; + global.context = { + eventName: "workflow_run", + repo: { owner: "org", repo: "repo" }, + payload: {}, + }; + global.github = { + rest: { + pulls: { + get: vi.fn().mockResolvedValue({ data: fetchedPR }), + }, + }, + graphql: vi.fn(), + }; + + const localBuffer = createReviewBuffer(); + const { main } = require("./submit_pr_review.cjs"); + const localHandler = await main({ + max: 1, + target: "55", + _prReviewBuffer: localBuffer, + }); + + const result = await localHandler({ type: "submit_pull_request_review", body: "Review", event: "COMMENT" }, {}); + + expect(result.success).toBe(true); + const ctx = localBuffer.getReviewContext(); + expect(ctx).not.toBeNull(); + // No commitId on context; GH_AW_HEAD_SHA env var is used at submit time + expect(ctx.commitId).toBeUndefined(); + expect(ctx.pullRequest.head.sha).toBe("live-head-after-push"); + }); +}); diff --git a/docs/adr/48738-pin-review-attribution-to-reviewed-commit.md b/docs/adr/48738-pin-review-attribution-to-reviewed-commit.md new file mode 100644 index 00000000000..e0348e2cdcd --- /dev/null +++ b/docs/adr/48738-pin-review-attribution-to-reviewed-commit.md @@ -0,0 +1,55 @@ +# ADR-48738: Automatic PR Review Attribution via Compiler-Injected Head SHA + +**Date**: 2026-07-29 +**Status**: Accepted +**Deciders**: pelikhan, copilot-swe-agent + +--- + +### Context + +Under `workflow_run` triggers, the safe-outputs runtime submits a PR review after a parent workflow completes. A new commit can land on the PR branch while the agent is running. When that happens, `pulls.get()` returns the new HEAD SHA, and the review is attributed to a commit the agent never actually reviewed โ€” making inline comments appear fabricated, outdated, or misaligned. + +### Decision + +The compiler automatically captures the PR head SHA at trigger time and injects it into the compiled workflow as the `GH_AW_HEAD_SHA` environment variable, without requiring any user YAML configuration. The JS safe-outputs runtime reads `process.env.GH_AW_HEAD_SHA` in `submitReview()` and uses it as the `commit_id` argument to `pulls.createReview()` when present, falling back to the live PR head SHA otherwise. + +The compiler emits the correct expression based on the workflow trigger: + +| Trigger | `GH_AW_HEAD_SHA` value | +|---|---| +| `workflow_run` | `${{ github.event.workflow_run.head_sha }}` | +| `pull_request` | `${{ github.event.pull_request.head.sha }}` | +| `pull_request_target` | `${{ github.event.pull_request.head.sha }}` | +| Other triggers (push, schedule, issues, โ€ฆ) | Not injected; live PR head SHA is used | + +No user YAML configuration is needed. The feature is transparent and automatic for all affected trigger types. + +### Alternatives Considered + +#### Alternative 1: Optional user-configurable `commit-id` YAML field + +Add a `commit-id` field to `submit-pull-request-review` and `create-pull-request-review-comment` so callers can explicitly pass the reviewed SHA. + +Why not chosen: Requires users to explicitly wire the SHA through their workflow YAML (e.g., from an eligibility job output). Forgetting to set `commit-id` leaves the race condition unaddressed. Also introduces multi-target ambiguity when `target: "*"` is configured, since a single handler-level SHA cannot be correct for multiple distinct PRs. The compiler knows the trigger type at compile time and can inject the right expression automatically. + +#### Alternative 2: Freeze the PR HEAD SHA at handler initialization + +Capture and freeze the SHA once when the handler initializes, before the review accumulation phase begins. + +Why not chosen: A commit pushed after handler initialization but during the agent run would still cause attribution drift. The correct SHA is the one that was in place when the triggering workflow ran, not when the safe-outputs job starts. + +### Consequences + +#### Positive +- Reviews are attributed to the commit the agent actually reviewed, eliminating false "outdated" or misaligned inline comments. +- GitHub correctly marks the review as outdated when HEAD moves, rather than incorrectly attaching stale comments to the new commit. +- Zero user configuration required โ€” all existing workflows benefit automatically after recompile. +- Multi-target (`target: "*"`) scenarios are unaffected because the SHA is read from an environment variable at runtime, not passed as a single handler-level config value. + +#### Negative +- For workflows with triggers other than `workflow_run`, `pull_request`, and `pull_request_target`, the env var is not injected and the live PR head SHA is used (existing behavior). If drift is a concern for those triggers, a separate mechanism would be needed. + +#### Neutral +- The `GH_AW_HEAD_SHA` env var follows the existing pattern of compiler-injected context variables (e.g., `GH_AW_WORKFLOW_ID`, `GH_AW_CALLER_WORKFLOW_ID`, `GH_AW_AMBIENT_CONTEXT`). +- When `GH_AW_HEAD_SHA` is absent or empty, `submitReview()` falls back to `pullRequest.head.sha`, preserving existing runtime behavior for unsupported trigger types. diff --git a/docs/src/content/docs/specs/safe-outputs-specification.md b/docs/src/content/docs/specs/safe-outputs-specification.md index a5af862cdb9..f7f92bf249b 100644 --- a/docs/src/content/docs/specs/safe-outputs-specification.md +++ b/docs/src/content/docs/specs/safe-outputs-specification.md @@ -1650,6 +1650,8 @@ submit-pull-request-review: footer: "always" | "none" | "if-body" # Footer on review body ``` +> **Review attribution pinning**: Under `workflow_run` triggers, the compiler automatically injects `GH_AW_HEAD_SHA` (set to `${{ github.event.workflow_run.head_sha }}`) into the safe-outputs job environment. For `pull_request` and `pull_request_target` triggers, it is set to `${{ github.event.pull_request.head.sha }}`. The runtime uses this value as `commit_id` when posting the review, so the review is always attributed to the commit the agent actually saw โ€” not a newer HEAD that may have landed while the agent was running. No user configuration is needed. + **Pull Request Extensions**: ```yaml diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index 1790466fa5f..22bcfb1d573 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -7171,6 +7171,10 @@ "required-title-prefix": { "type": "string", "description": "The target item's title must start with this prefix for this operation to proceed" + }, + "commit-id": { + "type": "string", + "description": "Pin the review to this specific commit SHA. When set, the review is attributed to this commit instead of the current PR head. GitHub will mark the review as outdated if the PR head has since moved." } }, "additionalProperties": false @@ -7280,6 +7284,10 @@ "required-title-prefix": { "type": "string", "description": "The target item's title must start with this prefix for this operation to proceed" + }, + "commit-id": { + "type": "string", + "description": "Pin the review to this specific commit SHA. When set, the review is attributed to this commit instead of the current PR head. GitHub will mark the review as outdated if the PR head has since moved." } }, "additionalProperties": false diff --git a/pkg/workflow/compiler_safe_outputs_job.go b/pkg/workflow/compiler_safe_outputs_job.go index 8f27c1646a2..ae8d7cd2e30 100644 --- a/pkg/workflow/compiler_safe_outputs_job.go +++ b/pkg/workflow/compiler_safe_outputs_job.go @@ -743,6 +743,36 @@ func (c *Compiler) buildSafeOutputsJobNeeds(data *WorkflowData, mainJobName stri return needs } +// headSHAExpressionForTrigger returns the GitHub Actions expression for the PR head SHA +// based on the workflow's `on:` field. Returns an empty string if the trigger type does +// not carry a directly accessible PR head SHA (e.g. push, schedule, issues). +// +// The returned expression is injected as GH_AW_HEAD_SHA in the safe_outputs job so that +// PR-review handlers automatically pin their reviews to the commit that was in place when +// the workflow triggered, preventing attribution drift if new commits land during the run. +func headSHAExpressionForTrigger(onField any) string { + switch v := onField.(type) { + case map[string]any: + if _, ok := v["workflow_run"]; ok { + return "${{ github.event.workflow_run.head_sha }}" + } + if _, ok := v["pull_request"]; ok { + return "${{ github.event.pull_request.head.sha }}" + } + if _, ok := v["pull_request_target"]; ok { + return "${{ github.event.pull_request.head.sha }}" + } + case string: + switch v { + case "workflow_run": + return "${{ github.event.workflow_run.head_sha }}" + case "pull_request", "pull_request_target": + return "${{ github.event.pull_request.head.sha }}" + } + } + return "" +} + // buildJobLevelSafeOutputEnvVars builds environment variables that should be set at the job level // for the consolidated safe_outputs job. These are variables that are common to all safe output steps. func (c *Compiler) buildJobLevelSafeOutputEnvVars(data *WorkflowData, workflowID string) map[string]string { @@ -872,6 +902,16 @@ func (c *Compiler) buildJobLevelSafeOutputEnvVars(data *WorkflowData, workflowID envVars["GH_AW_THREAT_DETECTION_AIC"] = fmt.Sprintf("${{ needs.%s.outputs.aic }}", constants.DetectionJobName) } + // Automatically inject the PR head SHA captured at trigger time so that PR-review + // handlers (submit_pull_request_review, create_pull_request_review_comment) pin their + // reviews to the reviewed commit without requiring any user YAML configuration. + // For workflow_run triggers this prevents attribution drift when a new commit lands + // on the PR while the agent is running (the safe_outputs job runs after the agent job + // and pulls.get() would otherwise return the new HEAD sha). + if headSHAExpr := headSHAExpressionForTrigger(data.RawFrontmatter["on"]); headSHAExpr != "" { + envVars["GH_AW_HEAD_SHA"] = headSHAExpr + } + return envVars } diff --git a/pkg/workflow/create_pr_review_comment.go b/pkg/workflow/create_pr_review_comment.go index 4c281831f5c..f84a62134ce 100644 --- a/pkg/workflow/create_pr_review_comment.go +++ b/pkg/workflow/create_pr_review_comment.go @@ -14,6 +14,7 @@ type CreatePullRequestReviewCommentsConfig struct { Target string `yaml:"target,omitempty"` // Target for comments: "triggering" (default), "*" (any PR), or explicit PR number TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository PR review comments AllowedRepos []string `yaml:"allowed-repos,omitempty"` // List of additional repositories that PR review comments can be added to (additionally to the target-repo) + CommitId string `yaml:"commit-id,omitempty"` // When set, pins the review to this commit SHA instead of the current PR head. } func (c *Compiler) parsePullRequestReviewCommentsConfig(outputMap map[string]any) *CreatePullRequestReviewCommentsConfig { @@ -52,6 +53,12 @@ func (c *Compiler) parsePullRequestReviewCommentsConfig(outputMap map[string]any } prReviewCommentsConfig.TargetRepoSlug = targetRepoSlug + if commitId, exists := configMap["commit-id"]; exists { + if commitIdStr, ok := commitId.(string); ok { + prReviewCommentsConfig.CommitId = commitIdStr + } + } + // Parse common base fields with default max of 10 c.parseBaseSafeOutputConfig(configMap, &prReviewCommentsConfig.BaseSafeOutputConfig, 10) } else { diff --git a/pkg/workflow/safe_outputs_handler_registry.go b/pkg/workflow/safe_outputs_handler_registry.go index cba92bfb055..c9904adff6e 100644 --- a/pkg/workflow/safe_outputs_handler_registry.go +++ b/pkg/workflow/safe_outputs_handler_registry.go @@ -423,6 +423,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix). AddIfNotEmpty("target-repo", c.TargetRepoSlug). AddStringSlice("allowed_repos", c.AllowedRepos). + AddIfNotEmpty("commit_id", c.CommitId). AddIfNotEmpty("github-token", c.GitHubToken). AddTemplatableBool("staged", templatableBoolPtrToStringPtr(c.Staged)). Build() @@ -441,6 +442,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddIfTrue("supersede_older_reviews", c.SupersedeOlderReviews).AddStringSlice("required_labels", c.RequiredLabels). AddIfNotEmpty("required_title_prefix", c.RequiredTitlePrefix).AddIfNotEmpty("github-token", c.GitHubToken). AddStringPtr("footer", getEffectiveFooterString(c.Footer, cfg.Footer)). + AddIfNotEmpty("commit_id", c.CommitId). AddTemplatableBool("staged", templatableBoolPtrToStringPtr(c.Staged)). Build() }, diff --git a/pkg/workflow/submit_pr_review.go b/pkg/workflow/submit_pr_review.go index 18ecdf44997..edc07bff36f 100644 --- a/pkg/workflow/submit_pr_review.go +++ b/pkg/workflow/submit_pr_review.go @@ -20,6 +20,7 @@ type SubmitPullRequestReviewConfig struct { Footer *string `yaml:"footer,omitempty"` // Controls when to show footer in PR review body: "always" (default), "none", or "if-body" (only when review has body text) AllowedEvents []string `yaml:"allowed-events,omitempty"` // Optional list of allowed review event types: APPROVE, COMMENT, REQUEST_CHANGES. If omitted, all event types are allowed. SupersedeOlderReviews bool `yaml:"supersede-older-reviews,omitempty"` // When true, dismisses older same-workflow REQUEST_CHANGES reviews after a replacement review is posted. + CommitId string `yaml:"commit-id,omitempty"` // When set, pins the review to this commit SHA instead of the current PR head. } // parseSubmitPullRequestReviewConfig handles submit-pull-request-review configuration @@ -112,6 +113,12 @@ func (c *Compiler) parseSubmitPullRequestReviewConfig(outputMap map[string]any) } } + if commitId, exists := configMap["commit-id"]; exists { + if commitIdStr, ok := commitId.(string); ok { + config.CommitId = commitIdStr + } + } + submitPRReviewLog.Printf("Parsed submit-pull-request-review config: max=%d, target=%s, target_repo=%s, allowed_events=%v, supersede_older_reviews=%t", templatableIntValue(config.Max), config.Target, config.TargetRepoSlug, config.AllowedEvents, config.SupersedeOlderReviews) } else { // If configData is nil or not a map, set the default max diff --git a/pkg/workflow/submit_pr_review_footer_test.go b/pkg/workflow/submit_pr_review_footer_test.go index 8b6db8d56fd..09eaf6dfe83 100644 --- a/pkg/workflow/submit_pr_review_footer_test.go +++ b/pkg/workflow/submit_pr_review_footer_test.go @@ -622,3 +622,114 @@ func TestSubmitPRReviewFooterInHandlerConfig(t *testing.T) { } }) } + +func TestHeadSHAExpressionForTrigger(t *testing.T) { + t.Run("workflow_run map trigger returns workflow_run head_sha expression", func(t *testing.T) { + onField := map[string]any{ + "workflow_run": map[string]any{"workflows": []any{"CI"}}, + } + result := headSHAExpressionForTrigger(onField) + assert.Equal(t, "${{ github.event.workflow_run.head_sha }}", result) + }) + + t.Run("pull_request map trigger returns pull_request head sha expression", func(t *testing.T) { + onField := map[string]any{ + "pull_request": map[string]any{"types": []any{"opened"}}, + } + result := headSHAExpressionForTrigger(onField) + assert.Equal(t, "${{ github.event.pull_request.head.sha }}", result) + }) + + t.Run("pull_request_target map trigger returns pull_request head sha expression", func(t *testing.T) { + onField := map[string]any{ + "pull_request_target": nil, + } + result := headSHAExpressionForTrigger(onField) + assert.Equal(t, "${{ github.event.pull_request.head.sha }}", result) + }) + + t.Run("workflow_run string trigger returns workflow_run head_sha expression", func(t *testing.T) { + result := headSHAExpressionForTrigger("workflow_run") + assert.Equal(t, "${{ github.event.workflow_run.head_sha }}", result) + }) + + t.Run("pull_request string trigger returns pull_request head sha expression", func(t *testing.T) { + result := headSHAExpressionForTrigger("pull_request") + assert.Equal(t, "${{ github.event.pull_request.head.sha }}", result) + }) + + t.Run("issues trigger returns empty string", func(t *testing.T) { + onField := map[string]any{ + "issues": map[string]any{"types": []any{"opened"}}, + } + result := headSHAExpressionForTrigger(onField) + assert.Empty(t, result) + }) + + t.Run("push trigger returns empty string", func(t *testing.T) { + onField := map[string]any{"push": nil} + result := headSHAExpressionForTrigger(onField) + assert.Empty(t, result) + }) + + t.Run("nil trigger returns empty string", func(t *testing.T) { + result := headSHAExpressionForTrigger(nil) + assert.Empty(t, result) + }) +} + +func TestBuildJobLevelSafeOutputEnvVarsHeadSHA(t *testing.T) { + t.Run("GH_AW_HEAD_SHA set for workflow_run trigger", func(t *testing.T) { + compiler := NewCompiler() + workflowData := &WorkflowData{ + Name: "Test", + RawFrontmatter: map[string]any{ + "on": map[string]any{ + "workflow_run": map[string]any{"workflows": []any{"CI"}}, + }, + }, + } + envVars := compiler.buildJobLevelSafeOutputEnvVars(workflowData, "test-workflow") + assert.Equal(t, "${{ github.event.workflow_run.head_sha }}", envVars["GH_AW_HEAD_SHA"]) + }) + + t.Run("GH_AW_HEAD_SHA set for pull_request trigger", func(t *testing.T) { + compiler := NewCompiler() + workflowData := &WorkflowData{ + Name: "Test", + RawFrontmatter: map[string]any{ + "on": map[string]any{ + "pull_request": map[string]any{"types": []any{"opened"}}, + }, + }, + } + envVars := compiler.buildJobLevelSafeOutputEnvVars(workflowData, "test-workflow") + assert.Equal(t, "${{ github.event.pull_request.head.sha }}", envVars["GH_AW_HEAD_SHA"]) + }) + + t.Run("GH_AW_HEAD_SHA not set for issues trigger", func(t *testing.T) { + compiler := NewCompiler() + workflowData := &WorkflowData{ + Name: "Test", + RawFrontmatter: map[string]any{ + "on": map[string]any{ + "issues": map[string]any{"types": []any{"opened"}}, + }, + }, + } + envVars := compiler.buildJobLevelSafeOutputEnvVars(workflowData, "test-workflow") + _, hasHeadSHA := envVars["GH_AW_HEAD_SHA"] + assert.False(t, hasHeadSHA, "GH_AW_HEAD_SHA should not be set for issues trigger") + }) + + t.Run("GH_AW_HEAD_SHA not set when no frontmatter on field", func(t *testing.T) { + compiler := NewCompiler() + workflowData := &WorkflowData{ + Name: "Test", + RawFrontmatter: map[string]any{}, + } + envVars := compiler.buildJobLevelSafeOutputEnvVars(workflowData, "test-workflow") + _, hasHeadSHA := envVars["GH_AW_HEAD_SHA"] + assert.False(t, hasHeadSHA, "GH_AW_HEAD_SHA should not be set when no on: field") + }) +}