diff --git a/.github/workflows/pr-sous-chef.lock.yml b/.github/workflows/pr-sous-chef.lock.yml
index c84c0e93892..525684f90ef 100644
--- a/.github/workflows/pr-sous-chef.lock.yml
+++ b/.github/workflows/pr-sous-chef.lock.yml
@@ -1,4 +1,4 @@
-# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fa3207d28328d1ed00778bc9408c6c6faba7bd90fa0167379955130b19014276","body_hash":"ad4167cef88709a9c4b0def593e43894b68de3afaa0538f265f1757254d42e9b","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.84.2"}}
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4c3bf0a87c9ea82fd4b3fad052aad4264974ce0b93d061def976b0f426da2024","body_hash":"bb4f60c624aab749a5aa0839691eda5e1cd9ebae260ed76c32d233d614ced936","strict":true,"agent_id":"pi","agent_model":"copilot/gpt-5.4","engine_versions":{"pi":"0.84.2"}}
# gh-aw-manifest: {"version":1,"secrets":["AWI_MAINTENANCE_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GH_AW_OTEL_GRAFANA_AUTHORIZATION","GH_AW_OTEL_GRAFANA_ENDPOINT","GH_AW_OTEL_SENTRY_AUTHORIZATION","GH_AW_OTEL_SENTRY_ENDPOINT","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-go","sha":"b7ad1dad31e06c5925ef5d2fc7ad053ef454303e","version":"v7.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1","digest":"sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.1@sha256:5e3f6ee27eeae07195838b97ac4aa2f8aea42a7c55f1c0d3e17d8e88e294ad0d"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1","digest":"sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.1@sha256:288e7d2a12d5b430500d739f9c16e20bb1ed51b91f986f3f3eccde189f489f5c"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1","digest":"sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610","pinned_image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.28.1@sha256:f931e5e1e13f765605d03ef9511fc755d779a51b76581ea14586e9871506a610"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1","digest":"sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.1@sha256:9d428af47899bf18ef2d5618075777d76ef344c91e76c1f44ec1aaa0ee347e5f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}]}
# This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md
#
@@ -316,7 +316,7 @@ jobs:
GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
GH_AW_INCLUDE_PR_CONTEXT: ${{ (github.event_name == 'issue_comment' && github.event.issue.pull_request != null) || github.event_name == 'pull_request_review_comment' || github.event_name == 'pull_request_review' }}
GH_AW_PROMPT_CONTENT_0000: "\n"
- GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment(max:4), create_issue, update_pull_request(max:10), dismiss_pull_request_review(max:20), resolve_pull_request_review_thread(max:40), push_to_pull_request_branch(max:10), missing_tool, missing_data, noop\n"
+ GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment(max:4), create_issue, update_pull_request(max:10), approve_workflow_run(max:8), dismiss_pull_request_review(max:20), resolve_pull_request_review_thread(max:40), push_to_pull_request_branch(max:10), missing_tool, missing_data, noop\n"
GH_AW_PROMPT_CONTENT_0002: "\n"
GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n- **checkouts**: The following repositories have been checked out and are available in the workspace:\n - repo `__GH_AW_GITHUB_REPOSITORY__` → `$GITHUB_WORKSPACE` (cwd) [full history, all branches available as remote-tracking refs] [additional refs fetched: refs/pulls/open/*]\n - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: [\"refs/pulls/open/*\"]` for all open PR refs, or `fetch: [\"main\", \"feature/my-branch\"]` for specific branches).\n - **Warning: No git credentials are available to the agent.** Credentials are\n intentionally removed after the checkout step for security. This means any git\n operation that needs to authenticate to the remote will fail. In private repositories, that includes:\n - `git fetch`, `git pull`, `git clone`, and `git push` (direct push, not via safe-output tools)\n - Checking out or switching to a remote branch that is not already fetched\n - Deepening a shallow clone (`git fetch --unshallow`)\n - On-demand blob fetches in partial/blobless clones (operations on files not in the initial checkout)\n Do NOT attempt to configure credentials, run `git credential fill`, or modify `.gitconfig` —\n authentication will not succeed. If you encounter credential prompts or authentication errors,\n stop immediately and report the limitation rather than spending turns trying to work around it.\n\n\n"
GH_AW_PROMPT_CONTENT_0004: "\n"
@@ -599,6 +599,10 @@ jobs:
# Determine pending-check state from the statusCheckRollup data already
# fetched in the gh pr list call above — no per-PR REST calls needed.
# CheckRun statuses are UPPERCASE in the GraphQL response.
+ # WAITING check runs are left eligible here so the agent can inspect the
+ # associated workflow run and approve it through the approve_workflow_run
+ # safe output when it belongs to the allowed CJS/CGO workflows. Other
+ # short-running pending checks still gate nudges.
# Checks that have been running for more than 1 hour are ignored so that
# long-running agentic checks (Q, coding agents) do not permanently block
# nudges. Short CI checks (< 1 hour) still gate nudges correctly.
@@ -612,7 +616,7 @@ jobs:
(now - 3600) as $cutoff |
if ($checks | any(
if .__typename == "CheckRun" then
- ((.status // "COMPLETED") | IN("QUEUED", "IN_PROGRESS", "WAITING", "REQUESTED", "PENDING")) and
+ ((.status // "COMPLETED") | IN("QUEUED", "IN_PROGRESS", "REQUESTED", "PENDING")) and
((.startedAt // .createdAt) as $ts |
$ts == null or (($ts | fromdateiso8601) > $cutoff))
elif .__typename == "StatusContext" then
@@ -691,13 +695,14 @@ jobs:
mergeStateStatus,
failed_checks: ((.statusCheckRollup // []) | if type == "array" then . else [] end | map(select(
if .__typename == "CheckRun" then
- ((.conclusion // "") | IN("FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"))
+ ((.conclusion // "") | IN("FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE")) or
+ ((.status // "") == "WAITING")
elif .__typename == "StatusContext" then
((.state // "") | IN("FAILURE", "ERROR"))
else false end
)) | map({
name: (if .__typename == "StatusContext" then (.context // "unknown") else (.name // "unknown") end),
- conclusion: (.conclusion // .state // "unknown"),
+ conclusion: (if .__typename == "CheckRun" and (.status // "") == "WAITING" then "ACTION_REQUIRED" else (.conclusion // .state // "unknown") end),
url: (.detailsUrl // .targetUrl // null)
}))
})
@@ -705,7 +710,9 @@ jobs:
> /tmp/gh-aw/agent/pr-sous-chef-candidates-compact.json
eligible_count="$(jq '.prs | length' /tmp/gh-aw/agent/pr-sous-chef-candidates-compact.json || echo 0)"
fetched_count="$(jq '.fetched' /tmp/gh-aw/agent/pr-sous-chef-candidates-compact.json || echo 0)"
+ eligible_pull_request_numbers="$(jq -c '[.prs[]?.number | tostring]' /tmp/gh-aw/agent/pr-sous-chef-candidates-compact.json || echo '[]')"
echo "eligible_count=$eligible_count" >> "$GITHUB_OUTPUT"
+ echo "eligible_pull_request_numbers=$eligible_pull_request_numbers" >> "$GITHUB_OUTPUT"
# Write prefilter summary to the step summary for visibility
{
@@ -826,7 +833,7 @@ jobs:
env:
GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw"
GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}"
- GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"github-token\":\"${GH_AW_SECRET_AWI_MAINTENANCE_TOKEN}\",\"max\":4,\"target\":\"*\"},\"create_issue\":{\"close_older_issues\":true,\"expires\":72,\"group_by_day\":true,\"labels\":[\"automation\"],\"max\":1,\"title_prefix\":\"[pr-sous-chef] \"},\"create_report_incomplete_issue\":{},\"dismiss_pull_request_review\":{\"max\":20,\"target\":\"*\"},\"mentions\":{\"allowed\":[\"copilot\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"commit_title_suffix\":\" [pr-sous-chef]\",\"excluded_files\":[\".github/workflows/**\"],\"if_no_changes\":\"ignore\",\"max\":10,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"],\"target\":\"*\"},\"report_incomplete\":{},\"resolve_pull_request_review_thread\":{\"max\":40},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":false,\"default_operation\":\"append\",\"max\":10,\"target\":\"*\",\"update_branch\":true,\"update_branch_stacks\":true}}"
+ GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"github-token\":\"${GH_AW_SECRET_AWI_MAINTENANCE_TOKEN}\",\"max\":4,\"target\":\"*\"},\"approve_workflow_run\":{\"allowed_pull_requests\":\"${{ needs.approval_allowlist.outputs.eligible_pull_request_numbers }}\",\"allowed_workflows\":[\"cjs.yml\",\"cgo.yml\"],\"fork\":false,\"github-token\":\"${GH_AW_SECRET_AWI_MAINTENANCE_TOKEN}\",\"max\":8,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":72,\"group_by_day\":true,\"labels\":[\"automation\"],\"max\":1,\"title_prefix\":\"[pr-sous-chef] \"},\"create_report_incomplete_issue\":{},\"dismiss_pull_request_review\":{\"max\":20,\"target\":\"*\"},\"mentions\":{\"allowed\":[\"copilot\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"commit_title_suffix\":\" [pr-sous-chef]\",\"excluded_files\":[\".github/workflows/**\"],\"if_no_changes\":\"ignore\",\"max\":10,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"],\"target\":\"*\"},\"report_incomplete\":{},\"resolve_pull_request_review_thread\":{\"max\":40},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":false,\"default_operation\":\"append\",\"max\":10,\"target\":\"*\",\"update_branch\":true,\"update_branch_stacks\":true}}"
GH_AW_SECRET_AWI_MAINTENANCE_TOKEN: ${{ secrets.AWI_MAINTENANCE_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
GH_AW_SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.AWI_MAINTENANCE_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
GH_AW_SECRET_GITHUB_TOKEN: ${{ secrets.AWI_MAINTENANCE_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
@@ -894,6 +901,15 @@ jobs:
}
}
},
+ "approve_workflow_run": {
+ "defaultMax": 1,
+ "fields": {
+ "run_id": {
+ "required": true,
+ "positiveInteger": true
+ }
+ }
+ },
"create_issue": {
"defaultMax": 1,
"fields": {
@@ -1465,10 +1481,46 @@ jobs:
/tmp/gh-aw/sandbox/firewall/awf-reflect.json
if-no-files-found: ignore
+ approval_allowlist:
+ needs: agent
+ if: always() && needs.agent.result != 'skipped'
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ outputs:
+ eligible_pull_request_numbers: ${{ steps.extract.outputs.eligible_pull_request_numbers }}
+ steps:
+ - name: Configure GH_HOST for enterprise compatibility
+ id: ghes-host-config
+ shell: bash
+ run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
+ # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
+ # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
+ GH_HOST="${GITHUB_SERVER_URL#https://}"
+ GH_HOST="${GH_HOST#http://}"
+ echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
+ - name: Download agent artifact
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: ${{ runner.temp }}/gh-aw
+ continue-on-error: true
+ - name: Extract eligible PR allowlist
+ id: extract
+ run: |
+ candidate_file="$(find "${RUNNER_TEMP}/gh-aw" -path '*/pr-sous-chef-candidates-compact.json' -print -quit)"
+ if [ -n "$candidate_file" ] && [ -f "$candidate_file" ]; then
+ eligible_pull_request_numbers="$(jq -c '[.prs[]?.number | tostring]' "$candidate_file" || echo '[]')"
+ else
+ eligible_pull_request_numbers='[]'
+ fi
+ echo "eligible_pull_request_numbers=$eligible_pull_request_numbers" >> "$GITHUB_OUTPUT"
+
conclusion:
needs:
- activation
- agent
+ - approval_allowlist
- detection
- evals
- push_evals_state
@@ -1479,7 +1531,7 @@ jobs:
needs.activation.outputs.daily_ai_credits_exceeded == 'true')
runs-on: ubuntu-slim
permissions:
- actions: read
+ actions: write
contents: write
issues: write
pull-requests: write
@@ -2371,10 +2423,12 @@ jobs:
needs:
- activation
- agent
+ - approval_allowlist
- detection
if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
runs-on: ubuntu-slim
permissions:
+ actions: write
contents: write
issues: write
pull-requests: write
@@ -2506,7 +2560,7 @@ jobs:
GH_AW_ALLOWED_DOMAINS: "*.grafana.net,*.sentry.io,api.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,go.dev,golang.org,goproxy.io,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,pkg.go.dev,ppa.launchpad.net,proxy.golang.org,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,storage.googleapis.com,sum.golang.org,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_API_URL: ${{ github.api_url }}
- GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"github-token\":\"${{ secrets.AWI_MAINTENANCE_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}\",\"max\":4,\"target\":\"*\"},\"create_issue\":{\"close_older_issues\":true,\"expires\":72,\"group_by_day\":true,\"labels\":[\"automation\"],\"max\":1,\"title_prefix\":\"[pr-sous-chef] \"},\"create_report_incomplete_issue\":{},\"dismiss_pull_request_review\":{\"max\":20,\"target\":\"*\"},\"mentions\":{\"allowed\":[\"copilot\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"commit_title_suffix\":\" [pr-sous-chef]\",\"excluded_files\":[\".github/workflows/**\"],\"if_no_changes\":\"ignore\",\"max\":10,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"],\"target\":\"*\"},\"report_incomplete\":{},\"resolve_pull_request_review_thread\":{\"max\":40},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":false,\"default_operation\":\"append\",\"max\":10,\"target\":\"*\",\"update_branch\":true,\"update_branch_stacks\":true}}"
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"github-token\":\"${{ secrets.AWI_MAINTENANCE_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}\",\"max\":4,\"target\":\"*\"},\"approve_workflow_run\":{\"allowed_pull_requests\":\"${{ needs.approval_allowlist.outputs.eligible_pull_request_numbers }}\",\"allowed_workflows\":[\"cjs.yml\",\"cgo.yml\"],\"fork\":false,\"github-token\":\"${{ secrets.AWI_MAINTENANCE_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}\",\"max\":8,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"]},\"create_issue\":{\"close_older_issues\":true,\"expires\":72,\"group_by_day\":true,\"labels\":[\"automation\"],\"max\":1,\"title_prefix\":\"[pr-sous-chef] \"},\"create_report_incomplete_issue\":{},\"dismiss_pull_request_review\":{\"max\":20,\"target\":\"*\"},\"mentions\":{\"allowed\":[\"copilot\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"commit_title_suffix\":\" [pr-sous-chef]\",\"excluded_files\":[\".github/workflows/**\"],\"if_no_changes\":\"ignore\",\"max\":10,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"PI.md\",\"AGENTS.md\"],\"target\":\"*\"},\"report_incomplete\":{},\"resolve_pull_request_review_thread\":{\"max\":40},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":false,\"default_operation\":\"append\",\"max\":10,\"target\":\"*\",\"update_branch\":true,\"update_branch_stacks\":true}}"
GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }}
with:
github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/pr-sous-chef.md b/.github/workflows/pr-sous-chef.md
index 0e42309e3c2..7af977f9bbf 100644
--- a/.github/workflows/pr-sous-chef.md
+++ b/.github/workflows/pr-sous-chef.md
@@ -118,6 +118,10 @@ steps:
# Determine pending-check state from the statusCheckRollup data already
# fetched in the gh pr list call above — no per-PR REST calls needed.
# CheckRun statuses are UPPERCASE in the GraphQL response.
+ # WAITING check runs are left eligible here so the agent can inspect the
+ # associated workflow run and approve it through the approve_workflow_run
+ # safe output when it belongs to the allowed CJS/CGO workflows. Other
+ # short-running pending checks still gate nudges.
# Checks that have been running for more than 1 hour are ignored so that
# long-running agentic checks (Q, coding agents) do not permanently block
# nudges. Short CI checks (< 1 hour) still gate nudges correctly.
@@ -131,7 +135,7 @@ steps:
(now - 3600) as $cutoff |
if ($checks | any(
if .__typename == "CheckRun" then
- ((.status // "COMPLETED") | IN("QUEUED", "IN_PROGRESS", "WAITING", "REQUESTED", "PENDING")) and
+ ((.status // "COMPLETED") | IN("QUEUED", "IN_PROGRESS", "REQUESTED", "PENDING")) and
((.startedAt // .createdAt) as $ts |
$ts == null or (($ts | fromdateiso8601) > $cutoff))
elif .__typename == "StatusContext" then
@@ -210,13 +214,14 @@ steps:
mergeStateStatus,
failed_checks: ((.statusCheckRollup // []) | if type == "array" then . else [] end | map(select(
if .__typename == "CheckRun" then
- ((.conclusion // "") | IN("FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"))
+ ((.conclusion // "") | IN("FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE")) or
+ ((.status // "") == "WAITING")
elif .__typename == "StatusContext" then
((.state // "") | IN("FAILURE", "ERROR"))
else false end
)) | map({
name: (if .__typename == "StatusContext" then (.context // "unknown") else (.name // "unknown") end),
- conclusion: (.conclusion // .state // "unknown"),
+ conclusion: (if .__typename == "CheckRun" and (.status // "") == "WAITING" then "ACTION_REQUIRED" else (.conclusion // .state // "unknown") end),
url: (.detailsUrl // .targetUrl // null)
}))
})
@@ -224,7 +229,9 @@ steps:
> /tmp/gh-aw/agent/pr-sous-chef-candidates-compact.json
eligible_count="$(jq '.prs | length' /tmp/gh-aw/agent/pr-sous-chef-candidates-compact.json || echo 0)"
fetched_count="$(jq '.fetched' /tmp/gh-aw/agent/pr-sous-chef-candidates-compact.json || echo 0)"
+ eligible_pull_request_numbers="$(jq -c '[.prs[]?.number | tostring]' /tmp/gh-aw/agent/pr-sous-chef-candidates-compact.json || echo '[]')"
echo "eligible_count=$eligible_count" >> "$GITHUB_OUTPUT"
+ echo "eligible_pull_request_numbers=$eligible_pull_request_numbers" >> "$GITHUB_OUTPUT"
# Write prefilter summary to the step summary for visibility
{
@@ -254,11 +261,44 @@ steps:
- name: Install formatter dependencies
if: steps.fetch-prs.outputs.eligible_count != '0'
run: npm ci --prefix actions/setup/js
+jobs:
+ approval_allowlist:
+ needs: agent
+ if: always() && needs.agent.result != 'skipped'
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ outputs:
+ eligible_pull_request_numbers: ${{ steps.extract.outputs.eligible_pull_request_numbers }}
+ steps:
+ - name: Download agent artifact
+ continue-on-error: true
+ uses: actions/download-artifact@v8.0.1
+ with:
+ name: agent
+ path: ${{ runner.temp }}/gh-aw
+ - name: Extract eligible PR allowlist
+ id: extract
+ run: |
+ candidate_file="$(find "${RUNNER_TEMP}/gh-aw" -path '*/pr-sous-chef-candidates-compact.json' -print -quit)"
+ if [ -n "$candidate_file" ] && [ -f "$candidate_file" ]; then
+ eligible_pull_request_numbers="$(jq -c '[.prs[]?.number | tostring]' "$candidate_file" || echo '[]')"
+ else
+ eligible_pull_request_numbers='[]'
+ fi
+ echo "eligible_pull_request_numbers=$eligible_pull_request_numbers" >> "$GITHUB_OUTPUT"
safe-outputs:
+ needs: [approval_allowlist]
add-comment:
max: 4
target: "*"
github-token: ${{ secrets.AWI_MAINTENANCE_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ approve-workflow-run:
+ max: 8
+ allowed-workflows: [cjs.yml, cgo.yml]
+ allowed-pull-requests: ${{ needs.approval_allowlist.outputs.eligible_pull_request_numbers }}
+ fork: false
+ github-token: ${{ secrets.AWI_MAINTENANCE_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
resolve-pull-request-review-thread:
max: 40
dismiss-pull-request-review:
@@ -349,19 +389,29 @@ Skip when **any** of these hold (candidate prefilter eliminates most; these are
For each PR that is not skipped:
-0. **Run formatters and push if needed**
+0. **Approve allowed action-required workflow runs**
+ - If the compact JSON `failed_checks` list contains `ACTION_REQUIRED`, inspect the PR's waiting workflow runs and call `safeoutputs approve_workflow_run --run_id ` only for matching CJS/CGO runs.
+ - To find run IDs, query `gh run list --repo "$EXPR_GITHUB_REPOSITORY" --branch --json databaseId,path,status,event,headBranch,headSha --jq '[.[] | select((.status == "waiting" or .status == "action_required") and (.path == ".github/workflows/cjs.yml" or .path == ".github/workflows/cgo.yml"))]'`; then keep only runs whose `headBranch` matches the PR head branch and whose `headSha` matches the PR `headRefOid` when present.
+ - Only approve workflow runs whose workflow file is exactly `cjs.yml` or `cgo.yml`; never approve any other action-required workflow.
+ - Only approve workflow runs associated with the current eligible PR. The safe output is additionally scoped to the eligible PR numbers from the prefilter.
+ - Increment `approved_workflow_runs` for each successful approval.
+ - If approval succeeds for all action-required `CJS`/`CGO` runs on the PR and there is no other forward-progress nudge to post, do not add a `@copilot` nudge comment for that PR.
+ - If approval fails, record `{pr_number: , skip_reason: "approve_workflow_run_failed"}` in the `skipped` array and continue with the remaining PR workflow.
+
+1. **Run formatters and push if needed**
- Checkout the PR branch: `git checkout `
- Run `make fmt` to format all code (Go, JavaScript, JSON)
- If dirty (`git diff --quiet` exits non-zero), call `push_to_pull_request_branch` with the PR number
- Return to the original branch: `git checkout -`
- Skip this step silently if `make fmt` exits non-zero (tools unavailable)
-1. **Update branch if possible (skip for CONFLICTING branches)**
+2. **Update branch if possible (skip for CONFLICTING branches)**
- If `mergeStateStatus` is `CONFLICTING`, **skip this step entirely**.
- Otherwise, attempt `update_pull_request` with `update_branch: true` and a minimal append body marker including `pr-sous-chef` and the run URL.
-2. **Post exactly one combined nudge comment** (at most ONE `add_comment` per PR per run)
- - Always start with `` as the first hidden marker line and a `@copilot` mention.
+3. **Post one combined nudge comment when forward-progress nudge is still needed** (at most ONE `add_comment` per PR per run)
+ - Skip this step when the approval step already approved all action-required `CJS`/`CGO` runs for the PR and there is no other forward-progress nudge to post.
+ - When posting, always start with `` as the first hidden marker line and a `@copilot` mention.
- **If `CONFLICTING`**: instruct `@copilot` to run `make merge-main` to resolve conflicts; increment `merge_main_scheduled`.
- **Otherwise**: combine into one comment — unresolved reviews (reviewer + direct link per thread, newest first), `failed_checks` from compact JSON (name + URL), branch refresh, and instruction to run the `pr-finisher` skill.
- Every `add_comment` must include `pr_number`. Never emit `add_comment` without a numeric target field.
@@ -369,7 +419,7 @@ For each PR that is not skipped:
- Always set `pr_number` to the current PR's numeric number. Use `safeoutputs add_comment --pr_number --body $'...'` syntax only. Never use `gh pr comment` or `gh api` for writes.
- Example: `safeoutputs add_comment --pr_number 12345 --body $'\n@copilot ...'`
-3. **Resolve review threads that already have a response using a safe output**
+4. **Resolve review threads that already have a response using a safe output**
- For `schedule` and `workflow_dispatch` runs, use the `resolve_review_threads` list returned by the `pr-processor` sub-agent.
- Include a thread only when all of the following are true: the thread is currently unresolved; contains reviewer feedback; and has a later reply from the PR author or `@copilot`.
- For each thread ID, call `safeoutputs resolve_pull_request_review_thread --thread_id `.
@@ -377,7 +427,7 @@ For each PR that is not skipped:
- Copy each `` verbatim, character-for-character, from the `reviewThreads` data returned for the current PR. Never guess, truncate, extend, or otherwise fabricate a thread ID.
- If resolving one thread fails, record `{thread_id: , skip_reason: "resolve_review_thread_failed"}` in the `skipped` array and continue.
-4. **Dismiss stale `github-actions[bot]` blocking reviews when all PR review threads are resolved**
+5. **Dismiss stale `github-actions[bot]` blocking reviews when all PR review threads are resolved**
- **Slash-command guard**: slash-command runs are acknowledgment nudges and must not perform automated review cleanup — skip this step entirely on `/souschef` slash-command runs.
- For `schedule` and `workflow_dispatch` runs, use the `dismiss_reviews` list returned by the `pr-processor` sub-agent (populated only when ALL review threads are resolved).
- For each review ID, call `safeoutputs dismiss_pull_request_review --pull_request_number --review_id --justification "Dismissing stale github-actions review because all PR review threads are resolved."`.
@@ -405,13 +455,14 @@ Then include the run counts as a compact table:
| nudged | N |
| branch_update_attempts | N |
| formatter_pushes | N |
+| approved_workflow_runs | N |
| merge_main_scheduled | N |
| resolved_review_threads | N |
| dismissed_reviews | N |
If any PRs were nudged, include a collapsible list of their numbers and titles.
-If `create_issue` is unavailable, fall back to `noop` with a condensed message, e.g. `"processed=4; skipped_checks_running=0; skipped_last_comment_from_sous_chef=1; skipped_cooldown=1; nudged=2; branch_update_attempts=0; formatter_pushes=0; merge_main_scheduled=1; resolved_review_threads=3; dismissed_reviews=1"`.
+If `create_issue` is unavailable, fall back to `noop` with a condensed message, e.g. `"processed=4; skipped_checks_running=0; skipped_last_comment_from_sous_chef=1; skipped_cooldown=1; nudged=2; branch_update_attempts=0; formatter_pushes=0; approved_workflow_runs=1; merge_main_scheduled=1; resolved_review_threads=3; dismissed_reviews=1"`.
## Formatting Requirements
diff --git a/actions/setup/js/approve_workflow_run.cjs b/actions/setup/js/approve_workflow_run.cjs
index 028c4b3ca27..0d94402e9b3 100644
--- a/actions/setup/js/approve_workflow_run.cjs
+++ b/actions/setup/js/approve_workflow_run.cjs
@@ -34,7 +34,18 @@ function parsePositiveInt(value) {
* @returns {Set}
*/
function parseAllowedPullRequests(value) {
- const parsed = (Array.isArray(value) ? value : [value]).map(parsePositiveInt).filter(candidate => candidate !== undefined);
+ let normalized = value;
+ if (typeof normalized === "string") {
+ const trimmed = normalized.trim();
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
+ try {
+ normalized = JSON.parse(trimmed);
+ } catch {
+ normalized = value;
+ }
+ }
+ }
+ const parsed = (Array.isArray(normalized) ? normalized : [normalized]).map(parsePositiveInt).filter(candidate => candidate !== undefined);
return new Set(parsed);
}
diff --git a/actions/setup/js/approve_workflow_run.test.cjs b/actions/setup/js/approve_workflow_run.test.cjs
index b2d34ede322..4eaa0a02e96 100644
--- a/actions/setup/js/approve_workflow_run.test.cjs
+++ b/actions/setup/js/approve_workflow_run.test.cjs
@@ -303,6 +303,20 @@ describe("approve_workflow_run", () => {
expect(mockApproveWorkflowRun).toHaveBeenCalledTimes(1);
});
+ it("approves a run for a pull request allowed by a JSON-string list", async () => {
+ global.context.payload = {};
+ mockGetWorkflowRun.mockResolvedValue({
+ data: { ...pendingPullRequestRun, pull_requests: [{ number: 43 }] },
+ });
+ const { main } = require("./approve_workflow_run.cjs");
+ const handler = await main({ ...externalTokenConfig, allowed_pull_requests: '["43"]' });
+
+ const result = await handler({ run_id: 123 }, {});
+
+ expect(result.success).toBe(true);
+ expect(mockApproveWorkflowRun).toHaveBeenCalledTimes(1);
+ });
+
it("approves a run when every associated pull request is triggering or explicitly allowed", async () => {
mockGetWorkflowRun.mockResolvedValue({
data: { ...pendingPullRequestRun, pull_requests: [{ number: 42 }, { number: 43 }] },