diff --git a/.github/aw/report.md b/.github/aw/report.md index 4fa1fb2d1f1..237b24841b9 100644 --- a/.github/aw/report.md +++ b/.github/aw/report.md @@ -40,23 +40,18 @@ safe-outputs: ### Header Levels -**Use `###` or lower for all headers in your report to maintain proper document hierarchy.** - -When creating GitHub issues or discussions: -- Use `###` (h3) for main sections (e.g., `### Test Summary`) -- Use `####` (h4) for subsections (e.g., `#### Device-Specific Results`) -- Never use `##` (h2) or `#` (h1) in reports — these are reserved for titles +- Use `###` (h3) for main sections — e.g., `### Test Summary` +- Use `####` (h4) for subsections — e.g., `#### Device-Specific Results` +- Never use `##` (h2) or `#` (h1) — those are reserved for titles ### Progressive Disclosure -**Wrap detailed content in `
Section Name` tags to improve readability and reduce scrolling.** - -Use collapsible sections for: +Wrap detailed content in `
Section Name` tags. Use for: - Verbose details (full logs, raw data) - Secondary information (minor warnings, extra context) - Per-item breakdowns when there are many items -Always keep critical information visible (summary, critical issues, key metrics). +Keep critical information visible (summary, critical issues, key metrics). ### Report Structure Pattern @@ -65,14 +60,6 @@ Always keep critical information visible (summary, critical issues, key metrics) 3. **Details**: Use `
Section Name` for expanded content 4. **Context**: Add helpful metadata (workflow run, date, trigger) -### Design Principles - -Reports should: -- **Build trust through clarity**: Most important info immediately visible -- **Exceed expectations**: Add helpful context like trends, comparisons -- **Create delight**: Use progressive disclosure to reduce overwhelm -- **Maintain consistency**: Follow patterns across all reports - ### Example Report Structure ```markdown diff --git a/.github/workflows/auto-triage-issues.lock.yml b/.github/workflows/auto-triage-issues.lock.yml index dee80b3908e..75e04625d43 100644 --- a/.github/workflows/auto-triage-issues.lock.yml +++ b/.github/workflows/auto-triage-issues.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"25ba385233ac59f16b778a09c4c4c4b85d046210322ff53ddd44d988cb26caa5","strict":true,"agent_id":"copilot","agent_model":"gpt-5-mini"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"44ee7fb4de16d0d981ae932dfa32c8560c65a9fade7a3563dde246dcd7e4c9c6","strict":true,"agent_id":"copilot","agent_model":"gpt-5-mini"} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.42"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -213,20 +213,20 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_22c0a58162bb6a9d_EOF' + cat << 'GH_AW_PROMPT_11cff80b0d29c151_EOF' - GH_AW_PROMPT_22c0a58162bb6a9d_EOF + GH_AW_PROMPT_11cff80b0d29c151_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_22c0a58162bb6a9d_EOF' + cat << 'GH_AW_PROMPT_11cff80b0d29c151_EOF' Tools: create_discussion, add_labels(max:10), missing_tool, missing_data, noop - GH_AW_PROMPT_22c0a58162bb6a9d_EOF + GH_AW_PROMPT_11cff80b0d29c151_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_22c0a58162bb6a9d_EOF' + cat << 'GH_AW_PROMPT_11cff80b0d29c151_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -255,16 +255,16 @@ jobs: {{/if}} - GH_AW_PROMPT_22c0a58162bb6a9d_EOF + GH_AW_PROMPT_11cff80b0d29c151_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_22c0a58162bb6a9d_EOF' + cat << 'GH_AW_PROMPT_11cff80b0d29c151_EOF' {{#runtime-import .github/workflows/shared/github-guard-policy.md}} {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/shared/observability-otlp.md}} {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/auto-triage-issues.md}} - GH_AW_PROMPT_22c0a58162bb6a9d_EOF + GH_AW_PROMPT_11cff80b0d29c151_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -500,9 +500,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_ad7601bdf062f098_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_2243e03bdea2bf84_EOF' {"add_labels":{"max":10},"create_discussion":{"category":"audits","close_older_discussions":true,"expires":24,"fallback_to_issue":true,"max":1,"title_prefix":"[Auto-Triage] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_ad7601bdf062f098_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_2243e03bdea2bf84_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -714,7 +714,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_565c3d3852153a7c_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_dc1e7d443e156803_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -745,7 +745,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_565c3d3852153a7c_EOF + GH_AW_MCP_CONFIG_dc1e7d443e156803_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true diff --git a/.github/workflows/auto-triage-issues.md b/.github/workflows/auto-triage-issues.md index ed0c3669e09..d94eb79520b 100644 --- a/.github/workflows/auto-triage-issues.md +++ b/.github/workflows/auto-triage-issues.md @@ -53,6 +53,7 @@ safe-outputs: category: "audits" close-older-discussions: true max: 1 + noop: timeout-minutes: 15 features: copilot-requests: true @@ -97,9 +98,13 @@ When running on schedule: When triggered manually as a backfill pass: 1. **Fetch ALL open issues without any labels** using GitHub tools — do not limit to a fixed count -2. **Process up to 10 unlabeled issues** in this run (respecting safe-output limits); if more exist, note the remainder in the report -3. **Apply labels** to each issue based on classification rules below, using title/body heuristics and existing triage rules -4. **Create a summary report** as a discussion listing every issue processed, the labels applied, and how many unlabeled issues (if any) still remain for the next pass +2. **If there are no unlabeled issues**, call `noop` with "No unlabeled issues found during manual backfill — no action needed" and stop. Do not create a discussion. + +When unlabeled issues exist: + +3. **Process up to 10 unlabeled issues** in this run (respecting safe-output limits); if more exist, note the remainder in the report +4. **Apply labels** to each issue based on classification rules below, using title/body heuristics and existing triage rules +5. **Create a summary report** as a discussion listing every issue processed, the labels applied, and how many unlabeled issues (if any) still remain for the next pass ## Classification Rules @@ -287,6 +292,16 @@ When running on schedule, create a discussion report following these formatting - **Learn from patterns** - Over time, notice which types of issues are frequently unlabeled - **Human override** - Maintainers can change labels; this is automation assistance, not replacement +## Mandatory Completion Rule + +**Before finishing, check whether you called any safe-output tool in this run.** If you did NOT call `add_labels` or `create_discussion`, you MUST call `noop`. Every run MUST end with at least one safe-output call — failing to do so causes the workflow to fail with a safe-output compliance error. + +Situations that require a `noop` call: +- No unlabeled issues were found (all trigger types) +- The triggering issue already had appropriate labels +- All issues analyzed were already labeled or had been processed +- You were uncertain and chose not to label rather than guess incorrectly + ## Success Metrics - Reduce unlabeled issue percentage from 8.6% to <5% diff --git a/.github/workflows/copilot-pr-nlp-analysis.md b/.github/workflows/copilot-pr-nlp-analysis.md index 37d7c599ade..78854044cdc 100644 --- a/.github/workflows/copilot-pr-nlp-analysis.md +++ b/.github/workflows/copilot-pr-nlp-analysis.md @@ -92,6 +92,7 @@ Generate a daily NLP-based analysis report of Copilot-created PRs merged within - Python analysis dependencies are already installed by pre-agent workflow steps. - **Do NOT run any `pip install` commands in agent turns.** +- If an import unexpectedly fails, report the missing package in the output and continue with reduced analysis instead of installing dependencies in agent turns. - Run Python scripts with `/tmp/gh-aw/venv/bin/python3` to use the preinstalled environment. ## Task Overview diff --git a/.github/workflows/daily-compiler-quality.lock.yml b/.github/workflows/daily-compiler-quality.lock.yml index c8afff991ea..dba83d8f3b7 100644 --- a/.github/workflows/daily-compiler-quality.lock.yml +++ b/.github/workflows/daily-compiler-quality.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7bc1a19e4a2af352c977f50c373f15abb29d5c9c10f502ba5972784b4aa91318","strict":true,"agent_id":"copilot"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"04b56ba3a0712696e891e0d3a56d877640a47c95425b656283b385634c1695bc","strict":true,"agent_id":"copilot"} # gh-aw-manifest: {"version":1,"secrets":["GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.42"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"ghcr.io/github/serena-mcp-server:latest","digest":"sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5","pinned_image":"ghcr.io/github/serena-mcp-server:latest@sha256:bf343399e3725c45528f531a230f3a04521d4cdef29f9a5af6282ff0d3c393c5"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -199,21 +199,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_f8c19ab5cd1cee06_EOF' + cat << 'GH_AW_PROMPT_008a45fba0979ffd_EOF' - GH_AW_PROMPT_f8c19ab5cd1cee06_EOF + GH_AW_PROMPT_008a45fba0979ffd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_f8c19ab5cd1cee06_EOF' + cat << 'GH_AW_PROMPT_008a45fba0979ffd_EOF' Tools: create_discussion, missing_tool, missing_data, noop - GH_AW_PROMPT_f8c19ab5cd1cee06_EOF + GH_AW_PROMPT_008a45fba0979ffd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_f8c19ab5cd1cee06_EOF' + cat << 'GH_AW_PROMPT_008a45fba0979ffd_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -242,9 +242,9 @@ jobs: {{/if}} - GH_AW_PROMPT_f8c19ab5cd1cee06_EOF + GH_AW_PROMPT_008a45fba0979ffd_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_f8c19ab5cd1cee06_EOF' + cat << 'GH_AW_PROMPT_008a45fba0979ffd_EOF' ## Serena Code Analysis @@ -283,7 +283,7 @@ jobs: {{#runtime-import .github/workflows/shared/mcp/serena-go.md}} {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/daily-compiler-quality.md}} - GH_AW_PROMPT_f8c19ab5cd1cee06_EOF + GH_AW_PROMPT_008a45fba0979ffd_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -520,9 +520,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f4b4b62b017a06f7_EOF' - {"create_discussion":{"category":"audits","close_older_discussions":true,"expires":24,"fallback_to_issue":true,"max":1,"title_prefix":"[daily-compiler-quality] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_f4b4b62b017a06f7_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_8edcb29d7b09758d_EOF' + {"create_discussion":{"category":"audits","close_older_discussions":true,"expires":24,"fallback_to_issue":true,"max":1,"min_body_length":200,"title_prefix":"[daily-compiler-quality] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_8edcb29d7b09758d_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -714,7 +714,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_ab9d2d1b2af2d4cc_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_9369973ac67aa334_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -774,7 +774,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_ab9d2d1b2af2d4cc_EOF + GH_AW_MCP_CONFIG_9369973ac67aa334_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1514,7 +1514,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"audits\",\"close_older_discussions\":true,\"expires\":24,\"fallback_to_issue\":true,\"max\":1,\"title_prefix\":\"[daily-compiler-quality] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_discussion\":{\"category\":\"audits\",\"close_older_discussions\":true,\"expires\":24,\"fallback_to_issue\":true,\"max\":1,\"min_body_length\":200,\"title_prefix\":\"[daily-compiler-quality] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/daily-compiler-quality.md b/.github/workflows/daily-compiler-quality.md index a59dff32192..e1cefee1a74 100644 --- a/.github/workflows/daily-compiler-quality.md +++ b/.github/workflows/daily-compiler-quality.md @@ -39,6 +39,15 @@ tools: - "mv /tmp/gh-aw/cache-memory/" - "echo" - "bc" +safe-outputs: + create-discussion: + category: "audits" + title-prefix: "[daily-compiler-quality] " + expires: 1d + close-older-discussions: true + fallback-to-issue: true + max: 1 + min-body-length: 200 timeout-minutes: 30 strict: true features: @@ -314,6 +323,13 @@ Compare current analysis with previous analyses: Generate a comprehensive discussion report with findings. +### Output Contract (Required) + +1. Emit **exactly one** `create_discussion` safe-output item. +2. Do **not** emit placeholder or draft bodies (for example: `test`, `.`, `todo`, or similar short placeholders). +3. Only emit `create_discussion` after the final report body is complete and fully rendered. +4. The workflow enforces a **minimum 200-character body length**, so very short outputs (placeholder or otherwise) will fail safe-outputs. + ### Discussion Title ``` diff --git a/.github/workflows/schema-consistency-checker.lock.yml b/.github/workflows/schema-consistency-checker.lock.yml index 6dffb710a91..72d301ff56f 100644 --- a/.github/workflows/schema-consistency-checker.lock.yml +++ b/.github/workflows/schema-consistency-checker.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"fd6107550d918d1f78b6879f3fce29e48f0aaa1d945e0d5439a4d87c1e07a5ed","strict":true,"agent_id":"claude"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"33eddff842e34023c051ae056f8916f21f51f308da15e88bf5688d52aabcbe1c","strict":true,"agent_id":"claude"} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.42"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -202,21 +202,21 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_458eb3e122e39af8_EOF' + cat << 'GH_AW_PROMPT_5fc2883be126916d_EOF' - GH_AW_PROMPT_458eb3e122e39af8_EOF + GH_AW_PROMPT_5fc2883be126916d_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_458eb3e122e39af8_EOF' + cat << 'GH_AW_PROMPT_5fc2883be126916d_EOF' Tools: create_discussion, missing_tool, missing_data, noop - GH_AW_PROMPT_458eb3e122e39af8_EOF + GH_AW_PROMPT_5fc2883be126916d_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_458eb3e122e39af8_EOF' + cat << 'GH_AW_PROMPT_5fc2883be126916d_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -248,15 +248,15 @@ jobs: - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). - GH_AW_PROMPT_458eb3e122e39af8_EOF + GH_AW_PROMPT_5fc2883be126916d_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_458eb3e122e39af8_EOF' + cat << 'GH_AW_PROMPT_5fc2883be126916d_EOF' {{#runtime-import .github/workflows/shared/observability-otlp.md}} {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/schema-consistency-checker.md}} - GH_AW_PROMPT_458eb3e122e39af8_EOF + GH_AW_PROMPT_5fc2883be126916d_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -493,9 +493,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_0d069088d5f3a80d_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_38f191250eb3a8b2_EOF' {"create_discussion":{"category":"audits","close_older_discussions":true,"expires":24,"fallback_to_issue":true,"max":1,"title_prefix":"[Schema Consistency] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_0d069088d5f3a80d_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_38f191250eb3a8b2_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -684,7 +684,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9dbfc34d51b702d6_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_c730ddeb5c313ff9_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -715,7 +715,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_9dbfc34d51b702d6_EOF + GH_AW_MCP_CONFIG_c730ddeb5c313ff9_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -828,7 +828,7 @@ jobs: printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.42/awf-config.schema.json","network":{"allowDomains":["*.githubusercontent.com","anthropic.com","api.anthropic.com","api.github.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","cdn.playwright.dev","codeload.github.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","files.pythonhosted.org","ghcr.io","github-cloud.githubusercontent.com","github-cloud.s3.amazonaws.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","lfs.github.com","objects.githubusercontent.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","playwright.download.prss.microsoft.com","ppa.launchpad.net","pypi.org","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","sentry.io","statsig.anthropic.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"maxEffectiveTokens":10000000,"models":{"auto":["large"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gpt-4.1":["copilot/gpt-4.1*","openai/gpt-4.1*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash"],"opus":["copilot/*opus*","anthropic/*opus*"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"]}},"container":{"imageTag":"0.25.42"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" && cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json # shellcheck disable=SC1003 sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" --tty --env-all --exclude-env ANTHROPIC_API_KEY --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ - -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/claude_harness.cjs claude --print --no-chrome --max-turns 60 --mcp-config "${{ runner.temp }}/gh-aw/mcp-config/mcp-servers.json" --allowed-tools '\''Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__safeoutputs'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + -- /bin/bash -c 'export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && export PATH="$(find /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/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_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/claude_harness.cjs claude --print --no-chrome --mcp-config "${{ runner.temp }}/gh-aw/mcp-config/mcp-servers.json" --allowed-tools '\''Bash,BashOutput,Edit,Edit(/tmp/gh-aw/cache-memory/*),ExitPlanMode,Glob,Grep,KillBash,LS,MultiEdit,MultiEdit(/tmp/gh-aw/cache-memory/*),NotebookEdit,NotebookRead,Read,Read(/tmp/gh-aw/cache-memory/*),Task,TodoWrite,Write,Write(/tmp/gh-aw/cache-memory/*),mcp__github__download_workflow_run_artifact,mcp__github__get_code_scanning_alert,mcp__github__get_commit,mcp__github__get_dependabot_alert,mcp__github__get_discussion,mcp__github__get_discussion_comments,mcp__github__get_file_contents,mcp__github__get_job_logs,mcp__github__get_label,mcp__github__get_latest_release,mcp__github__get_me,mcp__github__get_notification_details,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_diff,mcp__github__get_pull_request_files,mcp__github__get_pull_request_review_comments,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__get_release_by_tag,mcp__github__get_secret_scanning_alert,mcp__github__get_tag,mcp__github__get_workflow_run,mcp__github__get_workflow_run_logs,mcp__github__get_workflow_run_usage,mcp__github__issue_read,mcp__github__list_branches,mcp__github__list_code_scanning_alerts,mcp__github__list_commits,mcp__github__list_dependabot_alerts,mcp__github__list_discussion_categories,mcp__github__list_discussions,mcp__github__list_issue_types,mcp__github__list_issues,mcp__github__list_label,mcp__github__list_notifications,mcp__github__list_pull_requests,mcp__github__list_releases,mcp__github__list_secret_scanning_alerts,mcp__github__list_starred_repositories,mcp__github__list_tags,mcp__github__list_workflow_jobs,mcp__github__list_workflow_run_artifacts,mcp__github__list_workflow_runs,mcp__github__list_workflows,mcp__github__pull_request_read,mcp__github__search_code,mcp__github__search_issues,mcp__github__search_orgs,mcp__github__search_pull_requests,mcp__github__search_repositories,mcp__github__search_users,mcp__safeoutputs'\'' --debug-file /tmp/gh-aw/agent-stdio.log --verbose --permission-mode bypassPermissions --output-format stream-json --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt${GH_AW_MODEL_AGENT_CLAUDE:+ --model "$GH_AW_MODEL_AGENT_CLAUDE"}' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} BASH_DEFAULT_TIMEOUT_MS: 60000 @@ -837,7 +837,6 @@ jobs: DISABLE_BUG_COMMAND: 1 DISABLE_ERROR_REPORTING: 1 DISABLE_TELEMETRY: 1 - GH_AW_MAX_TURNS: 60 GH_AW_MCP_CONFIG: ${{ runner.temp }}/gh-aw/mcp-config/mcp-servers.json GH_AW_MODEL_AGENT_CLAUDE: ${{ vars.GH_AW_MODEL_AGENT_CLAUDE || '' }} GH_AW_PHASE: agent @@ -1172,7 +1171,7 @@ jobs: GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "30" - GH_AW_MAX_EFFECTIVE_TOKENS: "10000000" + GH_AW_MAX_EFFECTIVE_TOKENS: "20000000" GH_AW_CACHE_MEMORY_ENABLED: "true" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/schema-consistency-checker.md b/.github/workflows/schema-consistency-checker.md index 6d648734402..9b7540b6833 100644 --- a/.github/workflows/schema-consistency-checker.md +++ b/.github/workflows/schema-consistency-checker.md @@ -10,7 +10,9 @@ permissions: pull-requests: read engine: id: claude - max-turns: 60 +harness: + budget: + max-effective-tokens: 20000000 tools: edit: bash: ["*"] @@ -285,17 +287,29 @@ Here are proven strategies you can use or build upon: 2. Example: `grep -r "type.*string" pkg/parser/schemas/ | grep engine` 3. Cross-reference with parser implementation +## Turn Budget + +You have a maximum turn budget. **Spend turns wisely**: + +- **Batch multiple bash checks into a single command** using `&&`, `||`, or heredocs — one tool call is always better than two. +- **Stop investigating once you have enough findings** for the report. Depth on 3–5 real issues is better than breadth over 20 superficial checks. +- **Prioritize field_gaps** from the pre-computed data — they are high-signal and require no additional discovery turns. +- **Do NOT iterate through every schema field one-by-one** — use bulk grep/jq queries that scan all fields in a single pass. +- **Skip a category** if you have already found 3+ findings from other categories — you do not need to cover every area every run. + ## Implementation Steps -### Step 0: Read Pre-Computed Data (Start Here) +### Step 0: Read Pre-Computed Data + Strategies in One Pass (Start Here) -Before doing anything else, read the schema diff that was computed before your session began: +Before doing anything else, read both the schema diff and the strategy cache in a single command: ```bash -cat /tmp/gh-aw/agent/schema-diff.json +echo "=== SCHEMA DIFF ===" && cat /tmp/gh-aw/agent/schema-diff.json && \ +echo "=== STRATEGIES ===" && \ +([ -f /tmp/gh-aw/cache-memory/strategies.json ] && cat /tmp/gh-aw/cache-memory/strategies.json || echo "No strategies cached yet") ``` -This file contains: +The schema diff contains: - `schema_fields`: All top-level field names in the main JSON schema - `parser_yaml_fields`: All yaml-tagged struct fields in `pkg/parser/*.go` - `workflow_yaml_fields`: All yaml-tagged struct fields in `pkg/workflow/*.go` @@ -308,17 +322,9 @@ This file contains: **Use this pre-computed data as your primary starting point.** Do NOT re-run the field enumeration commands from scratch — instead, refine and supplement the pre-computed data with targeted follow-up queries (e.g., checking a specific file for a specific field). -### Step 1: Load Previous Strategies -```bash -# Check if strategies file exists -if [ -f /tmp/gh-aw/cache-memory/strategies.json ]; then - cat /tmp/gh-aw/cache-memory/strategies.json -fi -``` - -### Step 2: Choose Analysis Focus +### Step 1: Choose Analysis Focus -Using the pre-computed `field_gaps` from Step 0 plus the strategy cache from Step 1: +Using the pre-computed `field_gaps` plus the strategy cache: - If `field_gaps` show promising leads, start there (they are likely high-signal) - If cache has strategies, use a proven strategy 70% of the time; try a new approach 30% of the time @@ -332,27 +338,29 @@ else fi ``` -### Step 3: Execute Targeted Analysis +### Step 2: Execute Targeted Analysis (Batch Operations) -Use the pre-computed data as context and run **targeted** follow-up commands only when -deeper inspection is needed (e.g., checking how a specific field is actually processed in code). +Use the pre-computed data as context and run **targeted, batched** follow-up commands only when +deeper inspection is needed. -**Example: Verify a gap from pre-computed data** +**Batch multiple verifications together** (one call, multiple results): ```bash -# Verify a specific field gap by searching implementation files -grep -r "fieldName" pkg/parser/ pkg/workflow/ 2>/dev/null | grep -v "_test.go" +# Replace fieldA/fieldB/fieldC with actual field names from field_gaps in the pre-computed data +for field in fieldA fieldB fieldC; do + echo "=== $field ===" && \ + grep -r "$field" pkg/parser/ pkg/workflow/ 2>/dev/null | grep -v "_test.go" | head -5 || echo "(not found)" +done ``` -**Example: Type checking for a specific field** +**Bulk type checking — all fields in one jq pass**: ```bash -# Find schema field types (handles different JSON Schema patterns) jq -r ' (.properties // {}) | to_entries[] | "\(.key): \(.value.type // .value.oneOf // .value.anyOf // .value.allOf // "complex")" ' pkg/parser/schemas/main_workflow_schema.json 2>/dev/null || echo "Failed to parse schema" ``` -### Step 4: Record Findings +### Step 3: Record Findings Create a structured list of inconsistencies found: ```markdown @@ -377,7 +385,7 @@ Create a structured list of inconsistencies found: - Impact: Users may not understand error ``` -### Step 5: Update Cache +### Step 4: Update Cache Save successful strategy and findings to cache: ```bash # Update strategies.json with results @@ -389,7 +397,7 @@ cat > /tmp/gh-aw/cache-memory/strategies.json << 'EOF' EOF ``` -### Step 6: Create Discussion +### Step 5: Create Discussion **⚠️ MANDATORY STEP**: After completing your analysis, you **MUST** call the `create_discussion` safe-output tool with your findings report. **DO NOT just write the report in your output text** — you MUST actually invoke the tool. The workflow will fail if you skip this step. diff --git a/.github/workflows/unbloat-docs.lock.yml b/.github/workflows/unbloat-docs.lock.yml index 49acd0434cb..97cda0479c1 100644 --- a/.github/workflows/unbloat-docs.lock.yml +++ b/.github/workflows/unbloat-docs.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"74cfec818687614eb034b1fcb3b261722ba9804a51ecc2ed6cef09ac0e0c6515","strict":true,"agent_id":"claude"} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"7f58bc4a3041899fa0a0d8e5845e1b89e4f77795d39e817841e96332573b3e0e","strict":true,"agent_id":"claude"} # gh-aw-manifest: {"version":1,"secrets":["ANTHROPIC_API_KEY","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_ENDPOINT","GH_AW_OTEL_HEADERS","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/cache/save","sha":"27d5ce7f107fe9357f9df03efb73ab90386fccae","version":"v5.0.5"},{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.42"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.42"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) @@ -256,27 +256,27 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_4c45bf75252ca186_EOF' + cat << 'GH_AW_PROMPT_9189f0a2e98a4f36_EOF' - GH_AW_PROMPT_4c45bf75252ca186_EOF + GH_AW_PROMPT_9189f0a2e98a4f36_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/playwright_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/cache_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_4c45bf75252ca186_EOF' + cat << 'GH_AW_PROMPT_9189f0a2e98a4f36_EOF' Tools: add_comment, create_pull_request, upload_asset(max:10), missing_tool, missing_data, noop - GH_AW_PROMPT_4c45bf75252ca186_EOF + GH_AW_PROMPT_9189f0a2e98a4f36_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_4c45bf75252ca186_EOF' + cat << 'GH_AW_PROMPT_9189f0a2e98a4f36_EOF' upload_asset: provide a file path; returns a URL; assets are published after the workflow completes (safeoutputs). - GH_AW_PROMPT_4c45bf75252ca186_EOF + GH_AW_PROMPT_9189f0a2e98a4f36_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_4c45bf75252ca186_EOF' + cat << 'GH_AW_PROMPT_9189f0a2e98a4f36_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -305,9 +305,9 @@ jobs: {{/if}} - GH_AW_PROMPT_4c45bf75252ca186_EOF + GH_AW_PROMPT_9189f0a2e98a4f36_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_4c45bf75252ca186_EOF' + cat << 'GH_AW_PROMPT_9189f0a2e98a4f36_EOF' {{#runtime-import .github/workflows/shared/docs-server-lifecycle.md}} {{#runtime-import .github/workflows/shared/observability-otlp.md}} @@ -315,7 +315,7 @@ jobs: {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/shared/noop-reminder.md}} {{#runtime-import .github/workflows/unbloat-docs.md}} - GH_AW_PROMPT_4c45bf75252ca186_EOF + GH_AW_PROMPT_9189f0a2e98a4f36_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -567,11 +567,11 @@ jobs: - name: Pre-flight checks run: "mkdir -p /tmp/gh-aw/agent\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' -print \\\n | xargs 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\")\nCLEANED=$(awk -v cutoff=\"$RECENT_CUTOFF\" \\\n 'NF>0 && $1>=cutoff{count++} END{print count+0}' \\\n /tmp/gh-aw/cache-memory/cleaned-files.txt 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' -print \\\n | xargs 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\"\n" - name: Start documentation dev server - run: "mkdir -p /tmp/gh-aw\ncd docs\nnohup npm run dev -- --host 0.0.0.0 --port 4321 > /tmp/gh-aw/preview.log 2>&1 &\nPID=$!\necho $PID > /tmp/gh-aw/server.pid\necho \"Dev server started (PID: $PID)\"\n" + run: "# Skip if pre-flight check failed — no need to start the server\nif [ \"$(jq -r '.pass' /tmp/gh-aw/agent/preflight.json 2>/dev/null)\" != \"true\" ]; then\n echo \"Pre-flight check failed, skipping server startup\"\n exit 0\nfi\nmkdir -p /tmp/gh-aw\ncd docs\nnohup npm run dev -- --host 0.0.0.0 --port 4321 > /tmp/gh-aw/preview.log 2>&1 &\nPID=$!\necho $PID > /tmp/gh-aw/server.pid\necho \"Dev server started (PID: $PID)\"\n" - name: Wait for documentation server readiness - run: "URL=\"http://localhost:4321/gh-aw/\"\nSTATUS=\"\"\necho \"Readiness check target: $URL\"\necho \"Preview log: /tmp/gh-aw/preview.log\"\nfor i in $(seq 1 45); do\n STATUS=$(curl -sS -o /dev/null -w \"%{http_code}\" --connect-timeout 5 --max-time 5 \"$URL\" || true)\n [ \"$STATUS\" = \"200\" ] && echo \"Server ready at $URL\" && break\n if [ -z \"$STATUS\" ]; then STATUS=\"curl_error\"; fi\n echo \"Waiting for server... ($i/45) (status: $STATUS)\"\n sleep 3\ndone\nif [ \"$STATUS\" != \"200\" ]; then\n echo \"Dev server failed to start after 135 seconds:\"\n echo \"Final readiness status: $STATUS\"\n cat /tmp/gh-aw/preview.log || true\n exit 1\nfi\n" + run: "# Skip if pre-flight check failed — server was not started\nif [ \"$(jq -r '.pass' /tmp/gh-aw/agent/preflight.json 2>/dev/null)\" != \"true\" ]; then\n echo \"Pre-flight check failed, skipping server readiness check\"\n exit 0\nfi\nURL=\"http://localhost:4321/gh-aw/\"\nSTATUS=\"\"\necho \"Readiness check target: $URL\"\necho \"Preview log: /tmp/gh-aw/preview.log\"\nfor i in $(seq 1 45); do\n STATUS=$(curl -sS -o /dev/null -w \"%{http_code}\" --connect-timeout 5 --max-time 5 \"$URL\" || true)\n [ \"$STATUS\" = \"200\" ] && echo \"Server ready at $URL\" && break\n if [ -z \"$STATUS\" ]; then STATUS=\"curl_error\"; fi\n echo \"Waiting for server... ($i/45) (status: $STATUS)\"\n sleep 3\ndone\nif [ \"$STATUS\" != \"200\" ]; then\n echo \"Dev server failed to start after 135 seconds:\"\n echo \"Final readiness status: $STATUS\"\n cat /tmp/gh-aw/preview.log || true\n exit 1\nfi\n" - name: Write Playwright base URL - run: "mkdir -p /tmp/gh-aw/agent\necho \"http://localhost:4321/gh-aw/\" > /tmp/gh-aw/agent/playwright-base-url.txt\necho \"Playwright base URL: http://localhost:4321/gh-aw/\"\n" + run: "# Skip if pre-flight check failed — server was not started\nif [ \"$(jq -r '.pass' /tmp/gh-aw/agent/preflight.json 2>/dev/null)\" != \"true\" ]; then\n echo \"Pre-flight check failed, skipping Playwright base URL setup\"\n exit 0\nfi\nmkdir -p /tmp/gh-aw/agent\necho \"http://localhost:4321/gh-aw/\" > /tmp/gh-aw/agent/playwright-base-url.txt\necho \"Playwright base URL: http://localhost:4321/gh-aw/\"\n" - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.42 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.42 ghcr.io/github/gh-aw-firewall/cli-proxy:0.25.42 ghcr.io/github/gh-aw-firewall/squid:0.25.42 ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959 node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f @@ -582,9 +582,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << GH_AW_SAFE_OUTPUTS_CONFIG_f296a80275c5ed4e_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << GH_AW_SAFE_OUTPUTS_CONFIG_1b9d75dd88ccaf40_EOF {"add_comment":{"max":1},"create_pull_request":{"auto_merge":true,"draft":true,"expires":48,"fallback_as_issue":false,"labels":["documentation","automation","doc-unbloat"],"max":1,"max_patch_files":100,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","CLAUDE.md","AGENTS.md"],"reviewers":["copilot"],"title_prefix":"[docs] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"upload_asset":{"allowed-exts":[".png",".jpg",".jpeg",".svg"],"branch":"assets/${GITHUB_WORKFLOW}","max":10,"max-size":10240}} - GH_AW_SAFE_OUTPUTS_CONFIG_f296a80275c5ed4e_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_1b9d75dd88ccaf40_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -826,7 +826,7 @@ jobs: export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v /var/run/docker.sock:/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.6' GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4d4edb42845b5641_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_370d51e4f92ff7c4_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "safeoutputs": { @@ -857,7 +857,7 @@ jobs: } } } - GH_AW_MCP_CONFIG_4d4edb42845b5641_EOF + GH_AW_MCP_CONFIG_370d51e4f92ff7c4_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true diff --git a/.github/workflows/unbloat-docs.md b/.github/workflows/unbloat-docs.md index 3212773ee04..ed7cf9f6e87 100644 --- a/.github/workflows/unbloat-docs.md +++ b/.github/workflows/unbloat-docs.md @@ -172,6 +172,11 @@ pre-agent-steps: - name: Start documentation dev server run: | + # Skip if pre-flight check failed — no need to start the server + if [ "$(jq -r '.pass' /tmp/gh-aw/agent/preflight.json 2>/dev/null)" != "true" ]; then + echo "Pre-flight check failed, skipping server startup" + exit 0 + fi mkdir -p /tmp/gh-aw cd docs nohup npm run dev -- --host 0.0.0.0 --port 4321 > /tmp/gh-aw/preview.log 2>&1 & @@ -181,6 +186,11 @@ pre-agent-steps: - name: Wait for documentation server readiness run: | + # Skip if pre-flight check failed — server was not started + if [ "$(jq -r '.pass' /tmp/gh-aw/agent/preflight.json 2>/dev/null)" != "true" ]; then + echo "Pre-flight check failed, skipping server readiness check" + exit 0 + fi URL="http://localhost:4321/gh-aw/" STATUS="" echo "Readiness check target: $URL" @@ -201,6 +211,11 @@ pre-agent-steps: - name: Write Playwright base URL run: | + # Skip if pre-flight check failed — server was not started + if [ "$(jq -r '.pass' /tmp/gh-aw/agent/preflight.json 2>/dev/null)" != "true" ]; then + echo "Pre-flight check failed, skipping Playwright base URL setup" + exit 0 + fi mkdir -p /tmp/gh-aw/agent echo "http://localhost:4321/gh-aw/" > /tmp/gh-aw/agent/playwright-base-url.txt echo "Playwright base URL: http://localhost:4321/gh-aw/" @@ -237,7 +252,7 @@ You are a technical documentation editor focused on **clarity and conciseness**. ## 0. Pre-flight Validation -Read `/tmp/gh-aw/agent/preflight.json`. If `"pass"` is `false`, call `noop` with the `"reason"` value and stop. +Read `/tmp/gh-aw/agent/preflight.json`. If `"pass"` is `false`, **immediately** call `noop` with the `"reason"` value and stop — do not read any other files beyond `preflight.json`, do not proceed with any further steps. This is mandatory: failing to call `noop` when preflight fails causes a safe-output compliance error. Only proceed if `"pass"` is `true`. The list of candidate files is already available at `/tmp/gh-aw/agent/candidate-files.txt` (one path per line). diff --git a/actions/setup/js/create_discussion.cjs b/actions/setup/js/create_discussion.cjs index 94ba26308ed..f0717d3c3ac 100644 --- a/actions/setup/js/create_discussion.cjs +++ b/actions/setup/js/create_discussion.cjs @@ -295,6 +295,7 @@ async function main(config = {}) { const configCategory = config.category || ""; const maxCount = config.max || 10; const expiresHours = config.expires ? parseInt(String(config.expires), 10) : 0; + const minBodyLength = config.min_body_length ? parseInt(String(config.min_body_length), 10) : 0; const fallbackToIssue = config.fallback_to_issue !== false; // Default to true const closeOlderDiscussionsEnabled = parseBoolTemplatable(config.close_older_discussions, false); const rawCloseOlderKey = config.close_older_key ? String(config.close_older_key) : ""; @@ -302,6 +303,9 @@ async function main(config = {}) { if (rawCloseOlderKey && !closeOlderKey) { throw new Error(`${ERR_VALIDATION}: close-older-key "${rawCloseOlderKey}" is invalid: it must contain at least one alphanumeric character after normalization`); } + if (isNaN(minBodyLength) || minBodyLength < 0) { + throw new Error(`${ERR_VALIDATION}: min_body_length must be a non-negative integer (got: ${config.min_body_length})`); + } const includeFooter = parseBoolTemplatable(config.footer, true); // Create an authenticated GitHub client. Uses config["github-token"] when set @@ -321,6 +325,9 @@ async function main(config = {}) { .filter(l => l.length > 0); core.info(`Create discussion configuration: max=${maxCount}`); + if (minBodyLength > 0) { + core.info(`Minimum discussion body length guard enabled: ${minBodyLength}`); + } core.info(`Default target repo: ${defaultTargetRepo}`); if (allowedRepos.size > 0) { core.info(`Allowed repos: ${Array.from(allowedRepos).join(", ")}`); @@ -484,9 +491,18 @@ async function main(config = {}) { let title = item.title ? item.title.trim() : ""; let processedBody = replaceTemporaryIdReferences(item.body || "", temporaryIdMap, qualifiedItemRepo); processedBody = removeDuplicateTitleFromDescription(title, processedBody); + const preSanitizeBodyLength = processedBody.trim().length; // Sanitize body content to neutralize @mentions, URLs, and other security risks processedBody = sanitizeContent(processedBody); + if (minBodyLength > 0 && preSanitizeBodyLength < minBodyLength) { + const error = `Discussion body length ${preSanitizeBodyLength} is below configured minimum ${minBodyLength}`; + core.error(error); + return { + success: false, + error, + }; + } if (!title) { title = item.body || "Discussion"; diff --git a/actions/setup/js/create_discussion_sanitization.test.cjs b/actions/setup/js/create_discussion_sanitization.test.cjs index 2f806b855d7..979e0a677b5 100644 --- a/actions/setup/js/create_discussion_sanitization.test.cjs +++ b/actions/setup/js/create_discussion_sanitization.test.cjs @@ -169,4 +169,21 @@ describe("create_discussion body sanitization", () => { // System-generated footer marker must still be present expect(body).toContain("gh-aw-workflow-id"); }); + + it("should fail when body is below configured minimum length", async () => { + const handler = await createDiscussionMain({ max: 5, category: "general", min_body_length: 200 }); + const result = await handler( + { + title: "Too short", + body: "test", + }, + {} + ); + + expect(result.success).toBe(false); + expect(result.error).toContain("below configured minimum 200"); + + const createMutationCall = mockGithub.graphql.mock.calls.find(call => call[0].includes("createDiscussion")); + expect(createMutationCall).toBeUndefined(); + }); }); diff --git a/actions/setup/js/create_pull_request.cjs b/actions/setup/js/create_pull_request.cjs index 6c88f436a93..d8b3d278ab7 100644 --- a/actions/setup/js/create_pull_request.cjs +++ b/actions/setup/js/create_pull_request.cjs @@ -30,7 +30,7 @@ const { checkFileProtection } = require("./manifest_file_helpers.cjs"); const { renderTemplateFromFile, buildProtectedFileList, encodePathSegments, getPromptPath } = require("./messages_core.cjs"); const { COPILOT_REVIEWER_BOT, FAQ_CREATE_PR_PERMISSIONS_URL, MAX_ASSIGNEES } = require("./constants.cjs"); const { isStagedMode } = require("./safe_output_helpers.cjs"); -const { withRetry, isTransientError } = require("./error_recovery.cjs"); +const { withRetry, isTransientError, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs"); const { tryEnforceArrayLimit } = require("./limit_enforcement_helpers.cjs"); const { findAgent, getIssueDetails, assignAgentToIssue } = require("./assign_agent_helpers.cjs"); const { globPatternToRegex } = require("./glob_pattern_helpers.cjs"); @@ -195,9 +195,11 @@ function sanitizeFallbackAssignees(assignees) { } /** - * Creates a fallback GitHub issue, retrying without assignees if the API rejects them. + * Creates a fallback GitHub issue, retrying on rate-limit and other transient errors + * (with exponential back-off) and retrying without assignees if the API rejects them. * This ensures fallback issue creation remains reliable even if an assignee username - * is invalid or the repository does not have that collaborator. + * is invalid, the repository does not have that collaborator, or the installation token + * quota is temporarily exhausted. * @param {object} githubClient - Authenticated GitHub client * @param {{owner: string, repo: string}} repoParts - Repository owner and name * @param {string} title - Issue title @@ -216,19 +218,29 @@ async function createFallbackIssue(githubClient, repoParts, title, body, labels, ...(assignees && assignees.length > 0 && { assignees }), }; - try { - return await githubClient.rest.issues.create(payload); - } catch (error) { - const status = typeof error === "object" && error !== null && "status" in error ? error.status : undefined; - const message = getErrorMessage(error).toLowerCase(); - const isAssigneeError = status === 422 && (message.includes("assignee") || message.includes("assignees") || message.includes("unprocessable")); - if (isAssigneeError && assignees && assignees.length > 0) { - core.warning(`Fallback issue creation failed due to assignee error, retrying without assignees: ${getErrorMessage(error)}`); - const { assignees: _removed, ...payloadWithoutAssignees } = payload; - return await githubClient.rest.issues.create(payloadWithoutAssignees); - } - throw error; - } + return withRetry( + async () => { + try { + return await githubClient.rest.issues.create(payload); + } catch (error) { + const status = typeof error === "object" && error !== null && "status" in error ? error.status : undefined; + const message = getErrorMessage(error).toLowerCase(); + const isAssigneeError = status === 422 && (message.includes("assignee") || message.includes("assignees") || message.includes("unprocessable")); + if (isAssigneeError && payload.assignees && payload.assignees.length > 0) { + const removedAssignees = payload.assignees.join(", "); + core.warning(`Fallback issue creation failed due to assignee error, retrying without assignees: ${getErrorMessage(error)}`); + // Mutate payload in-place so that any subsequent withRetry attempts also + // omit assignees and do not re-trigger the same 422 path. + delete payload.assignees; + payload.body = `${payload.body}\n\n> [!NOTE]\n> Assignees (${removedAssignees}) could not be set on this issue due to an API error.`; + return await githubClient.rest.issues.create(payload); + } + throw error; + } + }, + RATE_LIMIT_RETRY_CONFIG, + `create fallback issue in ${repoParts.owner}/${repoParts.repo}` + ); } /** @@ -1743,15 +1755,20 @@ ${patchPreview}`; // Try to create the pull request, with fallback to issue creation try { - const { data: pullRequest } = await githubClient.rest.pulls.create({ - owner: repoParts.owner, - repo: repoParts.repo, - title: title, - body: body, - head: branchName, - base: baseBranch, - draft: draft, - }); + const { data: pullRequest } = await withRetry( + () => + githubClient.rest.pulls.create({ + owner: repoParts.owner, + repo: repoParts.repo, + title: title, + body: body, + head: branchName, + base: baseBranch, + draft: draft, + }), + RATE_LIMIT_RETRY_CONFIG, + `create pull request in ${repoParts.owner}/${repoParts.repo}` + ); core.info(`Created pull request #${pullRequest.number}: ${pullRequest.html_url}`); diff --git a/actions/setup/js/create_pull_request.test.cjs b/actions/setup/js/create_pull_request.test.cjs index 423c45953a3..bead8026a88 100644 --- a/actions/setup/js/create_pull_request.test.cjs +++ b/actions/setup/js/create_pull_request.test.cjs @@ -2114,3 +2114,206 @@ describe("create_pull_request - threat detection caution", () => { expect((between.match(/\n/g) || []).length).toBeGreaterThanOrEqual(2); }); }); + +describe("create_pull_request - rate-limit retry", () => { + let originalEnv; + let tempDir; + + /** + * Creates a mock GitHub API rate-limit error object (HTTP 403 with x-ratelimit-remaining: 0) + * that matches what octokit returns when the installation token quota is exhausted. + * @param {string} [message] + * @returns {Error} + */ + function createRateLimitError(message = "API rate limit exceeded") { + return Object.assign(new Error(message), { + status: 403, + response: { headers: { "x-ratelimit-remaining": "0" }, status: 403 }, + }); + } + + beforeEach(() => { + originalEnv = { ...process.env }; + process.env.GH_AW_WORKFLOW_ID = "test-workflow"; + process.env.GITHUB_REPOSITORY = "test-owner/test-repo"; + process.env.GITHUB_BASE_REF = "main"; + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-pr-rate-limit-test-")); + + global.core = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + setFailed: vi.fn(), + setOutput: vi.fn(), + startGroup: vi.fn(), + endGroup: vi.fn(), + summary: { + addRaw: vi.fn().mockReturnThis(), + write: vi.fn().mockResolvedValue(undefined), + }, + }; + + global.github = { + rest: { + pulls: { + create: vi.fn().mockResolvedValue({ data: { number: 42, html_url: "https://github.com/test/pull/42" } }), + requestReviewers: vi.fn().mockResolvedValue({}), + }, + repos: { + get: vi.fn().mockResolvedValue({ data: { default_branch: "main" } }), + }, + issues: { + create: vi.fn().mockResolvedValue({ data: { number: 99, html_url: "https://github.com/test/issues/99" } }), + addLabels: vi.fn().mockResolvedValue({}), + }, + }, + graphql: vi.fn(), + }; + + global.context = { + eventName: "issues", + repo: { owner: "test-owner", repo: "test-repo" }, + payload: {}, + runId: "12345", + }; + + global.exec = { + exec: vi.fn().mockResolvedValue(0), + getExecOutput: vi.fn().mockImplementation(async (program, args) => { + if (program === "git" && args[0] === "rev-list") { + return { exitCode: 0, stdout: "1", stderr: "" }; + } + return { exitCode: 0, stdout: "main", stderr: "" }; + }), + }; + + delete require.cache[require.resolve("./create_pull_request.cjs")]; + }); + + afterEach(() => { + for (const key of Object.keys(process.env)) { + if (!(key in originalEnv)) { + delete process.env[key]; + } + } + Object.assign(process.env, originalEnv); + + if (tempDir && fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + + delete global.core; + delete global.github; + delete global.context; + delete global.exec; + vi.clearAllMocks(); + }); + + it("should retry PR creation on rate limit error and succeed", async () => { + vi.useFakeTimers(); + try { + global.github.rest.pulls.create.mockRejectedValueOnce(createRateLimitError()).mockResolvedValue({ data: { number: 42, html_url: "https://github.com/test/pull/42" } }); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ allow_empty: true }); + + const resultPromise = handler({ title: "Test PR", body: "Test body" }, {}); + + await vi.runAllTimersAsync(); + + const result = await resultPromise; + + expect(result.success).toBe(true); + expect(result.pull_request_number).toBe(42); + // 1 initial (rate-limited) + 1 retry (succeeds) = 2 calls total + expect(global.github.rest.pulls.create).toHaveBeenCalledTimes(2); + expect(global.core.warning).toHaveBeenCalledWith(expect.stringContaining("create pull request")); + } finally { + vi.useRealTimers(); + } + }); + + it("should fall back to issue when PR creation fails after all rate-limit retries", async () => { + vi.useFakeTimers(); + try { + global.github.rest.pulls.create.mockRejectedValue(createRateLimitError()); + global.github.rest.issues.create.mockResolvedValue({ data: { number: 99, html_url: "https://github.com/test/issues/99" } }); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ allow_empty: true }); + + const resultPromise = handler({ title: "Test PR", body: "Test body" }, {}); + + await vi.runAllTimersAsync(); + + const result = await resultPromise; + + // Should fall back to issue creation after PR retries are exhausted + expect(result.success).toBe(true); + expect(result.fallback_used).toBe(true); + expect(result.issue_number).toBe(99); + // 1 initial + 5 retries = 6 total PR creation attempts (RATE_LIMIT_RETRY_CONFIG.maxRetries = 5) + expect(global.github.rest.pulls.create).toHaveBeenCalledTimes(6); + expect(global.github.rest.issues.create).toHaveBeenCalled(); + } finally { + vi.useRealTimers(); + } + }); + + it("should retry fallback issue creation on rate limit error and succeed", async () => { + vi.useFakeTimers(); + try { + // PR creation fails with a non-rate-limit error to trigger fallback immediately + global.github.rest.pulls.create.mockRejectedValue(new Error("Some PR creation error")); + // Fallback issue creation first fails with rate limit, then succeeds + global.github.rest.issues.create.mockRejectedValueOnce(createRateLimitError()).mockResolvedValue({ data: { number: 99, html_url: "https://github.com/test/issues/99" } }); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ allow_empty: true }); + + const resultPromise = handler({ title: "Test PR", body: "Test body" }, {}); + + await vi.runAllTimersAsync(); + + const result = await resultPromise; + + expect(result.success).toBe(true); + expect(result.fallback_used).toBe(true); + expect(result.issue_number).toBe(99); + // Fallback issue: 1 rate-limited attempt + 1 successful retry = 2 calls + expect(global.github.rest.issues.create).toHaveBeenCalledTimes(2); + expect(global.core.warning).toHaveBeenCalledWith(expect.stringContaining("create fallback issue")); + } finally { + vi.useRealTimers(); + } + }); + + it("should append a note to the fallback issue body when assignees are removed due to 422 error", async () => { + // PR creation fails with a non-rate-limit error to trigger fallback immediately + global.github.rest.pulls.create.mockRejectedValue(new Error("Some PR creation error")); + + const assigneeError = Object.assign(new Error("Validation Failed: assignees are invalid"), { + status: 422, + response: { status: 422 }, + }); + // First call fails with assignee 422, second succeeds + global.github.rest.issues.create.mockRejectedValueOnce(assigneeError).mockResolvedValue({ data: { number: 77, html_url: "https://github.com/test/issues/77" } }); + + const { main } = require("./create_pull_request.cjs"); + const handler = await main({ allow_empty: true, assignees: ["user1", "user2"] }); + + const result = await handler({ title: "Test PR", body: "Test body" }, {}); + + expect(result.success).toBe(true); + expect(result.fallback_used).toBe(true); + expect(result.issue_number).toBe(77); + expect(global.github.rest.issues.create).toHaveBeenCalledTimes(2); + // Second call (without assignees) should have a note in the body + const secondCall = global.github.rest.issues.create.mock.calls[1][0]; + expect(secondCall.assignees).toBeUndefined(); + expect(secondCall.body).toContain("user1"); + expect(secondCall.body).toContain("user2"); + expect(secondCall.body).toContain("could not be set"); + }); +}); diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index 5991b8a4a18..5454a7e4bbe 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -8,6 +8,7 @@ const { isStagedMode } = require("./safe_output_helpers.cjs"); const { pushSignedCommits } = require("./push_signed_commits.cjs"); const { updateActivationCommentWithCommit, updateActivationComment } = require("./update_activation_comment.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); +const { withRetry, RATE_LIMIT_RETRY_CONFIG } = require("./error_recovery.cjs"); const { normalizeBranchName } = require("./normalize_branch_name.cjs"); const { pushExtraEmptyCommit } = require("./extra_empty_commit.cjs"); const { detectForkPR, checkBranchPushable } = require("./pr_helpers.cjs"); @@ -471,13 +472,18 @@ async function main(config = {}) { }); try { - const { data: issue } = await githubClient.rest.issues.create({ - owner: repoParts.owner, - repo: repoParts.repo, - title: issueTitle, - body: issueBody, - labels: ["agentic-workflows"], - }); + const { data: issue } = await withRetry( + () => + githubClient.rest.issues.create({ + owner: repoParts.owner, + repo: repoParts.repo, + title: issueTitle, + body: issueBody, + labels: ["agentic-workflows"], + }), + RATE_LIMIT_RETRY_CONFIG, + `create manifest-protection review issue in ${repoParts.owner}/${repoParts.repo}` + ); core.info(`Created manifest-protection review issue #${issue.number}: ${issue.html_url}`); await updateActivationComment(github, context, core, issue.html_url, issue.number, "issue"); return { diff --git a/cmd/gh-aw/main_entry_test.go b/cmd/gh-aw/main_entry_test.go index dba6022dc53..197bc89b33a 100644 --- a/cmd/gh-aw/main_entry_test.go +++ b/cmd/gh-aw/main_entry_test.go @@ -11,6 +11,8 @@ import ( "testing" "github.com/github/gh-aw/pkg/cli" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestValidateEngine(t *testing.T) { @@ -82,21 +84,16 @@ func TestValidateEngine(t *testing.T) { err := validateEngine(tt.engine) if tt.expectErr { - if err == nil { - t.Errorf("validateEngine(%q) expected error but got none", tt.engine) - return - } + require.Error(t, err, "validateEngine(%q) should return an error for invalid engines", tt.engine) // Check that error message contains the expected format. // The engine list is dynamic, so only check the prefix. expectedPrefix := fmt.Sprintf("invalid engine value '%s'. Must be", tt.engine) - if tt.errMessage != "" && !strings.HasPrefix(err.Error(), expectedPrefix) { - t.Errorf("validateEngine(%q) error message = %v, want to start with %v", tt.engine, err.Error(), expectedPrefix) + if tt.errMessage != "" { + assert.True(t, strings.HasPrefix(err.Error(), expectedPrefix), "validateEngine(%q) error should start with %q, got %q", tt.engine, expectedPrefix, err.Error()) } } else { - if err != nil { - t.Errorf("validateEngine(%q) unexpected error: %v", tt.engine, err) - } + assert.NoError(t, err, "validateEngine(%q) should not return an error for valid engines", tt.engine) } }) } @@ -106,17 +103,13 @@ func TestInitFunction(t *testing.T) { // Test that init function doesn't panic t.Run("init function executes without panic", func(t *testing.T) { defer func() { - if r := recover(); r != nil { - t.Errorf("init() panicked: %v", r) - } + assert.Nil(t, recover(), "init() should not panic") }() // The init function has already been called when the package was loaded // We can't call it again, but we can verify that the initialization worked // by checking that the version was set - if version == "" { - t.Error("init() should have initialized version variable") - } + assert.NotEmpty(t, version, "init() should initialize the version variable") }) } @@ -126,22 +119,10 @@ func TestMainFunction(t *testing.T) { t.Run("main function setup", func(t *testing.T) { // Test that root command is properly configured - if rootCmd.Use == "" { - t.Error("rootCmd.Use should not be empty") - } - - if rootCmd.Short == "" { - t.Error("rootCmd.Short should not be empty") - } - - if rootCmd.Long == "" { - t.Error("rootCmd.Long should not be empty") - } - - // Test that commands are properly added - if len(rootCmd.Commands()) == 0 { - t.Error("rootCmd should have subcommands") - } + assert.NotEmpty(t, rootCmd.Use, "root command Use should not be empty") + assert.NotEmpty(t, rootCmd.Short, "root command Short description should not be empty") + assert.NotEmpty(t, rootCmd.Long, "root command Long description should not be empty") + assert.NotEmpty(t, rootCmd.Commands(), "root command should have subcommands") }) t.Run("version command is available", func(t *testing.T) { @@ -152,9 +133,7 @@ func TestMainFunction(t *testing.T) { break } } - if !found { - t.Error("version command should be available") - } + assert.True(t, found, "version command should be available") }) t.Run("root command help", func(t *testing.T) { @@ -188,13 +167,8 @@ func TestMainFunction(t *testing.T) { <-done output := buf.String() - if err != nil { - t.Errorf("root command help failed: %v", err) - } - - if output == "" { - t.Error("root command help should produce output") - } + require.NoError(t, err, "root command help should execute successfully") + assert.NotEmpty(t, output, "root command help should produce output") // Reset args for other tests rootCmd.SetArgs([]string{}) @@ -230,18 +204,11 @@ func TestMainFunction(t *testing.T) { <-done output := buf.String() - if err != nil { - t.Errorf("help all command failed: %v", err) - } - - if output == "" { - t.Error("help all command should produce output") - } + require.NoError(t, err, "help all command should execute successfully") + assert.NotEmpty(t, output, "help all command should produce output") // Verify output contains expected content - if !strings.Contains(output, "Complete Command Reference") { - t.Error("help all output should contain 'Complete Command Reference'") - } + assert.Contains(t, output, "Complete Command Reference", "help all output should include the complete command reference heading") // Verify output contains multiple commands commandCount := 0 @@ -252,9 +219,7 @@ func TestMainFunction(t *testing.T) { } } - if commandCount < len(expectedCommands) { - t.Errorf("help all should show help for all commands, found %d/%d", commandCount, len(expectedCommands)) - } + assert.GreaterOrEqual(t, commandCount, len(expectedCommands), "help all should show help for all expected commands") // Reset args for other tests rootCmd.SetArgs([]string{}) @@ -276,18 +241,11 @@ func TestMainFunctionExecutionPath(t *testing.T) { cmd.Dir = "." output, err := cmd.CombinedOutput() // Use CombinedOutput to capture stderr - if err != nil { - t.Fatalf("Failed to run main with --help: %v", err) - } + require.NoError(t, err, "running main with --help should succeed") outputStr := string(output) - if !strings.Contains(outputStr, "GitHub Agentic Workflows") { - t.Error("main function help output should contain 'GitHub Agentic Workflows'") - } - - if !strings.Contains(outputStr, "Usage:") { - t.Error("main function help output should contain usage information") - } + assert.Contains(t, outputStr, "GitHub Agentic Workflows", "main help output should contain the product name") + assert.Contains(t, outputStr, "Usage:", "main help output should contain usage information") }) t.Run("main function version command", func(t *testing.T) { @@ -296,15 +254,11 @@ func TestMainFunctionExecutionPath(t *testing.T) { cmd.Dir = "." output, err := cmd.CombinedOutput() // Use CombinedOutput to capture both stdout and stderr - if err != nil { - t.Fatalf("Failed to run main with version: %v", err) - } + require.NoError(t, err, "running main with version command should succeed") outputStr := string(output) // Should produce some version output (even if it's "unknown") - if len(strings.TrimSpace(outputStr)) == 0 { - t.Error("main function version command should produce output") - } + assert.NotEmpty(t, strings.TrimSpace(outputStr), "main version command should produce output") }) t.Run("main function error handling", func(t *testing.T) { @@ -313,16 +267,12 @@ func TestMainFunctionExecutionPath(t *testing.T) { cmd.Dir = "." _, err := cmd.Output() - if err == nil { - t.Error("main function should return non-zero exit code for invalid command") - } + require.Error(t, err, "main function should return a non-zero exit code for invalid command") // Check that it's an ExitError (non-zero exit code) - if exitError, ok := err.(*exec.ExitError); !ok { - t.Errorf("Expected ExitError for invalid command, got %T: %v", err, err) - } else if exitError.ExitCode() == 0 { - t.Error("Expected non-zero exit code for invalid command") - } + exitError, ok := err.(*exec.ExitError) + require.True(t, ok, "invalid command should return an *exec.ExitError, got %T", err) + assert.NotEqual(t, 0, exitError.ExitCode(), "invalid command should return a non-zero exit code") }) t.Run("main function version info setup", func(t *testing.T) { @@ -336,9 +286,7 @@ func TestMainFunctionExecutionPath(t *testing.T) { cli.SetVersionInfo("test-version") // Verify it was set - if cli.GetVersion() != "test-version" { - t.Error("SetVersionInfo should update the version in CLI package") - } + assert.Equal(t, "test-version", cli.GetVersion(), "SetVersionInfo should update the version in CLI package") // Restore original version cli.SetVersionInfo(originalVersion) @@ -358,14 +306,12 @@ func TestMainFunctionExecutionPath(t *testing.T) { // Some commands might return non-zero but still function properly t.Logf("Command returned exit code %d, output: %s", exitError.ExitCode(), string(output)) } else { - t.Fatalf("Failed to run main with version command: %v", err) + require.NoError(t, err, "running main with version command should not fail with an unexpected execution error") } } // Should produce some output - if len(output) == 0 { - t.Error("version command should produce some output") - } + assert.NotEmpty(t, output, "version command should produce some output") }) } @@ -373,9 +319,7 @@ func TestVersionCommandFunctionality(t *testing.T) { t.Run("version information is available", func(t *testing.T) { // The cli package should provide version functionality versionInfo := cli.GetVersion() - if versionInfo == "" { - t.Error("GetVersion() should return version information") - } + assert.NotEmpty(t, versionInfo, "GetVersion should return version information") }) t.Run("--version flag is supported", func(t *testing.T) { @@ -384,20 +328,14 @@ func TestVersionCommandFunctionality(t *testing.T) { cmd.Dir = "." output, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("Failed to run main with --version: %v", err) - } + require.NoError(t, err, "running main with --version should succeed") outputStr := string(output) // Should produce version output - if len(strings.TrimSpace(outputStr)) == 0 { - t.Error("--version flag should produce output") - } + assert.NotEmpty(t, strings.TrimSpace(outputStr), "--version flag should produce output") // Should contain "version" in the output - if !strings.Contains(outputStr, "version") { - t.Errorf("--version output should contain 'version', got: %s", outputStr) - } + assert.Contains(t, outputStr, "version", "--version output should contain the word 'version'") }) t.Run("version subcommand and --version flag produce same output", func(t *testing.T) { @@ -405,23 +343,16 @@ func TestVersionCommandFunctionality(t *testing.T) { cmdVersion := exec.Command("go", "run", ".", "version") cmdVersion.Dir = "." outputVersion, err := cmdVersion.CombinedOutput() - if err != nil { - t.Fatalf("Failed to run main with version subcommand: %v", err) - } + require.NoError(t, err, "running main with version subcommand should succeed") // Test --version flag cmdFlag := exec.Command("go", "run", ".", "--version") cmdFlag.Dir = "." outputFlag, err := cmdFlag.CombinedOutput() - if err != nil { - t.Fatalf("Failed to run main with --version flag: %v", err) - } + require.NoError(t, err, "running main with --version flag should succeed") // Both should produce the same output - if string(outputVersion) != string(outputFlag) { - t.Errorf("version subcommand and --version flag should produce same output.\nSubcommand: %s\nFlag: %s", - string(outputVersion), string(outputFlag)) - } + assert.Equal(t, string(outputVersion), string(outputFlag), "version subcommand and --version flag should produce identical output") }) } @@ -444,28 +375,19 @@ func TestCommandLineIntegration(t *testing.T) { } } - if len(missingCommands) > 0 { - t.Errorf("Missing expected commands: %v", missingCommands) - } + assert.Empty(t, missingCommands, "all expected commands should be present") }) t.Run("global flags are configured", func(t *testing.T) { // Test that global flags are properly configured flag := rootCmd.PersistentFlags().Lookup("verbose") - if flag == nil { - t.Error("verbose flag should be configured") - } - - if flag != nil && flag.DefValue != "false" { - t.Error("verbose flag should default to false") - } + require.NotNil(t, flag, "verbose flag should be configured") + assert.Equal(t, "false", flag.DefValue, "verbose flag should default to false") }) t.Run("SilenceUsage is enabled", func(t *testing.T) { // Test that SilenceUsage is set to prevent usage output on application errors - if !rootCmd.SilenceUsage { - t.Error("SilenceUsage should be true to prevent cluttering terminal output with usage on application errors") - } + assert.True(t, rootCmd.SilenceUsage, "SilenceUsage should be true to prevent usage output on application errors") }) } @@ -479,16 +401,12 @@ func TestMCPCommand(t *testing.T) { break } } - if !found { - t.Error("mcp command should be available") - } + assert.True(t, found, "mcp command should be available") }) t.Run("mcp command has inspect subcommand", func(t *testing.T) { mcpCmd, _, _ := rootCmd.Find([]string{"mcp"}) - if mcpCmd == nil { - t.Fatal("mcp command not found") - } + require.NotNil(t, mcpCmd, "mcp command should be found") found := false for _, subCmd := range mcpCmd.Commands() { @@ -497,30 +415,20 @@ func TestMCPCommand(t *testing.T) { break } } - if !found { - t.Error("mcp inspect subcommand should be available") - } + assert.True(t, found, "mcp inspect subcommand should be available") }) t.Run("mcp inspect command help", func(t *testing.T) { // Test help for nested command mcpCmd, _, _ := rootCmd.Find([]string{"mcp"}) - if mcpCmd == nil { - t.Fatal("mcp command not found") - } + require.NotNil(t, mcpCmd, "mcp command should be found") inspectCmd, _, _ := mcpCmd.Find([]string{"inspect"}) - if inspectCmd == nil { - t.Fatal("mcp inspect command not found") - } + require.NotNil(t, inspectCmd, "mcp inspect command should be found") // Basic validation that command structure is valid - if inspectCmd.Use == "" { - t.Error("mcp inspect command should have usage text") - } - if inspectCmd.Short == "" { - t.Error("mcp inspect command should have short description") - } + assert.NotEmpty(t, inspectCmd.Use, "mcp inspect command should have usage text") + assert.NotEmpty(t, inspectCmd.Short, "mcp inspect command should have a short description") }) } @@ -530,9 +438,7 @@ func TestCommandErrorHandling(t *testing.T) { rootCmd.SetArgs([]string{"invalid-command"}) err := rootCmd.Execute() - if err == nil { - t.Error("invalid command should produce an error") - } + assert.Error(t, err, "invalid command should produce an error") // With RunE and SilenceErrors, errors are returned but not automatically printed // The main() function is responsible for formatting and printing errors diff --git a/docs/public/robots.txt b/docs/public/robots.txt index 07e706d02c1..618266d1429 100644 --- a/docs/public/robots.txt +++ b/docs/public/robots.txt @@ -1,4 +1,89 @@ +# Allow all crawlers by default User-agent: * Allow: / +# Explicitly list major AI crawlers for clarity and signaling. +# The wildcard rule above already permits all bots; the entries below +# make it unambiguous to AI indexers that they are welcome here. +User-agent: GPTBot +Allow: / + +User-agent: ChatGPT-User +Allow: / + +User-agent: OAI-SearchBot +Allow: / + +User-agent: Google-Extended +Allow: / + +User-agent: Googlebot +Allow: / + +User-agent: ClaudeBot +Allow: / + +User-agent: Claude-Web +Allow: / + +User-agent: anthropic-ai +Allow: / + +User-agent: PerplexityBot +Allow: / + +User-agent: Perplexity-User +Allow: / + +User-agent: Applebot +Allow: / + +User-agent: Applebot-Extended +Allow: / + +User-agent: Amazonbot +Allow: / + +User-agent: cohere-ai +Allow: / + +User-agent: Omgilibot +Allow: / + +User-agent: FacebookBot +Allow: / + +User-agent: Bytespider +Allow: / + +User-agent: bingbot +Allow: / + +User-agent: BingPreview +Allow: / + +User-agent: DuckDuckBot +Allow: / + +User-agent: Slurp +Allow: / + +User-agent: YandexBot +Allow: / + +User-agent: Baiduspider +Allow: / + +User-agent: ia_archiver +Allow: / + +User-agent: CCBot +Allow: / + +User-agent: DataForSeoBot +Allow: / + +User-agent: Diffbot +Allow: / + Sitemap: https://github.github.com/gh-aw/sitemap-index.xml diff --git a/docs/src/components/CustomHead.astro b/docs/src/components/CustomHead.astro index 42b872f2d6f..1f0080642a9 100644 --- a/docs/src/components/CustomHead.astro +++ b/docs/src/components/CustomHead.astro @@ -47,7 +47,13 @@ const homeJsonLd = { '@id': 'https://github.github.com/gh-aw/#organization', name: 'GitHub', url: 'https://github.com/github', - sameAs: ['https://github.com/github/gh-aw'], + sameAs: [ + 'https://github.com/github/gh-aw', + 'https://github.com/github', + 'https://githubnext.com/', + 'https://github.blog/', + ], + dateModified: '2026-05-09', }, { '@type': 'SoftwareApplication', @@ -57,6 +63,7 @@ const homeJsonLd = { description: defaultDescription, applicationCategory: 'DeveloperApplication', operatingSystem: 'Any', + dateModified: '2026-05-09', creator: { '@id': 'https://github.github.com/gh-aw/#organization', }, diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index aa60b0b46f0..3e7155575d6 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -32,6 +32,17 @@ Developed by GitHub Next and Microsoft Research, workflows run with added guardr > ⓘ Note: GitHub Agentic Workflows is in early development and may change significantly. Using agentic workflows requires careful attention to security considerations and careful human supervision, and even then things can still go wrong. Use it with caution, and at your own risk. +## By the Numbers + +| Metric | Value | +|--------|-------| +| Supported AI engines | 4 (GitHub Copilot, Claude, OpenAI Codex, custom) | +| Security layers | 5 (read-only token, zero secrets, network firewall, safe outputs, threat detection) | +| Design patterns | 18+ (IssueOps, ChatOps, DailyOps, BatchOps, and more) | +| Supported GitHub event triggers | 10+ (issues, pull_request, push, schedule, discussion, label, …) | +| Safe output types | 8+ (create-issue, create-pull-request, add-comment, add-label, …) | +| Installation | 1 command: `gh extension install github/gh-aw` | + ## Key Features diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index 69a370b3d2c..deb02f0f56c 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -3631,6 +3631,12 @@ safe-outputs: # (optional) category: null + # Minimum required length of the discussion body content (before + # footer/metadata) in characters. If a create_discussion message body is + # shorter than this value, the safe-outputs job fails. + # (optional) + min-body-length: 200 + # Optional list of labels to attach to created discussions. Also used for matching # when close-older-discussions is enabled - discussions must have ALL specified # labels (AND logic). diff --git a/docs/src/content/docs/reference/safe-outputs-specification.md b/docs/src/content/docs/reference/safe-outputs-specification.md index 4447d784981..c51eae9faee 100644 --- a/docs/src/content/docs/reference/safe-outputs-specification.md +++ b/docs/src/content/docs/reference/safe-outputs-specification.md @@ -1612,6 +1612,7 @@ upload-asset: create-discussion: category: "General" # Discussion category (name/slug/ID) title-prefix: "[Report] " # Prepend to titles + min-body-length: 200 # Optional minimum report body length labels: [report, automated] # Auto-apply labels allowed-labels: [...] # Agent label restrictions ``` @@ -2654,11 +2655,14 @@ This section provides complete definitions for all remaining safe output types. 3. **Footer Injection**: Appends attribution footer to the discussion body when configured. 4. **Cross-Repository**: When `target-repo` is configured, creates in that repository (must be in `allowed-repos`). 5. **Temporary ID Support**: Supports `temporary_id` field for referencing before creation. +6. **Body Length Guard**: When `min-body-length` is configured, discussion creation is rejected if the body is shorter than the configured minimum. **Configuration Parameters**: - `max`: Operation limit (default: 1) - `category`: Default discussion category +- `title-prefix`: Prepend to titles +- `min-body-length`: Minimum required body length (characters, before footer/metadata) - `target-repo`: Cross-repository target - `allowed-repos`: Cross-repo allowlist - `footer`: Footer override diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 17af5ea9f5d..1865a9468e4 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -1013,6 +1013,7 @@ safe-outputs: create-discussion: title-prefix: "[ai] " # prefix for titles category: "announcements" # category slug, name, or ID (use lowercase) + min-body-length: 200 # optional minimum body length guard (fails safe-outputs job if shorter) expires: 3 # auto-close after 3 days (or false to disable) max: 3 # max discussions (default: 1) target-repo: "owner/repo" # cross-repository @@ -1021,6 +1022,8 @@ safe-outputs: github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions ``` +Use `min-body-length` when you want a hard floor for report quality (for example, to prevent accidental placeholder bodies like `test` from being posted). + #### Fallback to Issue Creation The `fallback-to-issue` field (default: `true`) automatically falls back to creating an issue when discussion creation fails (e.g., discussions disabled, insufficient `discussions: write` permissions, or org policy restrictions). The issue body notes it was intended to be a discussion. Set to `false` to fail instead of falling back. diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index dcdfe276e50..aa91bf2be95 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -5422,6 +5422,11 @@ "description": "Optional discussion category. Can be a category ID (string or numeric value), category name, or category slug/route. If not specified, uses the first available category. Matched first against category IDs, then against category names, then against category slugs. Numeric values are automatically converted to strings at runtime.", "examples": ["General", "audits", 123456789] }, + "min-body-length": { + "type": "integer", + "minimum": 1, + "description": "Minimum required length of the discussion body content (before footer/metadata) in characters. If a create_discussion message body is shorter than this value, the safe-outputs job fails." + }, "labels": { "type": "array", "items": { diff --git a/pkg/workflow/compiler_safe_outputs_config_test.go b/pkg/workflow/compiler_safe_outputs_config_test.go index bad798f3dbb..92176f0b239 100644 --- a/pkg/workflow/compiler_safe_outputs_config_test.go +++ b/pkg/workflow/compiler_safe_outputs_config_test.go @@ -1138,6 +1138,17 @@ func TestHandlerConfigBooleanFields(t *testing.T) { checkKey: "draft", expected: true, // AddTemplatableBool converts "true" string to JSON boolean }, + { + name: "create discussion minimum body length", + safeOutputs: &SafeOutputsConfig{ + CreateDiscussions: &CreateDiscussionsConfig{ + MinBodyLength: 200, + }, + }, + checkField: "create_discussion", + checkKey: "min_body_length", + expected: float64(200), + }, } for _, tt := range tests { diff --git a/pkg/workflow/compiler_safe_outputs_handlers.go b/pkg/workflow/compiler_safe_outputs_handlers.go index 92dbb2ca749..74be7eaabb7 100644 --- a/pkg/workflow/compiler_safe_outputs_handlers.go +++ b/pkg/workflow/compiler_safe_outputs_handlers.go @@ -69,6 +69,7 @@ var handlerRegistry = map[string]handlerBuilder{ AddTemplatableInt("max", c.Max). AddIfNotEmpty("category", c.Category). AddIfNotEmpty("title_prefix", c.TitlePrefix). + AddIfPositive("min_body_length", c.MinBodyLength). AddStringSlice("labels", c.Labels). AddStringSlice("allowed_labels", c.AllowedLabels). AddStringSlice("allowed_repos", c.AllowedRepos). diff --git a/pkg/workflow/create_discussion.go b/pkg/workflow/create_discussion.go index c5bfbf9c62c..da66c157b04 100644 --- a/pkg/workflow/create_discussion.go +++ b/pkg/workflow/create_discussion.go @@ -15,6 +15,7 @@ type CreateDiscussionsConfig struct { BaseSafeOutputConfig `yaml:",inline"` TitlePrefix string `yaml:"title-prefix,omitempty"` Category string `yaml:"category,omitempty"` // Discussion category ID or name + MinBodyLength int `yaml:"min-body-length,omitempty"` // Minimum required discussion body length before footer/markers Labels []string `yaml:"labels,omitempty"` // Labels to attach to discussions and match when closing older ones AllowedLabels []string `yaml:"allowed-labels,omitempty"` // Optional list of allowed labels. If omitted, any labels are allowed (including creating new ones). TargetRepoSlug string `yaml:"target-repo,omitempty"` // Target repository in format "owner/repo" for cross-repository discussions