diff --git a/.github/workflows/daily-byok-ollama-test.lock.yml b/.github/workflows/daily-byok-ollama-test.lock.yml index 6cdd5c1e509..f56d3024c7b 100644 --- a/.github/workflows/daily-byok-ollama-test.lock.yml +++ b/.github/workflows/daily-byok-ollama-test.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"653f339cff855c2c7fbed7bca90def5f655271aad8ea9cb31bba545892d3026a","body_hash":"bd80ca99e3f4cd56715c7a73bd9f5c56165dc64beee430b859670700764082f0","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6d3815161a2692b78e7773b5af1a19c7f550c9a1d3714696eac2c143563c0b1e","body_hash":"bd80ca99e3f4cd56715c7a73bd9f5c56165dc64beee430b859670700764082f0","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748","pinned_image":"ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -483,7 +483,7 @@ jobs: sleep 2 ollama serve & echo "Waiting for Ollama service..." - for i in $(seq 1 30); do + for _ in $(seq 1 30); do if curl -sf http://localhost:11434/api/version > /dev/null 2>&1; then echo "Ollama is ready" break @@ -505,7 +505,7 @@ jobs: echo "Waiting for Ollama OpenAI-compatible endpoint..." MAX_WAIT_SECONDS=30 - for i in $(seq 1 "$MAX_WAIT_SECONDS"); do + for _ in $(seq 1 "$MAX_WAIT_SECONDS"); do if curl -sf http://localhost:11434/v1/models > /dev/null 2>&1; then echo "Ollama /v1/models is ready" exit 0 diff --git a/.github/workflows/daily-byok-ollama-test.md b/.github/workflows/daily-byok-ollama-test.md index 3fddf3cbd46..fab1a026bf6 100644 --- a/.github/workflows/daily-byok-ollama-test.md +++ b/.github/workflows/daily-byok-ollama-test.md @@ -43,7 +43,7 @@ steps: sleep 2 ollama serve & echo "Waiting for Ollama service..." - for i in $(seq 1 30); do + for _ in $(seq 1 30); do if curl -sf http://localhost:11434/api/version > /dev/null 2>&1; then echo "Ollama is ready" break @@ -65,7 +65,7 @@ steps: echo "Waiting for Ollama OpenAI-compatible endpoint..." MAX_WAIT_SECONDS=30 - for i in $(seq 1 "$MAX_WAIT_SECONDS"); do + for _ in $(seq 1 "$MAX_WAIT_SECONDS"); do if curl -sf http://localhost:11434/v1/models > /dev/null 2>&1; then echo "Ollama /v1/models is ready" exit 0 diff --git a/.github/workflows/daily-multi-device-docs-tester.lock.yml b/.github/workflows/daily-multi-device-docs-tester.lock.yml index f3808adb1e8..2ad70151eff 100644 --- a/.github/workflows/daily-multi-device-docs-tester.lock.yml +++ b/.github/workflows/daily-multi-device-docs-tester.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"78253419017a30e65efbe892a08f02c2f94a40cf9cfaafa3d574ad738bf58745","body_hash":"861d8b6d83e50d1e4770b54788b665b4b2c1940d5a8a16110ab8420880942d0f","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.83.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"96ecc441f657df4be5f1e9cc54a761c5633082249ab75e4a939d804804d643f0","body_hash":"861d8b6d83e50d1e4770b54788b665b4b2c1940d5a8a16110ab8420880942d0f","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.83.0"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43","digest":"sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43@sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748","pinned_image":"ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -567,7 +567,7 @@ jobs: EXPR_GITHUB_WORKSPACE: ${{ github.workspace }} name: Resolve slide deck PDF run: | - cd "$EXPR_GITHUB_WORKSPACE/docs" + cd "$EXPR_GITHUB_WORKSPACE/docs" || exit 1 node ../scripts/ensure-docs-slide-pdf.js - env: EXPR_GITHUB_WORKSPACE: ${{ github.workspace }} @@ -580,7 +580,6 @@ jobs: run: | PREFLIGHT_LOG="$EXPR_GITHUB_WORKSPACE/.playwright/preflight.log" set +e - cd "$EXPR_GITHUB_WORKSPACE" playwright-cli open --config "$EXPR_GITHUB_WORKSPACE/.playwright/cli.config.json" about:blank > "$PREFLIGHT_LOG" 2>&1 PREFLIGHT_STATUS=$? playwright-cli close >> "$PREFLIGHT_LOG" 2>&1 || true @@ -595,7 +594,7 @@ jobs: EXPR_GITHUB_WORKSPACE: ${{ github.workspace }} name: Install and build docs run: |- - cd "$EXPR_GITHUB_WORKSPACE/docs" + cd "$EXPR_GITHUB_WORKSPACE/docs" || exit 1 npm install npm run build diff --git a/.github/workflows/daily-multi-device-docs-tester.md b/.github/workflows/daily-multi-device-docs-tester.md index e6ac55cafe6..f42b5f54ed4 100644 --- a/.github/workflows/daily-multi-device-docs-tester.md +++ b/.github/workflows/daily-multi-device-docs-tester.md @@ -68,7 +68,7 @@ pre-agent-steps: env: EXPR_GITHUB_WORKSPACE: ${{ github.workspace }} run: | - cd "$EXPR_GITHUB_WORKSPACE/docs" + cd "$EXPR_GITHUB_WORKSPACE/docs" || exit 1 node ../scripts/ensure-docs-slide-pdf.js - name: Configure Playwright CLI launch options env: @@ -92,7 +92,6 @@ pre-agent-steps: run: | PREFLIGHT_LOG="$EXPR_GITHUB_WORKSPACE/.playwright/preflight.log" set +e - cd "$EXPR_GITHUB_WORKSPACE" playwright-cli open --config "$EXPR_GITHUB_WORKSPACE/.playwright/cli.config.json" about:blank > "$PREFLIGHT_LOG" 2>&1 PREFLIGHT_STATUS=$? playwright-cli close >> "$PREFLIGHT_LOG" 2>&1 || true @@ -107,7 +106,7 @@ pre-agent-steps: env: EXPR_GITHUB_WORKSPACE: ${{ github.workspace }} run: | - cd "$EXPR_GITHUB_WORKSPACE/docs" + cd "$EXPR_GITHUB_WORKSPACE/docs" || exit 1 npm install npm run build features: diff --git a/.github/workflows/docs-noob-tester.lock.yml b/.github/workflows/docs-noob-tester.lock.yml index 46073ab3c0e..f5db282e456 100644 --- a/.github/workflows/docs-noob-tester.lock.yml +++ b/.github/workflows/docs-noob-tester.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"32574057bf59678b4a93f3219aae3ac60e0341d58b5f341c37a63334bfdc70f5","body_hash":"2f5141b0ebede7e38fb2ac88d14208d3a293eab87bf3674d06cc401776150a68","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77","copilot-sdk":"1.0.8"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"732bbb38d9a00d187f8bd61be9cb8934ce7b5482bd91c4c056296b7f53eca3cd","body_hash":"2f5141b0ebede7e38fb2ac88d14208d3a293eab87bf3674d06cc401776150a68","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77","copilot-sdk":"1.0.8"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748","pinned_image":"ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -565,7 +565,7 @@ jobs: - env: EXPR_GITHUB_WORKSPACE: ${{ github.workspace }} name: Start docs server - run: "cd \"$EXPR_GITHUB_WORKSPACE\"\nnohup make dev-docs > /tmp/gh-aw/agent/preview.log 2>&1 &\nPID=$!\necho $PID > /tmp/gh-aw/agent/server.pid\necho \"Server PID: $PID\"\n" + run: "cd \"$EXPR_GITHUB_WORKSPACE\" || exit 1 > /tmp/gh-aw/agent/preview.log 2>&1 &\nPID=$!\necho $PID > /tmp/gh-aw/agent/server.pid\necho \"Server PID: $PID\"\n" - name: Wait for server readiness run: "MAX_WAIT=135 # 45 attempts × 3s = 135s max wait\nWAITED=0\nuntil (echo > /dev/tcp/127.0.0.1/4321) > /dev/null 2>&1; do\n # Check if the server process has already died\n if [ -f /tmp/gh-aw/agent/server.pid ] && ! kill -0 \"$(cat /tmp/gh-aw/agent/server.pid)\" 2>/dev/null; then\n echo \"::error::Documentation server process died before opening port 4321. Server log:\"\n cat /tmp/gh-aw/agent/preview.log\n exit 1\n fi\n WAITED=$((WAITED + 3))\n if [ $WAITED -ge $MAX_WAIT ]; then\n echo \"::error::Documentation server port 4321 did not open after ${MAX_WAIT}s. Server log:\"\n cat /tmp/gh-aw/agent/preview.log\n exit 1\n fi\n echo \"Waiting for docs port... ($WAITED/${MAX_WAIT}s)\"\n sleep 3\ndone\nWAITED=0\nuntil curl -sf http://localhost:4321/gh-aw/ > /dev/null 2>&1; do\n # Check if the server process has already died\n if [ -f /tmp/gh-aw/agent/server.pid ] && ! kill -0 \"$(cat /tmp/gh-aw/agent/server.pid)\" 2>/dev/null; then\n echo \"::error::Documentation server process died before becoming ready. Server log:\"\n cat /tmp/gh-aw/agent/preview.log\n exit 1\n fi\n WAITED=$((WAITED + 3))\n if [ $WAITED -ge $MAX_WAIT ]; then\n echo \"::error::Documentation server did not start after ${MAX_WAIT}s. Server log:\"\n cat /tmp/gh-aw/agent/preview.log\n exit 1\n fi\n echo \"Waiting for server... ($WAITED/${MAX_WAIT}s)\"\n sleep 3\ndone\necho \"Server ready at http://localhost:4321/gh-aw/!\"\n" - name: Write server URL for agent diff --git a/.github/workflows/docs-noob-tester.md b/.github/workflows/docs-noob-tester.md index b95a3ed5290..4e8c99bf01a 100644 --- a/.github/workflows/docs-noob-tester.md +++ b/.github/workflows/docs-noob-tester.md @@ -53,8 +53,7 @@ pre-agent-steps: env: EXPR_GITHUB_WORKSPACE: ${{ github.workspace }} run: | - cd "$EXPR_GITHUB_WORKSPACE" - nohup make dev-docs > /tmp/gh-aw/agent/preview.log 2>&1 & + cd "$EXPR_GITHUB_WORKSPACE" || exit 1 > /tmp/gh-aw/agent/preview.log 2>&1 & PID=$! echo $PID > /tmp/gh-aw/agent/server.pid echo "Server PID: $PID" diff --git a/.github/workflows/pr-sous-chef.lock.yml b/.github/workflows/pr-sous-chef.lock.yml index 6a731618d2c..2a2070dd39b 100644 --- a/.github/workflows/pr-sous-chef.lock.yml +++ b/.github/workflows/pr-sous-chef.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"35516412c47b5db05184634e4226b00027fab53a54b5c057cf7c68261ae9d8fa","body_hash":"e30b1afca4d23528266bc4ba6447a5df79d5c99358000fccd7908a4d3b558083","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.83.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"794cf932ff9a2750f9d74020a7a961eaaf67d85a41e6c90515d63b75c3b7d533","body_hash":"e30b1afca4d23528266bc4ba6447a5df79d5c99358000fccd7908a4d3b558083","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.83.0"}} # gh-aw-manifest: {"version":1,"secrets":["AWI_MAINTENANCE_TOKEN","COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43","digest":"sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43@sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748","pinned_image":"ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -609,20 +609,19 @@ jobs: pr_list_ok=0 while [ "$pr_list_attempt" -le "$pr_list_retries" ]; do set +e - pr_list_error="$( - gh pr list --repo "$EXPR_GITHUB_REPOSITORY" \ - --state open \ - --search "is:pr is:open -is:draft sort:updated-desc" \ - --limit "$pr_limit" \ - --json number,title,url,headRefOid,headRefName,updatedAt,author,mergeStateStatus,statusCheckRollup \ - > "$candidate_file" 2>&1 - )" + gh pr list --repo "$EXPR_GITHUB_REPOSITORY" \ + --state open \ + --search "is:pr is:open -is:draft sort:updated-desc" \ + --limit "$pr_limit" \ + --json number,title,url,headRefOid,headRefName,updatedAt,author,mergeStateStatus,statusCheckRollup \ + > "$candidate_file" 2>&1 pr_list_status=$? set -e if [ "$pr_list_status" -eq 0 ]; then pr_list_ok=1 break fi + pr_list_error=$(cat "$candidate_file" 2>/dev/null || true) if echo "$pr_list_error" | grep -qiE 'HTTP 50[0234]|HTTP 429|Bad Gateway|timeout|temporarily unavailable|EOF'; then echo "Transient gh pr list failure on attempt $pr_list_attempt/$pr_list_retries; retrying: $pr_list_error" >&2 if [ "$pr_list_attempt" -lt "$pr_list_retries" ]; then diff --git a/.github/workflows/pr-sous-chef.md b/.github/workflows/pr-sous-chef.md index 075a89c1245..813dd83ab18 100644 --- a/.github/workflows/pr-sous-chef.md +++ b/.github/workflows/pr-sous-chef.md @@ -74,20 +74,19 @@ steps: pr_list_ok=0 while [ "$pr_list_attempt" -le "$pr_list_retries" ]; do set +e - pr_list_error="$( - gh pr list --repo "$EXPR_GITHUB_REPOSITORY" \ - --state open \ - --search "is:pr is:open -is:draft sort:updated-desc" \ - --limit "$pr_limit" \ - --json number,title,url,headRefOid,headRefName,updatedAt,author,mergeStateStatus,statusCheckRollup \ - > "$candidate_file" 2>&1 - )" + gh pr list --repo "$EXPR_GITHUB_REPOSITORY" \ + --state open \ + --search "is:pr is:open -is:draft sort:updated-desc" \ + --limit "$pr_limit" \ + --json number,title,url,headRefOid,headRefName,updatedAt,author,mergeStateStatus,statusCheckRollup \ + > "$candidate_file" 2>&1 pr_list_status=$? set -e if [ "$pr_list_status" -eq 0 ]; then pr_list_ok=1 break fi + pr_list_error=$(cat "$candidate_file" 2>/dev/null || true) if echo "$pr_list_error" | grep -qiE 'HTTP 50[0234]|HTTP 429|Bad Gateway|timeout|temporarily unavailable|EOF'; then echo "Transient gh pr list failure on attempt $pr_list_attempt/$pr_list_retries; retrying: $pr_list_error" >&2 if [ "$pr_list_attempt" -lt "$pr_list_retries" ]; then diff --git a/.github/workflows/slide-deck-maintainer.lock.yml b/.github/workflows/slide-deck-maintainer.lock.yml index 8b15f621993..8c6a4a5aabf 100644 --- a/.github/workflows/slide-deck-maintainer.lock.yml +++ b/.github/workflows/slide-deck-maintainer.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fd45811010fe2d89d7202511a5bc1c23e49445e9b32dc826b9555488e8163bd3","body_hash":"c2f3232e67aac248996a46f5305fb0dc917b04b56765de7b59934980a5150e5f","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"faff993d3137fc9016b613ab985f8236cc1696fdf37dcca649ed19cd201d47b5","body_hash":"c2f3232e67aac248996a46f5305fb0dc917b04b56765de7b59934980a5150e5f","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748","pinned_image":"ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -564,7 +564,7 @@ jobs: run: bash "${RUNNER_TEMP}/gh-aw/actions/setup_cache_memory_git.sh" - name: Install Marp dependencies run: |- - cd docs + cd docs || exit 1 npm ci - name: Configure Git credentials diff --git a/.github/workflows/slide-deck-maintainer.md b/.github/workflows/slide-deck-maintainer.md index b9aba582ebc..b01e524a9b4 100644 --- a/.github/workflows/slide-deck-maintainer.md +++ b/.github/workflows/slide-deck-maintainer.md @@ -80,7 +80,7 @@ steps: - name: Install Marp dependencies run: | - cd docs + cd docs || exit 1 npm ci --- diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 63c960230a6..769ab310125 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6cd75458f1e59539eb87ae9c6e23727cb42ea3c5cba18cb91c5ba56d3fb92aed","body_hash":"4fe2f07a2ac03520b96c2677feff2f69ca7c65e6f85de6fa2ad92f7786d7510b","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.83.0"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"3f6b45e928fe3af3d0486570baf2cfd1c569f83c1fb2744a7a88a6c922f567af","body_hash":"4fe2f07a2ac03520b96c2677feff2f69ca7c65e6f85de6fa2ad92f7786d7510b","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.83.0"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"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":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43","digest":"sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43@sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748","pinned_image":"ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -638,7 +638,7 @@ jobs: GH_AW_SKILL_DIR: ".pi/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Pre-flight checks - run: "mkdir -p /tmp/gh-aw/agent\nmkdir -p /tmp/gh-aw/cache-memory\n\n# Write a heartbeat timestamp so the cache always has fresh content to save,\n# even on noop runs where the agent writes nothing to the cache directory.\necho \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\" > /tmp/gh-aw/cache-memory/last-run.txt\n\n# Check 1: verify docs directory structure exists\nDIR_COUNT=$(find docs/src/content/docs -maxdepth 1 -type d 2>/dev/null | wc -l)\nif [ \"$DIR_COUNT\" -eq 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight failed: docs/src/content/docs directory not found — documentation structure is missing or repository is not set up correctly.\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# Check 2: count editable markdown files\nTOTAL=$(find docs/src/content/docs -path '*/blog*' -prune \\\n -o -name '*.md' -type f ! -name 'frontmatter-full.md' -print0 \\\n | xargs -0 grep -rL 'disable-agentic-editing: true' 2>/dev/null \\\n | wc -l)\nif [ \"$TOTAL\" -eq 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight failed: no editable markdown files found in docs/src/content/docs (all files may be protected or excluded).\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# Check 3: count uncleaned candidates (not cleaned in the past 7 days)\nRECENT_CUTOFF=$(date -d '7 days ago' '+%Y-%m-%d' 2>/dev/null \\\n || date -v-7d '+%Y-%m-%d' 2>/dev/null \\\n || echo \"0000-00-00\")\n\n# Expiration check: if the most recent cleanup entry is older than 14 days the\n# cache has gone cold (e.g. GitHub Actions evicted the 7-day cache entry).\n# Reset cleaned-files.txt so every file is eligible again and stale \"already\n# cleaned\" claims are not silently reused.\nCACHE_FILE=\"/tmp/gh-aw/cache-memory/cleaned-files.txt\"\nSTALE_CUTOFF=$(date -d '14 days ago' '+%Y-%m-%d' 2>/dev/null \\\n || date -v-14d '+%Y-%m-%d' 2>/dev/null \\\n || echo \"0000-00-00\")\nLATEST_ENTRY=$(awk 'NF>0{print $1}' \"$CACHE_FILE\" 2>/dev/null | sort | tail -1)\nif [ -n \"$LATEST_ENTRY\" ] && [ \"$LATEST_ENTRY\" \\< \"$STALE_CUTOFF\" ]; then\n echo \"Cache expiration: most recent entry $LATEST_ENTRY predates $STALE_CUTOFF — resetting cleaned-files.txt\"\n > \"$CACHE_FILE\"\nfi\n\nCLEANED=$(awk -v cutoff=\"$RECENT_CUTOFF\" \\\n 'NF>0 && $1>=cutoff{count++} END{print count+0}' \\\n \"$CACHE_FILE\" 2>/dev/null || echo \"0\")\nUNCLEANED=$(( TOTAL - CLEANED ))\nif [ \"$UNCLEANED\" -le 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight check: all eligible documentation files were cleaned recently — nothing to do this run.\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# All checks passed — write candidate file list and preflight result\nfind docs/src/content/docs -path '*/blog*' -prune \\\n -o -name '*.md' -type f ! -name 'frontmatter-full.md' -print0 \\\n | xargs -0 grep -rL 'disable-agentic-editing: true' 2>/dev/null \\\n > /tmp/gh-aw/agent/candidate-files.txt\nprintf '{\"pass\":true,\"reason\":\"All pre-flight checks passed. %d uncleaned candidates available.\",\"uncleaned\":%d,\"total\":%d}\\n' \\\n \"$UNCLEANED\" \"$UNCLEANED\" \"$TOTAL\" \\\n > /tmp/gh-aw/agent/preflight.json\n\necho \"Pre-flight passed: $UNCLEANED uncleaned candidates out of $TOTAL eligible files\"\necho \"Candidate files written to /tmp/gh-aw/agent/candidate-files.txt\"" + run: "mkdir -p /tmp/gh-aw/agent\nmkdir -p /tmp/gh-aw/cache-memory\n\n# Write a heartbeat timestamp so the cache always has fresh content to save,\n# even on noop runs where the agent writes nothing to the cache directory.\ndate -u +%Y-%m-%dT%H:%M:%SZ > /tmp/gh-aw/cache-memory/last-run.txt\n\n# Check 1: verify docs directory structure exists\nDIR_COUNT=$(find docs/src/content/docs -maxdepth 1 -type d 2>/dev/null | wc -l)\nif [ \"$DIR_COUNT\" -eq 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight failed: docs/src/content/docs directory not found — documentation structure is missing or repository is not set up correctly.\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# Check 2: count editable markdown files\nTOTAL=$(find docs/src/content/docs -path '*/blog*' -prune \\\n -o -name '*.md' -type f ! -name 'frontmatter-full.md' -print0 \\\n | xargs -0 grep -rL 'disable-agentic-editing: true' 2>/dev/null \\\n | wc -l)\nif [ \"$TOTAL\" -eq 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight failed: no editable markdown files found in docs/src/content/docs (all files may be protected or excluded).\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# Check 3: count uncleaned candidates (not cleaned in the past 7 days)\nRECENT_CUTOFF=$(date -d '7 days ago' '+%Y-%m-%d' 2>/dev/null \\\n || date -v-7d '+%Y-%m-%d' 2>/dev/null \\\n || echo \"0000-00-00\")\n\n# Expiration check: if the most recent cleanup entry is older than 14 days the\n# cache has gone cold (e.g. GitHub Actions evicted the 7-day cache entry).\n# Reset cleaned-files.txt so every file is eligible again and stale \"already\n# cleaned\" claims are not silently reused.\nCACHE_FILE=\"/tmp/gh-aw/cache-memory/cleaned-files.txt\"\nSTALE_CUTOFF=$(date -d '14 days ago' '+%Y-%m-%d' 2>/dev/null \\\n || date -v-14d '+%Y-%m-%d' 2>/dev/null \\\n || echo \"0000-00-00\")\nLATEST_ENTRY=$(awk 'NF>0{print $1}' \"$CACHE_FILE\" 2>/dev/null | sort | tail -1)\nif [ -n \"$LATEST_ENTRY\" ] && [ \"$LATEST_ENTRY\" \\< \"$STALE_CUTOFF\" ]; then\n echo \"Cache expiration: most recent entry $LATEST_ENTRY predates $STALE_CUTOFF — resetting cleaned-files.txt\"\n : > \"$CACHE_FILE\"\nfi\n\nCLEANED=$(awk -v cutoff=\"$RECENT_CUTOFF\" \\\n 'NF>0 && $1>=cutoff{count++} END{print count+0}' \\\n \"$CACHE_FILE\" 2>/dev/null || echo \"0\")\nUNCLEANED=$(( TOTAL - CLEANED ))\nif [ \"$UNCLEANED\" -le 0 ]; then\n echo '{\"pass\":false,\"reason\":\"Pre-flight check: all eligible documentation files were cleaned recently — nothing to do this run.\"}' \\\n > /tmp/gh-aw/agent/preflight.json\n exit 0\nfi\n\n# All checks passed — write candidate file list and preflight result\nfind docs/src/content/docs -path '*/blog*' -prune \\\n -o -name '*.md' -type f ! -name 'frontmatter-full.md' -print0 \\\n | xargs -0 grep -rL 'disable-agentic-editing: true' 2>/dev/null \\\n > /tmp/gh-aw/agent/candidate-files.txt\nprintf '{\"pass\":true,\"reason\":\"All pre-flight checks passed. %d uncleaned candidates available.\",\"uncleaned\":%d,\"total\":%d}\\n' \\\n \"$UNCLEANED\" \"$UNCLEANED\" \"$TOTAL\" \\\n > /tmp/gh-aw/agent/preflight.json\n\necho \"Pre-flight passed: $UNCLEANED uncleaned candidates out of $TOTAL eligible files\"\necho \"Candidate files written to /tmp/gh-aw/agent/candidate-files.txt\"" - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.43@sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00 ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748 ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 diff --git a/.github/workflows/unbloat-docs.md b/.github/workflows/unbloat-docs.md index 86d80fc355b..31f12a97115 100644 --- a/.github/workflows/unbloat-docs.md +++ b/.github/workflows/unbloat-docs.md @@ -100,7 +100,7 @@ pre-agent-steps: # Write a heartbeat timestamp so the cache always has fresh content to save, # even on noop runs where the agent writes nothing to the cache directory. - echo "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > /tmp/gh-aw/cache-memory/last-run.txt + date -u +%Y-%m-%dT%H:%M:%SZ > /tmp/gh-aw/cache-memory/last-run.txt # Check 1: verify docs directory structure exists DIR_COUNT=$(find docs/src/content/docs -maxdepth 1 -type d 2>/dev/null | wc -l) @@ -137,7 +137,7 @@ pre-agent-steps: LATEST_ENTRY=$(awk 'NF>0{print $1}' "$CACHE_FILE" 2>/dev/null | sort | tail -1) if [ -n "$LATEST_ENTRY" ] && [ "$LATEST_ENTRY" \< "$STALE_CUTOFF" ]; then echo "Cache expiration: most recent entry $LATEST_ENTRY predates $STALE_CUTOFF — resetting cleaned-files.txt" - > "$CACHE_FILE" + : > "$CACHE_FILE" fi CLEANED=$(awk -v cutoff="$RECENT_CUTOFF" \ diff --git a/cmd/gh-aw/main.go b/cmd/gh-aw/main.go index ee8629cfcd6..557c3516bf8 100644 --- a/cmd/gh-aw/main.go +++ b/cmd/gh-aw/main.go @@ -422,6 +422,7 @@ type compileCmdOptions struct { grype bool grant bool yamllint bool + noShellcheck bool jsonOutput bool showAllErrors bool fix bool @@ -464,6 +465,7 @@ func getCompileCmdOptions(cmd *cobra.Command) compileCmdOptions { grype, _ := cmd.Flags().GetBool("grype") grant, _ := cmd.Flags().GetBool("grant") yamllint, _ := cmd.Flags().GetBool("yamllint") + noShellcheck, _ := cmd.Flags().GetBool("no-shellcheck") jsonOutput, _ := cmd.Flags().GetBool("json") showAllErrors, _ := cmd.Flags().GetBool("show-all") fix, _ := cmd.Flags().GetBool("fix") @@ -483,7 +485,7 @@ func getCompileCmdOptions(cmd *cobra.Command) compileCmdOptions { dir: dir, workflowsDir: workflowsDir, logicalRepo: logicalRepo, scheduleSeed: scheduleSeed, priorManifestFile: priorManifestFile, validate: validate, watch: watch, noEmit: noEmit, purge: purge, strict: strict, trial: trial, dependabot: dependabot, forceOverwrite: forceOverwrite, refreshStopTime: refreshStopTime, forceRefreshActionPins: forceRefreshActionPins, allowActionRefs: allowActionRefs, - zizmor: zizmor, poutine: poutine, actionlint: actionlint, runnerGuard: runnerGuard, syft: syft, grype: grype, grant: grant, yamllint: yamllint, + zizmor: zizmor, poutine: poutine, actionlint: actionlint, runnerGuard: runnerGuard, syft: syft, grype: grype, grant: grant, yamllint: yamllint, noShellcheck: noShellcheck, jsonOutput: jsonOutput, showAllErrors: showAllErrors, fix: fix, stats: stats, failFast: failFast, noCheckUpdate: noCheckUpdate, staged: staged, approve: approve, validateImages: validateImages, ghes: ghes, verbose: verbose, useSamples: useSamples, } @@ -516,7 +518,7 @@ func (o *compileCmdOptions) toCompileConfig(args []string) cli.CompileConfig { NoEmit: o.noEmit, Purge: o.purge, TrialMode: o.trial, TrialLogicalRepoSlug: o.logicalRepo, Strict: o.strict, Dependabot: o.dependabot, ForceOverwrite: o.forceOverwrite, RefreshStopTime: o.refreshStopTime, ForceRefreshActionPins: o.forceRefreshActionPins, AllowActionRefs: o.allowActionRefs, Zizmor: o.zizmor, Poutine: o.poutine, Actionlint: o.actionlint, RunnerGuard: o.runnerGuard, - Syft: o.syft, Grype: o.grype, Grant: o.grant, Yamllint: o.yamllint, JSONOutput: o.jsonOutput, ShowAllErrors: o.showAllErrors, + Syft: o.syft, Grype: o.grype, Grant: o.grant, Yamllint: o.yamllint, NoShellcheck: o.noShellcheck, JSONOutput: o.jsonOutput, ShowAllErrors: o.showAllErrors, Stats: o.stats, FailFast: o.failFast, ScheduleSeed: o.scheduleSeed, Staged: o.staged, Approve: o.approve, ValidateImages: o.validateImages, PriorManifestFile: o.priorManifestFile, GHESCompat: o.ghes, UseSamples: o.useSamples, } @@ -773,6 +775,7 @@ func configureCompileToolFlags() { compileCmd.Flags().Bool("grype", false, "Run grype vulnerability scanner on container images referenced in compiled .lock.yml files (uses Docker image "+cli.GrypeImage+")") compileCmd.Flags().Bool("grant", false, "Run grant license scanner on container images referenced in compiled .lock.yml files (uses Docker image "+cli.GrantImage+")") compileCmd.Flags().Bool("yamllint", false, "Run yamllint YAML linter on generated .lock.yml files (uses Docker image "+cli.YamllintImage+")") + compileCmd.Flags().Bool("no-shellcheck", false, "Disable shellcheck linting of run step scripts (shellcheck runs by default when available)") compileCmd.Flags().Bool("fix", false, "Apply automatic codemod fixes to workflows before compiling") compileCmd.Flags().BoolP("json", "j", false, "Output results in JSON format") compileCmd.Flags().Bool("show-all", false, "Display all compilation errors instead of only the highest-priority subset (default: top 5)") diff --git a/docs/adr/49762-default-on-shellcheck-for-run-step-linting-in-compile-pipeline.md b/docs/adr/49762-default-on-shellcheck-for-run-step-linting-in-compile-pipeline.md new file mode 100644 index 00000000000..e3b54e5a1bb --- /dev/null +++ b/docs/adr/49762-default-on-shellcheck-for-run-step-linting-in-compile-pipeline.md @@ -0,0 +1,56 @@ +# ADR-49762: Default-on Shellcheck for Run Step Linting in Compile Pipeline + +**Date**: 2026-08-02 +**Status**: Draft +**Deciders**: pelikhan, copilot-swe-agent + +--- + +### Context + +The `gh aw compile` pipeline validates GitHub Actions workflow files and generates lock files, but it did not previously analyze the shell scripts inside `run:` steps for correctness. Bash/sh step scripts can contain subtle bugs (undefined variables, quoting issues, subshell errors) that are invisible to YAML-level validators and actionlint. Shellcheck is the standard static analysis tool for shell scripts and already handles the vast majority of common bash/sh issues. The existing external-tool integrations (yamllint, grype, zizmor, poutine, etc.) are all opt-in flags; this PR diverges from that pattern by making shellcheck default-on, with an escape hatch (`--no-shellcheck`) to disable it. + +### Decision + +We will integrate shellcheck as a **default-on** phase of the compile pipeline: shellcheck runs automatically after each lock file is emitted unless the user passes `--no-shellcheck`. The system binary is used (no Docker); the tool is silently skipped when not installed in normal mode, and emits a warning in `--validate` mode or an error in `--strict` mode. A curated set of GitHub Actions-specific false-positive codes (`SC2016`, `SC1090`, `SC1091`) is suppressed by default to avoid noise from `${{ }}` expression syntax and dynamic source paths. + +The primary driver is that run step scripts are the most likely place for shell-specific bugs; making coverage automatic ensures users benefit without needing to know to opt in. + +### Alternatives Considered + +#### Alternative 1: Opt-in flag (consistent with existing tools like `--yamllint`, `--grype`) + +Make shellcheck opt-in via a `--shellcheck` flag, matching the established pattern for all other post-compile linting tools. Users who want shellcheck checking would pass the flag explicitly. + +Why not chosen: Opt-in guarantees low adoption — developers rarely add new flags to existing workflows. Shell script bugs in `run:` steps are universal enough that silent coverage by default provides substantially more value. The cost of a false positive is low (a warning or skipped check), while the cost of a missed shell bug can be significant. + +#### Alternative 2: Docker-based shellcheck (consistent with yamllint/grype/syft) + +Run shellcheck inside a Docker container, mirroring how yamllint and the security scanners are invoked. This would remove the "must install separately" dependency. + +Why not chosen: Shellcheck is a lightweight, widely-available system binary with no complex runtime dependencies. Requiring Docker adds startup latency (~2–5 s per invocation), makes the feature unavailable in environments without Docker (CI runners, devcontainers without Docker-in-Docker), and increases operational complexity for a tool that most developers already have installed. The binary approach is faster and more portable. + +#### Alternative 3: Delegate to actionlint for shell checking + +Actionlint already performs some shell script analysis via an embedded shellcheck call when shellcheck is available. Relying on actionlint rather than adding a direct shellcheck phase would avoid duplication. + +Why not chosen: Actionlint's shellcheck integration is opportunistic and only activates when actionlint is also run. Separately wiring shellcheck ensures coverage regardless of whether actionlint is in use, and gives users direct control over shellcheck's behaviour (strict mode, ignore codes, etc.). + +### Consequences + +#### Positive +- Run step shell scripts are automatically linted on every compile invocation without any developer opt-in, improving baseline shell code quality across all workflows. +- Known GitHub Actions expression false positives (`SC2016`, `SC1090`, `SC1091`) are suppressed by default, reducing noise without sacrificing coverage. +- The feature is transparent — silently skipped when shellcheck is absent — so existing setups without shellcheck installed are unaffected. + +#### Negative +- Introduces an **implicit system dependency**: developers who encounter shellcheck findings must install shellcheck locally to understand and reproduce them; the dependency is not bundled or pinned. +- Default-on is a **breaking change in behaviour** for any existing integration tests or CI pipelines that run `gh aw compile` without expecting shellcheck output on stderr; tests that pass `--strict` and have shell issues in run steps will now fail until `--no-shellcheck` is added or findings are fixed. + +#### Neutral +- The opt-out flag name (`--no-shellcheck`) deviates from the positive-flag naming convention used by all other tools (`--yamllint`, `--grype`, etc.), which reflects the inverted default but may surprise users scanning the flag list. +- The suppressed code list (`shellcheckDefaultIgnoreCodes`) is hardcoded; teams with unusual expression patterns may need to request additions. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* diff --git a/pkg/cli/compile_command_test.go b/pkg/cli/compile_command_test.go index 3b854c818a1..6913b86058c 100644 --- a/pkg/cli/compile_command_test.go +++ b/pkg/cli/compile_command_test.go @@ -509,6 +509,9 @@ func TestCompileConfig_DefaultValues(t *testing.T) { if config.Actionlint { t.Error("Expected Actionlint to default to false") } + if config.NoShellcheck { + t.Error("Expected NoShellcheck to default to false (shellcheck runs by default when available)") + } } // TestCompilationStats_DefaultValues tests default stats values diff --git a/pkg/cli/compile_config.go b/pkg/cli/compile_config.go index 1336ea866d9..f30963d19ba 100644 --- a/pkg/cli/compile_config.go +++ b/pkg/cli/compile_config.go @@ -29,6 +29,7 @@ type CompileConfig struct { Grype bool // Run grype vulnerability scanner on container images referenced in compiled .lock.yml files Grant bool // Run grant license scanner on container images referenced in compiled .lock.yml files Yamllint bool // Run yamllint YAML linter on generated .lock.yml files + NoShellcheck bool // Skip shellcheck linting of run step scripts (shellcheck runs by default when available) JSONOutput bool // Output validation results as JSON ShowAllErrors bool // Display all prioritized errors instead of the default top five ActionMode string // How action scripts are referenced: dev, release, or action. Auto-detected if empty. diff --git a/pkg/cli/compile_external_tools.go b/pkg/cli/compile_external_tools.go index 07b29f2fd62..26ed52efec5 100644 --- a/pkg/cli/compile_external_tools.go +++ b/pkg/cli/compile_external_tools.go @@ -81,6 +81,14 @@ func RunYamllintOnFiles(lockFiles []string, verbose bool, strict bool) error { return runBatchLockFileTool("yamllint", lockFiles, verbose, strict, runYamllintOnFiles) } +// RunShellcheckOnLockFiles runs shellcheck on the run: step scripts extracted +// from the provided lock files. Shellcheck must be installed as a system binary; +// unlike other tools it does not use Docker. When shellcheck is not available +// the function returns nil (callers are responsible for warning the user). +func RunShellcheckOnLockFiles(lockFiles []string, verbose bool, strict bool) error { + return runBatchLockFileTool("shellcheck", lockFiles, verbose, strict, runShellcheckOnLockFiles) +} + // RunSyftOnLockFiles runs the syft SBOM scanner on container images extracted // from the gh-aw-manifest headers in the provided lock files. func RunSyftOnLockFiles(lockFiles []string, verbose bool, strict bool) error { diff --git a/pkg/cli/compile_orchestrator.go b/pkg/cli/compile_orchestrator.go index 66b49897e12..6964c1318ff 100644 --- a/pkg/cli/compile_orchestrator.go +++ b/pkg/cli/compile_orchestrator.go @@ -2,6 +2,7 @@ package cli import ( "context" + "errors" "fmt" "os" "path/filepath" @@ -63,6 +64,17 @@ func CompileWorkflows(ctx context.Context, config CompileConfig) ([]*workflow.Wo initActionlintStats() } + // Warn or error when shellcheck is enabled (the default) but not installed. + // Skip this check when --no-emit is set: no lock files are written so shellcheck + // is never invoked, regardless of the --strict or --validate flags. + if !config.NoShellcheck && !config.NoEmit && !isShellcheckAvailable() { + if config.Strict { + return nil, errors.New("shellcheck binary not found in PATH; run step linting requires shellcheck to be installed (use --no-shellcheck to skip)") + } else if config.Validate { + fmt.Fprintln(os.Stderr, console.FormatWarningMessageStderr("shellcheck binary not found in PATH; run step linting will be skipped. Install shellcheck to enable run step linting.")) + } + } + // Track compilation statistics stats := &CompilationStats{} diff --git a/pkg/cli/compile_pipeline.go b/pkg/cli/compile_pipeline.go index bb8a4941e3f..7f8ce965df1 100644 --- a/pkg/cli/compile_pipeline.go +++ b/pkg/cli/compile_pipeline.go @@ -74,7 +74,8 @@ func compileSpecificFiles( var lockFilesForGrype []string // lock files for grype container image vulnerability scanning var lockFilesForGrant []string // lock files for grant container image license scanning var strictGrantErr error - var lockFilesForYamllint []string // lock files for yamllint YAML linter + var lockFilesForYamllint []string // lock files for yamllint YAML linter + var lockFilesForShellcheck []string // lock files for shellcheck run step linting // Compile each specified file for _, markdownFile := range config.MarkdownFiles { @@ -173,6 +174,9 @@ func compileSpecificFiles( if config.Yamllint { lockFilesForYamllint = append(lockFilesForYamllint, fileResult.lockFile) } + if !config.NoShellcheck { + lockFilesForShellcheck = append(lockFilesForShellcheck, fileResult.lockFile) + } } } } @@ -292,6 +296,18 @@ func compileSpecificFiles( } } + // Run shellcheck on run step scripts in all collected lock files. + if !config.NoShellcheck && !config.NoEmit && len(lockFilesForShellcheck) > 0 { + if err := ctx.Err(); err != nil { + return workflowDataList, err + } + if err := RunShellcheckOnLockFiles(lockFilesForShellcheck, config.Verbose && !config.JSONOutput, config.Strict); err != nil { + if config.Strict { + return workflowDataList, err + } + } + } + // Get warning count from compiler stats.Warnings = compiler.GetWarningCount() @@ -401,7 +417,8 @@ func compileAllFilesInDirectory( var lockFilesForGrype []string // lock files for grype container image vulnerability scanning var lockFilesForGrant []string // lock files for grant container image license scanning var strictGrantErr error - var lockFilesForYamllint []string // lock files for yamllint YAML linter + var lockFilesForYamllint []string // lock files for yamllint YAML linter + var lockFilesForShellcheck []string // lock files for shellcheck run step linting for _, file := range mdFiles { // Respect context cancellation between files (e.g. Ctrl+C) @@ -469,6 +486,9 @@ func compileAllFilesInDirectory( if config.Yamllint { lockFilesForYamllint = append(lockFilesForYamllint, fileResult.lockFile) } + if !config.NoShellcheck { + lockFilesForShellcheck = append(lockFilesForShellcheck, fileResult.lockFile) + } } } } @@ -584,6 +604,18 @@ func compileAllFilesInDirectory( } } + // Run shellcheck on run step scripts in all collected lock files. + if !config.NoShellcheck && !config.NoEmit && len(lockFilesForShellcheck) > 0 { + if err := ctx.Err(); err != nil { + return workflowDataList, err + } + if err := RunShellcheckOnLockFiles(lockFilesForShellcheck, config.Verbose && !config.JSONOutput, config.Strict); err != nil { + if config.Strict { + return workflowDataList, err + } + } + } + // Emit recommendation when many slash commands are present without centralized strategy. displayCentralizedSlashCommandRecommendation(compiler, workflowDataList, config.JSONOutput) diff --git a/pkg/cli/compile_pipeline_yamllint_test.go b/pkg/cli/compile_pipeline_yamllint_test.go index a07b6b3a71a..03aed007f44 100644 --- a/pkg/cli/compile_pipeline_yamllint_test.go +++ b/pkg/cli/compile_pipeline_yamllint_test.go @@ -85,6 +85,7 @@ This is a test workflow for yamllint batch execution. NoEmit: false, Yamllint: true, Strict: strict, + NoShellcheck: true, // yamllint test is independent of shellcheck } _, err = CompileWorkflows(context.Background(), config) diff --git a/pkg/cli/mcp_tools_readonly.go b/pkg/cli/mcp_tools_readonly.go index c168c67a1b6..a4fb18b2ee3 100644 --- a/pkg/cli/mcp_tools_readonly.go +++ b/pkg/cli/mcp_tools_readonly.go @@ -192,8 +192,10 @@ Returns JSON array with validation results for each workflow: } // Build command arguments - // Always validate workflows during compilation and use JSON output for MCP - cmdArgs := []string{"compile", "--validate", "--json"} + // Always validate workflows during compilation and use JSON output for MCP. + // Shellcheck output goes to stderr (captured in subprocess error buffer only) and + // therefore never reaches the LLM via the JSON response — always disable it. + cmdArgs := []string{"compile", "--validate", "--json", "--no-shellcheck"} // Add fix flag if requested if args.Fix { diff --git a/pkg/cli/shellcheck.go b/pkg/cli/shellcheck.go new file mode 100644 index 00000000000..c60c6a001f0 --- /dev/null +++ b/pkg/cli/shellcheck.go @@ -0,0 +1,355 @@ +// This file provides shellcheck integration for workflow run step linting. +// +// It extracts run: step scripts from compiled lock files and runs shellcheck +// on each shell snippet, reporting issues and ignoring known false positives +// introduced by GitHub Actions expression syntax. +// +// # Key Functions +// +// - runShellcheckOnLockFiles() - Run shellcheck on run steps in multiple lock files +// - extractRunStepsFromLockFile() - Parse a lock file and extract run step info +// - isShellcheckAvailable() - Check whether the shellcheck binary is in PATH +// - isShellcheckableShell() - True for bash/sh steps; false for pwsh/python/etc. + +package cli + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + + "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/logger" + "github.com/goccy/go-yaml" +) + +var shellcheckLog = logger.New("cli:shellcheck") + +// ghaExpressionRE matches GitHub Actions ${{ ... }} expression syntax so it can +// be replaced with a shell-safe placeholder before linting. The (?s) flag lets +// '.' cross newline boundaries; the non-greedy *? stops at the earliest '}}'. +var ghaExpressionRE = regexp.MustCompile(`(?s)\$\{\{.*?\}\}`) + +// sanitizeGHAExpressions replaces every ${{ ... }} GitHub Actions expression in +// script with the identifier __GHA_EXPR__. This prevents shellcheck from +// generating spurious parse errors (e.g. SC1073, SC1083) caused by the +// otherwise-invalid dollar-brace-brace substitution syntax. +func sanitizeGHAExpressions(script string) string { + return ghaExpressionRE.ReplaceAllString(script, "__GHA_EXPR__") +} + +// shellcheckDefaultIgnoreCodes lists SC error codes that are false positives +// in GitHub Actions run: scripts and are always suppressed. +// +// Rationale for each code: +// +// SC2016: "${{ }}" GitHub Actions expression syntax appears in single-quoted +// strings which shellcheck flags as unexpanded variable references. +// sanitizeGHAExpressions handles most occurrences, but this code is +// retained as a safety net for any edge cases the regex may miss. +// SC1090: "Can't follow non-constant source" – scripts are downloaded and +// sourced dynamically at runtime; the source path is not resolvable at +// lint time. +// SC1091: "Not following: shell file doesn't exist" – same reason as SC1090. +// SC2129: "Consider using { cmd1; cmd2; } >> file" – style note about +// consecutive redirects; GITHUB_OUTPUT and GITHUB_STEP_SUMMARY commonly +// require individual echo >> appends to handle conditional branches. +// SC2153: "Possible misspelling: VAR may not be assigned" – env: step-level +// variables are set by the GHA runner and are invisible to shellcheck; +// this fires as a false positive for every uppercase env: variable. +// SC2154: "variable is referenced but not assigned" – common false positive in +// generated GHA scripts where variables are assigned inside trap strings +// (e.g., `trap 'var=$?; ...; echo "$var"' EXIT`) or set by the Actions +// runner environment. ShellCheck does not trace assignments inside trap +// body strings. +var shellcheckDefaultIgnoreCodes = []string{"SC2016", "SC1090", "SC1091", "SC2129", "SC2153", "SC2154"} + +// runStepInfo captures the information from a single run: step in a lock file +// that is needed to run shellcheck on the script snippet. +type runStepInfo struct { + // Name is the step's "name" field, used only for diagnostic messages. + Name string + // Script is the raw content of the run: field. + Script string + // Shell is the effective shell for the step, resolved from the step field, + // job-level defaults, and workflow-level defaults (in that priority order). + // An empty string means the GitHub Actions default (bash on Linux runners). + Shell string + // LockFile is the absolute path of the lock file that contains this step. + LockFile string +} + +// isShellcheckAvailable returns true when the shellcheck binary can be found in PATH. +func isShellcheckAvailable() bool { + _, err := exec.LookPath("shellcheck") + return err == nil +} + +// isShellcheckableShell returns true for shell values that shellcheck can lint. +// GitHub Actions supports bash (default), sh, pwsh, powershell, python, and +// custom shells. Only bash and sh are valid targets for shellcheck. +func isShellcheckableShell(shell string) bool { + if shell == "" || strings.EqualFold(shell, "bash") { + // Empty means GitHub Actions default (bash on Linux/macOS runners). + return true + } + return strings.EqualFold(shell, "sh") +} + +// shellcheckShell returns the value to pass to shellcheck's --shell flag. +// When shell is empty the GitHub Actions default (bash) is used. +func shellcheckShell(shell string) string { + if strings.EqualFold(shell, "sh") { + return "sh" + } + return "bash" +} + +// resolveDefaultShell extracts the shell value from a defaults.run block, if +// present. It returns the shell string (e.g. "bash", "pwsh") or "" when the +// block is absent or the shell field is not set. +func resolveDefaultShell(defaults map[string]any) string { + if defaults == nil { + return "" + } + run, ok := defaults["run"].(map[string]any) + if !ok { + return "" + } + shell, _ := run["shell"].(string) + return shell +} + +// extractRunStepsFromLockFile parses a compiled lock file and returns all +// run: steps whose effective shell is lintable by shellcheck. +// +// The effective shell for a step is resolved in priority order: +// 1. Step-level "shell" field +// 2. Job-level "defaults.run.shell" +// 3. Workflow-level "defaults.run.shell" +// 4. Empty string (GitHub Actions default: bash on Linux/macOS runners) +func extractRunStepsFromLockFile(lockFile string) ([]runStepInfo, error) { + shellcheckLog.Printf("Extracting run steps from %s", lockFile) + + content, err := os.ReadFile(lockFile) + if err != nil { + return nil, fmt.Errorf("failed to read lock file %s: %w", lockFile, err) + } + + var workflowYAML map[string]any + if err := yaml.Unmarshal(content, &workflowYAML); err != nil { + return nil, fmt.Errorf("failed to parse YAML in %s: %w", lockFile, err) + } + + var steps []runStepInfo + + jobs, ok := workflowYAML["jobs"].(map[string]any) + if !ok { + return steps, nil + } + + // Resolve workflow-level default shell (lowest priority). + workflowDefaultShell := resolveDefaultShell(func() map[string]any { + d, _ := workflowYAML["defaults"].(map[string]any) + return d + }()) + + for _, jobData := range jobs { + job, ok := jobData.(map[string]any) + if !ok { + continue + } + + // Resolve job-level default shell, falling back to workflow default. + jobDefaultShell := workflowDefaultShell + if jobDefaults, ok := job["defaults"].(map[string]any); ok { + if s := resolveDefaultShell(jobDefaults); s != "" { + jobDefaultShell = s + } + } + + rawSteps, ok := job["steps"].([]any) + if !ok { + continue + } + for _, stepData := range rawSteps { + step, ok := stepData.(map[string]any) + if !ok { + continue + } + runScript, ok := step["run"].(string) + if !ok || runScript == "" { + continue + } + + // Resolve effective shell: step > job default > workflow default. + shell, _ := step["shell"].(string) + effectiveShell := shell + if effectiveShell == "" { + effectiveShell = jobDefaultShell + } + + if !isShellcheckableShell(effectiveShell) { + continue + } + name, _ := step["name"].(string) + steps = append(steps, runStepInfo{ + Name: name, + Script: runScript, + Shell: effectiveShell, + LockFile: lockFile, + }) + } + } + + shellcheckLog.Printf("Found %d shellcheckable run steps in %s", len(steps), lockFile) + return steps, nil +} + +// runShellcheckOnScript writes script to a temporary file and invokes shellcheck. +// It prints any findings to stderr and returns a non-nil error when shellcheck +// reports one or more issues. +func runShellcheckOnScript(info runStepInfo, ignoreCodes []string, verbose bool) error { + shellcheckLog.Printf("Running shellcheck on step %q (shell=%s)", info.Name, info.Shell) + + // Sanitize GitHub Actions ${{ ... }} expressions before writing the script. + // Without this, shellcheck emits parse errors (SC1073, SC1083) because + // ${{ is not valid POSIX/bash substitution syntax. + sanitizedScript := sanitizeGHAExpressions(info.Script) + + // Write script to a temp file so shellcheck can lint it. + tmpFile, err := os.CreateTemp("", "gh-aw-shellcheck-*.sh") + if err != nil { + return fmt.Errorf("failed to create temp file for shellcheck: %w", err) + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.WriteString(sanitizedScript); err != nil { + tmpFile.Close() + return fmt.Errorf("failed to write shellcheck temp file: %w", err) + } + tmpFile.Close() + + args := []string{ + "--shell=" + shellcheckShell(info.Shell), + "--format=gcc", + } + for _, code := range ignoreCodes { + args = append(args, "--exclude="+code) + } + args = append(args, tmpFile.Name()) + + if verbose { + shellcheckLog.Printf("Invoking: shellcheck %s", strings.Join(args, " ")) + fmt.Fprintf(os.Stderr, "%s\n", console.FormatInfoMessage("shellcheck "+strings.Join(args[:len(args)-1], " ")+"