diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1420999..c684707 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,18 +15,3 @@ jobs:
- run: uv run pyright
- run: uv run bandit -r src/
- run: uv run pytest --cov --cov-fail-under=80 -v
-
- dispatch-ci-fixer:
- needs: check
- if: ${{ failure() && contains(github.event.pull_request.labels.*.name, 'aw') && !contains(github.event.pull_request.labels.*.name, 'ci-fix-attempted') }}
- runs-on: ubuntu-latest
- permissions:
- actions: write
- steps:
- - name: Dispatch CI fixer
- env:
- GH_TOKEN: ${{ secrets.GH_AW_WRITE_TOKEN }}
- run: |
- gh workflow run ci-fixer.lock.yml \
- -R ${{ github.repository }} \
- -f pr_number=${{ github.event.pull_request.number }}
diff --git a/.github/workflows/code-health.lock.yml b/.github/workflows/code-health.lock.yml
index ca5e929..622a5d2 100644
--- a/.github/workflows/code-health.lock.yml
+++ b/.github/workflows/code-health.lock.yml
@@ -22,7 +22,7 @@
# For more information: https://github.github.com/gh-aw/introduction/overview/
#
#
-# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"a1481161b1736062b00489dedfa3e297fdfb5a2be89b69ab88467bfeaa3c99c9","compiler_version":"v0.58.1","strict":true}
+# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"edb4986b898329a97e2c96676022c86002db2c80b926999fe2d3c84c0a92176c","compiler_version":"v0.58.1","strict":true}
name: "Code Health Analysis"
"on":
@@ -125,7 +125,7 @@ jobs:
cat "/opt/gh-aw/prompts/safe_outputs_prompt.md"
cat << 'GH_AW_PROMPT_EOF'
- Tools: create_issue, dispatch_workflow, missing_tool, missing_data, noop
+ Tools: create_issue, missing_tool, missing_data, noop
The following GitHub context information is available for this workflow:
@@ -311,7 +311,7 @@ jobs:
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
- {"create_issue":{"max":2},"dispatch_workflow":{"max":2,"workflow_files":{"issue-implementer":".lock.yml"},"workflows":["issue-implementer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1}}
+ {"create_issue":{"max":2},"missing_data":{},"missing_tool":{},"noop":{"max":1}}
GH_AW_SAFE_OUTPUTS_CONFIG_EOF
- name: Write Safe Outputs Tools
run: |
@@ -458,24 +458,6 @@ jobs:
"type": "object"
},
"name": "missing_data"
- },
- {
- "_workflow_name": "issue-implementer",
- "description": "Dispatch the 'issue-implementer' workflow with workflow_dispatch trigger. This workflow must support workflow_dispatch and be in .github/workflows/ directory in the same repository.",
- "inputSchema": {
- "additionalProperties": false,
- "properties": {
- "issue_number": {
- "description": "Issue number to fix",
- "type": "string"
- }
- },
- "required": [
- "issue_number"
- ],
- "type": "object"
- },
- "name": "issue_implementer"
}
]
GH_AW_SAFE_OUTPUTS_TOOLS_EOF
@@ -972,7 +954,6 @@ jobs:
if: (always()) && (needs.agent.result != 'skipped')
runs-on: ubuntu-slim
permissions:
- actions: write
contents: read
issues: write
concurrency:
@@ -1072,7 +1053,6 @@ jobs:
if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
runs-on: ubuntu-slim
permissions:
- actions: write
contents: read
issues: write
timeout-minutes: 15
@@ -1117,7 +1097,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"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
- GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":2},\"dispatch_workflow\":{\"max\":2,\"workflow_files\":{\"issue-implementer\":\".lock.yml\"},\"workflows\":[\"issue-implementer\"]},\"missing_data\":{},\"missing_tool\":{}}"
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":2},\"missing_data\":{},\"missing_tool\":{}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/code-health.md b/.github/workflows/code-health.md
index c744270..93ac6ab 100644
--- a/.github/workflows/code-health.md
+++ b/.github/workflows/code-health.md
@@ -23,9 +23,6 @@ safe-outputs:
create-issue:
max: 2
github-token: ${{ secrets.GH_AW_WRITE_TOKEN }}
- dispatch-workflow:
- workflows: [issue-implementer]
- max: 2
---
@@ -37,6 +34,6 @@ Analyze the entire codebase for cleanup opportunities and open issues for anythi
Read all files in the repository. Read all open issues in the repository. Identify genuine cleanup opportunities — refactoring, dead code, inconsistencies, stale docs, dependency hygiene, or anything else that would make the codebase meaningfully better.
-For each finding, open an issue with root cause analysis and a clear spec for resolving it. Each issue must include a testing requirement — regression tests for bugs, coverage for new functionality. Prefix each issue title with `[aw][code health]` and label each issue with `aw` and `code-health`. After creating each issue, dispatch the issue-implementer workflow with the issue number as input.
+For each finding, open an issue with root cause analysis and a clear spec for resolving it. Each issue must include a testing requirement — regression tests for bugs, coverage for new functionality. Prefix each issue title with `[aw][code health]` and label each issue with `aw` and `code-health`. The pipeline orchestrator will pick up the issue and dispatch the implementer — do NOT dispatch it yourself.
Do not open issues for things already caught by CI (ruff, pyright, bandit). Do not open issues for things that already have an open issue. Do not open an issue that is just a nit — if there are many small nits that together form a meaningful cleanup, bundle them into one issue. If nothing worth fixing is found, do not create any issues.
diff --git a/.github/workflows/pipeline-orchestrator.lock.yml b/.github/workflows/pipeline-orchestrator.lock.yml
new file mode 100644
index 0000000..79ac273
--- /dev/null
+++ b/.github/workflows/pipeline-orchestrator.lock.yml
@@ -0,0 +1,1364 @@
+#
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+# This file was automatically generated by gh-aw (v0.58.1). DO NOT EDIT.
+#
+# To update this file, edit the corresponding .md file and run:
+# gh aw compile
+# Not all edits will cause changes to this file.
+#
+# For more information: https://github.github.com/gh-aw/introduction/overview/
+#
+#
+# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"96df3a1d2d30a46810a72c84054becda8307a741c21cdb7751bef8f9d3f4d180","compiler_version":"v0.58.1","strict":true}
+
+name: "Pipeline Orchestrator"
+"on":
+ push:
+ branches:
+ - main
+ schedule:
+ - cron: "*/15 * * * *"
+ workflow_dispatch:
+
+permissions: {}
+
+concurrency:
+ cancel-in-progress: false
+ group: pipeline-orchestrator
+
+run-name: "Pipeline Orchestrator"
+
+jobs:
+ activation:
+ needs: pre_activation
+ if: needs.pre_activation.outputs.activated == 'true'
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ outputs:
+ comment_id: ""
+ comment_repo: ""
+ model: ${{ steps.generate_aw_info.outputs.model }}
+ secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw/actions/setup@fa061e89469ef007881d22d3af5a8c9e62363a0d # v0.58.1
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Generate agentic run info
+ id: generate_aw_info
+ env:
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
+ GH_AW_INFO_MODEL: "claude-opus-4.6"
+ GH_AW_INFO_VERSION: ""
+ GH_AW_INFO_AGENT_VERSION: "latest"
+ GH_AW_INFO_CLI_VERSION: "v0.58.1"
+ GH_AW_INFO_WORKFLOW_NAME: "Pipeline Orchestrator"
+ GH_AW_INFO_EXPERIMENTAL: "false"
+ GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
+ GH_AW_INFO_STAGED: "false"
+ GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]'
+ GH_AW_INFO_FIREWALL_ENABLED: "true"
+ GH_AW_INFO_AWF_VERSION: "v0.24.1"
+ GH_AW_INFO_AWMG_VERSION: ""
+ GH_AW_INFO_FIREWALL_TYPE: "squid"
+ GH_AW_COMPILED_STRICT: "true"
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { main } = require('/opt/gh-aw/actions/generate_aw_info.cjs');
+ await main(core, context);
+ - name: Validate COPILOT_GITHUB_TOKEN secret
+ id: validate-secret
+ run: /opt/gh-aw/actions/validate_multi_secret.sh COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ - name: Checkout .github and .agents folders
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ sparse-checkout: |
+ .github
+ .agents
+ sparse-checkout-cone-mode: true
+ fetch-depth: 1
+ - name: Check workflow file timestamps
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_WORKFLOW_FILE: "pipeline-orchestrator.lock.yml"
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_workflow_timestamp_api.cjs');
+ await main();
+ - name: Create prompt with built-in context
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ run: |
+ bash /opt/gh-aw/actions/create_prompt_first.sh
+ {
+ cat << 'GH_AW_PROMPT_EOF'
+
+ GH_AW_PROMPT_EOF
+ cat "/opt/gh-aw/prompts/xpia.md"
+ cat "/opt/gh-aw/prompts/temp_folder_prompt.md"
+ cat "/opt/gh-aw/prompts/markdown.md"
+ cat "/opt/gh-aw/prompts/safe_outputs_prompt.md"
+ cat << 'GH_AW_PROMPT_EOF'
+
+ Tools: add_comment, resolve_pull_request_review_thread, add_labels, remove_labels, add_reviewer, dispatch_workflow, missing_tool, missing_data, noop
+
+
+ The following GitHub context information is available for this workflow:
+ {{#if __GH_AW_GITHUB_ACTOR__ }}
+ - **actor**: __GH_AW_GITHUB_ACTOR__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_REPOSITORY__ }}
+ - **repository**: __GH_AW_GITHUB_REPOSITORY__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_WORKSPACE__ }}
+ - **workspace**: __GH_AW_GITHUB_WORKSPACE__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_ISSUE_NUMBER__ }}
+ - **issue-number**: #__GH_AW_GITHUB_EVENT_ISSUE_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__ }}
+ - **discussion-number**: #__GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__ }}
+ - **pull-request-number**: #__GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_EVENT_COMMENT_ID__ }}
+ - **comment-id**: __GH_AW_GITHUB_EVENT_COMMENT_ID__
+ {{/if}}
+ {{#if __GH_AW_GITHUB_RUN_ID__ }}
+ - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
+ {{/if}}
+
+
+ GH_AW_PROMPT_EOF
+ cat << 'GH_AW_PROMPT_EOF'
+
+ GH_AW_PROMPT_EOF
+ cat << 'GH_AW_PROMPT_EOF'
+ {{#runtime-import .github/workflows/pipeline-orchestrator.md}}
+ GH_AW_PROMPT_EOF
+ } > "$GH_AW_PROMPT"
+ - name: Interpolate variables and render templates
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/interpolate_prompt.cjs');
+ await main();
+ - name: Substitute placeholders
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: ${{ github.event.discussion.number }}
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }}
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+
+ const substitutePlaceholders = require('/opt/gh-aw/actions/substitute_placeholders.cjs');
+
+ // Call the substitution function
+ return await substitutePlaceholders({
+ file: process.env.GH_AW_PROMPT,
+ substitutions: {
+ GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
+ GH_AW_GITHUB_EVENT_COMMENT_ID: process.env.GH_AW_GITHUB_EVENT_COMMENT_ID,
+ GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER: process.env.GH_AW_GITHUB_EVENT_DISCUSSION_NUMBER,
+ GH_AW_GITHUB_EVENT_ISSUE_NUMBER: process.env.GH_AW_GITHUB_EVENT_ISSUE_NUMBER,
+ GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER: process.env.GH_AW_GITHUB_EVENT_PULL_REQUEST_NUMBER,
+ GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
+ GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
+ GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
+ GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED
+ }
+ });
+ - name: Validate prompt placeholders
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash /opt/gh-aw/actions/validate_prompt_placeholders.sh
+ - name: Print prompt
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ run: bash /opt/gh-aw/actions/print_prompt_summary.sh
+ - name: Upload activation artifact
+ if: success()
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: activation
+ path: |
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ retention-days: 1
+
+ agent:
+ needs: activation
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ issues: read
+ pull-requests: read
+ env:
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ GH_AW_ASSETS_ALLOWED_EXTS: ""
+ GH_AW_ASSETS_BRANCH: ""
+ GH_AW_ASSETS_MAX_SIZE_KB: 0
+ GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
+ GH_AW_SAFE_OUTPUTS: /opt/gh-aw/safeoutputs/outputs.jsonl
+ GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json
+ GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json
+ GH_AW_WORKFLOW_ID_SANITIZED: pipelineorchestrator
+ outputs:
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }}
+ detection_success: ${{ steps.detection_conclusion.outputs.success }}
+ has_patch: ${{ steps.collect_output.outputs.has_patch }}
+ inference_access_error: ${{ steps.detect-inference-error.outputs.inference_access_error || 'false' }}
+ model: ${{ needs.activation.outputs.model }}
+ output: ${{ steps.collect_output.outputs.output }}
+ output_types: ${{ steps.collect_output.outputs.output_types }}
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw/actions/setup@fa061e89469ef007881d22d3af5a8c9e62363a0d # v0.58.1
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - name: Create gh-aw temp directory
+ run: bash /opt/gh-aw/actions/create_gh_aw_tmp_dir.sh
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Checkout PR branch
+ id: checkout-pr
+ if: |
+ (github.event.pull_request) || (github.event.issue.pull_request)
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/checkout_pr_branch.cjs');
+ await main();
+ - name: Install GitHub Copilot CLI
+ run: /opt/gh-aw/actions/install_copilot_cli.sh latest
+ - name: Install AWF binary
+ run: bash /opt/gh-aw/actions/install_awf_binary.sh v0.24.1
+ - name: Determine automatic lockdown mode for GitHub MCP Server
+ id: determine-automatic-lockdown
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ with:
+ script: |
+ const determineAutomaticLockdown = require('/opt/gh-aw/actions/determine_automatic_lockdown.cjs');
+ await determineAutomaticLockdown(github, context, core);
+ - name: Download container images
+ run: bash /opt/gh-aw/actions/download_docker_images.sh ghcr.io/github/gh-aw-firewall/agent:0.24.1 ghcr.io/github/gh-aw-firewall/api-proxy:0.24.1 ghcr.io/github/gh-aw-firewall/squid:0.24.1 ghcr.io/github/gh-aw-mcpg:v0.1.14 ghcr.io/github/github-mcp-server:v0.32.0 node:lts-alpine
+ - name: Write Safe Outputs Config
+ run: |
+ mkdir -p /opt/gh-aw/safeoutputs
+ mkdir -p /tmp/gh-aw/safeoutputs
+ mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
+ cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
+ {"add_comment":{"max":5},"add_labels":{"max":10},"add_reviewer":{"max":3},"dispatch_workflow":{"max":1,"workflow_files":{"ci-fixer":".lock.yml","issue-implementer":".lock.yml"},"workflows":["issue-implementer","ci-fixer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1},"remove_labels":{"max":10},"resolve_pull_request_review_thread":{"max":10}}
+ GH_AW_SAFE_OUTPUTS_CONFIG_EOF
+ - name: Write Safe Outputs Tools
+ run: |
+ cat > /opt/gh-aw/safeoutputs/tools.json << 'GH_AW_SAFE_OUTPUTS_TOOLS_EOF'
+ [
+ {
+ "description": "Add a comment to an existing GitHub issue, pull request, or discussion. Use this to provide feedback, answer questions, or add information to an existing conversation. For creating new items, use create_issue, create_discussion, or create_pull_request instead. IMPORTANT: Comments are subject to validation constraints enforced by the MCP server - maximum 65536 characters for the complete comment (including footer which is added automatically), 10 mentions (@username), and 50 links. Exceeding these limits will result in an immediate error with specific guidance. NOTE: By default, this tool requires discussions:write permission. If your GitHub App lacks Discussions permission, set 'discussions: false' in the workflow's safe-outputs.add-comment configuration to exclude this permission. CONSTRAINTS: Maximum 5 comment(s) can be added.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "body": {
+ "description": "The comment text in Markdown format. This is the 'body' field - do not use 'comment_body' or other variations. Provide helpful, relevant information that adds value to the conversation. CONSTRAINTS: The complete comment (your body text + automatically added footer) must not exceed 65536 characters total. Maximum 10 mentions (@username), maximum 50 links (http/https URLs). A footer (~200-500 characters) is automatically appended with workflow attribution, so leave adequate space. If these limits are exceeded, the tool call will fail with a detailed error message indicating which constraint was violated.",
+ "type": "string"
+ },
+ "integrity": {
+ "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").",
+ "type": "string"
+ },
+ "item_number": {
+ "description": "The issue, pull request, or discussion number to comment on. This is the numeric ID from the GitHub URL (e.g., 123 in github.com/owner/repo/issues/123). Can also be a temporary_id (e.g., 'aw_abc123') from a previously created issue in the same workflow run. If omitted, the tool auto-targets the issue, PR, or discussion that triggered this workflow. Auto-targeting only works for issue, pull_request, discussion, and comment event triggers — it does NOT work for schedule, workflow_dispatch, push, or workflow_run triggers. For those trigger types, always provide item_number explicitly, or the tool call will fail with an error.",
+ "type": [
+ "number",
+ "string"
+ ]
+ },
+ "secrecy": {
+ "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").",
+ "type": "string"
+ },
+ "temporary_id": {
+ "description": "Unique temporary identifier for this comment. Format: 'aw_' followed by 3 to 12 alphanumeric characters (e.g., 'aw_abc1', 'aw_Test123'). Auto-generated if not provided. The temporary ID is returned in the tool response so you can reference this comment later.",
+ "pattern": "^aw_[A-Za-z0-9]{3,12}$",
+ "type": "string"
+ }
+ },
+ "required": [
+ "body"
+ ],
+ "type": "object"
+ },
+ "name": "add_comment"
+ },
+ {
+ "description": "Resolve a review thread on a pull request. Use this to mark a review conversation as resolved after addressing the feedback. The thread_id must be the node ID of the review thread (e.g., PRRT_kwDO...). CONSTRAINTS: Maximum 10 review thread(s) can be resolved.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "integrity": {
+ "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").",
+ "type": "string"
+ },
+ "secrecy": {
+ "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").",
+ "type": "string"
+ },
+ "thread_id": {
+ "description": "The node ID of the review thread to resolve (e.g., 'PRRT_kwDOABCD...'). This is the GraphQL node ID, not a numeric ID.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "thread_id"
+ ],
+ "type": "object"
+ },
+ "name": "resolve_pull_request_review_thread"
+ },
+ {
+ "description": "Add labels to an existing GitHub issue or pull request for categorization and filtering. Labels must already exist in the repository. For creating new issues with labels, use create_issue with the labels property instead. CONSTRAINTS: Maximum 10 label(s) can be added.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "integrity": {
+ "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").",
+ "type": "string"
+ },
+ "item_number": {
+ "description": "Issue or PR number to add labels to. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/issues/456). If omitted, adds labels to the issue or PR that triggered this workflow. Only works for issue or pull_request event triggers. For schedule, workflow_dispatch, or other triggers, item_number is required — omitting it will silently skip the label operation.",
+ "type": "number"
+ },
+ "labels": {
+ "description": "Label names to add (e.g., ['bug', 'priority-high']). Labels must exist in the repository.",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "secrecy": {
+ "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").",
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "name": "add_labels"
+ },
+ {
+ "description": "Remove labels from an existing GitHub issue or pull request. Silently skips labels that don't exist on the item. Use this to clean up labels or manage label lifecycles (e.g., removing 'needs-review' after review is complete). CONSTRAINTS: Maximum 10 label(s) can be removed.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "integrity": {
+ "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").",
+ "type": "string"
+ },
+ "item_number": {
+ "description": "Issue or PR number to remove labels from. This is the numeric ID from the GitHub URL (e.g., 456 in github.com/owner/repo/issues/456). If omitted, removes labels from the item that triggered this workflow.",
+ "type": "number"
+ },
+ "labels": {
+ "description": "Label names to remove (e.g., ['smoke', 'needs-triage']). Non-existent labels are silently skipped.",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "secrecy": {
+ "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").",
+ "type": "string"
+ }
+ },
+ "required": [
+ "labels"
+ ],
+ "type": "object"
+ },
+ "name": "remove_labels"
+ },
+ {
+ "description": "Add reviewers to a GitHub pull request. Reviewers receive notifications and can approve or request changes. Use 'copilot' as a reviewer name to request the Copilot PR review bot. CONSTRAINTS: Maximum 3 reviewer(s) can be added.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "integrity": {
+ "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").",
+ "type": "string"
+ },
+ "pull_request_number": {
+ "description": "Pull request number to add reviewers to. This is the numeric ID from the GitHub URL (e.g., 876 in github.com/owner/repo/pull/876). If omitted, adds reviewers to the PR that triggered this workflow. Only works for pull_request event triggers. For workflow_dispatch, schedule, or other triggers, pull_request_number is required — omitting it will silently skip the reviewer assignment.",
+ "type": [
+ "number",
+ "string"
+ ]
+ },
+ "reviewers": {
+ "description": "GitHub usernames to add as reviewers (e.g., ['octocat', 'copilot']). Users must have access to the repository.",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "secrecy": {
+ "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").",
+ "type": "string"
+ }
+ },
+ "required": [
+ "reviewers"
+ ],
+ "type": "object"
+ },
+ "name": "add_reviewer"
+ },
+ {
+ "description": "Report that a tool or capability needed to complete the task is not available, or share any information you deem important about missing functionality or limitations. Use this when you cannot accomplish what was requested because the required functionality is missing or access is restricted.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "alternatives": {
+ "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).",
+ "type": "string"
+ },
+ "integrity": {
+ "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").",
+ "type": "string"
+ },
+ "reason": {
+ "description": "Explanation of why this tool is needed or what information you want to share about the limitation (max 256 characters).",
+ "type": "string"
+ },
+ "secrecy": {
+ "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").",
+ "type": "string"
+ },
+ "tool": {
+ "description": "Optional: Name or description of the missing tool or capability (max 128 characters). Be specific about what functionality is needed.",
+ "type": "string"
+ }
+ },
+ "required": [
+ "reason"
+ ],
+ "type": "object"
+ },
+ "name": "missing_tool"
+ },
+ {
+ "description": "Log a transparency message when no significant actions are needed. Use this to confirm workflow completion and provide visibility when analysis is complete but no changes or outputs are required (e.g., 'No issues found', 'All checks passed'). This ensures the workflow produces human-visible output even when no other actions are taken.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "integrity": {
+ "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").",
+ "type": "string"
+ },
+ "message": {
+ "description": "Status or completion message to log. Should explain what was analyzed and the outcome (e.g., 'Code review complete - no issues found', 'Analysis complete - all tests passing').",
+ "type": "string"
+ },
+ "secrecy": {
+ "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").",
+ "type": "string"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "type": "object"
+ },
+ "name": "noop"
+ },
+ {
+ "description": "Report that data or information needed to complete the task is not available. Use this when you cannot accomplish what was requested because required data, context, or information is missing.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "alternatives": {
+ "description": "Any workarounds, manual steps, or alternative approaches the user could take (max 256 characters).",
+ "type": "string"
+ },
+ "context": {
+ "description": "Additional context about the missing data or where it should come from (max 256 characters).",
+ "type": "string"
+ },
+ "data_type": {
+ "description": "Type or description of the missing data or information (max 128 characters). Be specific about what data is needed.",
+ "type": "string"
+ },
+ "integrity": {
+ "description": "Trustworthiness level of the message source (e.g., \"low\", \"medium\", \"high\").",
+ "type": "string"
+ },
+ "reason": {
+ "description": "Explanation of why this data is needed to complete the task (max 256 characters).",
+ "type": "string"
+ },
+ "secrecy": {
+ "description": "Confidentiality level of the message content (e.g., \"public\", \"internal\", \"private\").",
+ "type": "string"
+ }
+ },
+ "required": [],
+ "type": "object"
+ },
+ "name": "missing_data"
+ },
+ {
+ "_workflow_name": "issue-implementer",
+ "description": "Dispatch the 'issue-implementer' workflow with workflow_dispatch trigger. This workflow must support workflow_dispatch and be in .github/workflows/ directory in the same repository.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "issue_number": {
+ "description": "Issue number to fix",
+ "type": "string"
+ }
+ },
+ "required": [
+ "issue_number"
+ ],
+ "type": "object"
+ },
+ "name": "issue_implementer"
+ },
+ {
+ "_workflow_name": "ci-fixer",
+ "description": "Dispatch the 'ci-fixer' workflow with workflow_dispatch trigger. This workflow must support workflow_dispatch and be in .github/workflows/ directory in the same repository.",
+ "inputSchema": {
+ "additionalProperties": false,
+ "properties": {
+ "pr_number": {
+ "description": "Pull request number to fix",
+ "type": "string"
+ }
+ },
+ "required": [
+ "pr_number"
+ ],
+ "type": "object"
+ },
+ "name": "ci_fixer"
+ }
+ ]
+ GH_AW_SAFE_OUTPUTS_TOOLS_EOF
+ cat > /opt/gh-aw/safeoutputs/validation.json << 'GH_AW_SAFE_OUTPUTS_VALIDATION_EOF'
+ {
+ "add_comment": {
+ "defaultMax": 1,
+ "fields": {
+ "body": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "item_number": {
+ "issueOrPRNumber": true
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ }
+ }
+ },
+ "add_labels": {
+ "defaultMax": 5,
+ "fields": {
+ "item_number": {
+ "issueOrPRNumber": true
+ },
+ "labels": {
+ "required": true,
+ "type": "array",
+ "itemType": "string",
+ "itemSanitize": true,
+ "itemMaxLength": 128
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ }
+ }
+ },
+ "add_reviewer": {
+ "defaultMax": 3,
+ "fields": {
+ "pull_request_number": {
+ "issueOrPRNumber": true
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "reviewers": {
+ "required": true,
+ "type": "array",
+ "itemType": "string",
+ "itemSanitize": true,
+ "itemMaxLength": 39
+ }
+ }
+ },
+ "missing_data": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "context": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "data_type": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ },
+ "reason": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ }
+ }
+ },
+ "missing_tool": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 512
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "tool": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ }
+ }
+ },
+ "noop": {
+ "defaultMax": 1,
+ "fields": {
+ "message": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ }
+ }
+ },
+ "remove_labels": {
+ "defaultMax": 5,
+ "fields": {
+ "item_number": {
+ "issueOrPRNumber": true
+ },
+ "labels": {
+ "required": true,
+ "type": "array",
+ "itemType": "string",
+ "itemSanitize": true,
+ "itemMaxLength": 128
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ }
+ }
+ },
+ "resolve_pull_request_review_thread": {
+ "defaultMax": 10,
+ "fields": {
+ "thread_id": {
+ "required": true,
+ "type": "string"
+ }
+ }
+ }
+ }
+ GH_AW_SAFE_OUTPUTS_VALIDATION_EOF
+ - name: Generate Safe Outputs MCP Server Config
+ id: safe-outputs-config
+ run: |
+ # Generate a secure random API key (360 bits of entropy, 40+ chars)
+ # Mask immediately to prevent timing vulnerabilities
+ API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${API_KEY}"
+
+ PORT=3001
+
+ # Set outputs for next steps
+ {
+ echo "safe_outputs_api_key=${API_KEY}"
+ echo "safe_outputs_port=${PORT}"
+ } >> "$GITHUB_OUTPUT"
+
+ echo "Safe Outputs MCP server will run on port ${PORT}"
+
+ - name: Start Safe Outputs MCP HTTP Server
+ id: safe-outputs-start
+ env:
+ DEBUG: '*'
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }}
+ GH_AW_SAFE_OUTPUTS_TOOLS_PATH: /opt/gh-aw/safeoutputs/tools.json
+ GH_AW_SAFE_OUTPUTS_CONFIG_PATH: /opt/gh-aw/safeoutputs/config.json
+ GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
+ run: |
+ # Environment variables are set above to prevent template injection
+ export DEBUG
+ export GH_AW_SAFE_OUTPUTS_PORT
+ export GH_AW_SAFE_OUTPUTS_API_KEY
+ export GH_AW_SAFE_OUTPUTS_TOOLS_PATH
+ export GH_AW_SAFE_OUTPUTS_CONFIG_PATH
+ export GH_AW_MCP_LOG_DIR
+
+ bash /opt/gh-aw/actions/start_safe_outputs_server.sh
+
+ - name: Start MCP Gateway
+ id: start-mcp-gateway
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }}
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }}
+ GITHUB_MCP_LOCKDOWN: ${{ steps.determine-automatic-lockdown.outputs.lockdown == 'true' && '1' || '0' }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ run: |
+ set -eo pipefail
+ mkdir -p /tmp/gh-aw/mcp-config
+
+ # Export gateway environment variables for MCP config and gateway script
+ export MCP_GATEWAY_PORT="80"
+ export MCP_GATEWAY_DOMAIN="host.docker.internal"
+ MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${MCP_GATEWAY_API_KEY}"
+ export MCP_GATEWAY_API_KEY
+ export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
+ mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288"
+ export DEBUG="*"
+
+ export GH_AW_ENGINE="copilot"
+ export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host -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_LOCKDOWN -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 -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.1.14'
+
+ mkdir -p /home/runner/.copilot
+ cat << GH_AW_MCP_CONFIG_EOF | bash /opt/gh-aw/actions/start_mcp_gateway.sh
+ {
+ "mcpServers": {
+ "github": {
+ "type": "stdio",
+ "container": "ghcr.io/github/github-mcp-server:v0.32.0",
+ "env": {
+ "GITHUB_LOCKDOWN_MODE": "$GITHUB_MCP_LOCKDOWN",
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
+ "GITHUB_READ_ONLY": "1",
+ "GITHUB_TOOLSETS": "context,repos,issues,pull_requests,actions"
+ }
+ },
+ "safeoutputs": {
+ "type": "http",
+ "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT",
+ "headers": {
+ "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}"
+ }
+ }
+ },
+ "gateway": {
+ "port": $MCP_GATEWAY_PORT,
+ "domain": "${MCP_GATEWAY_DOMAIN}",
+ "apiKey": "${MCP_GATEWAY_API_KEY}",
+ "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
+ }
+ }
+ GH_AW_MCP_CONFIG_EOF
+ - name: Download activation artifact
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: activation
+ path: /tmp/gh-aw
+ - name: Clean git credentials
+ run: bash /opt/gh-aw/actions/clean_git_credentials.sh
+ - name: Execute GitHub Copilot CLI
+ id: agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ # --allow-tool github
+ # --allow-tool safeoutputs
+ # --allow-tool shell(cat)
+ # --allow-tool shell(date)
+ # --allow-tool shell(echo)
+ # --allow-tool shell(gh:api:graphql)
+ # --allow-tool shell(grep)
+ # --allow-tool shell(head)
+ # --allow-tool shell(ls)
+ # --allow-tool shell(pwd)
+ # --allow-tool shell(sort)
+ # --allow-tool shell(tail)
+ # --allow-tool shell(uniq)
+ # --allow-tool shell(wc)
+ # --allow-tool shell(yq)
+ # --allow-tool write
+ timeout-minutes: 20
+ run: |
+ set -o pipefail
+ touch /tmp/gh-aw/agent-step-summary.md
+ # shellcheck disable=SC1003
+ sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-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" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.24.1 --skip-pull --enable-api-proxy \
+ -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(gh:api:graphql)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
+ env:
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: claude-opus-4.6
+ GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
+ GH_AW_PHASE: agent
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_VERSION: v0.58.1
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ XDG_CONFIG_HOME: /home/runner
+ - name: Detect inference access error
+ id: detect-inference-error
+ if: always()
+ continue-on-error: true
+ run: bash /opt/gh-aw/actions/detect_inference_access_error.sh
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${{ github.token }}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: |
+ # Copy Copilot session state files to logs folder for artifact collection
+ # This ensures they are in /tmp/gh-aw/ where secret redaction can scan them
+ SESSION_STATE_DIR="$HOME/.copilot/session-state"
+ LOGS_DIR="/tmp/gh-aw/sandbox/agent/logs"
+
+ if [ -d "$SESSION_STATE_DIR" ]; then
+ echo "Copying Copilot session state files from $SESSION_STATE_DIR to $LOGS_DIR"
+ mkdir -p "$LOGS_DIR"
+ cp -v "$SESSION_STATE_DIR"/*.jsonl "$LOGS_DIR/" 2>/dev/null || true
+ echo "Session state files copied successfully"
+ else
+ echo "No session-state directory found at $SESSION_STATE_DIR"
+ fi
+ - name: Stop MCP Gateway
+ if: always()
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash /opt/gh-aw/actions/stop_mcp_gateway.sh "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/redact_secrets.cjs');
+ await main();
+ env:
+ GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
+ SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Append agent step summary
+ if: always()
+ run: bash /opt/gh-aw/actions/append_agent_step_summary.sh
+ - name: Copy Safe Outputs
+ if: always()
+ run: |
+ mkdir -p /tmp/gh-aw
+ cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true
+ - name: Ingest agent output
+ id: collect_output
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ env.GH_AW_SAFE_OUTPUTS }}
+ 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"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/collect_ndjson_output.cjs');
+ await main();
+ - name: Parse agent logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/parse_copilot_log.cjs');
+ await main();
+ - name: Parse MCP Gateway logs for step summary
+ if: always()
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/parse_mcp_gateway_log.cjs');
+ await main();
+ - name: Print firewall logs
+ if: always()
+ continue-on-error: true
+ env:
+ AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
+ run: |
+ # Fix permissions on firewall logs so they can be uploaded as artifacts
+ # AWF runs with sudo, creating files owned by root
+ sudo chmod -R a+r /tmp/gh-aw/sandbox/firewall/logs 2>/dev/null || true
+ # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step)
+ if command -v awf &> /dev/null; then
+ awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
+ else
+ echo 'AWF binary not installed, skipping firewall log summary'
+ fi
+ - name: Upload agent artifacts
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: agent
+ path: |
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/sandbox/agent/logs/
+ /tmp/gh-aw/redacted-urls.log
+ /tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/sandbox/firewall/logs/
+ /tmp/gh-aw/agent-stdio.log
+ /tmp/gh-aw/agent/
+ /tmp/gh-aw/safeoutputs.jsonl
+ /tmp/gh-aw/agent_output.json
+ if-no-files-found: ignore
+ # --- Threat Detection (inline) ---
+ - name: Check if detection needed
+ id: detection_guard
+ if: always()
+ env:
+ OUTPUT_TYPES: ${{ steps.collect_output.outputs.output_types }}
+ HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }}
+ run: |
+ if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then
+ echo "run_detection=true" >> "$GITHUB_OUTPUT"
+ echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH"
+ else
+ echo "run_detection=false" >> "$GITHUB_OUTPUT"
+ echo "Detection skipped: no agent outputs or patches to analyze"
+ fi
+ - name: Clear MCP configuration for detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ rm -f /tmp/gh-aw/mcp-config/mcp-servers.json
+ rm -f /home/runner/.copilot/mcp-config.json
+ rm -f "$GITHUB_WORKSPACE/.gemini/settings.json"
+ - name: Prepare threat detection files
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection/aw-prompts
+ cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true
+ cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true
+ for f in /tmp/gh-aw/aw-*.patch; do
+ [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ done
+ echo "Prepared threat detection files:"
+ ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ - name: Setup threat detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ WORKFLOW_NAME: "Pipeline Orchestrator"
+ WORKFLOW_DESCRIPTION: "No description provided"
+ HAS_PATCH: ${{ steps.collect_output.outputs.has_patch }}
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/setup_threat_detection.cjs');
+ await main();
+ - name: Ensure threat-detection directory and log
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection
+ touch /tmp/gh-aw/threat-detection/detection.log
+ - name: Execute GitHub Copilot CLI
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ id: detection_agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ # --allow-tool shell(cat)
+ # --allow-tool shell(grep)
+ # --allow-tool shell(head)
+ # --allow-tool shell(jq)
+ # --allow-tool shell(ls)
+ # --allow-tool shell(tail)
+ # --allow-tool shell(wc)
+ timeout-minutes: 20
+ run: |
+ set -o pipefail
+ touch /tmp/gh-aw/agent-step-summary.md
+ # shellcheck disable=SC1003
+ sudo -E awf --env-all --container-workdir "${GITHUB_WORKSPACE}" --allow-domains "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,github.com,host.docker.internal,raw.githubusercontent.com,registry.npmjs.org,telemetry.enterprise.githubcopilot.com" --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --enable-host-access --image-tag 0.24.1 --skip-pull --enable-api-proxy \
+ -- /bin/bash -c '/usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --add-dir "${GITHUB_WORKSPACE}" --disable-builtin-mcps --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(jq)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(wc)'\'' --prompt "$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
+ env:
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: claude-opus-4.6
+ GH_AW_PHASE: detection
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_VERSION: v0.58.1
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ XDG_CONFIG_HOME: /home/runner
+ - name: Parse threat detection results
+ id: parse_detection_results
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ with:
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/parse_threat_detection_results.cjs');
+ await main();
+ - name: Upload threat detection log
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: detection
+ path: /tmp/gh-aw/threat-detection/detection.log
+ if-no-files-found: ignore
+ - name: Set detection conclusion
+ id: detection_conclusion
+ if: always()
+ env:
+ RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }}
+ DETECTION_SUCCESS: ${{ steps.parse_detection_results.outputs.success }}
+ run: |
+ if [[ "$RUN_DETECTION" != "true" ]]; then
+ echo "conclusion=skipped" >> "$GITHUB_OUTPUT"
+ echo "success=true" >> "$GITHUB_OUTPUT"
+ echo "Detection was not needed, marking as skipped"
+ elif [[ "$DETECTION_SUCCESS" == "true" ]]; then
+ echo "conclusion=success" >> "$GITHUB_OUTPUT"
+ echo "success=true" >> "$GITHUB_OUTPUT"
+ echo "Detection passed successfully"
+ else
+ echo "conclusion=failure" >> "$GITHUB_OUTPUT"
+ echo "success=false" >> "$GITHUB_OUTPUT"
+ echo "Detection found issues"
+ fi
+
+ conclusion:
+ needs:
+ - activation
+ - agent
+ - safe_outputs
+ if: (always()) && (needs.agent.result != 'skipped')
+ runs-on: ubuntu-slim
+ permissions:
+ actions: write
+ contents: read
+ discussions: write
+ issues: write
+ pull-requests: write
+ concurrency:
+ group: "gh-aw-conclusion-pipeline-orchestrator"
+ cancel-in-progress: false
+ outputs:
+ noop_message: ${{ steps.noop.outputs.noop_message }}
+ tools_reported: ${{ steps.missing_tool.outputs.tools_reported }}
+ total_count: ${{ steps.missing_tool.outputs.total_count }}
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw/actions/setup@fa061e89469ef007881d22d3af5a8c9e62363a0d # v0.58.1
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV"
+ - name: Process No-Op Messages
+ id: noop
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_NOOP_MAX: "1"
+ GH_AW_WORKFLOW_NAME: "Pipeline Orchestrator"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/noop.cjs');
+ await main();
+ - name: Record Missing Tool
+ id: missing_tool
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Pipeline Orchestrator"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/missing_tool.cjs');
+ await main();
+ - name: Handle Agent Failure
+ id: handle_agent_failure
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Pipeline Orchestrator"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
+ GH_AW_WORKFLOW_ID: "pipeline-orchestrator"
+ GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }}
+ GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
+ GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }}
+ GH_AW_GROUP_REPORTS: "false"
+ GH_AW_FAILURE_REPORT_AS_ISSUE: "true"
+ GH_AW_TIMEOUT_MINUTES: "20"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/handle_agent_failure.cjs');
+ await main();
+ - name: Handle No-Op Message
+ id: handle_noop_message
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Pipeline Orchestrator"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
+ GH_AW_NOOP_MESSAGE: ${{ steps.noop.outputs.noop_message }}
+ GH_AW_NOOP_REPORT_AS_ISSUE: "false"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/handle_noop_message.cjs');
+ await main();
+
+ pre_activation:
+ runs-on: ubuntu-slim
+ outputs:
+ activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }}
+ matched_command: ''
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw/actions/setup@fa061e89469ef007881d22d3af5a8c9e62363a0d # v0.58.1
+ with:
+ destination: /opt/gh-aw/actions
+ - name: Check team membership for workflow
+ id: check_membership
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_REQUIRED_ROLES: admin,maintainer,write
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/check_membership.cjs');
+ await main();
+
+ safe_outputs:
+ needs: agent
+ if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
+ runs-on: ubuntu-slim
+ permissions:
+ actions: write
+ contents: read
+ discussions: write
+ issues: write
+ pull-requests: write
+ timeout-minutes: 15
+ env:
+ GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/pipeline-orchestrator"
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_ENGINE_MODEL: "claude-opus-4.6"
+ GH_AW_WORKFLOW_ID: "pipeline-orchestrator"
+ GH_AW_WORKFLOW_NAME: "Pipeline Orchestrator"
+ outputs:
+ add_reviewer_reviewers_added: ${{ steps.process_safe_outputs.outputs.reviewers_added }}
+ code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }}
+ code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }}
+ comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }}
+ comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }}
+ create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }}
+ create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }}
+ process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }}
+ process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
+ steps:
+ - name: Setup Scripts
+ uses: github/gh-aw/actions/setup@fa061e89469ef007881d22d3af5a8c9e62363a0d # v0.58.1
+ with:
+ destination: /opt/gh-aw/actions
+ safe-output-custom-tokens: 'true'
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_ENV"
+ - name: Process Safe Outputs
+ id: process_safe_outputs
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ env.GH_AW_AGENT_OUTPUT }}
+ 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"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":5},\"add_labels\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":10},\"add_reviewer\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":3},\"dispatch_workflow\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":1,\"workflow_files\":{\"ci-fixer\":\".lock.yml\",\"issue-implementer\":\".lock.yml\"},\"workflows\":[\"issue-implementer\",\"ci-fixer\"]},\"missing_data\":{},\"missing_tool\":{},\"remove_labels\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":10},\"resolve_pull_request_review_thread\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":10}}"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('/opt/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('/opt/gh-aw/actions/safe_output_handler_manager.cjs');
+ await main();
+ - name: Upload Safe Output Items Manifest
+ if: always()
+ uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
+ with:
+ name: safe-output-items
+ path: /tmp/safe-output-items.jsonl
+ if-no-files-found: warn
+
diff --git a/.github/workflows/pipeline-orchestrator.md b/.github/workflows/pipeline-orchestrator.md
new file mode 100644
index 0000000..81d18fc
--- /dev/null
+++ b/.github/workflows/pipeline-orchestrator.md
@@ -0,0 +1,166 @@
+---
+on:
+ schedule:
+ - cron: "*/15 * * * *"
+ push:
+ branches: [main]
+ workflow_dispatch:
+
+concurrency:
+ group: pipeline-orchestrator
+ cancel-in-progress: false
+
+permissions:
+ contents: read
+ issues: read
+ pull-requests: read
+ actions: read
+
+engine:
+ id: copilot
+ model: claude-opus-4.6
+
+tools:
+ github:
+ toolsets: [default, actions]
+ bash:
+ - "gh:api:graphql"
+
+network:
+ allowed:
+ - defaults
+
+safe-outputs:
+ noop:
+ report-as-issue: false
+ dispatch-workflow:
+ github-token: ${{ secrets.GH_AW_WRITE_TOKEN }}
+ workflows: [issue-implementer, ci-fixer]
+ max: 1
+ add-reviewer:
+ github-token: ${{ secrets.GH_AW_WRITE_TOKEN }}
+ max: 3
+ resolve-pull-request-review-thread:
+ github-token: ${{ secrets.GH_AW_WRITE_TOKEN }}
+ max: 10
+ add-labels:
+ github-token: ${{ secrets.GH_AW_WRITE_TOKEN }}
+ max: 10
+ remove-labels:
+ github-token: ${{ secrets.GH_AW_WRITE_TOKEN }}
+ max: 10
+ add-comment:
+ github-token: ${{ secrets.GH_AW_WRITE_TOKEN }}
+ max: 5
+
+---
+
+# Pipeline Orchestrator
+
+Own the full lifecycle of agent work: from issue to merged PR. Detect what needs attention and push it forward one step at a time.
+
+## Context
+
+This repository has an automated pipeline:
+1. code-health or test-analysis creates issues (labeled `code-health` or `test-audit`)
+2. issue-implementer creates a PR from the issue (labeled `aw`, auto-merge enabled)
+3. Copilot auto-reviews the PR
+4. review-responder addresses review comments and resolves threads
+5. quality-gate approves if code quality is good and impact is low/medium
+6. auto-merge fires when CI passes + approved + threads resolved
+
+This orchestrator owns steps 2-6. It detects stalls and fixes them.
+
+## Instructions
+
+### Step 1: Find issues that need implementation
+
+First, check if there are any open PRs with the `aw` label. If there are, skip issue dispatch entirely — only one agent PR should be in flight at a time to avoid merge conflicts.
+
+Also check if any `issue-implementer` workflow runs are currently in progress (queued or running). If so, skip issue dispatch — an implementer is already working on something and will create a PR soon.
+
+If there are NO open `aw`-labeled PRs AND no in-progress implementer runs, list open issues with the `code-health` or `test-audit` label. For each issue, check if there is already an open or recently merged PR that references it (look for PRs whose body contains "Closes #N" or "#N" where N is the issue number).
+
+Dispatch the `issue-implementer` workflow for the **first** eligible issue only (one at a time). Add a comment on the issue: "Pipeline Orchestrator: dispatching issue-implementer."
+
+If no issues need implementation, move on to Step 2.
+
+### Step 2: Find stuck PRs
+
+List all open PRs with the `aw` label that have auto-merge enabled. Exclude any PR labeled `aw-conflict` (merge conflicts, needs manual intervention).
+
+If there are no issues to dispatch and no stuck PRs, stop with a noop.
+
+Sort PRs by progress: approved PRs first (closest to merging), then unapproved.
+
+### Step 3: Process each PR
+
+For each PR (in sorted order), gather its state:
+- `mergeStateStatus` (BEHIND, CLEAN, BLOCKED, etc.)
+- `reviewDecision` (APPROVED, REVIEW_REQUIRED, etc.)
+- Whether Copilot has submitted a review (look for reviews by author `copilot-pull-request-reviewer`)
+- Whether the latest CI check run (`check` job) has failed
+- Whether the PR has the `ci-fix-attempted` label
+
+Then apply the **first matching** action and move to the next PR:
+
+#### Action 1: Request Copilot review
+
+If Copilot has not reviewed this PR yet, request a review from `@copilot` using the add-reviewer safe-output. Stop processing this PR — the pipeline will continue from here next cycle.
+
+#### Action 2: Resolve unresolved threads
+
+Query the PR's review threads using bash:
+```
+gh api graphql -f query='query($owner: String!, $name: String!, $pr: Int!) {
+ repository(owner: $owner, name: $name) {
+ pullRequest(number: $pr) {
+ reviewThreads(first: 100) {
+ nodes {
+ id
+ isResolved
+ comments(last: 1) {
+ nodes { author { login } }
+ }
+ }
+ }
+ }
+ }
+}' -f owner="$GITHUB_REPOSITORY_OWNER" -f name="${GITHUB_REPOSITORY#*/}" -F pr=PR_NUMBER
+```
+Replace `PR_NUMBER` with the actual PR number.
+
+For each unresolved thread, check the last comment's author. The review-responder posts replies using a PAT owned by the repository owner, so its comments appear as the value of `$GITHUB_REPOSITORY_OWNER`. Check this environment variable to determine the responder's identity.
+
+If the last comment was posted by the responder (PAT owner) or by `github-actions[bot]`, resolve the thread using the resolve-pull-request-review-thread safe-output.
+
+If any unresolved threads remain where the last commenter is someone else (human or Copilot reviewer), stop processing this PR — it needs attention.
+
+#### Action 3: CI failure
+
+If the latest CI `check` run has failed and the PR does NOT have the `ci-fix-attempted` label, dispatch the `ci-fixer` workflow with the PR number as input. The ci-fixer will read the logs, fix the issues, and push. Stop processing this PR — next cycle will check again.
+
+If CI has failed but the PR already has `ci-fix-attempted`, skip — the fixer already tried once and manual intervention is needed.
+
+#### Action 4: Behind main
+
+If the PR is approved, all threads resolved, but `mergeStateStatus` is `BEHIND`:
+- Log that the PR is approved and ready but needs a rebase to proceed.
+- Do NOT attempt to rebase and do NOT comment on the PR — this requires manual intervention.
+- Move to the next PR.
+
+#### Action 5: All clear
+
+If the PR is approved, threads resolved, and not behind main — auto-merge should handle it. Log this and move on.
+
+### Step 4: Summary
+
+After processing all PRs, output a brief summary of actions taken.
+
+## Important rules
+
+- Process PRs one at a time, in sorted order
+- Apply only the FIRST matching action per PR, then move to the next
+- If any API call fails for a PR, skip it and continue — one failure must not stop the entire run
+- NEVER fabricate thread IDs — always use real IDs from the GraphQL response
+- The `aw-conflict` label means merge conflicts exist — skip these PRs entirely
diff --git a/.github/workflows/pr-rescue.yml b/.github/workflows/pr-rescue.yml
deleted file mode 100644
index 537ecce..0000000
--- a/.github/workflows/pr-rescue.yml
+++ /dev/null
@@ -1,106 +0,0 @@
-name: PR Rescue
-
-# Runs when main advances (PR merged). Rebases open agent PRs that are
-# behind main so auto-merge can proceed. With dismiss_stale_reviews
-# disabled, the existing quality-gate approval survives the rebase.
-
-on:
- push:
- branches: [main]
- workflow_dispatch:
-
-permissions:
- contents: write
- pull-requests: read
- actions: read
-
-concurrency:
- group: pr-rescue
- cancel-in-progress: false
-
-jobs:
- rescue:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout main
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- token: ${{ secrets.GH_AW_WRITE_TOKEN }}
-
- - name: Configure git identity
- run: |
- git config user.name "github-actions[bot]"
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
-
- - name: Find and rebase stuck agent PRs
- env:
- GH_TOKEN: ${{ secrets.GH_AW_WRITE_TOKEN }}
- run: |
- set -euo pipefail
-
- echo "::group::Finding open agent PRs with auto-merge enabled"
- prs=$(gh pr list --state open --label aw --json number,headRefName,autoMergeRequest \
- --jq '.[] | select(.autoMergeRequest != null) | "\(.number) \(.headRefName)"')
-
- if [ -z "$prs" ]; then
- echo "No agent PRs with auto-merge enabled. Nothing to rescue."
- exit 0
- fi
- echo "Found PRs to check:"
- echo "$prs"
- echo "::endgroup::"
-
- rescued=0
- while IFS=' ' read -r pr_number branch; do
- echo "::group::Processing PR #${pr_number} (branch: ${branch})"
-
- # Check if PR is specifically behind main (not blocked for other reasons)
- merge_state=$(gh pr view "$pr_number" --json mergeStateStatus --jq '.mergeStateStatus')
- echo "Merge state: ${merge_state}"
-
- if [ "$merge_state" != "BEHIND" ]; then
- echo "PR #${pr_number} is not behind main (state: ${merge_state}). Skipping."
- echo "::endgroup::"
- continue
- fi
-
- # Check if PR has an approving review (no point rebasing if not approved yet)
- review_decision=$(gh pr view "$pr_number" --json reviewDecision --jq '.reviewDecision')
- if [ "$review_decision" != "APPROVED" ]; then
- echo "PR #${pr_number} has no approval yet (decision: ${review_decision}). Skipping."
- echo "::endgroup::"
- continue
- fi
-
- echo "PR #${pr_number} is approved but behind main. Rebasing..."
-
- if ! git fetch origin "$branch" 2>&1; then
- echo "::warning::Failed to fetch branch for PR #${pr_number}. Branch may have been deleted. Skipping."
- echo "::endgroup::"
- continue
- fi
-
- if ! git checkout "$branch" 2>&1; then
- echo "::warning::Failed to checkout branch for PR #${pr_number}. Skipping."
- echo "::endgroup::"
- continue
- fi
-
- if git rebase origin/main; then
- if git push --force-with-lease origin "$branch" 2>&1; then
- echo "✅ PR #${pr_number} rebased. CI will rerun, approval survives, auto-merge will fire."
- rescued=$((rescued + 1))
- else
- echo "::warning::Push failed for PR #${pr_number}. Branch may have been updated. Skipping."
- fi
- else
- echo "::warning::Rebase failed for PR #${pr_number} — likely has conflicts. Skipping."
- git rebase --abort 2>/dev/null || true
- fi
-
- git checkout main
- echo "::endgroup::"
- done <<< "$prs"
-
- echo "Rescued ${rescued} PR(s)."
diff --git a/.github/workflows/review-responder.md b/.github/workflows/review-responder.md
index 0cb7647..e9c50a3 100644
--- a/.github/workflows/review-responder.md
+++ b/.github/workflows/review-responder.md
@@ -58,20 +58,26 @@ This workflow runs when a review is submitted on a pull request.
5. Add the label `review-response-attempted` to the PR.
-6. Read the unresolved review comment threads on the PR (not just the latest review — get all unresolved threads). If there are more than 10 unresolved threads, address the first 10 and leave a summary comment on the PR noting how many remain for manual follow-up.
+6. ***CRITICAL***: Look up real thread IDs using bash before doing anything else with threads. Run `gh api graphql` to query this PR's review threads. Use the repository owner and name from the environment variables `$GITHUB_REPOSITORY_OWNER` and `${GITHUB_REPOSITORY#*/}`, and substitute the actual PR number (from the workflow trigger context) into the query. Example:
+ ```
+ gh api graphql -f query='query($owner: String!, $name: String!, $pr: Int!) { repository(owner: $owner, name: $name) { pullRequest(number: $pr) { reviewThreads(first: 100) { nodes { id isResolved comments(first: 1) { nodes { id body path line } } } } } } }' -f owner="$GITHUB_REPOSITORY_OWNER" -f name="${GITHUB_REPOSITORY#*/}" -F pr=PR_NUMBER
+ ```
+ Replace `PR_NUMBER` with the actual pull request number from the trigger event. Parse the JSON response and extract the `id` field (starts with `PRRT_`) for each unresolved thread. You MUST use these real IDs when resolving threads — NEVER fabricate or guess thread IDs.
-7. For each unresolved review comment thread (up to 10):
+7. Read the unresolved review comment threads on the PR (not just the latest review — get all unresolved threads). If there are more than 10 unresolved threads, address the first 10 and leave a summary comment on the PR noting how many remain for manual follow-up.
+
+8. For each unresolved review comment thread (up to 10):
a. Read the comment and understand what change is being requested
b. Read the relevant file and surrounding code context
c. Make the requested fix in the code (edit the file locally — do NOT push yet)
d. Reply to the comment thread explaining what you changed
- e. Resolve the thread
+ e. Resolve the thread using the real thread ID from step 6
-8. ***MUST***: Reply to and resolve ALL threads BEFORE pushing any code. Pushing code invalidates thread IDs and makes them unresolvable. Do NOT emit a push_to_pull_request_branch safe-output until all reply and resolve safe-outputs have been emitted.
+9. ***MUST***: Reply to and resolve ALL threads BEFORE pushing any code. Pushing code invalidates thread IDs and makes them unresolvable. Do NOT emit a push_to_pull_request_branch safe-output until all reply and resolve safe-outputs have been emitted.
-9. After addressing all comments, run the CI checks locally to make sure your fixes don't break anything: `uv sync && uv run ruff check --fix . && uv run ruff format . && uv run pyright && uv run pytest --cov --cov-fail-under=80 -v`
+10. After addressing all comments, run the CI checks locally to make sure your fixes don't break anything: `uv sync && uv run ruff check --fix . && uv run ruff format . && uv run pyright && uv run pytest --cov --cov-fail-under=80 -v`
-10. ***DOUBLE CHECK***: Before you finish, verify that your safe-output calls are in the correct order: all reply_to_pull_request_review_comment and resolve_pull_request_review_thread calls MUST come BEFORE any push_to_pull_request_branch call. If you emitted them in the wrong order, you cannot fix it — the threads will fail to resolve and the PR will be stuck.
+11. ***DOUBLE CHECK***: Before you finish, verify that your safe-output calls are in the correct order: all reply_to_pull_request_review_comment and resolve_pull_request_review_thread calls MUST come BEFORE any push_to_pull_request_branch call. If you emitted them in the wrong order, you cannot fix it — the threads will fail to resolve and the PR will be stuck.
If a review comment requests a change that would be architecturally significant or you're unsure about, reply to the thread explaining your concern rather than making the change blindly. You MUST still resolve the thread after replying — an unresolved thread blocks the PR from merging regardless of whether you made the change.
diff --git a/.github/workflows/test-analysis.lock.yml b/.github/workflows/test-analysis.lock.yml
index b7c3cd4..aff4bc6 100644
--- a/.github/workflows/test-analysis.lock.yml
+++ b/.github/workflows/test-analysis.lock.yml
@@ -22,7 +22,7 @@
# For more information: https://github.github.com/gh-aw/introduction/overview/
#
#
-# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"f33ce13a0707fb37b0f206d4b1726f3f1f96840d928e4ffbff3edbda68040c8b","compiler_version":"v0.58.1","strict":true}
+# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"b9b1d4c9873e57676d3d4c1863c16aecf1033ce5568c4146fdf839ebc2774f19","compiler_version":"v0.58.1","strict":true}
name: "Test Suite Analysis"
"on":
@@ -125,7 +125,7 @@ jobs:
cat "/opt/gh-aw/prompts/safe_outputs_prompt.md"
cat << 'GH_AW_PROMPT_EOF'
- Tools: create_issue, dispatch_workflow, missing_tool, missing_data, noop
+ Tools: create_issue, missing_tool, missing_data, noop
The following GitHub context information is available for this workflow:
@@ -311,7 +311,7 @@ jobs:
mkdir -p /tmp/gh-aw/safeoutputs
mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
cat > /opt/gh-aw/safeoutputs/config.json << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
- {"create_issue":{"max":2},"dispatch_workflow":{"max":2,"workflow_files":{"issue-implementer":".lock.yml"},"workflows":["issue-implementer"]},"missing_data":{},"missing_tool":{},"noop":{"max":1}}
+ {"create_issue":{"max":2},"missing_data":{},"missing_tool":{},"noop":{"max":1}}
GH_AW_SAFE_OUTPUTS_CONFIG_EOF
- name: Write Safe Outputs Tools
run: |
@@ -458,24 +458,6 @@ jobs:
"type": "object"
},
"name": "missing_data"
- },
- {
- "_workflow_name": "issue-implementer",
- "description": "Dispatch the 'issue-implementer' workflow with workflow_dispatch trigger. This workflow must support workflow_dispatch and be in .github/workflows/ directory in the same repository.",
- "inputSchema": {
- "additionalProperties": false,
- "properties": {
- "issue_number": {
- "description": "Issue number to fix",
- "type": "string"
- }
- },
- "required": [
- "issue_number"
- ],
- "type": "object"
- },
- "name": "issue_implementer"
}
]
GH_AW_SAFE_OUTPUTS_TOOLS_EOF
@@ -972,7 +954,6 @@ jobs:
if: (always()) && (needs.agent.result != 'skipped')
runs-on: ubuntu-slim
permissions:
- actions: write
contents: read
issues: write
concurrency:
@@ -1072,7 +1053,6 @@ jobs:
if: ((!cancelled()) && (needs.agent.result != 'skipped')) && (needs.agent.outputs.detection_success == 'true')
runs-on: ubuntu-slim
permissions:
- actions: write
contents: read
issues: write
timeout-minutes: 15
@@ -1117,7 +1097,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"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
- GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":2},\"dispatch_workflow\":{\"max\":2,\"workflow_files\":{\"issue-implementer\":\".lock.yml\"},\"workflows\":[\"issue-implementer\"]},\"missing_data\":{},\"missing_tool\":{}}"
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"github-token\":\"${{ secrets.GH_AW_WRITE_TOKEN }}\",\"max\":2},\"missing_data\":{},\"missing_tool\":{}}"
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
script: |
diff --git a/.github/workflows/test-analysis.md b/.github/workflows/test-analysis.md
index ddd1293..717e246 100644
--- a/.github/workflows/test-analysis.md
+++ b/.github/workflows/test-analysis.md
@@ -23,9 +23,6 @@ safe-outputs:
create-issue:
max: 2
github-token: ${{ secrets.GH_AW_WRITE_TOKEN }}
- dispatch-workflow:
- workflows: [issue-implementer]
- max: 2
---
@@ -37,6 +34,6 @@ Analyze the test suite for coverage gaps and suggest new tests.
Read all files in the repository. Read all open issues in the repository. Identify meaningful test gaps across unit tests, e2e tests, and integration tests — untested code paths, missing scenarios, weak assertions, or anything else that would improve confidence in the code.
-For each area with gaps, open an issue with root cause analysis, repro steps where applicable, and a clear spec for what tests to add. Each issue must specify the expected behavior to assert and any regression scenarios to cover. Prefix each issue title with `[aw][test audit]` and label each issue with `aw` and `test-audit`. After creating each issue, dispatch the issue-implementer workflow with the issue number as input.
+For each area with gaps, open an issue with root cause analysis, repro steps where applicable, and a clear spec for what tests to add. Each issue must specify the expected behavior to assert and any regression scenarios to cover. Prefix each issue title with `[aw][test audit]` and label each issue with `aw` and `test-audit`. The pipeline orchestrator will pick up the issue and dispatch the implementer — do NOT dispatch it yourself.
Do not suggest trivial tests or tests that duplicate existing coverage. Do not open issues for things that already have an open issue. Do not open an issue that is just a nit — if there are many small gaps that together form a meaningful improvement, bundle them into one issue. If the test suite is already comprehensive, do not create any issues.
diff --git a/docs/agentic-workflows.md b/docs/agentic-workflows.md
index 2395045..d5247fb 100644
--- a/docs/agentic-workflows.md
+++ b/docs/agentic-workflows.md
@@ -27,16 +27,20 @@ gh aw fix --write # Auto-fix deprecated fields
Our autonomous pipeline:
```
-Audit/Health Agent → creates issue (max 2) → dispatches Implementer
+Audit/Health Agent → creates issue (labeled code-health or test-audit)
+ → Pipeline Orchestrator (15-min cron) picks up the issue:
+ → No aw-labeled PR in flight? → dispatches Implementer (one at a time)
→ Implementer creates PR (lint-clean, non-draft, auto-merge, aw label)
→ CI runs + Copilot auto-reviews (parallel, via ruleset)
- → CI fails? → CI Fixer agent (1 retry, label guard)
- → Copilot has comments? → Review Responder addresses them (pushes fixes)
+ → Copilot has comments? → Review Responder addresses them (pushes fixes, resolves threads via GraphQL)
→ Copilot reviews (COMMENTED state) → Quality Gate evaluates quality + blast radius
→ LOW/MEDIUM impact → approves + adds quality-gate-approved label → auto-merge fires
→ HIGH impact → flags for human review (auto-merge stays blocked)
- → PR behind main after another PR merges?
- → PR Rescue workflow (push to main trigger) → rebases → CI reruns → re-approves → auto-merge fires
+ → PR stalled? → Pipeline Orchestrator detects and fixes:
+ → No Copilot review → requests one
+ → Unresolved threads with responder replies → resolves them
+ → CI failed → dispatches CI Fixer (1 retry, label guard)
+ → Behind main → logs, skips (requires manual rebase)
```
@@ -390,18 +394,35 @@ done
This is a known limitation for solo repos. Agent PRs don't need this — the quality gate approves them.
-### PR Rescue Workflow
+### Pipeline Orchestrator
-When a PR merges to main, other open agent PRs fall behind. With `strict: true` (require branch to be up to date), auto-merge blocks until the branch is rebased.
+The **Pipeline Orchestrator** (`.github/workflows/pipeline-orchestrator.md`) owns the full lifecycle of agent work — from issue to merged PR. It runs every 15 minutes and on every push to main.
-The **PR Rescue workflow** (`.github/workflows/pr-rescue.yml`) fires on every push to main:
-1. Finds open `aw`-labeled PRs with auto-merge enabled that are behind main and already approved
-2. Rebases them onto latest main
-3. CI reruns on the rebased commit
-4. Since `dismiss_stale_reviews` is disabled, the existing approval survives
-5. Auto-merge fires
+**Issue dispatch** (one at a time):
+- If no `aw`-labeled PR is currently in flight, it finds open issues labeled `code-health` or `test-audit` that don't have a PR yet
+- Dispatches `issue-implementer` for the first eligible issue
+- Only one at a time — avoids concurrent PRs fighting over main
-Note: `dismiss_stale_reviews` is set to `false` to support this flow. This is safe for a solo-developer repo. If collaborators are added, re-evaluate this setting.
+**PR orchestration** (unstick what's in flight):
+For each open `aw`-labeled PR with auto-merge enabled (excluding `aw-conflict`), sorted by progress (approved first), applies the first matching action:
+
+1. **No Copilot review** → requests review from `@copilot`
+2. **Unresolved threads** → queries real thread IDs via GraphQL, resolves threads where the responder (PAT owner) posted the last comment
+3. **CI failure** → dispatches ci-fixer (if `ci-fix-attempted` label not present)
+4. **Behind main** → logs and skips (requires manual rebase)
+5. **All clear** → auto-merge should handle it
+
+The orchestrator is a pure reasoning agent — no git access, no `contents: write`. It uses safe-outputs (`dispatch-workflow`, `add-reviewer`, `resolve-pull-request-review-thread`, `add-comment`, `add-labels`) and bash for GraphQL queries.
+
+Replaces the old `pr-rescue.yml` bash script which only handled rebasing and required repeated bug-fix cycles.
+
+### Review Responder Thread ID Lookup
+
+The Review Responder queries real `PRRT_` thread IDs via `gh api graphql` before resolving. Without this, the agent hallucinates thread IDs because the MCP server doesn't expose them (#114). The responder runs `gh api graphql` to fetch thread IDs upfront, then uses those real IDs in resolve calls.
+
+No `bash:` tool config is needed — the responder already has `--allow-all-tools` from the compiler default (adding explicit `bash:` would restrict the allowlist and break CI commands like `uv`, `ruff`, `pyright`, `pytest`).
+
+This is a workaround until gh-aw upgrades their pinned MCP server (`github/gh-aw#21130`).
@@ -425,8 +446,8 @@ All changes go in the `.md` file. Run `gh aw compile` to regenerate. Copilot may
### 5. `dismiss_stale_reviews` only dismisses APPROVED reviews
`COMMENTED` reviews are NOT dismissed on new pushes. This means a Copilot `COMMENTED` review from before a rebase will persist.
-### 6. `pull_request_review` workflows run from default branch
-The workflow definition always comes from the default branch, not the PR branch. You cannot test workflow changes from a PR — they must be merged first.
+### 6. `pull_request_review` workflows run from the PR's head branch
+The workflow definition comes from the **PR's head branch**, not the default branch. This was verified empirically on PR #119 — the `if:` condition added on that branch was active immediately without merging to main first. This contradicts common web search results and many documentation sources. **Never trust web search over empirical evidence.**
### 7. GitHub's `action_required` is separate from gh-aw's `pre_activation`
`action_required` means GitHub itself blocked the run (first-time contributor approval). No jobs run at all. `pre_activation` is gh-aw's role/bot check within the workflow.
@@ -446,6 +467,21 @@ The `labels` field compiles into the lock file and the handler reads it, but the
### 12. Review thread IDs are invalidated by pushes
Pushing code to a PR branch can invalidate GraphQL thread IDs. If the responder pushes before resolving threads, the resolve calls fail with stale node IDs. Always resolve threads BEFORE pushing.
+### 13. MCP server doesn't expose thread IDs to agents (#114)
+The GitHub MCP server (pinned by gh-aw) does not return `PRRT_` thread node IDs in its tool responses. Agents hallucinate plausible-looking IDs that fail at the GraphQL API. The `resolve_pull_request_review_thread` safe-output works fine — the problem is the agent doesn't know which ID to pass. Workaround: instruct the agent to query real thread IDs via `gh api graphql` (the agent already has `--allow-all-tools` when no explicit `bash:` config is set). Do NOT add `bash:` to the tools config — that causes the compiler to switch from `--allow-all-tools` to a restricted allowlist, breaking CI commands. Tracked upstream in `github/gh-aw#21130`.
+
+### 14. `push-to-pull-request-branch` safe-output can't force-push
+The safe-output generates patches via `git format-patch` and applies them. It cannot do `git push --force-with-lease` after a rebase. If your workflow needs to rebase and force-push, it must either use a regular workflow (`.yml`) with `contents: write`, or use `strict: false` (not recommended). This is why the pipeline orchestrator delegates rebasing to humans instead of trying to do it.
+
+### 15. Don't write complex bash in GitHub Actions — use gh-aw agents
+Shell scripts under `set -euo pipefail` are fragile. Every API call needs `|| { warn; continue }` guards, every git command needs error handling, variable interpolation in GraphQL queries creates injection risks, and the bash gets longer with every bug fix. If the logic involves decisions and error recovery, an agent handles it better. The old `pr-rescue.yml` went through 4 rounds of Copilot review, a Gemini review, and an OpenAI Codex review — each finding new bugs. The orchestrator replacement is ~80 lines of natural language.
+
+### 16. `gh api user` resolves the PAT owner identity at runtime
+When agents post comments or replies using `GH_AW_WRITE_TOKEN` (a PAT), the comments appear as the PAT owner — not `github-actions[bot]`. Don't hardcode usernames. In a solo-developer repo, the PAT owner is the repository owner — use `$GITHUB_REPOSITORY_OWNER` to get the identity. Note: `gh api user` may not work in the agent sandbox because `GH_TOKEN` is not set for the agent's bash environment (it uses an installation token that returns 403 on `/user`).
+
+### 17. The `if:` frontmatter field gates at the infrastructure level
+Adding `if: "contains(github.event.pull_request.labels.*.name, 'aw')"` to a workflow's frontmatter compiles to a job-level `if:` on the activation job. When the condition is false, the workflow skips entirely at the GitHub Actions level — zero tokens burned, no agent activation. This is fundamentally different from checking labels in the agent prompt (which still activates the agent, burns compute, then noops).
+
---
@@ -581,4 +617,26 @@ gh run view --log-failed # View failed job logs
- PR #109: Reverts labels config, rewrites responder instructions with `***MUST***`/`***DOUBLE CHECK***` ordering enforcement.
- **Lesson reinforced**: NEVER add config without verifying the runtime behavior. Read the source code. The compiler accepting a field does not mean the handler implements it.
+### 2026-03-16 — Label gate fix + pipeline orchestrator
+
+- PR #119: Added `if:` frontmatter condition to review-responder and quality-gate — workflows now skip entirely when `aw` label is absent. Previously burned compute + tokens on every PR. (Issue #120)
+- **Discovery**: `pull_request_review` events use workflow files from the PR's **head branch**, not the default branch. The `if:` condition was active immediately on PR #119 itself — no agent workflows fired. Contradicts common web search results — verified empirically by checking workflow runs. **Rule: never trust web search over empirical evidence.**
+- Filed issue #120 for the label gate bug. Merged PR #119 using safe admin merge procedure.
+
+#### The pr-rescue saga
+
+The enhanced PR rescue (#116) went through three complete rewrites:
+
+1. **Bash script attempt (PR #118, #121)**: 230 lines of bash under `set -euo pipefail`. Copilot review found 6 bugs (unguarded API calls, `git checkout` on fresh runner, pagination cap). Gemini review found 3 more (shell injection via branch names, `first:0` invalid in GraphQL, bot error replies). OpenAI Codex found a logic bug (thread resolution checked for `github-actions[bot]` but responder posts as PAT owner). Then I hardcoded the username instead of deriving it from the token. Then Copilot found the hardcode. Then I added a stray `--` to `git checkout -B`. Every fix introduced new bugs. PR #121 accumulated 7 fix commits across 4 rounds of review.
+
+2. **gh-aw agent attempt (pr-rescue.md)**: Rewrote as a gh-aw agent to escape bash fragility. Compiled clean. Then on self-review discovered: no `bash:` tools but instructions reference `gh api graphql` and `git rebase`. Added tools. Then discovered `push-to-pull-request-branch` safe-output can't force-push after rebase — it only applies patches. The agent literally cannot do the core operation.
+
+3. **Pipeline orchestrator (final)**: User proposed a fundamentally different approach — instead of one workflow doing everything, split into an orchestrator agent (reasoning + safe-outputs, no git) that handles everything EXCEPT rebasing. Rebasing either stays as a simple dedicated workflow or is left to humans. The orchestrator is ~80 lines of natural language, compiles clean, needs no `contents: write`.
+
+- Updated review-responder instructions to query real `PRRT_` thread IDs via `gh api graphql` before resolving (#117). No `bash:` tool config needed — `--allow-all-tools` is granted by default when no explicit `bash:` is set. Adding `bash:` would restrict the allowlist and break CI commands (uv, ruff, pyright, pytest). Instruction-only fix.
+- Moved CI fixer dispatch from `ci.yml` into the orchestrator — all dispatch decisions (implementer + ci-fixer) now centralized.
+- Closed PR #121 (bash attempt). Abandoned pr-rescue.md (gh-aw attempt). Created pipeline-orchestrator.md (final approach).
+- Closed stale/noise issues: #94, #105 (auto-generated fallback issues from implementer), #115 (duplicate of #108), #120 (fixed in PR #119).
+- **Lessons learned**: (1) Complex bash in Actions is a bug factory. (2) gh-aw safe-outputs have limitations (no force-push). (3) Split reasoning from operations — agents reason, workflows operate. (4) Never hardcode values that can be derived at runtime. (5) Every round of review found bugs the previous round missed — self-review is not enough.
+
diff --git a/docs/changelog.md b/docs/changelog.md
index b9ebd08..386a3ed 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -4,6 +4,30 @@ Append-only history of repo-level changes (CI, infra, shared config). Tool-speci
---
+## feat: pipeline orchestrator + review-responder thread ID fix — 2026-03-16
+
+**Problem 1**: Agent PRs get stuck at multiple stages (no Copilot review, unresolved threads, behind main). The old `pr-rescue.yml` bash script only handled rebasing and was brittle — 4 rounds of review across 3 AI models found a combined 13 bugs in 230 lines of bash. (Issues #116, #90)
+
+**Fix 1**: New **pipeline orchestrator** (`pipeline-orchestrator.md`) — gh-aw agent that owns the full lifecycle. Dispatches implementer for eligible issues (one at a time, only if no aw PR in flight). Unsticks stuck PRs: requests Copilot reviews, resolves addressed threads via GraphQL. Pure reasoning agent with no git access. Replaces `pr-rescue.yml`. (Closes #116, #90. Refs #129)
+
+**Problem 2**: Review-responder hallucinates thread IDs because the MCP server doesn't expose `PRRT_` node IDs. All `resolve_pull_request_review_thread` calls fail silently. PRs stay stuck with unresolved threads. (Issues #114, #117)
+
+**Fix 2**: Updated review-responder instructions to query real thread IDs via `gh api graphql` before resolving. No `bash:` tool config added — the responder already has `--allow-all-tools` (adding `bash:` would restrict the allowlist and break CI commands). Instruction-only change. (Closes #117. Refs #114)
+
+**Also**: Moved CI fixer dispatch from `ci.yml` into the orchestrator — all dispatch decisions now centralized in one agent.
+
+---
+
+## fix: gate agent workflows on aw label — 2026-03-16
+
+**Problem**: Agent workflows (review-responder, quality-gate) fired on every `pull_request_review` event, including human-authored PRs. The `aw` label check was only in the agent prompt — a soft guard that still burned compute and inference tokens before noop'ing. Discovered on PR #118. (Issue #120)
+
+**Fix**: Added `if: "contains(github.event.pull_request.labels.*.name, 'aw')"` to both workflow frontmatters. gh-aw compiles this to a job-level `if:` on the activation job — workflow skips entirely at the GitHub Actions level when the label is absent. Zero tokens burned. (PR #119)
+
+**Finding**: `pull_request_review` events use the workflow file from the PR's **head branch**, not the default branch. The `if:` condition was active immediately on the PR itself — no need to merge first.
+
+---
+
## fix: revert labels config + strengthen responder resolve-before-push — 2026-03-15
**Problem 1**: PR #97 added `labels: ["aw"]` to `create-pull-request` config. This broke label application — the gh-aw handler's post-creation label API call fails non-deterministically with a node ID resolution error, and the tool description tells the agent "labels will be automatically added" so the agent stops including them. PR #104 was created without the `aw` label. (Issue #107)