diff --git a/.github/workflows/evergreen.lock.yml b/.github/workflows/evergreen.lock.yml index ee26e53e..1e88138e 100644 --- a/.github/workflows/evergreen.lock.yml +++ b/.github/workflows/evergreen.lock.yml @@ -1,5 +1,5 @@ -# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"9f30f831ba8062fb33a865a4b5490da0eac78d8de7b4bd72556ffa94204579c3","strict":true,"agent_id":"copilot"} -# gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw/actions/setup","sha":"092ce8b94ad531663e3efea1378acff0f5827639","version":"092ce8b94ad531663e3efea1378acff0f5827639"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.43"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} +# gh-aw-metadata: {"schema_version":"v3","frontmatter_hash":"633dcf28e92e435f542a182f2ea8ced20059e4fd04622ea92e923028189a9676","strict":true,"agent_id":"copilot"} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.43"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.43"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.6","digest":"sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.6@sha256:2bb8eef86006a4c5963c55616a9c51c32f27bfdecb023b8aa6f91f6718d9171c"},{"image":"ghcr.io/github/github-mcp-server:v1.0.3","digest":"sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959","pinned_image":"ghcr.io/github/github-mcp-server:v1.0.3@sha256:2ac27ef03461ef2b877031b838a7d1fd7f12b12d4ace7796d8cad91446d55959"},{"image":"node:lts-alpine","digest":"sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f","pinned_image":"node:lts-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f"}]} # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ @@ -43,7 +43,6 @@ # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw/actions/setup@092ce8b94ad531663e3efea1378acff0f5827639 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.25.43 @@ -94,9 +93,16 @@ jobs: setup-trace-id: ${{ steps.setup.outputs.trace-id }} stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false - name: Setup Scripts id: setup - uses: github/gh-aw/actions/setup@092ce8b94ad531663e3efea1378acff0f5827639 + uses: ./actions/setup with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -136,6 +142,7 @@ jobs: sparse-checkout: | .github .agents + actions/setup .claude .codex .crush @@ -179,24 +186,24 @@ jobs: run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { - cat << 'GH_AW_PROMPT_865ae3119cbf9832_EOF' + cat << 'GH_AW_PROMPT_d094037c680caacb_EOF' - GH_AW_PROMPT_865ae3119cbf9832_EOF + GH_AW_PROMPT_d094037c680caacb_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_865ae3119cbf9832_EOF' + cat << 'GH_AW_PROMPT_d094037c680caacb_EOF' Tools: add_comment(max:3), push_to_pull_request_branch(max:3), missing_tool, missing_data, noop - GH_AW_PROMPT_865ae3119cbf9832_EOF + GH_AW_PROMPT_d094037c680caacb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_865ae3119cbf9832_EOF' + cat << 'GH_AW_PROMPT_d094037c680caacb_EOF' - GH_AW_PROMPT_865ae3119cbf9832_EOF + GH_AW_PROMPT_d094037c680caacb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_865ae3119cbf9832_EOF' + cat << 'GH_AW_PROMPT_d094037c680caacb_EOF' The following GitHub context information is available for this workflow: {{#if __GH_AW_GITHUB_ACTOR__ }} @@ -228,13 +235,13 @@ jobs: - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it without proper authentication. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). - GH_AW_PROMPT_865ae3119cbf9832_EOF + GH_AW_PROMPT_d094037c680caacb_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_865ae3119cbf9832_EOF' + cat << 'GH_AW_PROMPT_d094037c680caacb_EOF' {{#runtime-import .github/workflows/shared/reporting.md}} {{#runtime-import .github/workflows/evergreen.md}} - GH_AW_PROMPT_865ae3119cbf9832_EOF + GH_AW_PROMPT_d094037c680caacb_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -366,9 +373,16 @@ jobs: setup-span-id: ${{ steps.setup.outputs.span-id }} setup-trace-id: ${{ steps.setup.outputs.trace-id }} steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false - name: Setup Scripts id: setup - uses: github/gh-aw/actions/setup@092ce8b94ad531663e3efea1378acff0f5827639 + uses: ./actions/setup with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -410,7 +424,7 @@ jobs: 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 auth_header = base64.b64encode(f\"x-access-token:{ci_token}\".encode()).decode()\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" + 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" # Repo memory git-based storage configuration from frontmatter processed below - name: Clone repo-memory branch (default) @@ -488,9 +502,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_07944ded4a815527_EOF' + cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_5e1bbdfe94f769b4_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":10240,"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","target":"*"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_07944ded4a815527_EOF + GH_AW_SAFE_OUTPUTS_CONFIG_5e1bbdfe94f769b4_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -700,7 +714,7 @@ jobs: mkdir -p /home/runner/.copilot GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_fecd328600933c14_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_255b9508a2d4d41b_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" { "mcpServers": { "github": { @@ -741,7 +755,7 @@ jobs: "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}" } } - GH_AW_MCP_CONFIG_fecd328600933c14_EOF + GH_AW_MCP_CONFIG_255b9508a2d4d41b_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true @@ -1002,9 +1016,16 @@ jobs: tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} total_count: ${{ steps.missing_tool.outputs.total_count }} steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false - name: Setup Scripts id: setup - uses: github/gh-aw/actions/setup@092ce8b94ad531663e3efea1378acff0f5827639 + uses: ./actions/setup with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1146,9 +1167,16 @@ jobs: detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false - name: Setup Scripts id: setup - uses: github/gh-aw/actions/setup@092ce8b94ad531663e3efea1378acff0f5827639 + uses: ./actions/setup with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1345,9 +1373,16 @@ jobs: validation_error_default: ${{ steps.push_repo_memory_default.outputs.validation_error }} validation_failed_default: ${{ steps.push_repo_memory_default.outputs.validation_failed }} steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false - name: Setup Scripts id: setup - uses: github/gh-aw/actions/setup@092ce8b94ad531663e3efea1378acff0f5827639 + uses: ./actions/setup with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1404,6 +1439,15 @@ jobs: setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/push_repo_memory.cjs'); await main(); + - name: Restore actions folder + if: always() + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions/setup + sparse-checkout-cone-mode: true + persist-credentials: false safe_outputs: needs: @@ -1440,9 +1484,16 @@ jobs: push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} steps: + - name: Checkout actions folder + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions + persist-credentials: false - name: Setup Scripts id: setup - uses: github/gh-aw/actions/setup@092ce8b94ad531663e3efea1378acff0f5827639 + uses: ./actions/setup with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1552,4 +1603,13 @@ jobs: /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json if-no-files-found: ignore + - name: Restore actions folder + if: always() + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: github/gh-aw + sparse-checkout: | + actions/setup + sparse-checkout-cone-mode: true + persist-credentials: false diff --git a/.github/workflows/evergreen.md b/.github/workflows/evergreen.md index c52732ad..456dfe69 100644 --- a/.github/workflows/evergreen.md +++ b/.github/workflows/evergreen.md @@ -176,7 +176,6 @@ steps: 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 - auth_header = base64.b64encode(f"x-access-token:{ci_token}".encode()).decode() try: # Get current HEAD SHA url = f"https://api.github.com/repos/{repo}/git/ref/heads/{branch}"