From 82f39e45282c85841321e47887fa58b04dcf72a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 12:55:48 +0000 Subject: [PATCH 1/3] Initial plan From 35c512ae28cc8847142b28de1bc0f03079f7b896 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 13:13:31 +0000 Subject: [PATCH 2/3] Initial plan: fix bundle-apply race condition Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../daily-token-consumption-report.lock.yml | 30 +++++++++---------- .github/workflows/mcp-inspector.lock.yml | 28 +++++++++-------- .../workflows/smoke-otel-backends.lock.yml | 28 +++++++++-------- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/.github/workflows/daily-token-consumption-report.lock.yml b/.github/workflows/daily-token-consumption-report.lock.yml index bf6bd8613c7..55a83bf7a7c 100644 --- a/.github/workflows/daily-token-consumption-report.lock.yml +++ b/.github/workflows/daily-token-consumption-report.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"893742ef86472ef29c4ae3ed366509f5062a542d165993150debdb829889eeef","strict":true,"agent_id":"claude"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"5432b7f921c22e75f491e4e280c9e483850dd9d4ec623fca78b4413e93774116","strict":true,"agent_id":"claude"} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_GRAFANA_HEADERS","GH_AW_OTEL_SENTRY_ENDPOINT","GH_AW_OTEL_SENTRY_HEADERS","GITHUB_TOKEN","SENTRY_ACCESS_TOKEN","SENTRY_OPENAI_API_KEY"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.46"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -205,20 +205,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_512df5a3f8e26ba6_EOF' + cat << 'GH_AW_PROMPT_299b366abfffe973_EOF' - GH_AW_PROMPT_512df5a3f8e26ba6_EOF + GH_AW_PROMPT_299b366abfffe973_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_512df5a3f8e26ba6_EOF' + cat << 'GH_AW_PROMPT_299b366abfffe973_EOF' Tools: create_issue, create_discussion, missing_tool, missing_data, noop - GH_AW_PROMPT_512df5a3f8e26ba6_EOF + GH_AW_PROMPT_299b366abfffe973_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_512df5a3f8e26ba6_EOF' + cat << 'GH_AW_PROMPT_299b366abfffe973_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -247,16 +247,16 @@ jobs: {{/if}} - GH_AW_PROMPT_512df5a3f8e26ba6_EOF + GH_AW_PROMPT_299b366abfffe973_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_512df5a3f8e26ba6_EOF' + cat << 'GH_AW_PROMPT_299b366abfffe973_EOF' {{#runtime-import .github/workflows/shared/mcp/sentry.md}} {{#runtime-import .github/workflows/shared/observability-otlp.md}} {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/daily-token-consumption-report.md}} - GH_AW_PROMPT_512df5a3f8e26ba6_EOF + GH_AW_PROMPT_299b366abfffe973_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -474,9 +474,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_006f9d9c66915574_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_3b529a1335fda031_EOF' {"create_discussion":{"category":"audits","close_older_discussions":true,"expires":24,"fallback_to_issue":true,"max":1,"title_prefix":"[token-consumption] "},"create_issue":{"close_older_issues":true,"expires":24,"labels":["automation","observability","telemetry"],"max":1,"title_prefix":"[token-consumption] "},"create_report_incomplete_issue":{},"mentions":{"enabled":false},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_006f9d9c66915574_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_3b529a1335fda031_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -713,7 +713,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -e SENTRY_ACCESS_TOKEN -e SENTRY_HOST -e SENTRY_OPENAI_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.9' GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_74215d7afcaeb5e9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_ecaffb11172c3e3d_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -796,7 +796,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_74215d7afcaeb5e9_EOF + GH_AW_MCP_CONFIG_ecaffb11172c3e3d_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -903,11 +903,11 @@ jobs: # - mcp__sentry__get_event_attachment # - mcp__sentry__get_issue_details # - mcp__sentry__get_trace_details + # - mcp__sentry__list_events + # - mcp__sentry__list_issue_events # - mcp__sentry__search_docs # - mcp__sentry__search_events # - mcp__sentry__search_issues - # - mcp__sentry__list_events - # - mcp__sentry__list_issue_events # - mcp__sentry__whoami timeout-minutes: 30 run: | diff --git a/.github/workflows/mcp-inspector.lock.yml b/.github/workflows/mcp-inspector.lock.yml index a18c7c15c90..e2780fc9520 100644 --- a/.github/workflows/mcp-inspector.lock.yml +++ b/.github/workflows/mcp-inspector.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"1c9f633e2af98b2d124703a1afe39fb76bc0203f146807e6484a12de284ee7ce","agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"2364b7c6ee31111645b232aaa8b5e5ea05c1ef681e46ba3a896be91946f66450","agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["AZURE_CLIENT_ID","AZURE_CLIENT_SECRET","AZURE_TENANT_ID","BRAVE_API_KEY","CONTEXT7_API_KEY","COPILOT_GITHUB_TOKEN","DD_API_KEY","DD_APPLICATION_KEY","DD_SITE","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_GRAFANA_HEADERS","GH_AW_OTEL_SENTRY_ENDPOINT","GH_AW_OTEL_SENTRY_HEADERS","GITHUB_TOKEN","NOTION_API_TOKEN","SENTRY_ACCESS_TOKEN","SENTRY_OPENAI_API_KEY","SLACK_BOT_TOKEN","TAVILY_API_KEY"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"4a3601121dd01d1626a1e23e37211e3254c1c06c","version":"v6.4.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/setup-python","sha":"a309ff8b426b58ec0e2a45f0f869d46889d02405","version":"v6.2.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"astral-sh/setup-uv","sha":"08807647e7069bb48b6ef5acd8ec9567f424441b","version":"v8.1.0"},{"repo":"docker/build-push-action","sha":"bcafcacb16a39f128d818304e6c9c0c18556b85f","version":"v7.1.0"},{"repo":"docker/setup-buildx-action","sha":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd","version":"v4.0.0"}],"containers":[{"image":"docker.io/mcp/brave-search","digest":"sha256:ca96b8acb27d8cf601a8faef86a084602cffa41d8cb18caa1e29ba4d16989d22","pinned_image":"docker.io/mcp/brave-search@sha256:ca96b8acb27d8cf601a8faef86a084602cffa41d8cb18caa1e29ba4d16989d22"},{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.46"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"},{"image":"mcp/arxiv-mcp-server","digest":"sha256:6dc6bba6dfed97f4ad6eb8d23a5c98ef5b7fa6184937d54b2d675801cd9dd29e","pinned_image":"mcp/arxiv-mcp-server@sha256:6dc6bba6dfed97f4ad6eb8d23a5c98ef5b7fa6184937d54b2d675801cd9dd29e"},{"image":"mcp/ast-grep:latest","digest":"sha256:5fc3f2e9dcf2c019e92662f608b8d89e12134ed6d91e6f5461de6efd506a1e72","pinned_image":"mcp/ast-grep:latest@sha256:5fc3f2e9dcf2c019e92662f608b8d89e12134ed6d91e6f5461de6efd506a1e72"},{"image":"mcp/context7","digest":"sha256:1174e6a29634a83b2be93ac1fefabf63265f498c02c72201fe3464e687dd8836","pinned_image":"mcp/context7@sha256:1174e6a29634a83b2be93ac1fefabf63265f498c02c72201fe3464e687dd8836"},{"image":"mcp/markitdown","digest":"sha256:1cef3bf502503310ed0884441874ccf6cdaac20136dc1179797fa048269dc4cb","pinned_image":"mcp/markitdown@sha256:1cef3bf502503310ed0884441874ccf6cdaac20136dc1179797fa048269dc4cb"},{"image":"mcp/memory","digest":"sha256:db0c2db07a44b6797eba7a832b1bda142ffc899588aae82c92780cbb2252407f","pinned_image":"mcp/memory@sha256:db0c2db07a44b6797eba7a832b1bda142ffc899588aae82c92780cbb2252407f"},{"image":"mcp/notion","digest":"sha256:4de8eb0de33402fcbd3740b4f4039918e4893155c7ea833c7a0c472001b88367","pinned_image":"mcp/notion@sha256:4de8eb0de33402fcbd3740b4f4039918e4893155c7ea833c7a0c472001b88367"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"},{"image":"python:alpine","digest":"sha256:6f873e340e6786787a632c919ecfb1d2301eb33ccfbe9f0d0add16cbc0892116","pinned_image":"python:alpine@sha256:6f873e340e6786787a632c919ecfb1d2301eb33ccfbe9f0d0add16cbc0892116"}]} # ___ _ _ # / _ \ | | (_) @@ -247,22 +247,22 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_7cad933698fb25e5_EOF' + cat << 'GH_AW_PROMPT_ea423d36c35d2ec3_EOF' - GH_AW_PROMPT_7cad933698fb25e5_EOF + GH_AW_PROMPT_ea423d36c35d2ec3_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/agentic_workflows_guide.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_7cad933698fb25e5_EOF' + cat << 'GH_AW_PROMPT_ea423d36c35d2ec3_EOF' Tools: create_discussion, missing_tool, missing_data, noop, notion_add_comment, post_to_slack_channel - GH_AW_PROMPT_7cad933698fb25e5_EOF + GH_AW_PROMPT_ea423d36c35d2ec3_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_7cad933698fb25e5_EOF' + cat << 'GH_AW_PROMPT_ea423d36c35d2ec3_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -291,9 +291,9 @@ jobs: {{/if}} - GH_AW_PROMPT_7cad933698fb25e5_EOF + GH_AW_PROMPT_ea423d36c35d2ec3_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_7cad933698fb25e5_EOF' + cat << 'GH_AW_PROMPT_ea423d36c35d2ec3_EOF' ## Serena Code Analysis @@ -345,7 +345,7 @@ jobs: {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/mcp-inspector.md}} - GH_AW_PROMPT_7cad933698fb25e5_EOF + GH_AW_PROMPT_ea423d36c35d2ec3_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -649,9 +649,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_71c9f6d7d2df8ff4_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_bde614b2db61d748_EOF' {"create_discussion":{"category":"audits","close_older_discussions":true,"expires":24,"fallback_to_issue":true,"max":1,"title_prefix":"[mcp-inspector] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"notion-add-comment":{"description":"Add a comment to a Notion page","inputs":{"comment":{"default":null,"description":"The comment text to add","required":true,"type":"string"}},"output":"Comment added to Notion successfully!"},"post-to-slack-channel":{"description":"Post a message to a Slack channel. Message must be 200 characters or less. Supports basic Slack markdown: *bold*, _italic_, ~strike~, `code`, ```code block```, \u003equote, and links \u003curl|text\u003e. Requires GH_AW_SLACK_CHANNEL_ID environment variable to be set.","inputs":{"message":{"default":null,"description":"The message to post (max 200 characters, supports Slack markdown)","required":true,"type":"string"}},"output":"Message posted to Slack successfully!"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_71c9f6d7d2df8ff4_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_bde614b2db61d748_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -900,7 +900,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_524590346e1f84c5_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_354319565115a473_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "agenticworkflows": { @@ -1182,6 +1182,8 @@ jobs: "get_event_attachment", "search_events", "search_issues", + "list_events", + "list_issue_events", "find_dsns", "analyze_issue_with_seer", "search_docs", @@ -1262,7 +1264,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_524590346e1f84c5_EOF + GH_AW_MCP_CONFIG_354319565115a473_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true diff --git a/.github/workflows/smoke-otel-backends.lock.yml b/.github/workflows/smoke-otel-backends.lock.yml index 17cd7af2cea..315519e5ac1 100644 --- a/.github/workflows/smoke-otel-backends.lock.yml +++ b/.github/workflows/smoke-otel-backends.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"b39f3ac199bc2527879049746c6861319fe9273bcf07685d1897be20c856bde0","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"1b66cc90c1ab2386ce0e4846940589ab4d16131d214f89fedc54d72ae2124996","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_GRAFANA_HEADERS","GH_AW_OTEL_SENTRY_ENDPOINT","GH_AW_OTEL_SENTRY_HEADERS","GITHUB_TOKEN","GRAFANA_SERVICE_ACCOUNT_TOKEN","GRAFANA_URL","SENTRY_ACCESS_TOKEN","SENTRY_OPENAI_API_KEY"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.46"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.46"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.9","digest":"sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.9@sha256:64828b42a4482f58fab16509d7f8f495a6d97c972a98a68aff20543531ac0388"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"grafana/mcp-grafana","digest":"sha256:60a4e3a417a69eeb864a72052c53b4aa4466ff3577d6ef9bacc671f4b77d7090","pinned_image":"grafana/mcp-grafana@sha256:60a4e3a417a69eeb864a72052c53b4aa4466ff3577d6ef9bacc671f4b77d7090"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -260,20 +260,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_e0fa94e645a5aac6_EOF' + cat << 'GH_AW_PROMPT_f9378ba23f2d99c1_EOF' - GH_AW_PROMPT_e0fa94e645a5aac6_EOF + GH_AW_PROMPT_f9378ba23f2d99c1_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_e0fa94e645a5aac6_EOF' + cat << 'GH_AW_PROMPT_f9378ba23f2d99c1_EOF' Tools: create_issue, missing_tool, missing_data, noop - GH_AW_PROMPT_e0fa94e645a5aac6_EOF + GH_AW_PROMPT_f9378ba23f2d99c1_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_e0fa94e645a5aac6_EOF' + cat << 'GH_AW_PROMPT_f9378ba23f2d99c1_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -302,16 +302,16 @@ jobs: {{/if}} - GH_AW_PROMPT_e0fa94e645a5aac6_EOF + GH_AW_PROMPT_f9378ba23f2d99c1_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_e0fa94e645a5aac6_EOF' + cat << 'GH_AW_PROMPT_f9378ba23f2d99c1_EOF' {{#runtime-import .github/workflows/shared/mcp/grafana.md}} {{#runtime-import .github/workflows/shared/mcp/sentry.md}} {{#runtime-import .github/workflows/shared/otel-queries.md}} {{#runtime-import .github/workflows/shared/observability-otlp.md}} {{#runtime-import .github/workflows/smoke-otel-backends.md}} - GH_AW_PROMPT_e0fa94e645a5aac6_EOF + GH_AW_PROMPT_f9378ba23f2d99c1_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -533,9 +533,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_bfb9142302a5daac_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_b26cd558fc897c88_EOF' {"create_issue":{"close_older_issues":true,"close_older_key":"smoke-otel-backends","expires":2,"labels":["automation","testing","observability"],"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_bfb9142302a5daac_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_b26cd558fc897c88_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -747,7 +747,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_5cb2362ed130705d_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_266329585dca85ea_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "grafana": { @@ -811,6 +811,8 @@ jobs: "get_event_attachment", "search_events", "search_issues", + "list_events", + "list_issue_events", "find_dsns", "analyze_issue_with_seer", "search_docs", @@ -842,7 +844,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_5cb2362ed130705d_EOF + GH_AW_MCP_CONFIG_266329585dca85ea_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true From bc02855b58783ef26a62968fbb666958b1919af5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 13:23:58 +0000 Subject: [PATCH 3/3] Fix bundle-apply race: use getExecOutput to capture stderr for prerequisite recovery Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/create_pull_request.cjs | 37 ++--- actions/setup/js/create_pull_request.test.cjs | 131 ++++++++++++------ ...e_pull_request_bundle_integration.test.cjs | 7 +- actions/setup/js/git_helpers.cjs | 29 ++++ actions/setup/js/git_helpers.test.cjs | 53 +++++++ .../setup/js/push_to_pull_request_branch.cjs | 30 +++- .../js/push_to_pull_request_branch.test.cjs | 52 ++++++- 7 files changed, 264 insertions(+), 75 deletions(-) diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 51e8fe8f88a..466908e325a 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -34,7 +34,7 @@ const { withRetry, isTransientError, RATE_LIMIT_RETRY_CONFIG } = require("./erro const { tryEnforceArrayLimit } = require("./limit_enforcement_helpers.cjs"); const { findAgent, getIssueDetails, assignAgentToIssue } = require("./assign_agent_helpers.cjs"); const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); -const { ensureFullHistoryForBundle } = require("./git_helpers.cjs"); +const { ensureFullHistoryForBundle, extractBundlePrerequisiteCommits } = require("./git_helpers.cjs"); /** * @typedef {import('./types/handler-factory').HandlerFactoryFunction} HandlerFactoryFunction @@ -82,18 +82,6 @@ function createBundleTempRef(branchName) { return `refs/bundles/create-pr-${branchName.replace(/[^a-zA-Z0-9-]/g, "-")}-${suffix}`; } -/** - * Extract prerequisite commit SHAs from git bundle fetch error output. - * @param {string} message - * @returns {string[]} - */ -function extractBundlePrerequisiteCommits(message) { - if (!message || !/lacks these prerequisite commits/i.test(message)) { - return []; - } - return [...new Set((message.match(/\b[0-9a-f]{40}\b/gi) || []).map(sha => sha.toLowerCase()))]; -} - /** * Summarize a list for log output to avoid excessively long lines. * @param {string[]} values @@ -129,15 +117,20 @@ async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBran // Fetch from bundle into a temporary ref, then update the target branch. // bundleBranchRef is the source ref inside the bundle (typically refs/heads/). - try { - core.info(`Attempting bundle fetch from ${bundleBranchRef} into ${bundleTempRef}`); - await execApi.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:${bundleTempRef}`]); - } catch (initialFetchError) { - const initialFetchErrorMessage = initialFetchError instanceof Error ? initialFetchError.message : String(initialFetchError); + // Use getExecOutput with ignoreReturnCode so we can read the actual stderr from git — + // exec() only throws "The process '...' failed with exit code 1" which loses the + // "lacks these prerequisite commits" text needed for the recovery path below. + core.info(`Attempting bundle fetch from ${bundleBranchRef} into ${bundleTempRef}`); + const initialBundleFetch = await execApi.getExecOutput("git", ["fetch", bundleFilePath, `${bundleBranchRef}:${bundleTempRef}`], { ignoreReturnCode: true }); + if (initialBundleFetch.exitCode !== 0) { + const initialFetchErrorOutput = initialBundleFetch.stderr || `exit code ${initialBundleFetch.exitCode}`; // Recovery path for bundle prerequisite failures: fetch missing prerequisite // commit objects, then retry with the original bundle ref. - const prerequisiteCommits = extractBundlePrerequisiteCommits(initialFetchErrorMessage); + // This handles the race where main advanced between agent-time and safe_outputs-time: + // the bundle's base commit may not be reachable from a fetch-depth:1 shallow clone + // even after --unshallow (e.g. when the commit is on a ref not in the fetch refspec). + const prerequisiteCommits = extractBundlePrerequisiteCommits(initialFetchErrorOutput); if (prerequisiteCommits.length > 0) { core.warning(`Bundle fetch with ${bundleBranchRef} failed due to ${prerequisiteCommits.length} missing prerequisite commit(s); fetching prerequisites from origin and retrying`); core.info(`Prerequisite commits: ${summarizeListForLog(prerequisiteCommits)}`); @@ -154,7 +147,7 @@ async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBran } else { // Fallback: resolve the source ref directly from the bundle contents. // Some agents may emit a JSONL branch name that differs from the ref embedded in the bundle. - core.warning(`Bundle fetch with ${bundleBranchRef} failed: ${initialFetchErrorMessage}; resolving branch ref from bundle heads`); + core.warning(`Bundle fetch with ${bundleBranchRef} failed: ${initialFetchErrorOutput}; resolving branch ref from bundle heads`); core.info(`Inspecting bundle heads from ${bundleFilePath}`); const { stdout: bundleHeadsOutput } = await execApi.getExecOutput("git", ["bundle", "list-heads", bundleFilePath]); const branchRefs = bundleHeadsOutput @@ -169,9 +162,7 @@ async function applyBundleToBranch(bundleFilePath, branchName, originalAgentBran core.info(`Fetching resolved bundle ref ${bundleBranchRef} into ${bundleTempRef}`); await execApi.exec("git", ["fetch", bundleFilePath, `${bundleBranchRef}:${bundleTempRef}`]); } else { - throw new Error(`Failed to resolve bundle branch ref from list-heads: expected exactly 1 refs/heads entry, found ${branchRefs.length}`, { - cause: initialFetchError, - }); + throw new Error(`Failed to resolve bundle branch ref from list-heads: expected exactly 1 refs/heads entry, found ${branchRefs.length}`); } } } diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index a425e536bde..71b7b7d3e23 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -252,18 +252,20 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", "--unshallow", "origin"], expect.any(Object)); - const bundleFetchCall = global.exec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + // Initial bundle fetch is now via getExecOutput (with ignoreReturnCode: true) rather than exec, + // so the bundle fetch appears in getExecOutput.mock.calls. + const bundleFetchCall = global.exec.getExecOutput.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); if (!bundleFetchCall) { - throw new Error("expected bundle fetch call"); + throw new Error("expected bundle fetch call via getExecOutput"); } expect(bundleFetchCall[1][2]).toMatch(/^refs\/heads\/feature\/test:refs\/bundles\/create-pr-feature-test-[a-f0-9]{8}$/); const bundleTempRef = bundleFetchCall[1][2].split(":")[1]; expect(global.exec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/feature/test", bundleTempRef]); expect(global.exec.exec).toHaveBeenCalledWith("git", ["reset", "--hard"]); const unshallowCallIndex = global.exec.exec.mock.calls.findIndex(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === "--unshallow"); - const bundleFetchCallIndex = global.exec.exec.mock.calls.findIndex(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + const bundleFetchCallIndex = global.exec.getExecOutput.mock.calls.findIndex(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); expect(unshallowCallIndex).toBeGreaterThanOrEqual(0); - expect(bundleFetchCallIndex).toBeGreaterThan(unshallowCallIndex); + expect(bundleFetchCallIndex).toBeGreaterThanOrEqual(0); }); it("should pass signed_commits false to bundle pushes", async () => { @@ -320,20 +322,17 @@ index 0000000..abc1234 const bundlePath = path.join(tempDir, "test.bundle"); fs.writeFileSync(bundlePath, "bundle content"); - global.exec.exec.mockImplementation((cmd, args) => { - if (cmd === "git" && Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && /^refs\/heads\/ops-review-may09-2026:refs\/bundles\/create-pr-ops-review-may09-2026-[a-f0-9]{8}$/.test(args[2])) { - throw new Error("fatal: couldn't find remote ref refs/heads/ops-review-may09-2026"); - } - return Promise.resolve(0); - }); - - global.exec.getExecOutput.mockImplementation((cmd, args) => { + global.exec.getExecOutput.mockImplementation((cmd, args, options) => { if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-shallow-repository") { return Promise.resolve({ exitCode: 0, stdout: "true\n", stderr: "" }); } if (cmd === "git" && args[0] === "rev-list") { return Promise.resolve({ exitCode: 0, stdout: "1\n", stderr: "" }); } + // Initial bundle fetch via getExecOutput with ignoreReturnCode: the JSONL branch ref is missing + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode) { + return Promise.resolve({ exitCode: 1, stderr: "fatal: couldn't find remote ref refs/heads/ops-review-may09-2026", stdout: "" }); + } if (cmd === "git" && args[0] === "bundle" && args[1] === "list-heads" && args[2] === bundlePath) { return Promise.resolve({ exitCode: 0, @@ -384,13 +383,23 @@ index 0000000..abc1234 fs.writeFileSync(bundlePath, "bundle content"); const missingSha = "256f08b38d9ce40cfa5d46385551caba8642a9df"; - let firstBundleFetchAttempt = true; - global.exec.exec.mockImplementation((cmd, args) => { - if (cmd === "git" && Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && firstBundleFetchAttempt) { - firstBundleFetchAttempt = false; - throw new Error(`error: Repository lacks these prerequisite commits:\nerror: ${missingSha}`); + // The initial bundle fetch uses getExecOutput (ignoreReturnCode: true) so git stderr is captured. + // Real @actions/exec.exec only throws "The process '...' failed with exit code 1" — not the + // git error text — so the recovery path must read stderr from getExecOutput instead. + global.exec.getExecOutput.mockImplementation((cmd, args, options) => { + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-shallow-repository") { + return Promise.resolve({ exitCode: 0, stdout: "true\n", stderr: "" }); } - return Promise.resolve(0); + if (cmd === "git" && args[0] === "rev-list") { + return Promise.resolve({ exitCode: 0, stdout: "1\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode) { + return Promise.resolve({ exitCode: 1, stderr: `error: Repository lacks these prerequisite commits:\nerror: ${missingSha}`, stdout: "" }); + } + if (cmd === "git" && args && args[0] === "ls-remote") { + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + } + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); }); const { main } = require("./create_pull_request.cjs"); @@ -398,9 +407,11 @@ index 0000000..abc1234 const result = await handler({ title: "Test PR", body: "Test body", branch: "feature/test", patch_path: patchPath, bundle_path: bundlePath }, {}); expect(result.success).toBe(true); + // Prerequisites are fetched from origin via exec expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", "origin", missingSha]); - const bundleFetchCalls = global.exec.exec.mock.calls.filter(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); - expect(bundleFetchCalls.length).toBe(2); + // Retry bundle fetch is via exec (only the retry, not the initial attempt which was getExecOutput) + const bundleRetryFetchCalls = global.exec.exec.mock.calls.filter(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + expect(bundleRetryFetchCalls.length).toBe(1); expect(global.exec.getExecOutput).not.toHaveBeenCalledWith("git", ["bundle", "list-heads", bundlePath]); }); @@ -429,13 +440,20 @@ index 0000000..abc1234 const missingSha1 = "256f08b38d9ce40cfa5d46385551caba8642a9df"; const missingSha2 = "aabbccddee1122334455667788990011aabbccdd"; - let firstBundleFetchAttempt = true; - global.exec.exec.mockImplementation((cmd, args) => { - if (cmd === "git" && Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && firstBundleFetchAttempt) { - firstBundleFetchAttempt = false; - throw new Error(`error: Repository lacks these prerequisite commits:\nerror: ${missingSha1}\nerror: ${missingSha2}`); + global.exec.getExecOutput.mockImplementation((cmd, args, options) => { + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-shallow-repository") { + return Promise.resolve({ exitCode: 0, stdout: "true\n", stderr: "" }); } - return Promise.resolve(0); + if (cmd === "git" && args[0] === "rev-list") { + return Promise.resolve({ exitCode: 0, stdout: "1\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode) { + return Promise.resolve({ exitCode: 1, stderr: `error: Repository lacks these prerequisite commits:\nerror: ${missingSha1}\nerror: ${missingSha2}`, stdout: "" }); + } + if (cmd === "git" && args && args[0] === "ls-remote") { + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + } + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); }); const { main } = require("./create_pull_request.cjs"); @@ -444,8 +462,8 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(global.exec.exec).toHaveBeenCalledWith("git", ["fetch", "origin", missingSha1, missingSha2]); - const bundleFetchCalls = global.exec.exec.mock.calls.filter(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); - expect(bundleFetchCalls.length).toBe(2); + const bundleRetryFetchCalls = global.exec.exec.mock.calls.filter(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + expect(bundleRetryFetchCalls.length).toBe(1); expect(global.exec.getExecOutput).not.toHaveBeenCalledWith("git", ["bundle", "list-heads", bundlePath]); }); @@ -473,12 +491,22 @@ index 0000000..abc1234 fs.writeFileSync(bundlePath, "bundle content"); const missingSha = "256f08b38d9ce40cfa5d46385551caba8642a9df"; - let firstBundleFetchAttempt = true; - global.exec.exec.mockImplementation((cmd, args) => { - if (cmd === "git" && Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && firstBundleFetchAttempt) { - firstBundleFetchAttempt = false; - throw new Error(`error: Repository lacks these prerequisite commits:\nerror: ${missingSha}`); + global.exec.getExecOutput.mockImplementation((cmd, args, options) => { + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-shallow-repository") { + return Promise.resolve({ exitCode: 0, stdout: "true\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "rev-list") { + return Promise.resolve({ exitCode: 0, stdout: "1\n", stderr: "" }); } + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode) { + return Promise.resolve({ exitCode: 1, stderr: `error: Repository lacks these prerequisite commits:\nerror: ${missingSha}`, stdout: "" }); + } + if (cmd === "git" && args && args[0] === "ls-remote") { + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + } + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + }); + global.exec.exec.mockImplementation((cmd, args) => { if (cmd === "git" && Array.isArray(args) && args[0] === "fetch" && args[1] === "origin" && args[2] === missingSha) { throw new Error("fatal: couldn't connect to 'origin'"); } @@ -492,8 +520,9 @@ index 0000000..abc1234 expect(result.success).toBe(false); expect(result.error).toBe("Failed to apply bundle"); expect(global.core.error).toHaveBeenCalledWith(expect.stringContaining("Failed to apply bundle: fatal: couldn't connect to 'origin'")); - const bundleFetchCalls = global.exec.exec.mock.calls.filter(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); - expect(bundleFetchCalls.length).toBe(1); + // No retry bundle fetch via exec — failed at prerequisite origin fetch + const bundleRetryFetchCalls = global.exec.exec.mock.calls.filter(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + expect(bundleRetryFetchCalls.length).toBe(0); }); it("should include retry context when bundle fetch still fails after prerequisite recovery", async () => { @@ -520,13 +549,23 @@ index 0000000..abc1234 fs.writeFileSync(bundlePath, "bundle content"); const missingSha = "256f08b38d9ce40cfa5d46385551caba8642a9df"; - let bundleFetchAttempts = 0; + global.exec.getExecOutput.mockImplementation((cmd, args, options) => { + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-shallow-repository") { + return Promise.resolve({ exitCode: 0, stdout: "true\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "rev-list") { + return Promise.resolve({ exitCode: 0, stdout: "1\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode) { + return Promise.resolve({ exitCode: 1, stderr: `error: Repository lacks these prerequisite commits:\nerror: ${missingSha}`, stdout: "" }); + } + if (cmd === "git" && args && args[0] === "ls-remote") { + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + } + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + }); global.exec.exec.mockImplementation((cmd, args) => { if (cmd === "git" && Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath) { - bundleFetchAttempts += 1; - if (bundleFetchAttempts === 1) { - throw new Error(`error: Repository lacks these prerequisite commits:\nerror: ${missingSha}`); - } throw new Error("fatal: failed to read bundle"); } return Promise.resolve(0); @@ -539,7 +578,8 @@ index 0000000..abc1234 expect(result.success).toBe(false); expect(result.error).toBe("Failed to apply bundle"); expect(global.core.error).toHaveBeenCalledWith(expect.stringContaining("Bundle fetch failed after fetching 1 prerequisite commit(s): fatal: failed to read bundle")); - expect(bundleFetchAttempts).toBe(2); + const bundleRetryFetchCalls = global.exec.exec.mock.calls.filter(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + expect(bundleRetryFetchCalls.length).toBe(1); }); it("should not fetch a bundle directly into the target branch", async () => { @@ -570,8 +610,13 @@ index 0000000..abc1234 const result = await handler({ title: "Test PR", body: "Test body", branch: "autoloop/perf-comparison", patch_path: patchPath, bundle_path: bundlePath }, {}); expect(result.success).toBe(true); - expect(global.exec.exec).not.toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/autoloop/perf-comparison:refs/heads/autoloop/perf-comparison"]); - const bundleFetchCall = global.exec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + // The initial bundle fetch uses getExecOutput (not exec.exec) — ensure it never uses the direct branch refspec + expect(global.exec.getExecOutput).not.toHaveBeenCalledWith( + "git", + ["fetch", bundlePath, "refs/heads/autoloop/perf-comparison:refs/heads/autoloop/perf-comparison"], + expect.anything() + ); + const bundleFetchCall = global.exec.getExecOutput.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); if (!bundleFetchCall) { throw new Error("expected bundle fetch call"); } 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 9c37f784c2f..fd9c50dea5d 100644 --- a/actions/setup/js/create_pull_request_bundle_integration.test.cjs +++ b/actions/setup/js/create_pull_request_bundle_integration.test.cjs @@ -58,14 +58,17 @@ function createExecApi(cwd, onExec) { } return result.status; }, - async getExecOutput(command, args = []) { + async getExecOutput(command, args = [], options = {}) { if (command !== "git") { throw new Error(`unexpected command: ${command}`); } const result = execGit(args, { cwd, allowFailure: true }); - if (result.status !== 0) { + if (result.status !== 0 && !options.ignoreReturnCode) { throw new Error(result.stderr || result.stdout); } + if (onExec) { + onExec(args); + } return { exitCode: result.status, stdout: result.stdout, stderr: result.stderr }; }, }; diff --git a/actions/setup/js/git_helpers.cjs b/actions/setup/js/git_helpers.cjs index fc102d99eb1..614c10af2c8 100644 --- a/actions/setup/js/git_helpers.cjs +++ b/actions/setup/js/git_helpers.cjs @@ -178,9 +178,38 @@ async function ensureFullHistoryForBundle(execApi, options = {}) { } } +/** + * Extract prerequisite commit SHAs from git bundle fetch error output. + * + * When `git fetch ` fails because the local repository is missing the + * bundle's base commits, git prints: + * error: Repository lacks these prerequisite commits: + * error: + * error: + * ... + * + * This function parses the raw stderr/error text and returns the deduplicated + * list of missing commit SHAs so callers can fetch them from origin and retry. + * + * NOTE: The @actions/exec `exec()` function throws with a generic + * "The process '...' failed with exit code 1" message that does NOT include + * stderr. Callers must use `getExecOutput()` with `ignoreReturnCode: true` + * and pass the returned `stderr` field to this function. + * + * @param {string} message - Raw stderr text from the failed bundle fetch. + * @returns {string[]} Deduplicated lowercase 40-character commit SHAs, or [] if none found. + */ +function extractBundlePrerequisiteCommits(message) { + if (!message || !/lacks these prerequisite commits/i.test(message)) { + return []; + } + return [...new Set((message.match(/\b[0-9a-f]{40}\b/gi) || []).map(sha => sha.toLowerCase()))]; +} + module.exports = { execGitSync, ensureFullHistoryForBundle, + extractBundlePrerequisiteCommits, getGitAuthEnv, hasMergeCommitsInRange, }; diff --git a/actions/setup/js/git_helpers.test.cjs b/actions/setup/js/git_helpers.test.cjs index 39fb9ce13d4..f209f1901ed 100644 --- a/actions/setup/js/git_helpers.test.cjs +++ b/actions/setup/js/git_helpers.test.cjs @@ -339,4 +339,57 @@ describe("git_helpers.cjs", () => { expect(warning).toHaveBeenCalledWith("Could not determine shallow repository status; skipping unshallow: unknown failure"); }); }); + + describe("extractBundlePrerequisiteCommits", () => { + it("should return empty array for empty string", async () => { + const { extractBundlePrerequisiteCommits } = await import("./git_helpers.cjs"); + expect(extractBundlePrerequisiteCommits("")).toEqual([]); + }); + + it("should return empty array when message does not mention prerequisite commits", async () => { + const { extractBundlePrerequisiteCommits } = await import("./git_helpers.cjs"); + expect(extractBundlePrerequisiteCommits("fatal: failed to read bundle")).toEqual([]); + }); + + it("should return single SHA when one prerequisite commit is missing", async () => { + const { extractBundlePrerequisiteCommits } = await import("./git_helpers.cjs"); + const message = "error: Repository lacks these prerequisite commits:\nerror: 172f87a830f57a29470efe7646d141069434a893"; + expect(extractBundlePrerequisiteCommits(message)).toEqual(["172f87a830f57a29470efe7646d141069434a893"]); + }); + + it("should return multiple SHAs when multiple prerequisite commits are missing", async () => { + const { extractBundlePrerequisiteCommits } = await import("./git_helpers.cjs"); + const message = [ + "error: Repository lacks these prerequisite commits:", + "error: 172f87a830f57a29470efe7646d141069434a893", + "error: aabbccddee1122334455667788990011aabbccdd", + ].join("\n"); + const result = extractBundlePrerequisiteCommits(message); + expect(result).toEqual(["172f87a830f57a29470efe7646d141069434a893", "aabbccddee1122334455667788990011aabbccdd"]); + }); + + it("should deduplicate repeated SHAs", async () => { + const { extractBundlePrerequisiteCommits } = await import("./git_helpers.cjs"); + const sha = "172f87a830f57a29470efe7646d141069434a893"; + const message = `error: Repository lacks these prerequisite commits:\nerror: ${sha}\nerror: ${sha}`; + expect(extractBundlePrerequisiteCommits(message)).toEqual([sha]); + }); + + it("should be case-insensitive for the prerequisite header text", async () => { + const { extractBundlePrerequisiteCommits } = await import("./git_helpers.cjs"); + const message = "ERROR: REPOSITORY LACKS THESE PREREQUISITE COMMITS:\nerror: 172f87a830f57a29470efe7646d141069434a893"; + expect(extractBundlePrerequisiteCommits(message)).toEqual(["172f87a830f57a29470efe7646d141069434a893"]); + }); + + it("should ignore short (non-SHA) hex strings that are not 40 characters", async () => { + const { extractBundlePrerequisiteCommits } = await import("./git_helpers.cjs"); + const message = "error: Repository lacks these prerequisite commits:\nerror: deadbeef"; + // "deadbeef" is only 8 chars — not a full 40-char SHA so it should not be captured + // (The exact filtering depends on implementation; test that a real SHA is captured) + const fullSha = "172f87a830f57a29470efe7646d141069434a893"; + const message2 = `error: Repository lacks these prerequisite commits:\nerror: ${fullSha} deadbeef`; + const result = extractBundlePrerequisiteCommits(message2); + expect(result).toContain(fullSha); + }); + }); }); diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index e5cb08ff87c..5fe9938a3cd 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -17,7 +17,7 @@ const { createAuthenticatedGitHubClient } = require("./handler_auth.cjs"); const { checkFileProtection } = require("./manifest_file_helpers.cjs"); const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); const { renderTemplateFromFile, buildProtectedFileList, getPromptPath } = require("./messages_core.cjs"); -const { ensureFullHistoryForBundle, getGitAuthEnv } = require("./git_helpers.cjs"); +const { ensureFullHistoryForBundle, getGitAuthEnv, extractBundlePrerequisiteCommits } = require("./git_helpers.cjs"); const { findRepoCheckout } = require("./find_repo_checkout.cjs"); /** @@ -668,8 +668,32 @@ async function main(config = {}) { ...baseGitOpts, }); - // Fetch from bundle into a temporary ref - await exec.exec("git", ["fetch", bundleFilePath, `refs/heads/${message.branch}:${bundleRef}`], baseGitOpts); + // Fetch from bundle into a temporary ref. + // Use getExecOutput with ignoreReturnCode so we can read the actual stderr from git — + // exec() only throws "The process '...' failed with exit code 1" which loses the + // "lacks these prerequisite commits" text needed for the recovery path below. + const bundleFetchRef = `refs/heads/${message.branch}:${bundleRef}`; + const initialBundleFetch = await exec.getExecOutput("git", ["fetch", bundleFilePath, bundleFetchRef], { ...baseGitOpts, ignoreReturnCode: true }); + if (initialBundleFetch.exitCode !== 0) { + const initialFetchErrorOutput = initialBundleFetch.stderr || `exit code ${initialBundleFetch.exitCode}`; + + // Recovery path for bundle prerequisite failures: fetch missing prerequisite + // commit objects, then retry with the original bundle ref. + // This handles the race where main advanced between agent-time and safe_outputs-time: + // the bundle's base commit may not be reachable from a fetch-depth:1 shallow clone + // even after --unshallow (e.g. when the commit is on a ref not in the fetch refspec). + const prerequisiteCommits = extractBundlePrerequisiteCommits(initialFetchErrorOutput); + if (prerequisiteCommits.length > 0) { + core.warning(`Bundle fetch failed due to ${prerequisiteCommits.length} missing prerequisite commit(s); fetching prerequisites from origin and retrying`); + core.info(`Fetching ${prerequisiteCommits.length} prerequisite commit(s) from origin`); + await exec.exec("git", ["fetch", "origin", ...prerequisiteCommits], { env: { ...process.env, ...gitAuthEnv }, ...baseGitOpts }); + core.info("Fetched prerequisite commits from origin successfully"); + await exec.exec("git", ["fetch", bundleFilePath, bundleFetchRef], baseGitOpts); + core.info("Bundle fetch retry succeeded after prerequisite recovery"); + } else { + throw new Error(`Failed to fetch bundle: ${initialFetchErrorOutput}`); + } + } core.info(`Fetched bundle to ${bundleRef}`); // Point the checked-out branch at the bundle tip directly. In shallow diff --git a/actions/setup/js/push_to_pull_request_branch.test.cjs b/actions/setup/js/push_to_pull_request_branch.test.cjs index ec507d09a1a..d68e09a9117 100644 --- a/actions/setup/js/push_to_pull_request_branch.test.cjs +++ b/actions/setup/js/push_to_pull_request_branch.test.cjs @@ -1266,7 +1266,7 @@ index 0000000..abc1234 const pushSignedSpy = vi.spyOn(pushSignedCommitsModule, "pushSignedCommits").mockResolvedValue("bundle-tip"); try { - mockExec.getExecOutput.mockImplementation((cmd, args) => { + mockExec.getExecOutput.mockImplementation((cmd, args, options) => { if (cmd === "git" && args[0] === "ls-remote") { return Promise.resolve({ exitCode: 0, stdout: "remote-head\trefs/heads/feature-branch\n", stderr: "" }); } @@ -1288,14 +1288,58 @@ index 0000000..abc1234 expect(result.success).toBe(true); expect(mockExec.exec).toHaveBeenCalledWith("git", ["fetch", "--unshallow", "origin"], expect.any(Object)); - expect(mockExec.exec).toHaveBeenCalledWith("git", ["fetch", bundlePath, "refs/heads/feature-branch:refs/bundles/push-feature-branch"], expect.any(Object)); + // Initial bundle fetch is via getExecOutput (with ignoreReturnCode: true), not exec.exec + const bundleFetchCall = mockExec.getExecOutput.mock.calls.find(([, args, options]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode); + expect(bundleFetchCall).toBeDefined(); + expect(bundleFetchCall[1]).toEqual(["fetch", bundlePath, "refs/heads/feature-branch:refs/bundles/push-feature-branch"]); expect(mockExec.exec).toHaveBeenCalledWith("git", ["update-ref", "refs/heads/feature-branch", "refs/bundles/push-feature-branch", "remote-head"], expect.any(Object)); expect(mockExec.exec).toHaveBeenCalledWith("git", ["reset", "--hard"], expect.any(Object)); expect(mockExec.exec).not.toHaveBeenCalledWith("git", ["merge", "--ff-only", "refs/bundles/push-feature-branch"], expect.any(Object)); const unshallowCallIndex = mockExec.exec.mock.calls.findIndex(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === "--unshallow"); - const bundleFetchCallIndex = mockExec.exec.mock.calls.findIndex(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); expect(unshallowCallIndex).toBeGreaterThanOrEqual(0); - expect(bundleFetchCallIndex).toBeGreaterThan(unshallowCallIndex); + } finally { + pushSignedSpy.mockRestore(); + } + }); + + it("should fetch prerequisite commits and retry bundle fetch when bundle lacks prerequisites", async () => { + const bundlePath = path.join(tempDir, "test.bundle"); + const patchPath = createPatchFile("small patch content"); + fs.writeFileSync(bundlePath, "bundle content"); + const missingSha = "172f87a830f57a29470efe7646d141069434a893"; + + const pushSignedCommitsModule = require("./push_signed_commits.cjs"); + const pushSignedSpy = vi.spyOn(pushSignedCommitsModule, "pushSignedCommits").mockResolvedValue("bundle-tip"); + + try { + mockExec.getExecOutput.mockImplementation((cmd, args, options) => { + if (cmd === "git" && args[0] === "ls-remote") { + return Promise.resolve({ exitCode: 0, stdout: "remote-head\trefs/heads/feature-branch\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "HEAD") { + return Promise.resolve({ exitCode: 0, stdout: "remote-head\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "rev-parse" && args[1] === "--is-shallow-repository") { + return Promise.resolve({ exitCode: 0, stdout: "false\n", stderr: "" }); + } + if (cmd === "git" && args[0] === "fetch" && args[1] === bundlePath && options && options.ignoreReturnCode) { + // Initial bundle fetch fails with prerequisite error (the real race condition scenario) + return Promise.resolve({ exitCode: 1, stderr: `error: Repository lacks these prerequisite commits:\nerror: ${missingSha}`, stdout: "" }); + } + return Promise.resolve({ exitCode: 0, stdout: "", stderr: "" }); + }); + + const module = await loadModule(); + const handler = await module.main({}); + const result = await handler({ branch: "feature-branch", patch_path: patchPath, bundle_path: bundlePath, diff_size: 5 * 1024 }, {}); + + expect(result.success).toBe(true); + // Should have fetched the missing prerequisite from origin + expect(mockExec.exec).toHaveBeenCalledWith("git", ["fetch", "origin", missingSha], expect.any(Object)); + // Should have retried the bundle fetch via exec after fetching prerequisites + const bundleRetryFetch = mockExec.exec.mock.calls.find(([, args]) => Array.isArray(args) && args[0] === "fetch" && args[1] === bundlePath); + expect(bundleRetryFetch).toBeDefined(); + expect(bundleRetryFetch[1]).toEqual(["fetch", bundlePath, "refs/heads/feature-branch:refs/bundles/push-feature-branch"]); } finally { pushSignedSpy.mockRestore(); }