From 2775701d7aa6712bef85418be72a58c6d8627b39 Mon Sep 17 00:00:00 2001 From: Landon Cox Date: Tue, 27 Oct 2026 18:55:24 -0700 Subject: [PATCH 1/2] fix: prebuild local images before smoke services execution Reuse the existing Smoke Copilot source-image preparation with a shared script. Give Smoke Services a separate bounded build step while keeping its ten-minute agent timeout and run-specific local image selection. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: abd2228c-5c4a-4d12-970d-1a1af87aa3e9 --- .github/workflows/smoke-copilot.lock.yml | 4 +-- .github/workflows/smoke-copilot.md | 15 +-------- .github/workflows/smoke-services.lock.yml | 7 ++-- .github/workflows/smoke-services.md | 3 ++ scripts/ci/apply-general-workflow-patches.ts | 6 ++-- scripts/ci/build-smoke-images.sh | 15 +++++++++ ...est.ts => smoke-image-preparation.test.ts} | 33 ++++++++++++------- 7 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 scripts/ci/build-smoke-images.sh rename scripts/ci/{smoke-copilot-image-preparation.test.ts => smoke-image-preparation.test.ts} (87%) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 01c69ff22..414de7d79 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"19d32afdff4ca99356c6e3d8ffafa42f9f85eac3d9c68bebf42c3d49da54ab33","body_hash":"5d4251ec5597159a42491dee93ded48c3b2a15daf78c264ef739074be6721b34","compiler_version":"v1.0.1","agent_id":"copilot","engine_versions":{"copilot":"1.0.101"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c5b17de996c078dc77d5e689a6593b9a60abc228f294e8ee54a29d56f1fd9b20","body_hash":"5d4251ec5597159a42491dee93ded48c3b2a15daf78c264ef739074be6721b34","compiler_version":"v1.0.1","agent_id":"copilot","engine_versions":{"copilot":"1.0.101"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"},{"repo":"github/gh-aw-actions/setup","sha":"221ba4f4319de6b8e809d1a15528b3ae6a818aa5","version":"v1.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.0","digest":"sha256:b14bd399376180b5649a9324be3b50fbfc82bfe8f0c80045840416b5a4e65b96","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.0@sha256:b14bd399376180b5649a9324be3b50fbfc82bfe8f0c80045840416b5a4e65b96"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.0","digest":"sha256:baff00b856a4bdc93bce134928c70af3d3492f2e518757115042040674d56b0b","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.0@sha256:baff00b856a4bdc93bce134928c70af3d3492f2e518757115042040674d56b0b"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.0","digest":"sha256:6e0cbf7f980beb78b5461563a8164f6439ca959bc93733d5a6b52972e523ed50","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.0@sha256:6e0cbf7f980beb78b5461563a8164f6439ca959bc93733d5a6b52972e523ed50"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.0","digest":"sha256:d3544a4222e7efeeeb38669d8a1af0c72691c55b8f6506b15aa5920f7f31b7bc","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.0@sha256:d3544a4222e7efeeeb38669d8a1af0c72691c55b8f6506b15aa5920f7f31b7bc"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"},{"image":"ghcr.io/github/github-mcp-server:v2.0.3","digest":"sha256:250bc6ce70788207608ef74cef4397230ff2e8481218da74d6093e4bd9101132","pinned_image":"ghcr.io/github/github-mcp-server:v2.0.3@sha256:250bc6ce70788207608ef74cef4397230ff2e8481218da74d6093e4bd9101132"}],"mcp_servers":[{"name":"github","tools":["list_pull_requests"]},{"name":"safeoutputs","tools":["add_comment","add_labels","noop","report_incomplete","report_missing_data","report_missing_tool"]}],"threat_detection":{"mode":"disabled"},"admission":false} # This file was automatically generated by gh-aw (v1.0.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -626,7 +626,7 @@ jobs: EOF sudo chmod +x /usr/local/bin/awf - name: Build local AWF smoke images - run: "set -euo pipefail\nAWF_SMOKE_IMAGE_TAG=\"run-${GITHUB_RUN_ID:?}-${GITHUB_RUN_ATTEMPT:?}\"\nHOST_UID=\"$(node -p \"require('./dist/host-identity').getSafeHostUid()\")\"\nHOST_GID=\"$(node -p \"require('./dist/host-identity').getSafeHostGid()\")\"\nfor image in squid agent api-proxy; do\n build_args=()\n if [ \"$image\" = agent ]; then\n build_args+=(--build-arg \"USER_UID=$HOST_UID\" --build-arg \"USER_GID=$HOST_GID\")\n fi\n docker build \"${build_args[@]}\" --tag \"awf-smoke/${image}:${AWF_SMOKE_IMAGE_TAG}\" \"containers/${image}\"\n docker image inspect \"awf-smoke/${image}:${AWF_SMOKE_IMAGE_TAG}\" > /dev/null\ndone\nprintf 'AWF_SMOKE_IMAGE_TAG=%s\\n' \"$AWF_SMOKE_IMAGE_TAG\" >> \"$GITHUB_ENV\"\n" + run: bash scripts/ci/build-smoke-images.sh timeout-minutes: 35 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index fac98c613..31a253a6f 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -65,20 +65,7 @@ jobs: steps: - name: Build local AWF smoke images timeout-minutes: 35 - run: | - set -euo pipefail - AWF_SMOKE_IMAGE_TAG="run-${GITHUB_RUN_ID:?}-${GITHUB_RUN_ATTEMPT:?}" - HOST_UID="$(node -p "require('./dist/host-identity').getSafeHostUid()")" - HOST_GID="$(node -p "require('./dist/host-identity').getSafeHostGid()")" - for image in squid agent api-proxy; do - build_args=() - if [ "$image" = agent ]; then - build_args+=(--build-arg "USER_UID=$HOST_UID" --build-arg "USER_GID=$HOST_GID") - fi - docker build "${build_args[@]}" --tag "awf-smoke/${image}:${AWF_SMOKE_IMAGE_TAG}" "containers/${image}" - docker image inspect "awf-smoke/${image}:${AWF_SMOKE_IMAGE_TAG}" > /dev/null - done - printf 'AWF_SMOKE_IMAGE_TAG=%s\n' "$AWF_SMOKE_IMAGE_TAG" >> "$GITHUB_ENV" + run: bash scripts/ci/build-smoke-images.sh - name: Pre-compute smoke test data run: | echo "::group::Fetching last 2 merged PRs" diff --git a/.github/workflows/smoke-services.lock.yml b/.github/workflows/smoke-services.lock.yml index fa121ab57..009141e6b 100644 --- a/.github/workflows/smoke-services.lock.yml +++ b/.github/workflows/smoke-services.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e7dffe5060929831399b2c4e3b9f09f3a4dc07cc2a4b349e9252ab0958708913","body_hash":"f4f4e95ed2088cfa712d232010407a7fadf0156c78f78400003d63e00ccedcdf","compiler_version":"v1.0.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.101"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2b614f13e41f211d49f32d99828560f8b6766ba6a5887c631b13bcab2bacd086","body_hash":"f4f4e95ed2088cfa712d232010407a7fadf0156c78f78400003d63e00ccedcdf","compiler_version":"v1.0.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.101"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"},{"repo":"github/gh-aw-actions/setup","sha":"221ba4f4319de6b8e809d1a15528b3ae6a818aa5","version":"v1.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.0","digest":"sha256:b14bd399376180b5649a9324be3b50fbfc82bfe8f0c80045840416b5a4e65b96","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.0@sha256:b14bd399376180b5649a9324be3b50fbfc82bfe8f0c80045840416b5a4e65b96"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.0","digest":"sha256:baff00b856a4bdc93bce134928c70af3d3492f2e518757115042040674d56b0b","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.0@sha256:baff00b856a4bdc93bce134928c70af3d3492f2e518757115042040674d56b0b"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.0","digest":"sha256:6e0cbf7f980beb78b5461563a8164f6439ca959bc93733d5a6b52972e523ed50","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.0@sha256:6e0cbf7f980beb78b5461563a8164f6439ca959bc93733d5a6b52972e523ed50"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.0","digest":"sha256:d3544a4222e7efeeeb38669d8a1af0c72691c55b8f6506b15aa5920f7f31b7bc","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.0@sha256:d3544a4222e7efeeeb38669d8a1af0c72691c55b8f6506b15aa5920f7f31b7bc"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"}],"mcp_servers":[{"name":"safeoutputs","tools":["add_comment","add_labels","noop","report_incomplete","report_missing_data","report_missing_tool"]}],"threat_detection":{"mode":"disabled"},"admission":false} # This file was automatically generated by gh-aw (v1.0.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -615,6 +615,9 @@ jobs: exec "${NODE_BIN}" "${WORKSPACE_PATH}/dist/cli.js" "\$@" EOF sudo chmod +x /usr/local/bin/awf + - name: Build local AWF smoke images + run: bash scripts/ci/build-smoke-images.sh + timeout-minutes: 35 - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: @@ -1006,7 +1009,7 @@ jobs: GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ - sudo --preserve-env="$(compgen -e | paste -sd, -)" /usr/bin/env PATH="$PATH" /usr/local/bin/awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --session-state-dir /tmp/gh-aw/sandbox/agent/session-state --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-host-service-ports "${{ job.services['postgres'].ports['5432'] }},${{ job.services['redis'].ports['6379'] }}" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_AW_OTLP_ALL_HEADERS --exclude-env GH_AW_OTLP_ENDPOINTS --exclude-env MCP_GATEWAY_AGENT_ID --exclude-env OTEL_EXPORTER_OTLP_ENDPOINT --exclude-env OTEL_EXPORTER_OTLP_HEADERS --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --legacy-security --enable-host-access --allow-host-ports 80,443,8080 --session-state-dir /tmp/gh-aw/sandbox/agent/session-state --build-local \ + sudo --preserve-env="$(compgen -e | paste -sd, -)" /usr/bin/env PATH="$PATH" /usr/local/bin/awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --session-state-dir /tmp/gh-aw/sandbox/agent/session-state --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --allow-host-service-ports "${{ job.services['postgres'].ports['5432'] }},${{ job.services['redis'].ports['6379'] }}" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_AW_OTLP_ALL_HEADERS --exclude-env GH_AW_OTLP_ENDPOINTS --exclude-env MCP_GATEWAY_AGENT_ID --exclude-env OTEL_EXPORTER_OTLP_ENDPOINT --exclude-env OTEL_EXPORTER_OTLP_HEADERS --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --legacy-security --enable-host-access --allow-host-ports 80,443,8080 --session-state-dir /tmp/gh-aw/sandbox/agent/session-state --image-registry awf-smoke --image-tag "${AWF_SMOKE_IMAGE_TAG:?}" --skip-pull \ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; GH_AW_TOOL_BINS="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr "\n" ":")"; GH_AW_TOOL_BINS="${GH_AW_TOOL_BINS%:}"; export PATH="$PATH${GH_AW_TOOL_BINS:+:}$GH_AW_TOOL_BINS"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/bin/copilot" --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-tool safeoutputs --allow-tool shell --allow-tool write --deny-tool workflow --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 diff --git a/.github/workflows/smoke-services.md b/.github/workflows/smoke-services.md index 2aa8c90b9..0213e8ad4 100644 --- a/.github/workflows/smoke-services.md +++ b/.github/workflows/smoke-services.md @@ -70,6 +70,9 @@ sandbox: runtime: docker-sudo-iptables strict: true steps: + - name: Build local AWF smoke images + timeout-minutes: 35 + run: bash scripts/ci/build-smoke-images.sh - name: Pre-install service client tools run: | sudo apt-get update -qq && sudo apt-get install -y -qq redis-tools postgresql-client diff --git a/scripts/ci/apply-general-workflow-patches.ts b/scripts/ci/apply-general-workflow-patches.ts index 2e010ffe8..0b18af8b4 100644 --- a/scripts/ci/apply-general-workflow-patches.ts +++ b/scripts/ci/apply-general-workflow-patches.ts @@ -80,7 +80,7 @@ export function applyGeneralWorkflowPatches( workflowPath: string ): PatchResult { const log: string[] = []; - const preparedSmokeImages = /(?:^|[/\\])smoke-copilot\.lock\.yml$/.test(workflowPath) + const preparedSmokeImages = /(?:^|[/\\])smoke-(?:copilot|services)\.lock\.yml$/.test(workflowPath) && content.includes('- name: Build local AWF smoke images\n'); const preparedSmokeImageArgs = '--image-registry awf-smoke --image-tag "${AWF_SMOKE_IMAGE_TAG:?}" --skip-pull'; @@ -548,12 +548,12 @@ export function applyGeneralWorkflowPatches( ); const localInstall = /^ {6}- name: Install awf binary \(local\)\n(?: {8}.*\n|\n)*/m; if (!preparation || !localInstall.test(content)) { - throw new Error('Smoke Copilot image preparation requires the local AWF install step'); + throw new Error('Smoke image preparation requires the local AWF install step'); } content = content.replace(preparation[0], ''); content = content.replace(localInstall, step => step + preparation[0]); content = content.replace(/--build-local\b/g, preparedSmokeImageArgs); - log.push(' Reused prepared PR images outside the Smoke Copilot execution budget'); + log.push(' Reused prepared PR images outside the smoke execution budget'); } } diff --git a/scripts/ci/build-smoke-images.sh b/scripts/ci/build-smoke-images.sh new file mode 100644 index 000000000..f6e5610ba --- /dev/null +++ b/scripts/ci/build-smoke-images.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +AWF_SMOKE_IMAGE_TAG="run-${GITHUB_RUN_ID:?}-${GITHUB_RUN_ATTEMPT:?}" +HOST_UID="$(node -p "require('./dist/host-identity').getSafeHostUid()")" +HOST_GID="$(node -p "require('./dist/host-identity').getSafeHostGid()")" +for image in squid agent api-proxy; do + build_args=() + if [ "$image" = agent ]; then + build_args+=(--build-arg "USER_UID=$HOST_UID" --build-arg "USER_GID=$HOST_GID") + fi + docker build "${build_args[@]}" --tag "awf-smoke/${image}:${AWF_SMOKE_IMAGE_TAG}" "containers/${image}" + docker image inspect "awf-smoke/${image}:${AWF_SMOKE_IMAGE_TAG}" > /dev/null +done +printf 'AWF_SMOKE_IMAGE_TAG=%s\n' "$AWF_SMOKE_IMAGE_TAG" >> "$GITHUB_ENV" diff --git a/scripts/ci/smoke-copilot-image-preparation.test.ts b/scripts/ci/smoke-image-preparation.test.ts similarity index 87% rename from scripts/ci/smoke-copilot-image-preparation.test.ts rename to scripts/ci/smoke-image-preparation.test.ts index 95dc6c672..a768249ab 100644 --- a/scripts/ci/smoke-copilot-image-preparation.test.ts +++ b/scripts/ci/smoke-image-preparation.test.ts @@ -19,16 +19,19 @@ interface Step { } const root = path.resolve(__dirname, '../..'); -const workflowPath = path.join(root, '.github/workflows/smoke-copilot.lock.yml'); -const source = fs.readFileSync(path.join(root, '.github/workflows/smoke-copilot.md'), 'utf8'); -const frontmatter = yaml.load(source.split('---')[1]) as { - steps: Step[]; - 'timeout-minutes': number; -}; -const preparation = frontmatter.steps.find(step => step.name === 'Build local AWF smoke images')!; const imageArgs = '--image-registry awf-smoke --image-tag "${AWF_SMOKE_IMAGE_TAG:?}" --skip-pull'; -describe('Smoke Copilot local image preparation', () => { +describe.each([ + ['copilot', 15], + ['services', 10], +])('Smoke %s local image preparation', (workflow, executionTimeout) => { + const workflowPath = path.join(root, `.github/workflows/smoke-${workflow}.lock.yml`); + const source = fs.readFileSync(path.join(root, `.github/workflows/smoke-${workflow}.md`), 'utf8'); + const frontmatter = yaml.load(source.split('---')[1]) as { + steps: Step[]; + 'timeout-minutes': number; + }; + const preparation = frontmatter.steps.find(step => step.name === 'Build local AWF smoke images')!; let directory: string; beforeEach(() => { directory = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-smoke-images-')); @@ -45,6 +48,7 @@ docker() { printf '%s\\n' "$*" >> "$CALLS" case "$*" in *"$FAILURE"*) if [ -n "$FAILURE" ]; then return 7; fi ;; esac } +export -f docker ${preparation.run} `; const result = spawnSync('bash', ['-c', script], { @@ -103,7 +107,14 @@ ${preparation.run} for (const service of Object.values(execution.services)) expect(service.build).toBeUndefined(); }); - it.each(['build --tag awf-smoke/squid', 'image inspect awf-smoke/agent'])( + it.each([ + 'build --tag awf-smoke/squid', + `build --build-arg USER_UID=${getSafeHostUid()} --build-arg USER_GID=${getSafeHostGid()} --tag awf-smoke/agent`, + 'build --tag awf-smoke/api-proxy', + 'image inspect awf-smoke/squid', + 'image inspect awf-smoke/agent', + 'image inspect awf-smoke/api-proxy', + ])( 'fails closed before publishing image selection on %s failure', failure => { const { result } = runPreparation(failure); expect(result.status).toBe(7); @@ -123,7 +134,7 @@ ${preparation.run} it('rejects prepared-image workflows without a local CLI install anchor', () => { expect(() => applyGeneralWorkflowPatches( ` - name: ${preparation.name}\n run: echo prepare\n`, workflowPath, - )).toThrow('Smoke Copilot image preparation requires the local AWF install step'); + )).toThrow('Smoke image preparation requires the local AWF install step'); }); it('gives preparation its own bounded budget before the unchanged timed execution', () => { @@ -138,7 +149,7 @@ ${preparation.run} expect(steps[prepareIndex].run).toBe(preparation.run); expect(steps[prepareIndex]['timeout-minutes']).toBe(35); expect(steps[executeIndex]['timeout-minutes']).toBe(frontmatter['timeout-minutes']); - expect(frontmatter['timeout-minutes']).toBe(15); + expect(frontmatter['timeout-minutes']).toBe(executionTimeout); expect(lock.jobs.agent['timeout-minutes']).toBeGreaterThanOrEqual(50); expect(steps[executeIndex].run).toContain(imageArgs); expect(steps[executeIndex].run).not.toContain('--build-local'); From 75eabb52309550f1092f9de8a07a50fc60adbcfc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 28 Oct 2026 02:13:32 +0000 Subject: [PATCH 2/2] fix: preserve smoke helper across PR checkout --- .github/workflows/smoke-copilot.lock.yml | 10 +++++++--- .github/workflows/smoke-copilot.md | 2 +- .github/workflows/smoke-services.lock.yml | 10 +++++++--- .github/workflows/smoke-services.md | 2 +- scripts/ci/apply-general-workflow-patches.ts | 12 ++++++++++++ scripts/ci/smoke-image-preparation.test.ts | 18 +++++++++++++++++- 6 files changed, 45 insertions(+), 9 deletions(-) diff --git a/.github/workflows/smoke-copilot.lock.yml b/.github/workflows/smoke-copilot.lock.yml index 414de7d79..bec6f9771 100644 --- a/.github/workflows/smoke-copilot.lock.yml +++ b/.github/workflows/smoke-copilot.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c5b17de996c078dc77d5e689a6593b9a60abc228f294e8ee54a29d56f1fd9b20","body_hash":"5d4251ec5597159a42491dee93ded48c3b2a15daf78c264ef739074be6721b34","compiler_version":"v1.0.1","agent_id":"copilot","engine_versions":{"copilot":"1.0.101"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"07119e34329868bfbf5442c50420de1014fa46a4ac32ef70cda28c6ee10a6d71","body_hash":"5d4251ec5597159a42491dee93ded48c3b2a15daf78c264ef739074be6721b34","compiler_version":"v1.0.1","agent_id":"copilot","engine_versions":{"copilot":"1.0.101"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"},{"repo":"github/gh-aw-actions/setup","sha":"221ba4f4319de6b8e809d1a15528b3ae6a818aa5","version":"v1.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.0","digest":"sha256:b14bd399376180b5649a9324be3b50fbfc82bfe8f0c80045840416b5a4e65b96","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.0@sha256:b14bd399376180b5649a9324be3b50fbfc82bfe8f0c80045840416b5a4e65b96"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.0","digest":"sha256:baff00b856a4bdc93bce134928c70af3d3492f2e518757115042040674d56b0b","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.0@sha256:baff00b856a4bdc93bce134928c70af3d3492f2e518757115042040674d56b0b"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.0","digest":"sha256:6e0cbf7f980beb78b5461563a8164f6439ca959bc93733d5a6b52972e523ed50","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.0@sha256:6e0cbf7f980beb78b5461563a8164f6439ca959bc93733d5a6b52972e523ed50"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.0","digest":"sha256:d3544a4222e7efeeeb38669d8a1af0c72691c55b8f6506b15aa5920f7f31b7bc","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.0@sha256:d3544a4222e7efeeeb38669d8a1af0c72691c55b8f6506b15aa5920f7f31b7bc"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"},{"image":"ghcr.io/github/github-mcp-server:v2.0.3","digest":"sha256:250bc6ce70788207608ef74cef4397230ff2e8481218da74d6093e4bd9101132","pinned_image":"ghcr.io/github/github-mcp-server:v2.0.3@sha256:250bc6ce70788207608ef74cef4397230ff2e8481218da74d6093e4bd9101132"}],"mcp_servers":[{"name":"github","tools":["list_pull_requests"]},{"name":"safeoutputs","tools":["add_comment","add_labels","noop","report_incomplete","report_missing_data","report_missing_tool"]}],"threat_detection":{"mode":"disabled"},"admission":false} # This file was automatically generated by gh-aw (v1.0.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -54,7 +54,7 @@ on: - labeled # roles: all # Roles processed as role check in pre-activation job schedule: - - cron: "28 */12 * * *" + - cron: "20 */12 * * *" workflow_dispatch: inputs: aw_context: @@ -574,6 +574,10 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" + - name: Preserve smoke image preparation helper + run: | + mkdir -p "${RUNNER_TEMP}/awf-smoke" + cp "${GITHUB_WORKSPACE}/scripts/ci/build-smoke-images.sh" "${RUNNER_TEMP}/awf-smoke/build-smoke-images.sh" - name: Checkout PR branch id: checkout-pr if: | @@ -626,7 +630,7 @@ jobs: EOF sudo chmod +x /usr/local/bin/awf - name: Build local AWF smoke images - run: bash scripts/ci/build-smoke-images.sh + run: bash "${RUNNER_TEMP}/awf-smoke/build-smoke-images.sh" timeout-minutes: 35 - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown diff --git a/.github/workflows/smoke-copilot.md b/.github/workflows/smoke-copilot.md index 31a253a6f..198634f80 100644 --- a/.github/workflows/smoke-copilot.md +++ b/.github/workflows/smoke-copilot.md @@ -65,7 +65,7 @@ jobs: steps: - name: Build local AWF smoke images timeout-minutes: 35 - run: bash scripts/ci/build-smoke-images.sh + run: bash "${RUNNER_TEMP}/awf-smoke/build-smoke-images.sh" - name: Pre-compute smoke test data run: | echo "::group::Fetching last 2 merged PRs" diff --git a/.github/workflows/smoke-services.lock.yml b/.github/workflows/smoke-services.lock.yml index 009141e6b..c510266d6 100644 --- a/.github/workflows/smoke-services.lock.yml +++ b/.github/workflows/smoke-services.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"2b614f13e41f211d49f32d99828560f8b6766ba6a5887c631b13bcab2bacd086","body_hash":"f4f4e95ed2088cfa712d232010407a7fadf0156c78f78400003d63e00ccedcdf","compiler_version":"v1.0.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.101"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d69aef83021783e423bba6aaccbd67eb438f8b16ad94bd75b1d3c3c5d1e89fe7","body_hash":"f4f4e95ed2088cfa712d232010407a7fadf0156c78f78400003d63e00ccedcdf","compiler_version":"v1.0.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.101"}} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"},{"repo":"github/gh-aw-actions/setup","sha":"221ba4f4319de6b8e809d1a15528b3ae6a818aa5","version":"v1.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.29.0","digest":"sha256:b14bd399376180b5649a9324be3b50fbfc82bfe8f0c80045840416b5a4e65b96","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.29.0@sha256:b14bd399376180b5649a9324be3b50fbfc82bfe8f0c80045840416b5a4e65b96"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.0","digest":"sha256:baff00b856a4bdc93bce134928c70af3d3492f2e518757115042040674d56b0b","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.29.0@sha256:baff00b856a4bdc93bce134928c70af3d3492f2e518757115042040674d56b0b"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.29.0","digest":"sha256:6e0cbf7f980beb78b5461563a8164f6439ca959bc93733d5a6b52972e523ed50","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.29.0@sha256:6e0cbf7f980beb78b5461563a8164f6439ca959bc93733d5a6b52972e523ed50"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.5.0","digest":"sha256:d3544a4222e7efeeeb38669d8a1af0c72691c55b8f6506b15aa5920f7f31b7bc","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.5.0@sha256:d3544a4222e7efeeeb38669d8a1af0c72691c55b8f6506b15aa5920f7f31b7bc"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"}],"mcp_servers":[{"name":"safeoutputs","tools":["add_comment","add_labels","noop","report_incomplete","report_missing_data","report_missing_tool"]}],"threat_detection":{"mode":"disabled"},"admission":false} # This file was automatically generated by gh-aw (v1.0.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -53,7 +53,7 @@ on: - labeled # roles: all # Roles processed as role check in pre-activation job schedule: - - cron: "14 */12 * * *" + - cron: "30 */12 * * *" workflow_dispatch: inputs: aw_context: @@ -564,6 +564,10 @@ jobs: GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" + - name: Preserve smoke image preparation helper + run: | + mkdir -p "${RUNNER_TEMP}/awf-smoke" + cp "${GITHUB_WORKSPACE}/scripts/ci/build-smoke-images.sh" "${RUNNER_TEMP}/awf-smoke/build-smoke-images.sh" - name: Checkout PR branch id: checkout-pr if: | @@ -616,7 +620,7 @@ jobs: EOF sudo chmod +x /usr/local/bin/awf - name: Build local AWF smoke images - run: bash scripts/ci/build-smoke-images.sh + run: bash "${RUNNER_TEMP}/awf-smoke/build-smoke-images.sh" timeout-minutes: 35 - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' diff --git a/.github/workflows/smoke-services.md b/.github/workflows/smoke-services.md index 0213e8ad4..7bcb7b321 100644 --- a/.github/workflows/smoke-services.md +++ b/.github/workflows/smoke-services.md @@ -72,7 +72,7 @@ strict: true steps: - name: Build local AWF smoke images timeout-minutes: 35 - run: bash scripts/ci/build-smoke-images.sh + run: bash "${RUNNER_TEMP}/awf-smoke/build-smoke-images.sh" - name: Pre-install service client tools run: | sudo apt-get update -qq && sudo apt-get install -y -qq redis-tools postgresql-client diff --git a/scripts/ci/apply-general-workflow-patches.ts b/scripts/ci/apply-general-workflow-patches.ts index 0b18af8b4..3082bc8ef 100644 --- a/scripts/ci/apply-general-workflow-patches.ts +++ b/scripts/ci/apply-general-workflow-patches.ts @@ -550,6 +550,18 @@ export function applyGeneralWorkflowPatches( if (!preparation || !localInstall.test(content)) { throw new Error('Smoke image preparation requires the local AWF install step'); } + const preserveHelperStep = + ' - name: Preserve smoke image preparation helper\n' + + ' run: |\n' + + ' mkdir -p "${RUNNER_TEMP}/awf-smoke"\n' + + ' cp "${GITHUB_WORKSPACE}/scripts/ci/build-smoke-images.sh" "${RUNNER_TEMP}/awf-smoke/build-smoke-images.sh"\n'; + if (!content.includes(preserveHelperStep)) { + const prCheckout = /^ - name: Checkout PR branch\n id: checkout-pr\n/m; + if (!prCheckout.test(content)) { + throw new Error('Smoke image preparation requires the PR checkout step'); + } + content = content.replace(prCheckout, `${preserveHelperStep}$&`); + } content = content.replace(preparation[0], ''); content = content.replace(localInstall, step => step + preparation[0]); content = content.replace(/--build-local\b/g, preparedSmokeImageArgs); diff --git a/scripts/ci/smoke-image-preparation.test.ts b/scripts/ci/smoke-image-preparation.test.ts index a768249ab..2bb646524 100644 --- a/scripts/ci/smoke-image-preparation.test.ts +++ b/scripts/ci/smoke-image-preparation.test.ts @@ -35,6 +35,11 @@ describe.each([ let directory: string; beforeEach(() => { directory = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-smoke-images-')); + fs.mkdirSync(path.join(directory, 'awf-smoke'), { recursive: true }); + fs.copyFileSync( + path.join(root, 'scripts/ci/build-smoke-images.sh'), + path.join(directory, 'awf-smoke/build-smoke-images.sh'), + ); }); afterEach(() => { fs.rmSync(directory, { recursive: true, force: true }); @@ -57,6 +62,7 @@ ${preparation.run} timeout: 10000, env: { PATH: process.env.PATH, + RUNNER_TEMP: directory, GITHUB_RUN_ID: '12345', GITHUB_RUN_ATTEMPT: '2', GITHUB_ENV: path.join(directory, 'env'), @@ -142,8 +148,15 @@ ${preparation.run} jobs: { agent: { steps: Step[]; 'timeout-minutes': number } }; }; const steps = lock.jobs.agent.steps; + const preserveIndex = steps.findIndex(step => step.name === 'Preserve smoke image preparation helper'); + const checkoutIndex = steps.findIndex(step => step.name === 'Checkout PR branch'); const prepareIndex = steps.findIndex(step => step.name === preparation.name); const executeIndex = steps.findIndex(step => step.name === 'Execute GitHub Copilot CLI'); + expect(preserveIndex).toBeGreaterThan(-1); + expect(preserveIndex).toBeLessThan(checkoutIndex); + expect(steps[preserveIndex].run).toContain( + '"${GITHUB_WORKSPACE}/scripts/ci/build-smoke-images.sh" "${RUNNER_TEMP}/awf-smoke/build-smoke-images.sh"', + ); expect(prepareIndex).toBeGreaterThan(steps.findIndex(step => step.name === 'Build awf')); expect(prepareIndex).toBeLessThan(executeIndex); expect(steps[prepareIndex].run).toBe(preparation.run); @@ -160,12 +173,15 @@ ${preparation.run} it.each(['--skip-pull', '--image-tag 0.29.0 --skip-pull', '--build-local'])( 'replaces compiler image selection %s and remains idempotent', flags => { - const input = ` - name: ${preparation.name}\n run: echo prepare\n` + + const input = ' - name: Checkout PR branch\n id: checkout-pr\n' + + ` - name: ${preparation.name}\n run: echo prepare\n` + ' - name: Install AWF binary\n' + ' run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.29.0 --rootless\n' + ` - name: Execute GitHub Copilot CLI\n run: awf ${flags} -- echo smoke\n`; const first = applyGeneralWorkflowPatches(input, workflowPath).content; expect(first).toContain(imageArgs); + expect(first.indexOf('Preserve smoke image preparation helper')) + .toBeLessThan(first.indexOf('Checkout PR branch')); expect(first).not.toContain('--build-local'); expect(applyGeneralWorkflowPatches(first, workflowPath).content).toBe(first); const other = applyGeneralWorkflowPatches(input, '/tmp/smoke-claude.lock.yml').content;