From 0e83b65cf071481486b0e28e80290b6116a24681 Mon Sep 17 00:00:00 2001 From: mrjf Date: Fri, 26 Jun 2026 19:48:45 -0700 Subject: [PATCH] Install Evergreen workflow --- .github/workflows/evergreen.lock.yml | 367 +++++--- .github/workflows/evergreen.md | 792 ++++++------------ .../workflows/shared/evergreen/repo-policy.md | 79 ++ .../shared/evergreen/report-template.md | 19 + .../shared/evergreen/safe-output-policy.md | 18 + .../shared/evergreen/state-labels.md | 11 + .../shared/skills/attempt-memory-writer.md | 11 + .../shared/skills/autoloop-coordinator.md | 10 + .../shared/skills/ci-gate-evaluator.md | 15 + .../workflows/shared/skills/ci-log-parser.md | 12 + .../workflows/shared/skills/ci-run-deduper.md | 11 + .../shared/skills/dependency-gate-repair.md | 5 + .../shared/skills/deterministic-repair.md | 14 + .../workflows/shared/skills/diff-risk-map.md | 20 + .../shared/skills/docs-release-gate-repair.md | 8 + .../shared/skills/infra-ci-repair.md | 5 + .../shared/skills/lint-policy-review.md | 5 + .../skills/merge-blocker-comment-reader.md | 12 + .../shared/skills/merge-gate-reporter.md | 14 + .../shared/skills/playground-e2e-diagnoser.md | 12 + .github/workflows/shared/skills/pr-intake.md | 13 + .../shared/skills/repo-memory-reader.md | 13 + .../shared/skills/safe-output-verifier.md | 11 + 23 files changed, 829 insertions(+), 648 deletions(-) create mode 100644 .github/workflows/shared/evergreen/repo-policy.md create mode 100644 .github/workflows/shared/evergreen/report-template.md create mode 100644 .github/workflows/shared/evergreen/safe-output-policy.md create mode 100644 .github/workflows/shared/evergreen/state-labels.md create mode 100644 .github/workflows/shared/skills/attempt-memory-writer.md create mode 100644 .github/workflows/shared/skills/autoloop-coordinator.md create mode 100644 .github/workflows/shared/skills/ci-gate-evaluator.md create mode 100644 .github/workflows/shared/skills/ci-log-parser.md create mode 100644 .github/workflows/shared/skills/ci-run-deduper.md create mode 100644 .github/workflows/shared/skills/dependency-gate-repair.md create mode 100644 .github/workflows/shared/skills/deterministic-repair.md create mode 100644 .github/workflows/shared/skills/diff-risk-map.md create mode 100644 .github/workflows/shared/skills/docs-release-gate-repair.md create mode 100644 .github/workflows/shared/skills/infra-ci-repair.md create mode 100644 .github/workflows/shared/skills/lint-policy-review.md create mode 100644 .github/workflows/shared/skills/merge-blocker-comment-reader.md create mode 100644 .github/workflows/shared/skills/merge-gate-reporter.md create mode 100644 .github/workflows/shared/skills/playground-e2e-diagnoser.md create mode 100644 .github/workflows/shared/skills/pr-intake.md create mode 100644 .github/workflows/shared/skills/repo-memory-reader.md create mode 100644 .github/workflows/shared/skills/safe-output-verifier.md diff --git a/.github/workflows/evergreen.lock.yml b/.github/workflows/evergreen.lock.yml index 57cd6215..5523a25f 100644 --- a/.github/workflows/evergreen.lock.yml +++ b/.github/workflows/evergreen.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a1ba6da6b2cef664425783385422f29847d27100d4bd1f169ca05e9687f98f37","body_hash":"fcb1b772d58a4b2379aed665f753fac432e29019fb9c9af1bd8d789bea042b9f","compiler_version":"v0.79.4","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"d059700c6a8ec3b5fd798b9ea60f5d048447b918","version":"v0.79.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.0"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"bdc01a34c314507890520907a0850c0e228c462cafec04a6d035bf2e68b3b27c","body_hash":"3a4e876b6303cad1e6abbc362f4f611fc76b388435aeba963f08d13eb8e3daef","compiler_version":"v0.79.4","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"d059700c6a8ec3b5fd798b9ea60f5d048447b918","version":"v0.79.4"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.0"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.0"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -16,19 +16,42 @@ # # This file was automatically generated by gh-aw (v0.79.4). DO NOT EDIT. # -# To update this file, edit the corresponding .md file and run: +# To update this file, edit githubnext/evergreen 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/ # -# Evergreen — keeps pull requests healthy by automatically fixing merge conflicts -# and failing CI checks. Runs on a short schedule, deterministically selects one -# PR per run, and gives up after 5 attempts that don't improve the same repo state. +# Evergreen keeps opt-in pull requests green and merge-ready. A persistent +# `evergreen` label grants permission to diagnose merge blockers, repair CI +# failures, update the PR branch, manage Evergreen state labels, and report +# readiness without directly merging. +# +# Source: githubnext/evergreen # # Resolved workflow manifest: # Imports: -# - shared/reporting.md +# - shared/evergreen/repo-policy.md +# - shared/evergreen/report-template.md +# - shared/evergreen/safe-output-policy.md +# - shared/evergreen/state-labels.md +# - shared/skills/attempt-memory-writer.md +# - shared/skills/autoloop-coordinator.md +# - shared/skills/ci-gate-evaluator.md +# - shared/skills/ci-log-parser.md +# - shared/skills/ci-run-deduper.md +# - shared/skills/dependency-gate-repair.md +# - shared/skills/deterministic-repair.md +# - shared/skills/diff-risk-map.md +# - shared/skills/docs-release-gate-repair.md +# - shared/skills/infra-ci-repair.md +# - shared/skills/lint-policy-review.md +# - shared/skills/merge-blocker-comment-reader.md +# - shared/skills/merge-gate-reporter.md +# - shared/skills/playground-e2e-diagnoser.md +# - shared/skills/pr-intake.md +# - shared/skills/repo-memory-reader.md +# - shared/skills/safe-output-verifier.md # # Secrets used: # - COPILOT_GITHUB_TOKEN @@ -49,16 +72,23 @@ # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.0 # - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.0 +# - ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.0 # - ghcr.io/github/gh-aw-firewall/squid:0.27.0 # - ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa # - ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c # - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 -name: "Evergreen — PR Health Keeper" +name: "Evergreen" on: + pull_request: + types: + - labeled + - synchronize + - reopened + - ready_for_review schedule: - - cron: "*/5 * * * *" - # Friendly format: every 5m + - cron: "*/15 * * * *" + # Friendly format: every 15m workflow_dispatch: inputs: aw_context: @@ -67,26 +97,32 @@ on: required: false type: string pr_number: - description: Fix a specific PR by number (bypasses scheduling) + description: Run Evergreen on a specific PR number required: false type: string permissions: {} concurrency: - group: "gh-aw-${{ github.workflow }}" + group: evergreen-${{ github.event.pull_request.number || github.event.inputs.pr_number || 'schedule' }} + queue: max -run-name: "Evergreen — PR Health Keeper" +run-name: "Evergreen" jobs: activation: + needs: pre_activation + if: > + needs.pre_activation.outputs.activated == 'true' && ((github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'evergreen')) && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id)) runs-on: ubuntu-slim permissions: actions: read contents: read env: - GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_MAX_DAILY_AI_CREDITS: "200000" outputs: + body: ${{ steps.sanitized.outputs.body }} comment_id: "" comment_repo: "" daily_effective_workflow_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_effective_workflow_exceeded == 'true' }} @@ -100,6 +136,8 @@ jobs: setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} + text: ${{ steps.sanitized.outputs.text }} + title: ${{ steps.sanitized.outputs.title }} steps: - name: Setup Scripts id: setup @@ -107,12 +145,15 @@ jobs: with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} + trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }} + parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }} safe-output-artifact-client: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Evergreen — PR Health Keeper" + GH_AW_SETUP_WORKFLOW_NAME: "Evergreen" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/evergreen.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AWF_VERSION: "v0.27.0" + GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info @@ -123,15 +164,17 @@ jobs: GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AGENT_VERSION: "1.0.60" GH_AW_INFO_CLI_VERSION: "v0.79.4" - GH_AW_INFO_WORKFLOW_NAME: "Evergreen — PR Health Keeper" + GH_AW_INFO_WORKFLOW_NAME: "Evergreen" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" - GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","node","releaseassets.githubusercontent.com"]' + GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github","node","python","playwright"]' GH_AW_INFO_FIREWALL_ENABLED: "true" GH_AW_INFO_AWF_VERSION: "v0.27.0" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_FRONTMATTER_SOURCE: "githubnext/evergreen" + GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -145,12 +188,12 @@ jobs: if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_WORKFLOW_NAME: "Evergreen — PR Health Keeper" + GH_AW_WORKFLOW_NAME: "Evergreen" GH_AW_WORKFLOW_ID: "evergreen" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_WORKFLOW_DISPATCH_AW_CONTEXT: ${{ github.event.inputs.aw_context || '' }} GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} + GH_AW_MAX_DAILY_AI_CREDITS: "200000" with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | @@ -207,6 +250,17 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); await main(); + - name: Compute current body text + id: sanitized + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,binstar.org,bootstrap.pypa.io,bun.sh,cdn.jsdelivr.net,cdn.playwright.dev,codeload.github.com,conda.anaconda.org,conda.binstar.org,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,deb.nodesource.com,deno.land,docs.github.com,esm.sh,files.pythonhosted.org,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,pip.pypa.io,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + with: + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + await main(); - name: Create prompt with built-in context env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt @@ -224,24 +278,24 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_7c6eae1ae0391719_EOF' + cat << 'GH_AW_PROMPT_108080aa61ef7578_EOF' - GH_AW_PROMPT_7c6eae1ae0391719_EOF + GH_AW_PROMPT_108080aa61ef7578_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md" cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md" cat "${RUNNER_TEMP}/gh-aw/prompts/repo_memory_prompt.md" cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md" - cat << 'GH_AW_PROMPT_7c6eae1ae0391719_EOF' + cat << 'GH_AW_PROMPT_108080aa61ef7578_EOF' - Tools: add_comment(max:3), push_to_pull_request_branch(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_7c6eae1ae0391719_EOF + Tools: add_comment(max:2), add_labels(max:4), remove_labels(max:5), push_to_pull_request_branch(max:2), dispatch_workflow, missing_tool, missing_data, noop + GH_AW_PROMPT_108080aa61ef7578_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_7c6eae1ae0391719_EOF' + cat << 'GH_AW_PROMPT_108080aa61ef7578_EOF' - GH_AW_PROMPT_7c6eae1ae0391719_EOF + GH_AW_PROMPT_108080aa61ef7578_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_7c6eae1ae0391719_EOF' + cat << 'GH_AW_PROMPT_108080aa61ef7578_EOF' The following GitHub context information is available for this workflow: {{#if github.actor}} @@ -283,13 +337,33 @@ jobs: stop immediately and report the limitation rather than spending turns trying to work around it. - GH_AW_PROMPT_7c6eae1ae0391719_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_7c6eae1ae0391719_EOF' + GH_AW_PROMPT_108080aa61ef7578_EOF + cat "${RUNNER_TEMP}/gh-aw/prompts/cli_proxy_with_safeoutputs_prompt.md" + cat << 'GH_AW_PROMPT_108080aa61ef7578_EOF' - {{#runtime-import .github/workflows/shared/reporting.md}} + {{#runtime-import .github/workflows/shared/skills/pr-intake.md}} + {{#runtime-import .github/workflows/shared/skills/repo-memory-reader.md}} + {{#runtime-import .github/workflows/shared/skills/diff-risk-map.md}} + {{#runtime-import .github/workflows/shared/skills/ci-run-deduper.md}} + {{#runtime-import .github/workflows/shared/skills/ci-gate-evaluator.md}} + {{#runtime-import .github/workflows/shared/skills/ci-log-parser.md}} + {{#runtime-import .github/workflows/shared/skills/merge-blocker-comment-reader.md}} + {{#runtime-import .github/workflows/shared/skills/deterministic-repair.md}} + {{#runtime-import .github/workflows/shared/skills/safe-output-verifier.md}} + {{#runtime-import .github/workflows/shared/skills/attempt-memory-writer.md}} + {{#runtime-import .github/workflows/shared/skills/merge-gate-reporter.md}} + {{#runtime-import .github/workflows/shared/skills/infra-ci-repair.md}} + {{#runtime-import .github/workflows/shared/skills/playground-e2e-diagnoser.md}} + {{#runtime-import .github/workflows/shared/skills/autoloop-coordinator.md}} + {{#runtime-import .github/workflows/shared/skills/docs-release-gate-repair.md}} + {{#runtime-import .github/workflows/shared/skills/dependency-gate-repair.md}} + {{#runtime-import .github/workflows/shared/skills/lint-policy-review.md}} + {{#runtime-import .github/workflows/shared/evergreen/repo-policy.md}} + {{#runtime-import .github/workflows/shared/evergreen/state-labels.md}} + {{#runtime-import .github/workflows/shared/evergreen/safe-output-policy.md}} + {{#runtime-import .github/workflows/shared/evergreen/report-template.md}} {{#runtime-import .github/workflows/evergreen.md}} - GH_AW_PROMPT_7c6eae1ae0391719_EOF + GH_AW_PROMPT_108080aa61ef7578_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -316,10 +390,11 @@ jobs: GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools' GH_AW_MEMORY_BRANCH_NAME: 'memory/evergreen' - GH_AW_MEMORY_CONSTRAINTS: "\n\n**Constraints:**\n- **Allowed Files**: Only files matching patterns: *.md\n- **Max File Size**: 102400 bytes (0.10 MB) per file\n- **Max File Count**: 100 files per commit\n- **Max Patch Size**: 10240 bytes (10 KB) total per push (max: 1024 KB)\n" + GH_AW_MEMORY_CONSTRAINTS: "\n\n**Constraints:**\n- **Allowed Files**: Only files matching patterns: *.json, *.jsonl, *.md\n- **Max File Size**: 40960 bytes (0.04 MB) per file\n- **Max File Count**: 100 files per commit\n- **Max Patch Size**: 10240 bytes (10 KB) total per push (max: 1024 KB)\n" GH_AW_MEMORY_DESCRIPTION: '' GH_AW_MEMORY_DIR: '/tmp/gh-aw/repo-memory/default/' GH_AW_MEMORY_TARGET_REPO: ' of the current repository' + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} GH_AW_WIKI_NOTE: '' with: script: | @@ -346,6 +421,7 @@ jobs: GH_AW_MEMORY_DESCRIPTION: process.env.GH_AW_MEMORY_DESCRIPTION, GH_AW_MEMORY_DIR: process.env.GH_AW_MEMORY_DIR, GH_AW_MEMORY_TARGET_REPO: process.env.GH_AW_MEMORY_TARGET_REPO, + GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, GH_AW_WIKI_NOTE: process.env.GH_AW_WIKI_NOTE } }); @@ -383,10 +459,13 @@ jobs: needs: activation if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true' runs-on: ubuntu-latest - permissions: read-all - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - queue: max + permissions: + actions: read + checks: read + contents: read + issues: read + pull-requests: read + statuses: read env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" @@ -421,10 +500,11 @@ jobs: trace-id: ${{ needs.activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Evergreen — PR Health Keeper" + GH_AW_SETUP_WORKFLOW_NAME: "Evergreen" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/evergreen.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AWF_VERSION: "v0.27.0" + GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths @@ -452,13 +532,14 @@ jobs: env: GH_TOKEN: ${{ github.token }} - env: - FORCED_PR: ${{ github.event.inputs.pr_number }} - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + EVENT_PATH: ${{ github.event_path }} + GH_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} - GITHUB_TOKEN: ${{ github.token }} - id: find-pr - name: Find a PR that needs attention - run: "python3 - << 'PYEOF'\nimport os, json, re, subprocess, sys\nimport urllib.request, urllib.error\n\ndef emit_selected_output(pr_number):\n \"\"\"Expose `selected` as a step output for workflow gating.\n Empty string means no PR needs attention; otherwise the PR number.\"\"\"\n gh_output = os.environ.get(\"GITHUB_OUTPUT\")\n if gh_output:\n with open(gh_output, \"a\") as f:\n f.write(f\"selected={'' if pr_number is None else pr_number}\\n\")\n\ntoken = os.environ.get(\"GITHUB_TOKEN\", \"\")\nrepo = os.environ.get(\"GITHUB_REPOSITORY\", \"\")\nforced_pr = os.environ.get(\"FORCED_PR\", \"\").strip()\n\nrepo_memory_dir = \"/tmp/gh-aw/repo-memory/evergreen\"\noutput_file = \"/tmp/gh-aw/evergreen.json\"\nos.makedirs(\"/tmp/gh-aw\", exist_ok=True)\n\nMAX_ATTEMPTS = 5\n\ndef api_get(url):\n \"\"\"Make an authenticated GET request to the GitHub API.\"\"\"\n req = urllib.request.Request(url, headers={\n \"Authorization\": f\"token {token}\",\n \"Accept\": \"application/vnd.github.v3+json\",\n })\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read().decode())\n\ndef get_all_open_prs():\n \"\"\"Fetch all open PRs, paginated.\"\"\"\n prs = []\n page = 1\n while True:\n url = f\"https://api.github.com/repos/{repo}/pulls?state=open&per_page=100&page={page}&sort=number&direction=asc\"\n batch = api_get(url)\n if not batch:\n break\n prs.extend(batch)\n if len(batch) < 100:\n break\n page += 1\n return prs\n\ndef get_check_status(pr):\n \"\"\"Get combined CI check status for a PR's head commit.\"\"\"\n head_sha = pr[\"head\"][\"sha\"]\n url = f\"https://api.github.com/repos/{repo}/commits/{head_sha}/status\"\n try:\n status = api_get(url)\n return status.get(\"state\", \"unknown\")\n except Exception as e:\n print(f\" Warning: could not fetch status for PR #{pr['number']}: {e}\")\n return \"unknown\"\n\ndef get_check_runs(pr):\n \"\"\"Get check runs for a PR's head commit.\"\"\"\n head_sha = pr[\"head\"][\"sha\"]\n url = f\"https://api.github.com/repos/{repo}/commits/{head_sha}/check-runs\"\n try:\n data = api_get(url)\n return data.get(\"check_runs\", [])\n except Exception as e:\n print(f\" Warning: could not fetch check runs for PR #{pr['number']}: {e}\")\n return []\n\ndef read_attempt_state(pr_number):\n \"\"\"Read attempt tracking state from repo-memory.\"\"\"\n state_file = os.path.join(repo_memory_dir, f\"pr-{pr_number}.md\")\n if not os.path.isfile(state_file):\n return {\"attempts\": 0, \"head_sha\": None}\n with open(state_file, encoding=\"utf-8\") as f:\n content = f.read()\n state = {\"attempts\": 0, \"head_sha\": None}\n m = re.search(r'\\|\\s*head_sha\\s*\\|\\s*(\\S+)\\s*\\|', content)\n if m:\n state[\"head_sha\"] = m.group(1)\n m = re.search(r'\\|\\s*attempts\\s*\\|\\s*(\\d+)\\s*\\|', content)\n if m:\n state[\"attempts\"] = int(m.group(1))\n return state\n\ndef get_commit_date(sha):\n \"\"\"Return the committer date (ISO 8601) for a given commit SHA, or None.\"\"\"\n url = f\"https://api.github.com/repos/{repo}/commits/{sha}\"\n try:\n data = api_get(url)\n return data.get(\"commit\", {}).get(\"committer\", {}).get(\"date\")\n except Exception as e:\n print(f\" Warning: could not fetch commit {sha[:12]}: {e}\")\n return None\n\ndef is_autoloop_pr(pr):\n \"\"\"Return True if the PR is from an autoloop branch.\n Branch name is the primary gate (labels can be added by anyone on\n public repos); the `autoloop` label is just an additional signal.\"\"\"\n head_ref = pr.get(\"head\", {}).get(\"ref\", \"\") or \"\"\n return head_ref.startswith(\"autoloop/\")\n\ndef get_behind_by(pr):\n \"\"\"Return how many commits the PR base branch is ahead of the PR head.\"\"\"\n base = pr[\"base\"][\"ref\"]\n head_sha = pr[\"head\"][\"sha\"]\n url = f\"https://api.github.com/repos/{repo}/compare/{base}...{head_sha}\"\n try:\n data = api_get(url)\n return int(data.get(\"behind_by\", 0) or 0)\n except Exception as e:\n print(f\" Warning: could not fetch compare for PR #{pr['number']}: {e}\")\n return 0\n\ndef trigger_ci_workflow(branch):\n \"\"\"Trigger CI on `branch` by pushing an empty commit. This fires the\n `push` event on `autoloop/**` branches, which triggers ci.yml without\n needing workflow_dispatch approval. Uses GH_AW_CI_TRIGGER_TOKEN so\n the push is attributed to a real user (pushes via GITHUB_TOKEN don't\n trigger other workflows).\"\"\"\n ci_token = os.environ.get(\"GH_AW_CI_TRIGGER_TOKEN\", \"\") or token\n try:\n # Get current HEAD SHA\n url = f\"https://api.github.com/repos/{repo}/git/ref/heads/{branch}\"\n req = urllib.request.Request(url, headers={\n \"Authorization\": f\"token {ci_token}\",\n \"Accept\": \"application/vnd.github.v3+json\",\n })\n with urllib.request.urlopen(req, timeout=30) as resp:\n head_sha = json.loads(resp.read().decode())[\"object\"][\"sha\"]\n\n # Create an empty commit on top of HEAD\n url = f\"https://api.github.com/repos/{repo}/git/commits\"\n payload = json.dumps({\n \"message\": \"chore: trigger CI [evergreen]\",\n \"tree\": json.loads(urllib.request.urlopen(\n urllib.request.Request(\n f\"https://api.github.com/repos/{repo}/git/commits/{head_sha}\",\n headers={\"Authorization\": f\"token {ci_token}\", \"Accept\": \"application/vnd.github.v3+json\"},\n ), timeout=30\n ).read().decode())[\"tree\"][\"sha\"],\n \"parents\": [head_sha],\n }).encode()\n req = urllib.request.Request(url, data=payload, method=\"POST\", headers={\n \"Authorization\": f\"token {ci_token}\",\n \"Accept\": \"application/vnd.github.v3+json\",\n \"Content-Type\": \"application/json\",\n })\n with urllib.request.urlopen(req, timeout=30) as resp:\n new_sha = json.loads(resp.read().decode())[\"sha\"]\n\n # Update the branch ref to the new commit\n url = f\"https://api.github.com/repos/{repo}/git/refs/heads/{branch}\"\n payload = json.dumps({\"sha\": new_sha}).encode()\n req = urllib.request.Request(url, data=payload, method=\"PATCH\", headers={\n \"Authorization\": f\"token {ci_token}\",\n \"Accept\": \"application/vnd.github.v3+json\",\n \"Content-Type\": \"application/json\",\n })\n with urllib.request.urlopen(req, timeout=30) as resp:\n return 200 <= resp.status < 300\n except urllib.error.HTTPError as e:\n print(f\" Warning: CI trigger (empty commit) failed for {branch}: HTTP {e.code} {e.reason}\")\n return False\n except Exception as e:\n print(f\" Warning: CI trigger (empty commit) failed for {branch}: {e}\")\n return False\n\ndef post_pr_comment(pr_number, body):\n \"\"\"Post a comment on a PR using the issues comments API.\"\"\"\n url = f\"https://api.github.com/repos/{repo}/issues/{pr_number}/comments\"\n payload = json.dumps({\"body\": body}).encode()\n req = urllib.request.Request(url, data=payload, method=\"POST\", headers={\n \"Authorization\": f\"token {token}\",\n \"Accept\": \"application/vnd.github.v3+json\",\n \"Content-Type\": \"application/json\",\n })\n try:\n with urllib.request.urlopen(req, timeout=30) as resp:\n return 200 <= resp.status < 300\n except Exception as e:\n print(f\" Warning: could not post comment on PR #{pr_number}: {e}\")\n return False\n\ndef pr_needs_attention(pr):\n \"\"\"Check if a PR has merge conflicts, is behind main, or has failing CI.\n Returns a list of issues.\"\"\"\n issues = []\n\n # Check mergeable state\n # Need to fetch full PR details for mergeable info\n pr_url = f\"https://api.github.com/repos/{repo}/pulls/{pr['number']}\"\n try:\n full_pr = api_get(pr_url)\n mergeable = full_pr.get(\"mergeable\")\n mergeable_state = full_pr.get(\"mergeable_state\", \"unknown\")\n if mergeable is False:\n issues.append(\"merge_conflict\")\n elif mergeable_state == \"dirty\":\n issues.append(\"merge_conflict\")\n except Exception as e:\n print(f\" Warning: could not fetch mergeable state for PR #{pr['number']}: {e}\")\n\n # Check if the PR branch is behind its base branch (e.g., main moved forward).\n # We always want to merge main first before fixing CI, so flag this explicitly.\n behind_by = get_behind_by(pr)\n if behind_by > 0 and \"merge_conflict\" not in issues:\n issues.append(f\"behind_main: {behind_by} commit(s)\")\n\n # Check CI status via check runs\n check_runs = get_check_runs(pr)\n failed_checks = []\n for cr in check_runs:\n conclusion = cr.get(\"conclusion\")\n status = cr.get(\"status\")\n name = cr.get(\"name\", \"unknown\")\n if conclusion in (\"failure\", \"timed_out\", \"action_required\"):\n failed_checks.append(name)\n elif status == \"completed\" and conclusion not in (\"success\", \"neutral\", \"skipped\"):\n if conclusion is not None:\n failed_checks.append(name)\n if failed_checks:\n issues.append(f\"failing_checks: {', '.join(failed_checks)}\")\n\n # Also check commit status API (some checks use the older status API)\n combined_status = get_check_status(pr)\n if combined_status == \"failure\":\n if not failed_checks:\n issues.append(\"failing_status\")\n\n # Detect missing/stale CI for autoloop PRs.\n # Pushes via GITHUB_TOKEN don't trigger workflows, so autoloop PRs\n # can sit indefinitely with no checks. Only autoloop branches are\n # eligible — never trigger CI automatically on outside-contributor PRs.\n if is_autoloop_pr(pr):\n completed_runs = [cr for cr in check_runs if cr.get(\"status\") == \"completed\"]\n # No check runs at all on this HEAD SHA\n if not check_runs:\n issues.append(\"missing_checks: no check runs on HEAD\")\n # All runs are still queued / in_progress and the HEAD has been\n # sitting around for a while — likely a stuck/missing trigger.\n elif not completed_runs:\n head_date = get_commit_date(pr[\"head\"][\"sha\"])\n if head_date:\n # If HEAD is older than 15 minutes and nothing has completed,\n # treat it as missing (a real CI run would have started by now).\n try:\n from datetime import datetime, timezone\n ht = datetime.fromisoformat(head_date.replace(\"Z\", \"+00:00\"))\n age_s = (datetime.now(timezone.utc) - ht).total_seconds()\n if age_s > 15 * 60:\n issues.append(\"missing_checks: only queued/in-progress checks on HEAD\")\n except Exception:\n pass\n\n return issues\n\n# --- Main logic ---\n\nprint(\"=== Evergreen PR Health Check ===\")\nprint(f\"Repository: {repo}\")\n\nprs = get_all_open_prs()\nprint(f\"Found {len(prs)} open PR(s)\")\n\nif not prs:\n print(\"No open PRs. Nothing to do.\")\n with open(output_file, \"w\") as f:\n json.dump({\"selected\": None, \"reason\": \"no_open_prs\"}, f)\n emit_selected_output(None)\n sys.exit(0)\n\n# Evaluate each PR deterministically (sorted by PR number ascending)\ncandidates = []\nskipped = []\nci_triggered = []\n\n# If a specific PR is forced, only check that one\nif forced_pr:\n prs = [pr for pr in prs if str(pr[\"number\"]) == forced_pr]\n if not prs:\n print(f\"ERROR: PR #{forced_pr} not found among open PRs.\")\n sys.exit(1)\n print(f\"FORCED: checking only PR #{forced_pr}\")\n\nfor pr in sorted(prs, key=lambda p: p[\"number\"]):\n pr_num = pr[\"number\"]\n head_sha = pr[\"head\"][\"sha\"]\n print(f\"\\nChecking PR #{pr_num}: {pr['title'][:60]}...\")\n print(f\" Head SHA: {head_sha[:12]}\")\n\n issues = pr_needs_attention(pr)\n if not issues:\n print(f\" Status: healthy (no issues)\")\n continue\n\n print(f\" Issues: {issues}\")\n\n # Handle `missing_checks` for autoloop PRs directly in the pre-flight,\n # without invoking the agent. The action is purely an API dispatch —\n # no code fix is needed — and keeping the privileged CI trigger token\n # out of the agent context is a security win. We only do this for\n # autoloop branches (the detector enforces this), and only if\n # `missing_checks` is the *only* issue: any other issue (merge\n # conflict, behind main, failing checks) still needs the agent.\n if (\n len(issues) == 1\n and issues[0].startswith(\"missing_checks\")\n and is_autoloop_pr(pr)\n ):\n branch = pr[\"head\"][\"ref\"]\n # Cap retries on the same SHA so we don't spam-dispatch on a\n # truly-broken workflow.\n attempt_state = read_attempt_state(pr_num)\n prior_attempts = (\n attempt_state[\"attempts\"] if attempt_state[\"head_sha\"] == head_sha else 0\n )\n if prior_attempts >= MAX_ATTEMPTS:\n skipped.append({\n \"pr\": pr_num,\n \"reason\": (\n f\"missing_checks: max dispatch attempts ({MAX_ATTEMPTS}) \"\n f\"reached on SHA {head_sha[:12]}\"\n ),\n })\n print(f\" SKIPPED: max missing_checks attempts reached\")\n continue\n print(f\" Triggering ci.yml on branch {branch} (attempt {prior_attempts + 1}/{MAX_ATTEMPTS})\")\n ok = trigger_ci_workflow(branch)\n if ok:\n print(f\" ✓ Dispatched ci.yml on {branch}\")\n workflow_url = (\n f\"https://github.com/{repo}/actions/workflows/ci.yml\"\n f\"?query=branch%3A{branch}\"\n )\n post_pr_comment(\n pr_num,\n (\n \"Evergreen: this PR's HEAD had no completed CI checks, \"\n \"so I dispatched the `ci.yml` workflow on this branch. \"\n f\"See [recent CI runs]({workflow_url}).\\n\\n\"\n \"_(Triggered automatically because pushes via `GITHUB_TOKEN` \"\n \"do not start workflows.)_\"\n ),\n )\n ci_triggered.append({\n \"pr\": pr_num,\n \"branch\": branch,\n \"head_sha\": head_sha,\n })\n # Persist attempt count so we eventually give up if dispatching\n # never produces check runs.\n os.makedirs(repo_memory_dir, exist_ok=True)\n state_path = os.path.join(repo_memory_dir, f\"pr-{pr_num}.md\")\n from datetime import datetime, timezone\n ts = datetime.now(timezone.utc).strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n with open(state_path, \"w\", encoding=\"utf-8\") as sf:\n sf.write(\n f\"# Evergreen: PR #{pr_num}\\n\\n\"\n f\"## State\\n\\n\"\n f\"| Field | Value |\\n\"\n f\"|:---|:---|\\n\"\n f\"| head_sha | {head_sha} |\\n\"\n f\"| attempts | {prior_attempts + 1} |\\n\"\n f\"| last_run | {ts} |\\n\"\n f\"| last_result | ci_dispatched |\\n\"\n )\n else:\n print(f\" ✗ Failed to dispatch ci.yml on {branch}\")\n skipped.append({\n \"pr\": pr_num,\n \"reason\": \"missing_checks: workflow_dispatch API call failed\",\n })\n # Do NOT add to candidates — the agent has nothing to fix here.\n continue\n\n # Check attempt tracking\n attempt_state = read_attempt_state(pr_num)\n if attempt_state[\"head_sha\"] == head_sha:\n attempts = attempt_state[\"attempts\"]\n print(f\" Attempts on this SHA: {attempts}/{MAX_ATTEMPTS}\")\n if attempts >= MAX_ATTEMPTS:\n skipped.append({\n \"pr\": pr_num,\n \"reason\": f\"max attempts ({MAX_ATTEMPTS}) reached on SHA {head_sha[:12]}\",\n })\n print(f\" SKIPPED: max attempts reached\")\n continue\n else:\n attempts = 0\n print(f\" New SHA detected — resetting attempt counter\")\n\n candidates.append({\n \"pr_number\": pr_num,\n \"title\": pr[\"title\"],\n \"head_sha\": head_sha,\n \"base_branch\": pr[\"base\"][\"ref\"],\n \"head_branch\": pr[\"head\"][\"ref\"],\n \"issues\": issues,\n \"attempts\": attempts,\n })\n\n# Select the first candidate (lowest PR number — deterministic)\nselected = candidates[0] if candidates else None\n\nresult = {\n \"selected\": selected,\n \"skipped\": skipped,\n \"ci_triggered\": ci_triggered,\n \"total_open_prs\": len(prs),\n \"candidates_found\": len(candidates),\n}\n\nwith open(output_file, \"w\") as f:\n json.dump(result, f, indent=2)\n\nif selected:\n branch = selected[\"head_branch\"]\n print(f\"Checking out PR branch before agent run: {branch}\")\n subprocess.check_call([\"git\", \"checkout\", \"-B\", branch, f\"origin/{branch}\"])\n subprocess.check_call([\"git\", \"branch\", \"--set-upstream-to\", f\"origin/{branch}\", branch])\n print(f\"\\n>>> Selected PR #{selected['pr_number']}: {selected['title']}\")\n print(f\" Issues: {selected['issues']}\")\n print(f\" Attempt: {selected['attempts'] + 1}/{MAX_ATTEMPTS}\")\n emit_selected_output(selected[\"pr_number\"])\nelse:\n print(\"\\nNo PRs need attention. Nothing to do.\")\n emit_selected_output(None)\n sys.exit(0)\nPYEOF\n" + INPUT_PR: ${{ github.event.inputs.pr_number }} + id: select-pr + name: Select Evergreen target PR + run: "python3 - << 'PYEOF'\nimport json\nimport os\nimport urllib.error\nimport urllib.parse\nimport urllib.request\n\ntoken = os.environ[\"GH_TOKEN\"]\nrepo = os.environ[\"GITHUB_REPOSITORY\"]\nevent_name = os.environ.get(\"EVENT_NAME\", \"\")\ninput_pr = (os.environ.get(\"INPUT_PR\") or \"\").strip()\nevent_path = os.environ.get(\"EVENT_PATH\", \"\")\nout_dir = \"/tmp/gh-aw/evergreen\"\nos.makedirs(out_dir, exist_ok=True)\n\ndef api(path):\n req = urllib.request.Request(\n f\"https://api.github.com/repos/{repo}{path}\",\n headers={\n \"Authorization\": f\"Bearer {token}\",\n \"Accept\": \"application/vnd.github+json\",\n \"X-GitHub-Api-Version\": \"2022-11-28\",\n },\n )\n with urllib.request.urlopen(req, timeout=30) as resp:\n return json.loads(resp.read().decode())\n\ndef pr_labels(pr):\n return [label[\"name\"] for label in pr.get(\"labels\", [])]\n\ndef load_event():\n if not event_path or not os.path.exists(event_path):\n return {}\n with open(event_path, encoding=\"utf-8\") as f:\n return json.load(f)\n\ntarget = None\nreason = \"\"\nevent = load_event()\n\ntry:\n if input_pr:\n pr = api(f\"/pulls/{int(input_pr)}\")\n labels = pr_labels(pr)\n if pr.get(\"state\") == \"open\" and \"evergreen\" in labels:\n target = pr\n reason = \"manual dispatch\"\n else:\n reason = \"manual PR is not open with evergreen label\"\n elif event_name == \"pull_request\" and event.get(\"pull_request\"):\n number = event[\"pull_request\"][\"number\"]\n pr = api(f\"/pulls/{number}\")\n labels = pr_labels(pr)\n if pr.get(\"state\") == \"open\" and \"evergreen\" in labels:\n target = pr\n reason = \"pull_request event\"\n else:\n reason = \"pull_request event without evergreen label\"\n else:\n params = urllib.parse.urlencode({\n \"state\": \"open\",\n \"labels\": \"evergreen\",\n \"per_page\": \"50\",\n \"sort\": \"updated\",\n \"direction\": \"asc\",\n })\n issues = api(f\"/issues?{params}\")\n candidates = [issue for issue in issues if \"pull_request\" in issue]\n for issue in candidates:\n labels = [label[\"name\"] for label in issue.get(\"labels\", [])]\n if \"evergreen-exhausted\" not in labels:\n target = api(f\"/pulls/{issue['number']}\")\n reason = \"scheduled oldest labeled PR\"\n break\n if target is None:\n reason = \"no open non-exhausted PR has evergreen label\"\nexcept (ValueError, urllib.error.HTTPError, urllib.error.URLError) as exc:\n reason = f\"target selection failed: {exc}\"\n\npayload = {\n \"selected\": target is not None,\n \"reason\": reason,\n \"event_name\": event_name,\n \"target_pr_number\": target[\"number\"] if target else None,\n \"target_head_sha\": target.get(\"head\", {}).get(\"sha\") if target else None,\n \"target_head_ref\": target.get(\"head\", {}).get(\"ref\") if target else None,\n \"target_base_ref\": target.get(\"base\", {}).get(\"ref\") if target else None,\n}\nwith open(f\"{out_dir}/target.json\", \"w\", encoding=\"utf-8\") as f:\n json.dump(payload, f, indent=2)\n\ngh_output = os.environ.get(\"GITHUB_OUTPUT\")\nif gh_output:\n with open(gh_output, \"a\", encoding=\"utf-8\") as f:\n f.write(f\"selected={payload['target_pr_number'] or ''}\\n\")\nprint(json.dumps(payload, indent=2))\nPYEOF\n" # Repo memory git-based storage configuration from frontmatter processed below - name: Clone repo-memory branch (default) @@ -534,25 +615,38 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - name: Download container images - run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.0 ghcr.io/github/gh-aw-firewall/squid:0.27.0 ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.0 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.0 ghcr.io/github/gh-aw-firewall/cli-proxy:0.27.0 ghcr.io/github/gh-aw-firewall/squid:0.27.0 ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14 - name: Generate Safe Outputs Config run: | mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_cf6418e23a028cfc_EOF' - {"add_comment":{"max":3,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":102400,"max_patch_size":10240}]},"push_to_pull_request_branch":{"if_no_changes":"warn","max":3,"max_patch_size":1024,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"allowed","signed_commits":false,"target":"*"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_cf6418e23a028cfc_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_f7fa8d558a2a1e30_EOF' + {"add_comment":{"hide_older_comments":false,"max":2,"required_labels":["evergreen"],"target":"*"},"add_labels":{"allowed":["evergreen-ready","evergreen-blocked","evergreen-human-needed","evergreen-exhausted"],"max":4,"target":"*"},"create_report_incomplete_issue":{},"dispatch_workflow":{"max":1,"workflow_files":{"CI":".yml"},"workflows":["CI"]},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"push_repo_memory":{"memories":[{"dir":"/tmp/gh-aw/repo-memory/default","id":"default","max_file_count":100,"max_file_size":40960,"max_patch_size":10240}]},"push_to_pull_request_branch":{"allowed_files":["src/**","tests/**","tests-e2e/**","playground/**","golden/**","scripts/**","benchmarks/**","docs/**"],"excluded_files":["README.md",".autoloop/programs/**",".github/workflows/**",".github/aw/**","package.json","bun.lock","tsconfig.json","biome.json","bunfig.toml"],"if_no_changes":"warn","max":2,"max_patch_size":10240,"protect_top_level_dot_folders":true,"protected_files":["bun.lockb","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","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"fallback-to-issue","required_labels":["evergreen"],"signed_commits":false,"target":"*"},"remove_labels":{"allowed":["evergreen","evergreen-ready","evergreen-blocked","evergreen-human-needed","evergreen-exhausted"],"max":5,"target":"*"},"report_incomplete":{}} + GH_AW_SAFE_OUTPUTS_CONFIG_f7fa8d558a2a1e30_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { - "add_comment": " CONSTRAINTS: Maximum 3 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", - "push_to_pull_request_branch": " CONSTRAINTS: Maximum 3 push(es) can be made." + "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", + "add_labels": " CONSTRAINTS: Maximum 4 label(s) can be added. Only these labels are allowed: [\"evergreen-ready\" \"evergreen-blocked\" \"evergreen-human-needed\" \"evergreen-exhausted\"]. Target: *.", + "push_to_pull_request_branch": " CONSTRAINTS: Maximum 2 push(es) can be made.", + "remove_labels": " CONSTRAINTS: Maximum 5 label(s) can be removed. Only these labels can be removed: [evergreen evergreen-ready evergreen-blocked evergreen-human-needed evergreen-exhausted]. Target: *." }, "repo_params": {}, - "dynamic_tools": [] + "dynamic_tools": [ + { + "_workflow_name": "CI", + "description": "Dispatch the 'CI' 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": {}, + "type": "object" + }, + "name": "CI" + } + ] } GH_AW_VALIDATION_JSON: | { @@ -578,6 +672,25 @@ jobs: } } }, + "add_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, "missing_data": { "defaultMax": 20, "fields": { @@ -655,6 +768,25 @@ jobs: } } }, + "remove_labels": { + "defaultMax": 5, + "fields": { + "item_number": { + "issueNumberOrTemporaryId": true + }, + "labels": { + "required": true, + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 128 + }, + "repo": { + "type": "string", + "maxLength": 256 + } + } + }, "report_incomplete": { "defaultMax": 5, "fields": { @@ -725,9 +857,6 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.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_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }} - GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }} - 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 "${RUNNER_TEMP}/gh-aw/mcp-config" @@ -757,25 +886,9 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_b09e2819f524bcf0_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_01ac58299f295f38_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { - "github": { - "type": "stdio", - "container": "ghcr.io/github/github-mcp-server:v1.1.2", - "env": { - "GITHUB_HOST": "\${GITHUB_SERVER_URL}", - "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}", - "GITHUB_READ_ONLY": "1", - "GITHUB_TOOLSETS": "repos,pull_requests,issues,actions" - }, - "guard-policies": { - "allow-only": { - "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", - "repos": "$GITHUB_MCP_GUARD_REPOS" - } - } - }, "safeoutputs": { "type": "http", "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT", @@ -798,7 +911,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_b09e2819f524bcf0_EOF + GH_AW_MCP_CONFIG_01ac58299f295f38_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -820,6 +933,14 @@ jobs: id: pre_agent_audit continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" + - name: Start CLI Proxy + env: + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + GITHUB_SERVER_URL: ${{ github.server_url }} + CLI_PROXY_POLICY: '{"allow-only":{"repos":"all","min-integrity":"none"}}' + CLI_PROXY_IMAGE: 'ghcr.io/github/gh-aw-mcpg:v0.3.25' + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/start_cli_proxy.sh" - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): @@ -836,7 +957,7 @@ jobs: export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) GH_AW_MAX_AI_CREDITS="${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.0/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.npms.io\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"bun.sh\",\"cdn.jsdelivr.net\",\"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\",\"deb.nodesource.com\",\"deno.land\",\"esm.sh\",\"get.pnpm.io\",\"github.com\",\"googleapis.deno.dev\",\"googlechromelabs.github.io\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"jsr.io\",\"keyserver.ubuntu.com\",\"nodejs.org\",\"npm.pkg.github.com\",\"npmjs.com\",\"npmjs.org\",\"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.bower.io\",\"registry.npmjs.com\",\"registry.npmjs.org\",\"registry.yarnpkg.com\",\"releaseassets.githubusercontent.com\",\"repo.yarnpkg.com\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"skimdb.npmjs.com\",\"storage.googleapis.com\",\"telemetry.enterprise.githubcopilot.com\",\"telemetry.vercel.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\",\"www.npmjs.com\",\"www.npmjs.org\",\"yarnpkg.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.0/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"*.pythonhosted.org\",\"anaconda.org\",\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"api.npms.io\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"binstar.org\",\"bootstrap.pypa.io\",\"bun.sh\",\"cdn.jsdelivr.net\",\"cdn.playwright.dev\",\"codeload.github.com\",\"conda.anaconda.org\",\"conda.binstar.org\",\"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\",\"deb.nodesource.com\",\"deno.land\",\"docs.github.com\",\"esm.sh\",\"files.pythonhosted.org\",\"get.pnpm.io\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"googleapis.deno.dev\",\"googlechromelabs.github.io\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"jsr.io\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"nodejs.org\",\"npm.pkg.github.com\",\"npmjs.com\",\"npmjs.org\",\"objects.githubusercontent.com\",\"ocsp.digicert.com\",\"ocsp.geotrust.com\",\"ocsp.globalsign.com\",\"ocsp.identrust.com\",\"ocsp.sectigo.com\",\"ocsp.ssl.com\",\"ocsp.thawte.com\",\"ocsp.usertrust.com\",\"ocsp.verisign.com\",\"packagecloud.io\",\"packages.cloud.google.com\",\"packages.microsoft.com\",\"patch-diff.githubusercontent.com\",\"pip.pypa.io\",\"playwright.download.prss.microsoft.com\",\"ppa.launchpad.net\",\"pypi.org\",\"pypi.python.org\",\"raw.githubusercontent.com\",\"registry.bower.io\",\"registry.npmjs.com\",\"registry.npmjs.org\",\"registry.yarnpkg.com\",\"repo.anaconda.com\",\"repo.continuum.io\",\"repo.yarnpkg.com\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"skimdb.npmjs.com\",\"storage.googleapis.com\",\"telemetry.enterprise.githubcopilot.com\",\"telemetry.vercel.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\",\"www.npmjs.com\",\"www.npmjs.org\",\"yarnpkg.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\"],\"computer-use\":[\"copilot/*computer-use*\",\"google/*computer-use*\",\"gemini/*computer-use*\",\"openai/*computer-use*\"],\"copilot\":[\"agent\"],\"deep-research\":[\"copilot/deep-research*\",\"copilot/o3-deep-research*\",\"copilot/o4-mini-deep-research*\",\"google/deep-research*\",\"gemini/deep-research*\",\"openai/o3-deep-research*\",\"openai/o4-mini-deep-research*\"],\"gemini\":[\"agent\"],\"gemini-3-flash\":[\"copilot/gemini-3*flash*\",\"google/gemini-3*flash*\",\"gemini/gemini-3*flash*\"],\"gemini-3-pro\":[\"copilot/gemini-3*pro*\",\"google/gemini-3*pro*\",\"google/nano-banana*\",\"gemini/gemini-3*pro*\"],\"gemini-3.1-flash\":[\"copilot/gemini-3.1*flash*\",\"google/gemini-3.1*flash*\",\"gemini/gemini-3.1*flash*\"],\"gemini-3.1-pro\":[\"copilot/gemini-3.1*pro*\",\"google/gemini-3.1*pro*\",\"gemini/gemini-3.1*pro*\"],\"gemini-3.5-flash\":[\"copilot/gemini-3.5*flash*\",\"google/gemini-3.5*flash*\",\"gemini/gemini-3.5*flash*\"],\"gemini-flash\":[\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"],\"gemini-flash-lite\":[\"copilot/gemini-*flash*lite*\",\"google/gemini-*flash*lite*\",\"gemini/gemini-*flash*lite*\"],\"gemini-pro\":[\"copilot/gemini-*pro*\",\"google/gemini-*pro*\",\"gemini/gemini-*pro*\"],\"gemma\":[\"copilot/gemma*\",\"google/gemma*\",\"gemini/gemma*\"],\"gpt-5\":[\"copilot/gpt-5*\",\"openai/gpt-5*\"],\"gpt-5-codex\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\"],\"gpt-5-mini\":[\"copilot/gpt-5*mini*\",\"openai/gpt-5*mini*\"],\"gpt-5-nano\":[\"copilot/gpt-5*nano*\",\"openai/gpt-5*nano*\"],\"gpt-5-pro\":[\"copilot/gpt-5*pro*\",\"openai/gpt-5*pro*\"],\"gpt-5.2\":[\"copilot/gpt-5.2*\",\"openai/gpt-5.2*\"],\"gpt-5.3\":[\"copilot/gpt-5.3*\",\"openai/gpt-5.3*\"],\"gpt-5.4\":[\"copilot/gpt-5.4*\",\"openai/gpt-5.4*\"],\"gpt-5.5\":[\"copilot/gpt-5.5*\",\"openai/gpt-5.5*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"vision\":[\"copilot/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.0\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" @@ -854,7 +975,7 @@ jobs: GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro" fi # shellcheck disable=SC1003 - sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \ + sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GH_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull --difc-proxy-host host.docker.internal:18443 --difc-proxy-ca-cert /tmp/gh-aw/difc-proxy-tls/ca.crt \ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; GH_AW_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log env: AWF_REFLECT_ENABLED: 1 @@ -869,6 +990,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 360 GH_AW_VERSION: v0.79.4 + GH_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || github.token }} GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -884,6 +1006,10 @@ jobs: GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} XDG_CONFIG_HOME: /home/runner + - name: Stop CLI Proxy + if: always() + continue-on-error: true + run: bash "${RUNNER_TEMP}/gh-aw/actions/stop_cli_proxy.sh" - name: Detect agent errors if: always() id: detect-agent-errors @@ -925,9 +1051,8 @@ jobs: const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); await main(); env: - GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_CI_TRIGGER_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' + 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_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_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 }} @@ -947,7 +1072,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.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.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,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,deb.nodesource.com,deno.land,esm.sh,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,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.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,releaseassets.githubusercontent.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,binstar.org,bootstrap.pypa.io,bun.sh,cdn.jsdelivr.net,cdn.playwright.dev,codeload.github.com,conda.anaconda.org,conda.binstar.org,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,deb.nodesource.com,deno.land,docs.github.com,esm.sh,files.pythonhosted.org,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,pip.pypa.io,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} with: @@ -1071,6 +1196,7 @@ jobs: needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_effective_workflow_exceeded == 'true') runs-on: ubuntu-slim permissions: + actions: write contents: write discussions: write issues: write @@ -1094,10 +1220,11 @@ jobs: trace-id: ${{ needs.activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Evergreen — PR Health Keeper" + GH_AW_SETUP_WORKFLOW_NAME: "Evergreen" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/evergreen.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AWF_VERSION: "v0.27.0" + GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1153,8 +1280,8 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_NOOP_MAX: "1" - GH_AW_WORKFLOW_NAME: "Evergreen — PR Health Keeper" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/evergreen.md" + GH_AW_WORKFLOW_NAME: "Evergreen" + GH_AW_WORKFLOW_SOURCE: "githubnext/evergreen" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }} GH_AW_NOOP_REPORT_AS_ISSUE: "true" @@ -1174,8 +1301,8 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Evergreen — PR Health Keeper" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/evergreen.md" + GH_AW_WORKFLOW_NAME: "Evergreen" + GH_AW_WORKFLOW_SOURCE: "githubnext/evergreen" GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} @@ -1192,8 +1319,8 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_MISSING_TOOL_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "Evergreen — PR Health Keeper" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/evergreen.md" + GH_AW_WORKFLOW_NAME: "Evergreen" + GH_AW_WORKFLOW_SOURCE: "githubnext/evergreen" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1207,8 +1334,8 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true" - GH_AW_WORKFLOW_NAME: "Evergreen — PR Health Keeper" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/evergreen.md" + GH_AW_WORKFLOW_NAME: "Evergreen" + GH_AW_WORKFLOW_SOURCE: "githubnext/evergreen" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | @@ -1222,8 +1349,8 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} - GH_AW_WORKFLOW_NAME: "Evergreen — PR Health Keeper" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/evergreen.md" + GH_AW_WORKFLOW_NAME: "Evergreen" + GH_AW_WORKFLOW_SOURCE: "githubnext/evergreen" 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: "evergreen" @@ -1288,10 +1415,11 @@ jobs: trace-id: ${{ needs.activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Evergreen — PR Health Keeper" + GH_AW_SETUP_WORKFLOW_NAME: "Evergreen" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/evergreen.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AWF_VERSION: "v0.27.0" + GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1361,8 +1489,8 @@ jobs: if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - WORKFLOW_NAME: "Evergreen — PR Health Keeper" - WORKFLOW_DESCRIPTION: "Evergreen — keeps pull requests healthy by automatically fixing merge conflicts\nand failing CI checks. Runs on a short schedule, deterministically selects one\nPR per run, and gives up after 5 attempts that don't improve the same repo state." + WORKFLOW_NAME: "Evergreen" + WORKFLOW_DESCRIPTION: "Evergreen keeps opt-in pull requests green and merge-ready. A persistent\n`evergreen` label grants permission to diagnose merge blockers, repair CI\nfailures, update the PR branch, manage Evergreen state labels, and report\nreadiness without directly merging." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | @@ -1502,6 +1630,44 @@ jobs: } } + pre_activation: + if: > + (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'evergreen')) && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id) + runs-on: ubuntu-slim + outputs: + activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} + matched_command: '' + setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }} + setup-span-id: ${{ steps.setup.outputs.span-id }} + setup-trace-id: ${{ steps.setup.outputs.trace-id }} + steps: + - name: Setup Scripts + id: setup + uses: github/gh-aw-actions/setup@d059700c6a8ec3b5fd798b9ea60f5d048447b918 # v0.79.4 + with: + destination: ${{ runner.temp }}/gh-aw/actions + job-name: ${{ github.job }} + env: + GH_AW_SETUP_WORKFLOW_NAME: "Evergreen" + GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/evergreen.lock.yml@${{ github.ref }} + GH_AW_INFO_VERSION: "1.0.60" + GH_AW_INFO_AWF_VERSION: "v0.27.0" + GH_AW_INFO_BODY_MODIFIED: "false" + GH_AW_INFO_ENGINE_ID: "copilot" + - name: Check team membership for workflow + id: check_membership + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_REQUIRED_ROLES: "admin,maintainer,write" + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + await main(); + push_repo_memory: needs: - activation @@ -1530,10 +1696,11 @@ jobs: trace-id: ${{ needs.activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Evergreen — PR Health Keeper" + GH_AW_SETUP_WORKFLOW_NAME: "Evergreen" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/evergreen.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AWF_VERSION: "v0.27.0" + GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" - name: Checkout repository uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -1571,11 +1738,11 @@ jobs: MEMORY_ID: default TARGET_REPO: ${{ github.repository }} BRANCH_NAME: memory/evergreen - MAX_FILE_SIZE: 102400 + MAX_FILE_SIZE: 40960 MAX_FILE_COUNT: 100 MAX_PATCH_SIZE: 10240 ALLOWED_EXTENSIONS: '[]' - FILE_GLOB_FILTER: "*.md" + FILE_GLOB_FILTER: "*.json *.jsonl *.md" with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); @@ -1591,6 +1758,7 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: + actions: write contents: write discussions: write issues: write @@ -1609,8 +1777,8 @@ jobs: GH_AW_ENGINE_VERSION: "1.0.60" GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "evergreen" - GH_AW_WORKFLOW_NAME: "Evergreen — PR Health Keeper" - GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/evergreen.md" + GH_AW_WORKFLOW_NAME: "Evergreen" + GH_AW_WORKFLOW_SOURCE: "githubnext/evergreen" outputs: 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 }} @@ -1632,10 +1800,11 @@ jobs: trace-id: ${{ needs.activation.outputs.setup-trace-id }} parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }} env: - GH_AW_SETUP_WORKFLOW_NAME: "Evergreen — PR Health Keeper" + GH_AW_SETUP_WORKFLOW_NAME: "Evergreen" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/evergreen.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.60" GH_AW_INFO_AWF_VERSION: "v0.27.0" + GH_AW_INFO_BODY_MODIFIED: "false" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output @@ -1713,10 +1882,10 @@ jobs: env: GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }} GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} - GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,bun.sh,cdn.jsdelivr.net,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,deb.nodesource.com,deno.land,esm.sh,get.pnpm.io,github.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,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.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,releaseassets.githubusercontent.com,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,*.pythonhosted.org,anaconda.org,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.npms.io,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,binstar.org,bootstrap.pypa.io,bun.sh,cdn.jsdelivr.net,cdn.playwright.dev,codeload.github.com,conda.anaconda.org,conda.binstar.org,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,deb.nodesource.com,deno.land,docs.github.com,esm.sh,files.pythonhosted.org,get.pnpm.io,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,googleapis.deno.dev,googlechromelabs.github.io,host.docker.internal,json-schema.org,json.schemastore.org,jsr.io,keyserver.ubuntu.com,lfs.github.com,nodejs.org,npm.pkg.github.com,npmjs.com,npmjs.org,objects.githubusercontent.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,patch-diff.githubusercontent.com,pip.pypa.io,playwright.download.prss.microsoft.com,ppa.launchpad.net,pypi.org,pypi.python.org,raw.githubusercontent.com,registry.bower.io,registry.npmjs.com,registry.npmjs.org,registry.yarnpkg.com,repo.anaconda.com,repo.continuum.io,repo.yarnpkg.com,s.symcb.com,s.symcd.com,security.ubuntu.com,skimdb.npmjs.com,storage.googleapis.com,telemetry.enterprise.githubcopilot.com,telemetry.vercel.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com,www.npmjs.com,www.npmjs.org,yarnpkg.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":3,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max\":3,\"max_patch_size\":1024,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"allowed\",\"signed_commits\":false,\"target\":\"*\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":false,\"max\":2,\"required_labels\":[\"evergreen\"],\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"evergreen-ready\",\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\"],\"max\":4,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"dispatch_workflow\":{\"max\":1,\"workflow_files\":{\"CI\":\".yml\"},\"workflows\":[\"CI\"]},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"push_to_pull_request_branch\":{\"allowed_files\":[\"src/**\",\"tests/**\",\"tests-e2e/**\",\"playground/**\",\"golden/**\",\"scripts/**\",\"benchmarks/**\",\"docs/**\"],\"excluded_files\":[\"README.md\",\".autoloop/programs/**\",\".github/workflows/**\",\".github/aw/**\",\"package.json\",\"bun.lock\",\"tsconfig.json\",\"biome.json\",\"bunfig.toml\"],\"if_no_changes\":\"warn\",\"max\":2,\"max_patch_size\":10240,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"bun.lockb\",\"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\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"fallback-to-issue\",\"required_labels\":[\"evergreen\"],\"signed_commits\":false,\"target\":\"*\"},\"remove_labels\":{\"allowed\":[\"evergreen\",\"evergreen-ready\",\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\"],\"max\":5,\"target\":\"*\"},\"report_incomplete\":{}}" 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/evergreen.md b/.github/workflows/evergreen.md index 4e31155d..0efe39d7 100644 --- a/.github/workflows/evergreen.md +++ b/.github/workflows/evergreen.md @@ -1,603 +1,297 @@ --- description: | - Evergreen — keeps pull requests healthy by automatically fixing merge conflicts - and failing CI checks. Runs on a short schedule, deterministically selects one - PR per run, and gives up after 5 attempts that don't improve the same repo state. + Evergreen keeps opt-in pull requests green and merge-ready. A persistent + `evergreen` label grants permission to diagnose merge blockers, repair CI + failures, update the PR branch, manage Evergreen state labels, and report + readiness without directly merging. on: - schedule: every 5m + pull_request: + types: [labeled, synchronize, reopened, ready_for_review] + schedule: every 15m workflow_dispatch: inputs: pr_number: - description: "Fix a specific PR by number (bypasses scheduling)" + description: "Run Evergreen on a specific PR number" required: false type: string -permissions: read-all +if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'evergreen') }} + +permissions: + contents: read + pull-requests: read + issues: read + actions: read + checks: read + statuses: read timeout-minutes: 360 +max-daily-ai-credits: 200K network: allowed: - - defaults - - node - - releaseassets.githubusercontent.com - -safe-outputs: - push-to-pull-request-branch: - signed-commits: false - target: "*" - max: 3 - protected-files: allowed - add-comment: - max: 3 - target: "*" + - defaults + - github + - node + - python + - playwright checkout: fetch: ["*"] fetch-depth: 0 +concurrency: + group: evergreen-${{ github.event.pull_request.number || github.event.inputs.pr_number || 'schedule' }} + queue: max + tools: github: + mode: gh-proxy toolsets: [repos, pull_requests, issues, actions] bash: true repo-memory: branch-name: memory/evergreen - file-glob: ["*.md"] + file-glob: ["*.json", "*.jsonl", "*.md"] + max-file-size: 40960 + +safe-outputs: + max-patch-size: 10240 + max-patch-files: 200 + add-comment: + max: 2 + target: "*" + required-labels: [evergreen] + hide-older-comments: false + add-labels: + allowed: + - evergreen-ready + - evergreen-blocked + - evergreen-human-needed + - evergreen-exhausted + target: "*" + max: 4 + remove-labels: + allowed: + - evergreen + - evergreen-ready + - evergreen-blocked + - evergreen-human-needed + - evergreen-exhausted + target: "*" + max: 5 + dispatch-workflow: + workflows: [CI] + max: 1 + push-to-pull-request-branch: + target: "*" + required-labels: [evergreen] + signed-commits: false + github-token-for-extra-empty-commit: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + allowed-files: + - "src/**" + - "tests/**" + - "tests-e2e/**" + - "playground/**" + - "golden/**" + - "scripts/**" + - "benchmarks/**" + - "docs/**" + excluded-files: + - "README.md" + - ".autoloop/programs/**" + - ".github/workflows/**" + - ".github/aw/**" + - "package.json" + - "bun.lock" + - "tsconfig.json" + - "biome.json" + - "bunfig.toml" + protected-files: + policy: fallback-to-issue + exclude: + - "README.md" + - ".autoloop/programs/**" + - ".github/workflows/**" + - ".github/aw/**" + - "package.json" + - "bun.lock" + - "tsconfig.json" + - "biome.json" + - "bunfig.toml" + max: 2 imports: - - shared/reporting.md + - shared/skills/pr-intake.md + - shared/skills/repo-memory-reader.md + - shared/skills/diff-risk-map.md + - shared/skills/ci-run-deduper.md + - shared/skills/ci-gate-evaluator.md + - shared/skills/ci-log-parser.md + - shared/skills/merge-blocker-comment-reader.md + - shared/skills/deterministic-repair.md + - shared/skills/safe-output-verifier.md + - shared/skills/attempt-memory-writer.md + - shared/skills/merge-gate-reporter.md + - shared/skills/infra-ci-repair.md + - shared/skills/playground-e2e-diagnoser.md + - shared/skills/autoloop-coordinator.md + - shared/skills/docs-release-gate-repair.md + - shared/skills/dependency-gate-repair.md + - shared/skills/lint-policy-review.md + - shared/evergreen/repo-policy.md + - shared/evergreen/state-labels.md + - shared/evergreen/safe-output-policy.md + - shared/evergreen/report-template.md steps: - - name: Find a PR that needs attention - id: find-pr + - name: Select Evergreen target PR + id: select-pr env: - GITHUB_TOKEN: ${{ github.token }} - GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} + GH_TOKEN: ${{ github.token }} GITHUB_REPOSITORY: ${{ github.repository }} - FORCED_PR: ${{ github.event.inputs.pr_number }} + EVENT_NAME: ${{ github.event_name }} + EVENT_PATH: ${{ github.event_path }} + INPUT_PR: ${{ github.event.inputs.pr_number }} run: | python3 - << 'PYEOF' - import os, json, re, subprocess, sys - import urllib.request, urllib.error - - def emit_selected_output(pr_number): - """Expose `selected` as a step output for workflow gating. - Empty string means no PR needs attention; otherwise the PR number.""" - gh_output = os.environ.get("GITHUB_OUTPUT") - if gh_output: - with open(gh_output, "a") as f: - f.write(f"selected={'' if pr_number is None else pr_number}\n") - - token = os.environ.get("GITHUB_TOKEN", "") - repo = os.environ.get("GITHUB_REPOSITORY", "") - forced_pr = os.environ.get("FORCED_PR", "").strip() - - repo_memory_dir = "/tmp/gh-aw/repo-memory/evergreen" - output_file = "/tmp/gh-aw/evergreen.json" - os.makedirs("/tmp/gh-aw", exist_ok=True) - - MAX_ATTEMPTS = 5 - - def api_get(url): - """Make an authenticated GET request to the GitHub API.""" - req = urllib.request.Request(url, headers={ - "Authorization": f"token {token}", - "Accept": "application/vnd.github.v3+json", - }) + import json + import os + import urllib.error + import urllib.parse + import urllib.request + + token = os.environ["GH_TOKEN"] + repo = os.environ["GITHUB_REPOSITORY"] + event_name = os.environ.get("EVENT_NAME", "") + input_pr = (os.environ.get("INPUT_PR") or "").strip() + event_path = os.environ.get("EVENT_PATH", "") + out_dir = "/tmp/gh-aw/evergreen" + os.makedirs(out_dir, exist_ok=True) + + def api(path): + req = urllib.request.Request( + f"https://api.github.com/repos/{repo}{path}", + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + }, + ) with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode()) - def get_all_open_prs(): - """Fetch all open PRs, paginated.""" - prs = [] - page = 1 - while True: - url = f"https://api.github.com/repos/{repo}/pulls?state=open&per_page=100&page={page}&sort=number&direction=asc" - batch = api_get(url) - if not batch: - break - prs.extend(batch) - if len(batch) < 100: - break - page += 1 - return prs - - def get_check_status(pr): - """Get combined CI check status for a PR's head commit.""" - head_sha = pr["head"]["sha"] - url = f"https://api.github.com/repos/{repo}/commits/{head_sha}/status" - try: - status = api_get(url) - return status.get("state", "unknown") - except Exception as e: - print(f" Warning: could not fetch status for PR #{pr['number']}: {e}") - return "unknown" - - def get_check_runs(pr): - """Get check runs for a PR's head commit.""" - head_sha = pr["head"]["sha"] - url = f"https://api.github.com/repos/{repo}/commits/{head_sha}/check-runs" - try: - data = api_get(url) - return data.get("check_runs", []) - except Exception as e: - print(f" Warning: could not fetch check runs for PR #{pr['number']}: {e}") - return [] - - def read_attempt_state(pr_number): - """Read attempt tracking state from repo-memory.""" - state_file = os.path.join(repo_memory_dir, f"pr-{pr_number}.md") - if not os.path.isfile(state_file): - return {"attempts": 0, "head_sha": None} - with open(state_file, encoding="utf-8") as f: - content = f.read() - state = {"attempts": 0, "head_sha": None} - m = re.search(r'\|\s*head_sha\s*\|\s*(\S+)\s*\|', content) - if m: - state["head_sha"] = m.group(1) - m = re.search(r'\|\s*attempts\s*\|\s*(\d+)\s*\|', content) - if m: - state["attempts"] = int(m.group(1)) - return state - - def get_commit_date(sha): - """Return the committer date (ISO 8601) for a given commit SHA, or None.""" - url = f"https://api.github.com/repos/{repo}/commits/{sha}" - try: - data = api_get(url) - return data.get("commit", {}).get("committer", {}).get("date") - except Exception as e: - print(f" Warning: could not fetch commit {sha[:12]}: {e}") - return None - - def is_autoloop_pr(pr): - """Return True if the PR is from an autoloop branch. - Branch name is the primary gate (labels can be added by anyone on - public repos); the `autoloop` label is just an additional signal.""" - head_ref = pr.get("head", {}).get("ref", "") or "" - return head_ref.startswith("autoloop/") - - def get_behind_by(pr): - """Return how many commits the PR base branch is ahead of the PR head.""" - base = pr["base"]["ref"] - head_sha = pr["head"]["sha"] - url = f"https://api.github.com/repos/{repo}/compare/{base}...{head_sha}" - try: - data = api_get(url) - return int(data.get("behind_by", 0) or 0) - except Exception as e: - print(f" Warning: could not fetch compare for PR #{pr['number']}: {e}") - return 0 - - def trigger_ci_workflow(branch): - """Trigger CI on `branch` by pushing an empty commit. This fires the - `push` event on `autoloop/**` branches, which triggers ci.yml without - needing workflow_dispatch approval. Uses GH_AW_CI_TRIGGER_TOKEN so - the push is attributed to a real user (pushes via GITHUB_TOKEN don't - trigger other workflows).""" - ci_token = os.environ.get("GH_AW_CI_TRIGGER_TOKEN", "") or token - try: - # Get current HEAD SHA - url = f"https://api.github.com/repos/{repo}/git/ref/heads/{branch}" - req = urllib.request.Request(url, headers={ - "Authorization": f"token {ci_token}", - "Accept": "application/vnd.github.v3+json", - }) - with urllib.request.urlopen(req, timeout=30) as resp: - head_sha = json.loads(resp.read().decode())["object"]["sha"] - - # Create an empty commit on top of HEAD - url = f"https://api.github.com/repos/{repo}/git/commits" - payload = json.dumps({ - "message": "chore: trigger CI [evergreen]", - "tree": json.loads(urllib.request.urlopen( - urllib.request.Request( - f"https://api.github.com/repos/{repo}/git/commits/{head_sha}", - headers={"Authorization": f"token {ci_token}", "Accept": "application/vnd.github.v3+json"}, - ), timeout=30 - ).read().decode())["tree"]["sha"], - "parents": [head_sha], - }).encode() - req = urllib.request.Request(url, data=payload, method="POST", headers={ - "Authorization": f"token {ci_token}", - "Accept": "application/vnd.github.v3+json", - "Content-Type": "application/json", - }) - with urllib.request.urlopen(req, timeout=30) as resp: - new_sha = json.loads(resp.read().decode())["sha"] - - # Update the branch ref to the new commit - url = f"https://api.github.com/repos/{repo}/git/refs/heads/{branch}" - payload = json.dumps({"sha": new_sha}).encode() - req = urllib.request.Request(url, data=payload, method="PATCH", headers={ - "Authorization": f"token {ci_token}", - "Accept": "application/vnd.github.v3+json", - "Content-Type": "application/json", - }) - with urllib.request.urlopen(req, timeout=30) as resp: - return 200 <= resp.status < 300 - except urllib.error.HTTPError as e: - print(f" Warning: CI trigger (empty commit) failed for {branch}: HTTP {e.code} {e.reason}") - return False - except Exception as e: - print(f" Warning: CI trigger (empty commit) failed for {branch}: {e}") - return False - - def post_pr_comment(pr_number, body): - """Post a comment on a PR using the issues comments API.""" - url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments" - payload = json.dumps({"body": body}).encode() - req = urllib.request.Request(url, data=payload, method="POST", headers={ - "Authorization": f"token {token}", - "Accept": "application/vnd.github.v3+json", - "Content-Type": "application/json", - }) - try: - with urllib.request.urlopen(req, timeout=30) as resp: - return 200 <= resp.status < 300 - except Exception as e: - print(f" Warning: could not post comment on PR #{pr_number}: {e}") - return False - - def pr_needs_attention(pr): - """Check if a PR has merge conflicts, is behind main, or has failing CI. - Returns a list of issues.""" - issues = [] - - # Check mergeable state - # Need to fetch full PR details for mergeable info - pr_url = f"https://api.github.com/repos/{repo}/pulls/{pr['number']}" - try: - full_pr = api_get(pr_url) - mergeable = full_pr.get("mergeable") - mergeable_state = full_pr.get("mergeable_state", "unknown") - if mergeable is False: - issues.append("merge_conflict") - elif mergeable_state == "dirty": - issues.append("merge_conflict") - except Exception as e: - print(f" Warning: could not fetch mergeable state for PR #{pr['number']}: {e}") - - # Check if the PR branch is behind its base branch (e.g., main moved forward). - # We always want to merge main first before fixing CI, so flag this explicitly. - behind_by = get_behind_by(pr) - if behind_by > 0 and "merge_conflict" not in issues: - issues.append(f"behind_main: {behind_by} commit(s)") - - # Check CI status via check runs - check_runs = get_check_runs(pr) - failed_checks = [] - for cr in check_runs: - conclusion = cr.get("conclusion") - status = cr.get("status") - name = cr.get("name", "unknown") - if conclusion in ("failure", "timed_out", "action_required"): - failed_checks.append(name) - elif status == "completed" and conclusion not in ("success", "neutral", "skipped"): - if conclusion is not None: - failed_checks.append(name) - if failed_checks: - issues.append(f"failing_checks: {', '.join(failed_checks)}") - - # Also check commit status API (some checks use the older status API) - combined_status = get_check_status(pr) - if combined_status == "failure": - if not failed_checks: - issues.append("failing_status") - - # Detect missing/stale CI for autoloop PRs. - # Pushes via GITHUB_TOKEN don't trigger workflows, so autoloop PRs - # can sit indefinitely with no checks. Only autoloop branches are - # eligible — never trigger CI automatically on outside-contributor PRs. - if is_autoloop_pr(pr): - completed_runs = [cr for cr in check_runs if cr.get("status") == "completed"] - # No check runs at all on this HEAD SHA - if not check_runs: - issues.append("missing_checks: no check runs on HEAD") - # All runs are still queued / in_progress and the HEAD has been - # sitting around for a while — likely a stuck/missing trigger. - elif not completed_runs: - head_date = get_commit_date(pr["head"]["sha"]) - if head_date: - # If HEAD is older than 15 minutes and nothing has completed, - # treat it as missing (a real CI run would have started by now). - try: - from datetime import datetime, timezone - ht = datetime.fromisoformat(head_date.replace("Z", "+00:00")) - age_s = (datetime.now(timezone.utc) - ht).total_seconds() - if age_s > 15 * 60: - issues.append("missing_checks: only queued/in-progress checks on HEAD") - except Exception: - pass - - return issues - - # --- Main logic --- - - print("=== Evergreen PR Health Check ===") - print(f"Repository: {repo}") - - prs = get_all_open_prs() - print(f"Found {len(prs)} open PR(s)") - - if not prs: - print("No open PRs. Nothing to do.") - with open(output_file, "w") as f: - json.dump({"selected": None, "reason": "no_open_prs"}, f) - emit_selected_output(None) - sys.exit(0) - - # Evaluate each PR deterministically (sorted by PR number ascending) - candidates = [] - skipped = [] - ci_triggered = [] - - # If a specific PR is forced, only check that one - if forced_pr: - prs = [pr for pr in prs if str(pr["number"]) == forced_pr] - if not prs: - print(f"ERROR: PR #{forced_pr} not found among open PRs.") - sys.exit(1) - print(f"FORCED: checking only PR #{forced_pr}") - - for pr in sorted(prs, key=lambda p: p["number"]): - pr_num = pr["number"] - head_sha = pr["head"]["sha"] - print(f"\nChecking PR #{pr_num}: {pr['title'][:60]}...") - print(f" Head SHA: {head_sha[:12]}") - - issues = pr_needs_attention(pr) - if not issues: - print(f" Status: healthy (no issues)") - continue - - print(f" Issues: {issues}") - - # Handle `missing_checks` for autoloop PRs directly in the pre-flight, - # without invoking the agent. The action is purely an API dispatch — - # no code fix is needed — and keeping the privileged CI trigger token - # out of the agent context is a security win. We only do this for - # autoloop branches (the detector enforces this), and only if - # `missing_checks` is the *only* issue: any other issue (merge - # conflict, behind main, failing checks) still needs the agent. - if ( - len(issues) == 1 - and issues[0].startswith("missing_checks") - and is_autoloop_pr(pr) - ): - branch = pr["head"]["ref"] - # Cap retries on the same SHA so we don't spam-dispatch on a - # truly-broken workflow. - attempt_state = read_attempt_state(pr_num) - prior_attempts = ( - attempt_state["attempts"] if attempt_state["head_sha"] == head_sha else 0 - ) - if prior_attempts >= MAX_ATTEMPTS: - skipped.append({ - "pr": pr_num, - "reason": ( - f"missing_checks: max dispatch attempts ({MAX_ATTEMPTS}) " - f"reached on SHA {head_sha[:12]}" - ), - }) - print(f" SKIPPED: max missing_checks attempts reached") - continue - print(f" Triggering ci.yml on branch {branch} (attempt {prior_attempts + 1}/{MAX_ATTEMPTS})") - ok = trigger_ci_workflow(branch) - if ok: - print(f" ✓ Dispatched ci.yml on {branch}") - workflow_url = ( - f"https://github.com/{repo}/actions/workflows/ci.yml" - f"?query=branch%3A{branch}" - ) - post_pr_comment( - pr_num, - ( - "Evergreen: this PR's HEAD had no completed CI checks, " - "so I dispatched the `ci.yml` workflow on this branch. " - f"See [recent CI runs]({workflow_url}).\n\n" - "_(Triggered automatically because pushes via `GITHUB_TOKEN` " - "do not start workflows.)_" - ), - ) - ci_triggered.append({ - "pr": pr_num, - "branch": branch, - "head_sha": head_sha, - }) - # Persist attempt count so we eventually give up if dispatching - # never produces check runs. - os.makedirs(repo_memory_dir, exist_ok=True) - state_path = os.path.join(repo_memory_dir, f"pr-{pr_num}.md") - from datetime import datetime, timezone - ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") - with open(state_path, "w", encoding="utf-8") as sf: - sf.write( - f"# Evergreen: PR #{pr_num}\n\n" - f"## State\n\n" - f"| Field | Value |\n" - f"|:---|:---|\n" - f"| head_sha | {head_sha} |\n" - f"| attempts | {prior_attempts + 1} |\n" - f"| last_run | {ts} |\n" - f"| last_result | ci_dispatched |\n" - ) + def pr_labels(pr): + return [label["name"] for label in pr.get("labels", [])] + + def load_event(): + if not event_path or not os.path.exists(event_path): + return {} + with open(event_path, encoding="utf-8") as f: + return json.load(f) + + target = None + reason = "" + event = load_event() + + try: + if input_pr: + pr = api(f"/pulls/{int(input_pr)}") + labels = pr_labels(pr) + if pr.get("state") == "open" and "evergreen" in labels: + target = pr + reason = "manual dispatch" else: - print(f" ✗ Failed to dispatch ci.yml on {branch}") - skipped.append({ - "pr": pr_num, - "reason": "missing_checks: workflow_dispatch API call failed", - }) - # Do NOT add to candidates — the agent has nothing to fix here. - continue - - # Check attempt tracking - attempt_state = read_attempt_state(pr_num) - if attempt_state["head_sha"] == head_sha: - attempts = attempt_state["attempts"] - print(f" Attempts on this SHA: {attempts}/{MAX_ATTEMPTS}") - if attempts >= MAX_ATTEMPTS: - skipped.append({ - "pr": pr_num, - "reason": f"max attempts ({MAX_ATTEMPTS}) reached on SHA {head_sha[:12]}", - }) - print(f" SKIPPED: max attempts reached") - continue + reason = "manual PR is not open with evergreen label" + elif event_name == "pull_request" and event.get("pull_request"): + number = event["pull_request"]["number"] + pr = api(f"/pulls/{number}") + labels = pr_labels(pr) + if pr.get("state") == "open" and "evergreen" in labels: + target = pr + reason = "pull_request event" + else: + reason = "pull_request event without evergreen label" else: - attempts = 0 - print(f" New SHA detected — resetting attempt counter") - - candidates.append({ - "pr_number": pr_num, - "title": pr["title"], - "head_sha": head_sha, - "base_branch": pr["base"]["ref"], - "head_branch": pr["head"]["ref"], - "issues": issues, - "attempts": attempts, - }) - - # Select the first candidate (lowest PR number — deterministic) - selected = candidates[0] if candidates else None - - result = { - "selected": selected, - "skipped": skipped, - "ci_triggered": ci_triggered, - "total_open_prs": len(prs), - "candidates_found": len(candidates), + params = urllib.parse.urlencode({ + "state": "open", + "labels": "evergreen", + "per_page": "50", + "sort": "updated", + "direction": "asc", + }) + issues = api(f"/issues?{params}") + candidates = [issue for issue in issues if "pull_request" in issue] + for issue in candidates: + labels = [label["name"] for label in issue.get("labels", [])] + if "evergreen-exhausted" not in labels: + target = api(f"/pulls/{issue['number']}") + reason = "scheduled oldest labeled PR" + break + if target is None: + reason = "no open non-exhausted PR has evergreen label" + except (ValueError, urllib.error.HTTPError, urllib.error.URLError) as exc: + reason = f"target selection failed: {exc}" + + payload = { + "selected": target is not None, + "reason": reason, + "event_name": event_name, + "target_pr_number": target["number"] if target else None, + "target_head_sha": target.get("head", {}).get("sha") if target else None, + "target_head_ref": target.get("head", {}).get("ref") if target else None, + "target_base_ref": target.get("base", {}).get("ref") if target else None, } - - with open(output_file, "w") as f: - json.dump(result, f, indent=2) - - if selected: - branch = selected["head_branch"] - print(f"Checking out PR branch before agent run: {branch}") - subprocess.check_call(["git", "checkout", "-B", branch, f"origin/{branch}"]) - subprocess.check_call(["git", "branch", "--set-upstream-to", f"origin/{branch}", branch]) - print(f"\n>>> Selected PR #{selected['pr_number']}: {selected['title']}") - print(f" Issues: {selected['issues']}") - print(f" Attempt: {selected['attempts'] + 1}/{MAX_ATTEMPTS}") - emit_selected_output(selected["pr_number"]) - else: - print("\nNo PRs need attention. Nothing to do.") - emit_selected_output(None) - sys.exit(0) + with open(f"{out_dir}/target.json", "w", encoding="utf-8") as f: + json.dump(payload, f, indent=2) + + gh_output = os.environ.get("GITHUB_OUTPUT") + if gh_output: + with open(gh_output, "a", encoding="utf-8") as f: + f.write(f"selected={payload['target_pr_number'] or ''}\n") + print(json.dumps(payload, indent=2)) PYEOF +source: githubnext/evergreen engine: copilot - -features: - copilot-requests: true --- -# Evergreen — PR Health Keeper - -You are the Evergreen agent. Your job is to fix pull requests that have merge conflicts or failing CI checks. - -## Context - -A pre-flight step has already identified a PR that needs attention. Read the selection data from `/tmp/gh-aw/evergreen.json` to understand which PR to fix and what issues it has. - -## Workflow - -1. **Read the selection file** at `/tmp/gh-aw/evergreen.json`. It contains: - - `selected.pr_number` — the PR to fix - - `selected.issues` — list of problems (e.g., `"merge_conflict"`, `"failing_checks: Test & Lint"`) - - `selected.head_sha` — current HEAD of the PR branch - - `selected.head_branch` — the PR's branch name - - `selected.base_branch` — the target branch (usually `main`) - - `selected.attempts` — how many times we've already tried on this SHA - - > If `selected` is `null`, no PRs need attention right now. Call the **noop** tool with a message like "All PRs are healthy — nothing to fix." and stop. - -2. The pre-flight step already checks out `selected.head_branch` as a named local tracking branch before you start. Keep working on that branch (do not switch back to `main` or use detached HEAD). - -3. **Fix the issues** — always follow this sequence, in order. Each push is a separate `push-to-pull-request-branch` call: - - ### Step 1 — Merge `main` first if the PR is behind (or has conflicts) - If `selected.issues` contains `"merge_conflict"` **or** any `"behind_main: …"` entry, you must bring the branch up to date with `main` before doing anything else: - - - `git fetch origin main` - - `git merge --no-ff origin/main` (or `origin/` if the base isn't `main`) - - Resolve any conflicts intelligently by understanding the intent of both sides. If the PR is from an autoloop branch, prefer the PR's changes for feature code and `main`'s changes for infrastructure/config. - - Run tests/lint/typecheck locally to make sure the merge is clean. - - ### Step 2 — Push the merge as its own update - - Push the merged branch using `push-to-pull-request-branch` **before doing anything else**. - - This is the *first* push of the run. It contains *only* the merge with `main` (plus any conflict resolutions). Do **not** mix CI-fix changes into this patch. - - Merging `main` often fixes CI on its own (the failure was just drift). After the push, re-check whether CI is still failing on the new HEAD. - - ### Step 3 — Re-check CI after the merge - - Look at the failing checks for the new HEAD SHA (the one you just pushed). - - If everything is green or pending-but-likely-green, you're done — skip to step 5. - - If checks are still failing, continue to step 4. - - ### Step 4 — Fix the failing checks (second push) - - Read the failing check logs using GitHub tools. - - Identify the root cause (test failures, lint errors, type errors, build failures). - - Fix the code on the (now-merged) PR branch. - - Run the relevant checks locally to verify the fix before pushing. - - Push the fix using `push-to-pull-request-branch`. This is the *second* push of the run, and it contains *only* the CI fix — no merge commits. - - ### Step 5 — Update tracking and comment - Continue to steps 4 and 5 below. - -4. **Update attempt tracking** by writing to repo-memory. Write a file to the repo-memory directory at `/tmp/gh-aw/repo-memory/evergreen/pr-{number}.md` with this format: - - ```markdown - # Evergreen: PR #{number} - - ## State - - | Field | Value | - |:---|:---| - | head_sha | {sha_after_push} | - | attempts | {new_attempt_count} | - | last_run | {ISO 8601 timestamp} | - | last_result | {success or failure} | - ``` - - - If you **pushed a fix** (or a clean merge), set `head_sha` to the new SHA (post-push), reset `attempts` to `0`, and set `last_result` to `success`. - - If you **could not fix** the issue, keep the original `head_sha`, increment `attempts` by 1, and set `last_result` to `failure`. - -5. **Add a comment** on the PR summarizing what you did (or why you couldn't fix it). If you pushed both a merge and a CI fix, mention both. - -## Rules - -- **Be surgical**: make the minimum changes needed to fix the issue. Do not refactor, improve, or add features. -- **Don't break things**: always run tests/lint/typecheck locally before pushing. -- **Always merge `main` first, as its own push.** If the PR branch is behind `main` (or has merge conflicts), the *first* `push-to-pull-request-branch` call of the run must contain only the merge commit (and any conflict resolutions). Do **NOT** include a merge of `main` inside a CI-fix patch — that's a separate, second push. Mixing the two causes patch conflicts when the remote PR branch hasn't been merged yet. -- **Never rebase PR branches.** Do not run `git rebase`, `git pull --rebase`, or any history-rewriting command. Evergreen updates branches with merge commits so the safe-output push remains a fast-forward update. -- **One concern per push**: the merge push contains only the merge; the fix push contains only the fix. Never combine them. -- **Give up gracefully**: if you cannot fix the issue after investigating, update the attempt counter and leave a comment explaining what went wrong. Do not force-push or make destructive changes. -- **One PR per run**: only fix the selected PR. Do not touch other PRs. -- **Respect the 5-attempt limit**: the pre-flight step will stop selecting this PR once attempts reach 5 on the same HEAD SHA. If the SHA changes (someone else pushes), the counter resets. +# Evergreen -## Autoloop PRs +You are Evergreen, a mergeability repair orchestrator for opt-in pull requests. -Evergreen treats Autoloop draft PRs (branch name `autoloop/*`, label `autoloop`) the same as human-authored PRs for CI-failure and merge-conflict fixing. These PRs are produced by the Autoloop agent (`.github/workflows/autoloop.md`), which has its own in-iteration fix-retry loop (up to 5 attempts per iteration). If Autoloop exhausts its budget or hits its per-iteration wall-clock cap, it sets `paused: true` in its state file (`{program-name}.md` on the `memory/autoloop` branch) with a `pause_reason` like `"ci-fix-exhausted: "` or `"stuck in CI fix loop: "`. The PR is left in a failing state — deliberately, so Evergreen (or a human) can continue from there. +## Start Here -When Evergreen is selected for an Autoloop PR: +Read `/tmp/gh-aw/evergreen/target.json` first. -1. Identify it by the branch prefix `autoloop/` and/or the `autoloop` label. -2. Attempt the fix as usual — read failing check logs, make the minimum change, run local checks, push via `push-to-pull-request-branch`. -3. If the push succeeds **and** you believe the fix is correct, also **un-pause the Autoloop program**: - - Clone or checkout the `memory/autoloop` branch. - - Find the state file `{program-name}.md` where `{program-name}` is the part of the branch name after `autoloop/`. - - In the **⚙️ Machine State** table, set `Paused` to `false` and `Pause Reason` to `—`. - - Commit and push the state-file change to `memory/autoloop`. - - Leave a comment on the Autoloop program issue (`[Autoloop: {program-name}]`, labeled `autoloop-program`) noting that Evergreen pushed a CI fix and un-paused the program, with links to the commit and the newly-green check run. -4. If you cannot fix it, the standard attempt-tracker (5 attempts per HEAD SHA) applies — do **not** un-pause. Autoloop remains paused for human review. +If `selected` is false or `target_pr_number` is null, call `noop` with the reason from that file and stop. Do not inspect unrelated PRs. -> The same 5-attempts-per-SHA rule applies to Autoloop PRs: Evergreen eventually gives up rather than burning cycles on a hopelessly broken change. +If a target PR exists, work only that PR. Confirm the PR still has the `evergreen` label before any repair, comment, label, dispatch, or branch push. -## Missing CI checks (autoloop PRs only — handled in pre-flight) +## Operating Loop -If an autoloop PR's HEAD has no CI check runs (or only stuck queued/in-progress runs), the **pre-flight step** detects this (`missing_checks` issue) and dispatches the `ci.yml` workflow directly via the GitHub API using `GH_AW_CI_TRIGGER_TOKEN`. It also posts a comment on the PR. No agent run is needed in that case, so the PR will not appear in `selected` for `missing_checks`-only issues. +1. Run `pr-intake`. +2. Run `repo-memory-reader`. +3. Run `diff-risk-map`. +4. Run `ci-run-deduper`, `ci-gate-evaluator`, and `merge-blocker-comment-reader`. +5. If a CI gate fails, run `ci-log-parser` before editing. +6. Run `deterministic-repair` before agentic edits. +7. Invoke conditional skills only when the evidence calls for them. +8. Re-run targeted verification commands after edits. +9. Use safe outputs only for comments, Evergreen labels, CI dispatch, and PR branch pushes. +10. Run `safe-output-verifier`. +11. Run `attempt-memory-writer`. +12. Run `merge-gate-reporter` and stop as ready, blocked, human-needed, exhausted, continuing, or noop. -You will only see a `missing_checks` issue in `selected.issues` if it is combined with another fixable issue (e.g. `merge_conflict` or `failing_checks`). In that case, focus on the *other* issue: pre-flight will re-trigger CI on the next scheduled run if checks are still missing after your push. +## Required Behavior -**Security gate:** the pre-flight only ever triggers CI for branches matching `autoloop/*`. Do not bypass this check or trigger CI yourself for PRs from outside contributors. +- Never directly merge a pull request. +- Never remove `evergreen` unless quota is exhausted. +- Never claim a push, label, comment, or dispatch succeeded until verified. +- Use `dispatch-workflow` for `CI` only when checks are missing, stale, or blocked. +- Use `push-to-pull-request-branch` only for a labeled PR and only after local verification. +- If all configured gates pass, add `evergreen-ready` and remove stale `evergreen-blocked` or `evergreen-human-needed` labels. +- If a merge blocker exists but Evergreen can keep monitoring, add `evergreen-blocked` and comment with the blocker. +- If a human decision or protected edit is required, add `evergreen-human-needed` and comment with the exact decision needed. +- If quota is exhausted, add `evergreen-exhausted`, remove `evergreen`, and comment tersely. +- If no visible action is needed, call `noop` with a short evidence-backed reason. diff --git a/.github/workflows/shared/evergreen/repo-policy.md b/.github/workflows/shared/evergreen/repo-policy.md new file mode 100644 index 00000000..449ff9a8 --- /dev/null +++ b/.github/workflows/shared/evergreen/repo-policy.md @@ -0,0 +1,79 @@ +# Evergreen Repo Policy + +## Merge Gates + +Evergreen may report `evergreen-ready` only when the target PR currently has the `evergreen` label and all installed gates are satisfied. + +Required gates inferred for this repository: +- PR is open and not draft. +- PR is conflict-free and mergeable. +- Current PR head SHA has passing CI gates: + - `Test & Lint` + - `Playground E2E (Playwright)` + - `Validate Python Examples` + - `Build` +- `OpenEvolve benchmark` is required only for `autoloop/*-evolve` branches or when that check is present and non-skipped for the current head. +- Requested-changes reviews, unresolved review threads, and explicit maintainer blocker comments prevent ready status. + +Repository settings observed during install: +- Default branch: `main`. +- No branch protection or rulesets were configured on `main`. +- GitHub auto-merge is enabled. Evergreen must never directly merge PRs, but making a PR green can indirectly allow a PR with auto-merge enabled to merge. + +## Branch Updates + +Prefer merging `main` into the PR branch when the branch is behind and freshness is needed. Do not force-push, rebase, squash, or amend. If a fork or permission boundary prevents branch updates, comment and apply `evergreen-human-needed`. + +## CI/CD Activation + +Use `GITHUB_TOKEN` where possible. The repository already has `GH_AW_CI_TRIGGER_TOKEN`; use it only when token-authored pushes or workflow dispatch are needed to trigger CI. Scheduled observation alone must not rerun green checks. + +Prefer actions in this order: +- Wait for current pending checks. +- Dispatch `CI` when checks are missing or stale. +- Push a verified repair commit when a failure is understood. +- Use an empty trigger commit only as a last resort, and do not count it as a semantic repair attempt. + +## Repair Policy + +Allowed repair areas: +- `src/**` +- `tests/**` +- `tests-e2e/**` +- `playground/**` +- `golden/**` +- `scripts/**` +- `benchmarks/**` +- `docs/**` + +Forbidden or human-confirmation areas: +- Never edit `README.md`. +- Never edit `.autoloop/programs/**`. +- Do not edit workflow, agentic workflow, or skill files at runtime unless the PR is explicitly about those files and a human has confirmed the workflow-change path. +- Treat `package.json`, `bun.lock`, `tsconfig.json`, `biome.json`, and `bunfig.toml` as protected high-risk files. + +Strict TypeScript rules: +- No `any`. +- No `as` casts. +- No `@ts-ignore` or equivalent escape hatches. +- Keep zero core dependencies. + +## Review Policy + +Evergreen must not mark draft PRs ready for review, request reviewers, approve PRs, or resolve review threads. If a reviewer or maintainer decision is needed, label `evergreen-human-needed` and explain the decision needed. + +## Quotas + +Quota unit: one continuous application of the `evergreen` label. + +Defaults: +- Maximum 10 Evergreen runs per PR label application. +- Maximum 3 semantic repair attempts per failure signature. +- Maximum 100K AI credits per PR label application. +- Maximum 6 hours wall-clock per run. + +When quota is exhausted, remove `evergreen`, add `evergreen-exhausted`, and leave a concise blocker comment. + +## Discovered Repo Context + +This is a Bun/TypeScript package named `tsb`, a TypeScript port of pandas. CI runs typecheck, lint, unit tests with coverage, pandas golden snapshot validation, cross-validation tests, Playwright playground e2e, Python example validation, and browser build. Recent merged PRs consistently passed the main CI gates. Autoloop PRs commonly have duplicate push and pull_request check runs. diff --git a/.github/workflows/shared/evergreen/report-template.md b/.github/workflows/shared/evergreen/report-template.md new file mode 100644 index 00000000..7cae999e --- /dev/null +++ b/.github/workflows/shared/evergreen/report-template.md @@ -0,0 +1,19 @@ +# Evergreen Report Template + +Keep comments short and evidence-backed. + +Use this shape: + +```markdown +### Evergreen status + +| Gate | State | Evidence | +| --- | --- | --- | +| Test & Lint | passing | current head SHA | + +**Result:** ready | blocked | human-needed | exhausted | continuing + +Next action: one sentence. +``` + +Include at most three workflow run links. Do not paste long logs; summarize the failure signature and link to the run. diff --git a/.github/workflows/shared/evergreen/safe-output-policy.md b/.github/workflows/shared/evergreen/safe-output-policy.md new file mode 100644 index 00000000..9364de15 --- /dev/null +++ b/.github/workflows/shared/evergreen/safe-output-policy.md @@ -0,0 +1,18 @@ +# Evergreen Safe Output Policy + +Use safe outputs for every visible write or branch mutation. + +Allowed actions: +- Add concise PR comments. +- Add or remove only Evergreen state labels. +- Dispatch the `CI` workflow when checks are missing, stale, or blocked. +- Push repair commits to PR branches that still have the `evergreen` label. + +Disallowed actions: +- Do not merge PRs. +- Do not approve PRs. +- Do not resolve review threads. +- Do not request reviewers. +- Do not use shell commands or GitHub tools for write operations that have configured safe outputs. + +Before reporting success, verify the side effect with current GitHub state. diff --git a/.github/workflows/shared/evergreen/state-labels.md b/.github/workflows/shared/evergreen/state-labels.md new file mode 100644 index 00000000..0c402b01 --- /dev/null +++ b/.github/workflows/shared/evergreen/state-labels.md @@ -0,0 +1,11 @@ +# Evergreen State Labels + +Use labels as current state, not as a run log. + +- `evergreen`: persistent opt-in and permission for Evergreen to work on a PR. +- `evergreen-ready`: all configured merge gates currently pass. +- `evergreen-blocked`: a blocker exists, but Evergreen may keep monitoring. +- `evergreen-human-needed`: a human decision, credential, review, or protected edit is required. +- `evergreen-exhausted`: quota was exhausted; remove `evergreen` when applying this label. + +Remove stale state labels when the underlying state changes. For example, remove `evergreen-ready` after a new commit, stale check, failing gate, or newly discovered blocker. diff --git a/.github/workflows/shared/skills/attempt-memory-writer.md b/.github/workflows/shared/skills/attempt-memory-writer.md new file mode 100644 index 00000000..5bde98dc --- /dev/null +++ b/.github/workflows/shared/skills/attempt-memory-writer.md @@ -0,0 +1,11 @@ +# Skill: attempt-memory-writer + +Store semantic attempt state without turning memory into a run log. + +Write small structured entries under `/tmp/gh-aw/repo-memory/evergreen/`: +- `ci-signatures.jsonl` for reusable failure signatures and outcomes. +- `skill-outcomes.jsonl` for selected skills and whether they helped. +- `review-patterns.jsonl` for repeated merge-blocking human feedback. +- `velocity.jsonl` for label-to-action, label-to-green, and label-to-ready timing. + +Do not count empty CI trigger commits as semantic repair attempts. Preserve enough source identifiers to audit the memory later. diff --git a/.github/workflows/shared/skills/autoloop-coordinator.md b/.github/workflows/shared/skills/autoloop-coordinator.md new file mode 100644 index 00000000..cee38b51 --- /dev/null +++ b/.github/workflows/shared/skills/autoloop-coordinator.md @@ -0,0 +1,10 @@ +# Skill: autoloop-coordinator + +Use this for PRs from `autoloop/**` branches or PRs labeled `autoloop`. + +Coordinate with the installed Autoloop conventions: +- Do not edit `.autoloop/programs/**`. +- Treat duplicate push and pull_request CI runs as one logical gate. +- For `autoloop/*-evolve` branches, include `OpenEvolve benchmark` when deciding readiness. +- Prefer fixing merge blockers over continuing feature iteration. +- Preserve evidence that Autoloop can use after Evergreen finishes. diff --git a/.github/workflows/shared/skills/ci-gate-evaluator.md b/.github/workflows/shared/skills/ci-gate-evaluator.md new file mode 100644 index 00000000..c01df0f6 --- /dev/null +++ b/.github/workflows/shared/skills/ci-gate-evaluator.md @@ -0,0 +1,15 @@ +# Skill: ci-gate-evaluator + +Evaluate CI/check gates for the current PR head SHA. + +Classify each gate as: +- passing +- failing +- pending +- stale +- missing +- skipped but expected +- skipped and acceptable +- blocked by permissions or environment + +Recommend the smallest next action: wait, rerun/dispatch, parse logs, repair deterministic failure, merge base, or ask a human. diff --git a/.github/workflows/shared/skills/ci-log-parser.md b/.github/workflows/shared/skills/ci-log-parser.md new file mode 100644 index 00000000..e784b49e --- /dev/null +++ b/.github/workflows/shared/skills/ci-log-parser.md @@ -0,0 +1,12 @@ +# Skill: ci-log-parser + +Extract normalized failure signatures from failing CI logs. + +For each failing check, capture: +- check name and workflow name +- failing command or step +- failure class: typecheck, lint, unit test, cross-validation, golden snapshot, e2e, build, dependency install, infrastructure, timeout, or unknown +- relevant file, line, test name, or stack frame when present +- whether the next move is deterministic repair, targeted reproduction, rerun, or human escalation + +Keep signatures compact enough to store in `ci-signatures.jsonl`. diff --git a/.github/workflows/shared/skills/ci-run-deduper.md b/.github/workflows/shared/skills/ci-run-deduper.md new file mode 100644 index 00000000..73690cf0 --- /dev/null +++ b/.github/workflows/shared/skills/ci-run-deduper.md @@ -0,0 +1,11 @@ +# Skill: ci-run-deduper + +Collapse duplicate check runs into logical merge gates. + +Group check runs by: +- PR head SHA +- workflow name +- job/check name +- conclusion and status + +Treat duplicate `push` and `pull_request` runs for the same head as one logical gate unless their conclusions disagree. Ignore stale check runs from older SHAs. Return the logical gate list and the raw run IDs used as evidence. diff --git a/.github/workflows/shared/skills/dependency-gate-repair.md b/.github/workflows/shared/skills/dependency-gate-repair.md new file mode 100644 index 00000000..f52d47a3 --- /dev/null +++ b/.github/workflows/shared/skills/dependency-gate-repair.md @@ -0,0 +1,5 @@ +# Skill: dependency-gate-repair + +Use this when dependency installation, lockfiles, package manifests, or supply-chain checks block mergeability. + +This repository has zero core dependencies. Treat changes to `package.json`, `bun.lock`, `tsconfig.json`, `biome.json`, and `bunfig.toml` as high-risk protected edits unless the PR is explicitly about dependency/tooling work. If a protected edit is required, label `evergreen-human-needed` and explain the smallest requested human action. diff --git a/.github/workflows/shared/skills/deterministic-repair.md b/.github/workflows/shared/skills/deterministic-repair.md new file mode 100644 index 00000000..966bc922 --- /dev/null +++ b/.github/workflows/shared/skills/deterministic-repair.md @@ -0,0 +1,14 @@ +# Skill: deterministic-repair + +Prefer repo-native deterministic work before agentic edits. + +Use the installed repo policy and discovered scripts. For this repository, prefer: +- `bun run typecheck` +- `bun run lint` +- `bun test ./tests/` +- `bun run test:e2e` +- `bun run build` +- `python scripts/validate-python-examples.py playground/` +- `python golden/generate.py` followed by `git diff --exit-code -- golden/snapshots` + +Run targeted commands first when a failure points to a specific area. Apply the smallest patch that can make the gate pass. diff --git a/.github/workflows/shared/skills/diff-risk-map.md b/.github/workflows/shared/skills/diff-risk-map.md new file mode 100644 index 00000000..4e228ab0 --- /dev/null +++ b/.github/workflows/shared/skills/diff-risk-map.md @@ -0,0 +1,20 @@ +# Skill: diff-risk-map + +Classify changed files so the orchestrator can choose focused repair skills. + +Risk classes: +- tests only +- docs or examples +- playground/browser UI +- public API or exported TypeScript +- core data structure behavior +- I/O or golden snapshot behavior +- dependency or lockfile +- CI, workflow, or agent configuration +- Autoloop generated or program files +- benchmark or performance behavior + +Report: +- The primary risk class. +- Conditional skills that should run and why. +- Files that require human confirmation before edit under the installed policy. diff --git a/.github/workflows/shared/skills/docs-release-gate-repair.md b/.github/workflows/shared/skills/docs-release-gate-repair.md new file mode 100644 index 00000000..f57bb0bc --- /dev/null +++ b/.github/workflows/shared/skills/docs-release-gate-repair.md @@ -0,0 +1,8 @@ +# Skill: docs-release-gate-repair + +Use this when docs, examples, golden snapshots, or generated documentation block mergeability. + +Respect repository rules: +- Do not edit `README.md` unless the PR explicitly requires it and a human confirms. +- Keep docs changes tied to a failing gate or explicit blocker. +- For playground examples, validate Python snippets with `python scripts/validate-python-examples.py playground/`. diff --git a/.github/workflows/shared/skills/infra-ci-repair.md b/.github/workflows/shared/skills/infra-ci-repair.md new file mode 100644 index 00000000..c4510335 --- /dev/null +++ b/.github/workflows/shared/skills/infra-ci-repair.md @@ -0,0 +1,5 @@ +# Skill: infra-ci-repair + +Use this when the blocker is in GitHub Actions, runner setup, package installation, workflow permissions, generated workflow locks, or CI activation. + +First determine whether the failure is caused by the PR code, workflow configuration, external infrastructure, or missing credentials. For workflow-source changes under `.github/workflows/*.md`, the repo requires `gh aw compile` and `apm compile`; if the runtime cannot safely push workflow files, label `evergreen-human-needed` and comment with exact next steps. diff --git a/.github/workflows/shared/skills/lint-policy-review.md b/.github/workflows/shared/skills/lint-policy-review.md new file mode 100644 index 00000000..8167d146 --- /dev/null +++ b/.github/workflows/shared/skills/lint-policy-review.md @@ -0,0 +1,5 @@ +# Skill: lint-policy-review + +Use this when lint or formatting failures look like repository policy, not a local code mistake. + +Check whether the failure can be fixed with `bun run lint:fix`. If the fix would violate strict TypeScript rules, introduce `any`, use `as`, add ignore comments, or change style configuration, stop and ask for human input with `evergreen-human-needed`. diff --git a/.github/workflows/shared/skills/merge-blocker-comment-reader.md b/.github/workflows/shared/skills/merge-blocker-comment-reader.md new file mode 100644 index 00000000..4ab2ab21 --- /dev/null +++ b/.github/workflows/shared/skills/merge-blocker-comment-reader.md @@ -0,0 +1,12 @@ +# Skill: merge-blocker-comment-reader + +Read human comments only for merge-blocking signals. + +Look for: +- requested changes reviews +- unresolved review threads +- comments that explicitly block merge +- comments asking for required tests, docs, screenshots, or policy decisions +- maintainer instructions that change the repair plan + +Ignore non-blocking suggestions, thanks, progress chatter, and general discussion. Return a blocker map with source URLs. diff --git a/.github/workflows/shared/skills/merge-gate-reporter.md b/.github/workflows/shared/skills/merge-gate-reporter.md new file mode 100644 index 00000000..0934e2d5 --- /dev/null +++ b/.github/workflows/shared/skills/merge-gate-reporter.md @@ -0,0 +1,14 @@ +# Skill: merge-gate-reporter + +Decide whether the PR is ready, blocked, human-needed, exhausted, or should continue. + +Evaluate: +- draft state +- conflict and mergeability state +- current-head CI gates +- review decision and unresolved threads +- required and blocker labels +- repo-specific policy gates +- quota state + +Produce a concise gate table for comments. State only evidence-backed conclusions. Never directly merge a pull request. diff --git a/.github/workflows/shared/skills/playground-e2e-diagnoser.md b/.github/workflows/shared/skills/playground-e2e-diagnoser.md new file mode 100644 index 00000000..f989a21d --- /dev/null +++ b/.github/workflows/shared/skills/playground-e2e-diagnoser.md @@ -0,0 +1,12 @@ +# Skill: playground-e2e-diagnoser + +Use this when Playwright, browser, playground, or visual behavior blocks mergeability. + +Collect: +- failing test name and trace/artifact links +- browser console errors +- network failures +- screenshots when available +- whether the failure reproduces with `bun run test:e2e` + +Do not call a failure flaky without evidence from a rerun or a known memory signature. diff --git a/.github/workflows/shared/skills/pr-intake.md b/.github/workflows/shared/skills/pr-intake.md new file mode 100644 index 00000000..9a419138 --- /dev/null +++ b/.github/workflows/shared/skills/pr-intake.md @@ -0,0 +1,13 @@ +# Skill: pr-intake + +Build a factual snapshot of the target pull request before choosing any repair. + +Collect: +- PR number, title, author, draft state, base branch, head branch, and head SHA. +- Current labels and whether the `evergreen` label is still present. +- Changed files, grouped by directory and file type. +- Current mergeability, behind/base freshness, review decision, and unresolved review threads when available. +- Current check runs and workflow runs for the PR head SHA only. +- Recent PR comments, reviews, and review comments that may affect mergeability. + +Return facts only. Do not recommend fixes from this skill. diff --git a/.github/workflows/shared/skills/repo-memory-reader.md b/.github/workflows/shared/skills/repo-memory-reader.md new file mode 100644 index 00000000..dc0d9b22 --- /dev/null +++ b/.github/workflows/shared/skills/repo-memory-reader.md @@ -0,0 +1,13 @@ +# Skill: repo-memory-reader + +Load durable repository knowledge before making a repair plan. + +Read relevant files under `/tmp/gh-aw/repo-memory/evergreen/` if they exist: +- `gates.json` +- `labels.json` +- `ci-signatures.jsonl` +- `review-patterns.jsonl` +- `skill-outcomes.jsonl` +- `velocity.jsonl` + +Summarize only knowledge that applies to the current PR. Treat current GitHub state and the installed repo policy as more authoritative than memory when they disagree. Do not copy large memory files into comments. diff --git a/.github/workflows/shared/skills/safe-output-verifier.md b/.github/workflows/shared/skills/safe-output-verifier.md new file mode 100644 index 00000000..4acfe3ad --- /dev/null +++ b/.github/workflows/shared/skills/safe-output-verifier.md @@ -0,0 +1,11 @@ +# Skill: safe-output-verifier + +Verify intended GitHub side effects before reporting success. + +After requesting a safe output: +- For labels, reload the issue/PR and confirm the expected labels changed. +- For comments, confirm the comment exists and points at the intended PR. +- For branch pushes, confirm the PR head SHA changed to the expected commit and only allowed files changed. +- For workflow dispatch, confirm the dispatch request was accepted or explain what could not be verified. + +If verification fails, report the operation as blocked. Do not use completion language for unverified side effects.