From 7cfb943d743e194859db3e031ba8b134b4c3c24b Mon Sep 17 00:00:00 2001 From: mrjf Date: Thu, 9 Jul 2026 22:47:41 -0500 Subject: [PATCH] Add Evergreen active PR leasing --- .github/workflows/evergreen.lock.yml | 50 ++++++++++++++----- .github/workflows/evergreen.md | 48 ++++++++++++++++-- .github/workflows/shared/evergreen/labels.md | 1 + .../workflows/shared/evergreen/repo-policy.md | 3 ++ 4 files changed, 85 insertions(+), 17 deletions(-) diff --git a/.github/workflows/evergreen.lock.yml b/.github/workflows/evergreen.lock.yml index e661b15a..391256fd 100644 --- a/.github/workflows/evergreen.lock.yml +++ b/.github/workflows/evergreen.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"be122690f0dfe1fa95a5b9b66f3bcb398beeb8616907971304fd99cddfb3227f","body_hash":"60be2e0ddf52d99ba316d4459973165cbd008f52390b85dea83dd45186241b6d","compiler_version":"v0.79.4","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"27a9a3244def67a4c930ff6abe6b787bef8dad3d65bf0a7cdd3421ac0bc2300b","body_hash":"0db8335b57a7858a2eab1dcaea3a2b399463c65a12cad7f87ace682522b9e696","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"}]} # ___ _ _ # / _ \ | | (_) @@ -93,8 +93,9 @@ on: permissions: {} concurrency: - cancel-in-progress: true - group: gh-aw-${{ github.workflow }}-${{ github.event.inputs.pr || github.event.pull_request.number || github.ref || github.run_id }} + cancel-in-progress: false + group: gh-aw-${{ github.workflow }}-${{ github.event.inputs.pr || github.event.pull_request.number || github.run_id }} + queue: max run-name: "Evergreen" @@ -435,8 +436,7 @@ jobs: pull-requests: read statuses: read concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" - queue: max + group: "gh-aw-copilot-${{ github.workflow }}-${{ needs.preflight.outputs.pr || github.run_id }}" env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" @@ -565,6 +565,7 @@ jobs: GH_AW_SKILL_DIR: ".github/skills" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh" - env: + ACTIVE_LABEL: evergreen_active EXPECTED_HEAD_SHA: ${{ needs.preflight.outputs.head_sha }} GH_TOKEN: ${{ github.token }} OPT_IN_LABEL: evergreen @@ -572,7 +573,7 @@ jobs: REPO: ${{ github.repository }} if: needs.preflight.outputs.pr != '' && needs.preflight.outputs.head_sha != '' name: Checkout selected PR head - run: "set -euo pipefail\n\nif ! grep -Eq '^[0-9]+$' <<<\"$PR_NUMBER\"; then\n echo \"Invalid PR number '$PR_NUMBER'; refusing to construct a PR ref.\"\n exit 1\nfi\n\nif ! grep -Eiq '^[0-9a-f]{40}$' <<<\"$EXPECTED_HEAD_SHA\"; then\n echo \"Invalid expected head SHA '$EXPECTED_HEAD_SHA'; refusing to check out PR code.\"\n exit 1\nfi\n\npayload=\"$(gh pr view \"$PR_NUMBER\" --repo \"$REPO\" \\\n --json state,labels,headRefName,headRefOid)\"\n\nstate=\"$(jq -r '.state' <<<\"$payload\")\"\nif [ \"$state\" != \"OPEN\" ]; then\n echo \"PR #$PR_NUMBER is $state; refusing to run Evergreen outside an open PR branch.\"\n exit 1\nfi\n\nif ! jq -e --arg label \"$OPT_IN_LABEL\" '[.labels[].name] | index($label) != null' <<<\"$payload\" >/dev/null; then\n echo \"PR #$PR_NUMBER no longer has the $OPT_IN_LABEL label; refusing to check out PR code.\"\n exit 1\nfi\n\nactual_head_sha=\"$(jq -r '.headRefOid' <<<\"$payload\")\"\nif [ \"$actual_head_sha\" != \"$EXPECTED_HEAD_SHA\" ]; then\n echo \"PR #$PR_NUMBER head changed from $EXPECTED_HEAD_SHA to $actual_head_sha; refusing stale checkout.\"\n exit 1\nfi\n\ngit fetch origin \"+refs/pull/${PR_NUMBER}/head:refs/remotes/evergreen/pr-${PR_NUMBER}\"\n\nfetched_head_sha=\"$(git rev-parse \"refs/remotes/evergreen/pr-${PR_NUMBER}\")\"\nif [ \"$fetched_head_sha\" != \"$EXPECTED_HEAD_SHA\" ]; then\n echo \"Fetched PR #$PR_NUMBER at $fetched_head_sha, expected $EXPECTED_HEAD_SHA; refusing stale checkout.\"\n exit 1\nfi\n\nhead_ref=\"$(jq -r '.headRefName // \"\"' <<<\"$payload\")\"\nlocal_branch=\"evergreen/pr-${PR_NUMBER}\"\nif [ -n \"$head_ref\" ] &&\n [ \"${head_ref#-}\" = \"$head_ref\" ] &&\n git check-ref-format --branch \"$head_ref\" >/dev/null 2>&1; then\n local_branch=\"$head_ref\"\nfi\n\ngit checkout -B \"$local_branch\" \"$EXPECTED_HEAD_SHA\"\n\ncurrent_head_sha=\"$(git rev-parse HEAD)\"\ncurrent_branch=\"$(git branch --show-current)\"\nif [ \"$current_head_sha\" != \"$EXPECTED_HEAD_SHA\" ] || [ -z \"$current_branch\" ]; then\n echo \"Workspace is not on a local branch at selected PR head; refusing to run agent.\"\n exit 1\nfi\n\necho \"Evergreen workspace is on branch $current_branch at $current_head_sha for PR #$PR_NUMBER.\"\n" + run: "set -euo pipefail\n\nif ! grep -Eq '^[0-9]+$' <<<\"$PR_NUMBER\"; then\n echo \"Invalid PR number '$PR_NUMBER'; refusing to construct a PR ref.\"\n exit 1\nfi\n\nif ! grep -Eiq '^[0-9a-f]{40}$' <<<\"$EXPECTED_HEAD_SHA\"; then\n echo \"Invalid expected head SHA '$EXPECTED_HEAD_SHA'; refusing to check out PR code.\"\n exit 1\nfi\n\npayload=\"$(gh pr view \"$PR_NUMBER\" --repo \"$REPO\" \\\n --json state,labels,headRefName,headRefOid)\"\n\nstate=\"$(jq -r '.state' <<<\"$payload\")\"\nif [ \"$state\" != \"OPEN\" ]; then\n echo \"PR #$PR_NUMBER is $state; refusing to run Evergreen outside an open PR branch.\"\n exit 1\nfi\n\nif ! jq -e --arg label \"$OPT_IN_LABEL\" '[.labels[].name] | index($label) != null' <<<\"$payload\" >/dev/null; then\n echo \"PR #$PR_NUMBER no longer has the $OPT_IN_LABEL label; refusing to check out PR code.\"\n exit 1\nfi\n\nif ! jq -e --arg label \"$ACTIVE_LABEL\" '[.labels[].name] | index($label) != null' <<<\"$payload\" >/dev/null; then\n echo \"PR #$PR_NUMBER no longer has the $ACTIVE_LABEL lease; refusing to run without a controller claim.\"\n exit 1\nfi\n\nactual_head_sha=\"$(jq -r '.headRefOid' <<<\"$payload\")\"\nif [ \"$actual_head_sha\" != \"$EXPECTED_HEAD_SHA\" ]; then\n echo \"PR #$PR_NUMBER head changed from $EXPECTED_HEAD_SHA to $actual_head_sha; refusing stale checkout.\"\n exit 1\nfi\n\ngit fetch origin \"+refs/pull/${PR_NUMBER}/head:refs/remotes/evergreen/pr-${PR_NUMBER}\"\n\nfetched_head_sha=\"$(git rev-parse \"refs/remotes/evergreen/pr-${PR_NUMBER}\")\"\nif [ \"$fetched_head_sha\" != \"$EXPECTED_HEAD_SHA\" ]; then\n echo \"Fetched PR #$PR_NUMBER at $fetched_head_sha, expected $EXPECTED_HEAD_SHA; refusing stale checkout.\"\n exit 1\nfi\n\nhead_ref=\"$(jq -r '.headRefName // \"\"' <<<\"$payload\")\"\nlocal_branch=\"evergreen/pr-${PR_NUMBER}\"\nif [ -n \"$head_ref\" ] &&\n [ \"${head_ref#-}\" = \"$head_ref\" ] &&\n git check-ref-format --branch \"$head_ref\" >/dev/null 2>&1; then\n local_branch=\"$head_ref\"\nfi\n\ngit checkout -B \"$local_branch\" \"$EXPECTED_HEAD_SHA\"\n\ncurrent_head_sha=\"$(git rev-parse HEAD)\"\ncurrent_branch=\"$(git branch --show-current)\"\nif [ \"$current_head_sha\" != \"$EXPECTED_HEAD_SHA\" ] || [ -z \"$current_branch\" ]; then\n echo \"Workspace is not on a local branch at selected PR head; refusing to run agent.\"\n exit 1\nfi\n\necho \"Evergreen workspace is on branch $current_branch at $current_head_sha for PR #$PR_NUMBER.\"\n" shell: bash - name: Download container images @@ -582,9 +583,9 @@ jobs: mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs" mkdir -p /tmp/gh-aw/safeoutputs mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs - cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_5196e65e30ccc55c_EOF' - {"add_comment":{"max":2},"add_labels":{"allowed":["evergreen-blocked","evergreen-human-needed","evergreen-exhausted","priority/*","gate/*"],"max":5},"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_patch_size":10240,"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"],"required_labels":["evergreen"],"target":"*"},"remove_labels":{"allowed":["evergreen","evergreen-blocked","evergreen-human-needed","evergreen-exhausted","gate/*"],"max":5},"report_incomplete":{},"submit_pull_request_review":{"max":1},"update_pull_request":{"allow_body":true,"allow_title":true,"max":1,"update_branch":false}} - GH_AW_SAFE_OUTPUTS_CONFIG_5196e65e30ccc55c_EOF + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_87f0ba73e2f88db6_EOF' + {"add_comment":{"max":2},"add_labels":{"allowed":["evergreen-blocked","evergreen-human-needed","evergreen-exhausted","priority/*","gate/*"],"max":5},"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_patch_size":10240,"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"],"required_labels":["evergreen"],"target":"*"},"remove_labels":{"allowed":["evergreen","evergreen_active","evergreen-blocked","evergreen-human-needed","evergreen-exhausted","gate/*"],"max":5},"report_incomplete":{},"submit_pull_request_review":{"max":1},"update_pull_request":{"allow_body":true,"allow_title":true,"max":1,"update_branch":false}} + GH_AW_SAFE_OUTPUTS_CONFIG_87f0ba73e2f88db6_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -592,7 +593,7 @@ jobs: "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 2 comment(s) can be added. Supports reply_to_id for discussion threading.", "add_labels": " CONSTRAINTS: Maximum 5 label(s) can be added. Only these labels are allowed: [\"evergreen-blocked\" \"evergreen-human-needed\" \"evergreen-exhausted\" \"priority/*\" \"gate/*\"].", - "remove_labels": " CONSTRAINTS: Maximum 5 label(s) can be removed. Only these labels can be removed: [evergreen evergreen-blocked evergreen-human-needed evergreen-exhausted gate/*].", + "remove_labels": " CONSTRAINTS: Maximum 5 label(s) can be removed. Only these labels can be removed: [evergreen evergreen_active evergreen-blocked evergreen-human-needed evergreen-exhausted gate/*].", "submit_pull_request_review": " CONSTRAINTS: Maximum 1 review(s) can be submitted.", "update_pull_request": " CONSTRAINTS: Maximum 1 pull request(s) can be updated." }, @@ -1683,6 +1684,11 @@ jobs: pull-requests: read statuses: read + concurrency: + cancel-in-progress: false + group: gh-aw-${{ github.workflow }}-preflight + queue: max + outputs: head_sha: ${{ steps.evaluate.outputs.head_sha }} pr: ${{ steps.evaluate.outputs.pr }} @@ -1732,6 +1738,13 @@ jobs: jq -e --arg label "$label" '[.labels[].name] | index($label) != null' <<<"$payload" >/dev/null } + ensure_active_label() { + gh label create "$ACTIVE_LABEL" --repo "$REPO" \ + --color "fbca04" \ + --description "Evergreen lease: a run is currently working this PR" \ + >/dev/null 2>&1 || true + } + check_names() { local payload="$1" jq -r '.statusCheckRollup[]? | .name // .context // empty' <<<"$payload" @@ -1939,6 +1952,11 @@ jobs: return 1 fi + if pr_has_label "$payload" "$ACTIVE_LABEL"; then + echo "PR #$pr already has the $ACTIVE_LABEL lease; another Evergreen run is working it." + return 1 + fi + if [ -n "$event_head_sha" ] && [ "$event_head_sha" != "$head_sha" ]; then echo "PR #$pr head changed from $event_head_sha to $head_sha; waiting for a fresh event." set_result "false" "$pr" "$head_sha" "out_of_scope" "$reason:head_changed" @@ -1958,6 +1976,8 @@ jobs: return 1 ;; needs_branch_update|needs_repair) + ensure_active_label + gh issue edit "$pr" --repo "$REPO" --add-label "$ACTIVE_LABEL" set_result "true" "$pr" "$head_sha" "$state" "$reason:$state" return 0 ;; @@ -1987,9 +2007,14 @@ jobs: fi candidates="${RUNNER_TEMP:-.}/evergreen-pr-candidates.tsv" + unordered_candidates="${RUNNER_TEMP:-.}/evergreen-pr-candidates-unordered.tsv" gh pr list --repo "$REPO" --state open --label "$OPT_IN_LABEL" \ --json number,headRefOid \ - --jq '.[] | [.number, .headRefOid] | @tsv' > "$candidates" + --jq '.[] | [.number, .headRefOid] | @tsv' > "$unordered_candidates" + + while IFS= read -r line; do + printf "%05d\t%s\n" "$RANDOM" "$line" + done < "$unordered_candidates" | sort -n | cut -f2- > "$candidates" while IFS=$'\t' read -r pr head_sha; do if consider_pr "$pr" "$head_sha" "$reason"; then @@ -1997,6 +2022,7 @@ jobs: fi done < "$candidates" env: + ACTIVE_LABEL: evergreen_active CHECK_GATE_MODE: configured CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN || '' }} EVENT_ACTION: ${{ github.event.action }} @@ -2147,7 +2173,7 @@ jobs: GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} - GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\",\"priority/*\",\"gate/*\"],\"max\":5},\"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_patch_size\":10240,\"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\"],\"required_labels\":[\"evergreen\"],\"target\":\"*\"},\"remove_labels\":{\"allowed\":[\"evergreen\",\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\",\"gate/*\"],\"max\":5},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"update_branch\":false}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\",\"priority/*\",\"gate/*\"],\"max\":5},\"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_patch_size\":10240,\"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\"],\"required_labels\":[\"evergreen\"],\"target\":\"*\"},\"remove_labels\":{\"allowed\":[\"evergreen\",\"evergreen_active\",\"evergreen-blocked\",\"evergreen-human-needed\",\"evergreen-exhausted\",\"gate/*\"],\"max\":5},\"report_incomplete\":{},\"submit_pull_request_review\":{\"max\":1},\"update_pull_request\":{\"allow_body\":true,\"allow_title\":true,\"max\":1,\"update_branch\":false}}" 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 a14087a9..0b67832e 100644 --- a/.github/workflows/evergreen.md +++ b/.github/workflows/evergreen.md @@ -15,8 +15,9 @@ on: required: false concurrency: - group: gh-aw-${{ github.workflow }}-${{ github.event.inputs.pr || github.event.pull_request.number || github.ref || github.run_id }} - cancel-in-progress: true + group: gh-aw-${{ github.workflow }}-${{ github.event.inputs.pr || github.event.pull_request.number || github.run_id }} + cancel-in-progress: false + queue: max timeout-minutes: 60 @@ -32,6 +33,10 @@ jobs: preflight: name: Evergreen deterministic preflight runs-on: ubuntu-latest + concurrency: + group: gh-aw-${{ github.workflow }}-preflight + cancel-in-progress: false + queue: max permissions: actions: write checks: read @@ -57,6 +62,7 @@ jobs: EVENT_ACTION: ${{ github.event.action }} READY_LABEL: evergreen-ready OPT_IN_LABEL: evergreen + ACTIVE_LABEL: evergreen_active EXHAUSTED_LABEL: evergreen-exhausted REQUIRED_CHECKS_JSON: '["Test & Lint","Playground E2E (Playwright)","Build","Validate Python Examples"]' CHECK_GATE_MODE: configured @@ -95,6 +101,13 @@ jobs: jq -e --arg label "$label" '[.labels[].name] | index($label) != null' <<<"$payload" >/dev/null } + ensure_active_label() { + gh label create "$ACTIVE_LABEL" --repo "$REPO" \ + --color "fbca04" \ + --description "Evergreen lease: a run is currently working this PR" \ + >/dev/null 2>&1 || true + } + check_names() { local payload="$1" jq -r '.statusCheckRollup[]? | .name // .context // empty' <<<"$payload" @@ -302,6 +315,11 @@ jobs: return 1 fi + if pr_has_label "$payload" "$ACTIVE_LABEL"; then + echo "PR #$pr already has the $ACTIVE_LABEL lease; another Evergreen run is working it." + return 1 + fi + if [ -n "$event_head_sha" ] && [ "$event_head_sha" != "$head_sha" ]; then echo "PR #$pr head changed from $event_head_sha to $head_sha; waiting for a fresh event." set_result "false" "$pr" "$head_sha" "out_of_scope" "$reason:head_changed" @@ -321,6 +339,8 @@ jobs: return 1 ;; needs_branch_update|needs_repair) + ensure_active_label + gh issue edit "$pr" --repo "$REPO" --add-label "$ACTIVE_LABEL" set_result "true" "$pr" "$head_sha" "$state" "$reason:$state" return 0 ;; @@ -350,9 +370,14 @@ jobs: fi candidates="${RUNNER_TEMP:-.}/evergreen-pr-candidates.tsv" + unordered_candidates="${RUNNER_TEMP:-.}/evergreen-pr-candidates-unordered.tsv" gh pr list --repo "$REPO" --state open --label "$OPT_IN_LABEL" \ --json number,headRefOid \ - --jq '.[] | [.number, .headRefOid] | @tsv' > "$candidates" + --jq '.[] | [.number, .headRefOid] | @tsv' > "$unordered_candidates" + + while IFS= read -r line; do + printf "%05d\t%s\n" "$RANDOM" "$line" + done < "$unordered_candidates" | sort -n | cut -f2- > "$candidates" while IFS=$'\t' read -r pr head_sha; do if consider_pr "$pr" "$head_sha" "$reason"; then @@ -362,7 +387,11 @@ jobs: if: needs.preflight.outputs.should_run == 'true' -engine: copilot +engine: + id: copilot + concurrency: + group: gh-aw-copilot-${{ github.workflow }}-${{ needs.preflight.outputs.pr || github.run_id }} + cancel-in-progress: false network: defaults @@ -380,6 +409,7 @@ pre-agent-steps: PR_NUMBER: ${{ needs.preflight.outputs.pr }} EXPECTED_HEAD_SHA: ${{ needs.preflight.outputs.head_sha }} OPT_IN_LABEL: evergreen + ACTIVE_LABEL: evergreen_active run: | set -euo pipefail @@ -407,6 +437,11 @@ pre-agent-steps: exit 1 fi + if ! jq -e --arg label "$ACTIVE_LABEL" '[.labels[].name] | index($label) != null' <<<"$payload" >/dev/null; then + echo "PR #$PR_NUMBER no longer has the $ACTIVE_LABEL lease; refusing to run without a controller claim." + exit 1 + fi + actual_head_sha="$(jq -r '.headRefOid' <<<"$payload")" if [ "$actual_head_sha" != "$EXPECTED_HEAD_SHA" ]; then echo "PR #$PR_NUMBER head changed from $EXPECTED_HEAD_SHA to $actual_head_sha; refusing stale checkout." @@ -503,7 +538,7 @@ safe-outputs: allowed: ["evergreen-blocked", "evergreen-human-needed", "evergreen-exhausted", "priority/*", "gate/*"] max: 5 remove-labels: - allowed: ["evergreen", "evergreen-blocked", "evergreen-human-needed", "evergreen-exhausted", "gate/*"] + allowed: ["evergreen", "evergreen_active", "evergreen-blocked", "evergreen-human-needed", "evergreen-exhausted", "gate/*"] max: 5 push-to-pull-request-branch: target: "*" @@ -555,6 +590,9 @@ are merge gates. 10. Verify every intended side effect before describing it as complete. 11. Stop on quota exhaustion, repeated safe-output failure, repeated failure signatures, trust-policy denial, or any human-owned decision. +12. Before ending any run that reached the agent, request safe-output removal of + the `evergreen_active` lease label from the selected PR. This is lease + cleanup, not readiness or blocker state. ## Required Pass Order diff --git a/.github/workflows/shared/evergreen/labels.md b/.github/workflows/shared/evergreen/labels.md index aec1a218..dcac3971 100644 --- a/.github/workflows/shared/evergreen/labels.md +++ b/.github/workflows/shared/evergreen/labels.md @@ -6,6 +6,7 @@ Default labels: | --- | --- | --- | | `evergreen` | Human | Persistent opt-in for greenkeeping work. | | `evergreen-ready` | Deterministic controller | Configured gates pass for the current PR head. | +| `evergreen_active` | Deterministic controller | Lease label: an Evergreen run is currently working this PR. | | `evergreen-blocked` | Orchestrator | A blocker exists, but future work may still be useful. | | `evergreen-human-needed` | Orchestrator | A human decision, credential, review, or protected edit is needed. | | `evergreen-exhausted` | Orchestrator | Per-PR quota is exhausted. | diff --git a/.github/workflows/shared/evergreen/repo-policy.md b/.github/workflows/shared/evergreen/repo-policy.md index d0838469..935e1673 100644 --- a/.github/workflows/shared/evergreen/repo-policy.md +++ b/.github/workflows/shared/evergreen/repo-policy.md @@ -21,6 +21,9 @@ readiness controller and the agentic orchestrator must both respect this file. PRs, when the trust model allows branch repair. Do not mark drafts ready for review automatically. - Required labels: `evergreen` opts a PR into the work loop. +- Active lease label: `evergreen_active` is controller-owned. The preflight + selector applies it before dispatching the agent, other selectors skip PRs + with this label, and cleanup removes it when the run finishes. - Blocker labels: `evergreen-blocked`, `evergreen-human-needed`. - Deployment/environment gates: none. - Auto-merge behavior: GitHub auto-merge is ENABLED on the repository. Evergreen