/
githubmirror
/
aspnetcore
Обзор
Документация
Войти
/
githubmirror
/
aspnetcore
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
.github/workflows/test-quarantine.lock.yml
4 192 строки
240 KB
William Godbe
Recompile agentic workflow lockfiles (#68208)
05 авг 2026, 00:16
Не верифицирован
05 авг 2026, 00:16
c7e9984
Код
Авторство
О чём код?
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"ed08480e2cb83779f83fdd2092a16bde144b3ca17f3886613cd2dbfa506d4bff","body_hash":"0517fa5ff93dc354545699d46f38a2e119152ad588364fbb25f9e9d4d2339bbe","compiler_version":"v0.84.3","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.77"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_PAT_0","COPILOT_PAT_1","COPILOT_PAT_2","COPILOT_PAT_3","COPILOT_PAT_4","COPILOT_PAT_5","COPILOT_PAT_6","COPILOT_PAT_7","COPILOT_PAT_8","COPILOT_PAT_9","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"c863074b673419603d146aab585e2986ef08deec","version":"v0.84.3"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43","digest":"sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43","digest":"sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43","digest":"sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.7","digest":"sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.8.0","digest":"sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520","pinned_image":"ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520"}]} # This file was automatically generated by gh-aw (v0.84.3). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) # | |_| | __ _ ___ _ __ | |_ _ ___ # | _ |/ _` |/ _ \ '_ \| __| |/ __| # | | | | (_| | __/ | | | |_| | (__ # \_| |_/\__, |\___|_| |_|\__|_|\___| # __/ | # _ _ |___/ # | | | | / _| | # | | | | ___ _ __ _ __| |_| | _____ ____ # | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| # \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ # \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ # # # To update this file, edit the corresponding .md file and run: # gh aw compile # Not all edits will cause changes to this file. # # For more information: https://github.github.com/gh-aw/introduction/overview/ # # Daily quarantine/unquarantine flaky tests based on Azure DevOps pipeline analytics # # Resolved workflow manifest: # Imports: # - shared/pat_pool.md # # Secrets used: # - COPILOT_PAT_0 # - COPILOT_PAT_1 # - COPILOT_PAT_2 # - COPILOT_PAT_3 # - COPILOT_PAT_4 # - COPILOT_PAT_5 # - COPILOT_PAT_6 # - COPILOT_PAT_7 # - COPILOT_PAT_8 # - COPILOT_PAT_9 # - GH_AW_CI_TRIGGER_TOKEN # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN # # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) # - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 # - github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 # # Container images used: # - ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6 # - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1 # - ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d # - ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00 # - ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 # - ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 name: "Daily Test Quarantine Management" on: schedule: - cron: "0 10 */2 * *" # steps: # Steps injected into pre-activation job # - env: # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # id: requarantine_prs # name: Fetch re-quarantine PRs # run: | # # Fetch all merged PRs with re-quarantine label or title, bypassing DIFC filtering. # # The agent's MCP search tools filter out PRs from external contributors, which # # can hide legitimate re-quarantine PRs. This deterministic step runs with full # # GitHub token access and writes results that get injected into the agent prompt. # python3 << 'SCRIPT' # import json, os, urllib.request # # token = os.environ["GH_TOKEN"] # headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"} # # def search_prs(query): # results = [] # url = f"https://api.github.com/search/issues?q={query}&per_page=100" # while url: # req = urllib.request.Request(url, headers=headers) # with urllib.request.urlopen(req) as resp: # data = json.loads(resp.read()) # results.extend(data.get("items", [])) # # Follow pagination # link = resp.headers.get("Link", "") # url = None # for part in link.split(","): # if 'rel="next"' in part: # url = part.split("<")[1].split(">")[0] # return results # # def get_changed_files(pr_number): # url = f"https://api.github.com/repos/dotnet/aspnetcore/pulls/{pr_number}/files?per_page=100" # files = [] # while url: # req = urllib.request.Request(url, headers=headers) # with urllib.request.urlopen(req) as resp: # files.extend(json.loads(resp.read())) # link = resp.headers.get("Link", "") # url = None # for part in link.split(","): # if 'rel="next"' in part: # url = part.split("<")[1].split(">")[0] # return files # # # Search by label and by title # by_label = search_prs("repo:dotnet/aspnetcore+is:pr+is:merged+label:re-quarantine") # by_title = search_prs("repo:dotnet/aspnetcore+is:pr+is:merged+%22Re-quarantine%22+in:title") # # # Deduplicate by PR number # seen = set() # prs = [] # for pr in by_label + by_title: # if pr["number"] not in seen: # seen.add(pr["number"]) # prs.append(pr) # # # For each PR, get changed files and check for QuarantinedTest additions. # # Store the added lines containing [QuarantinedTest so the agent can match at # # method/class/assembly level, not just file level. # requarantine_data = [] # for pr in prs: # files = get_changed_files(pr["number"]) # quarantine_entries = [] # for f in files: # patch = f.get("patch", "") # if not patch and f.get("status") in ("modified", "added"): # # Patch may be omitted for large diffs — fail closed by # # treating the whole file as potentially re-quarantined # quarantine_entries.append({ # "filename": f["filename"], # "added_lines": [], # "patch_truncated": True # }) # continue # added = [line[1:] for line in patch.split("\n") # if line.startswith("+") and "[QuarantinedTest" in line] # if added: # quarantine_entries.append({ # "filename": f["filename"], # "added_lines": added, # "patch_truncated": False # }) # requarantine_data.append({ # "number": pr["number"], # "title": pr["title"], # "quarantine_entries": quarantine_entries # }) # # # Write JSON to GITHUB_OUTPUT so it flows through jobs.pre_activation.outputs # # into the agent prompt. /tmp/ is NOT shared between pre_activation and agent jobs. # import sys # # # Filter out PRs with no quarantine_entries — they're irrelevant and # # keeping them wastes step output / prompt token budget. # requarantine_data = [pr for pr in requarantine_data if pr["quarantine_entries"]] # # json_str = json.dumps(requarantine_data) # github_output = os.environ.get("GITHUB_OUTPUT", "") # if not github_output: # print("ERROR: GITHUB_OUTPUT is not set, cannot pass data to agent", file=sys.stderr) # sys.exit(1) # with open(github_output, "a") as gh_out: # gh_out.write(f"requarantine_data<<REQUARANTINE_EOF\n{json_str}\nREQUARANTINE_EOF\n") # # print(f"Found {len(requarantine_data)} re-quarantine PRs, wrote to step output") # SCRIPT # - env: # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # id: requarantine_issues # name: Fetch re-quarantine issue numbers # run: | # # Fetch the numbers of every issue carrying the `re-quarantine` label. A test # # tracked by one of these issues has been deliberately re-quarantined and must # # NEVER be auto-unquarantined. This is a deterministic complement to the # # re-quarantine-PR diff check: matching a candidate's [QuarantinedTest] issue URL # # against this set is exact and cannot be missed by fuzzy diff parsing. # python3 << 'SCRIPT' # import json, os, sys, urllib.parse, urllib.request # # token = os.environ["GH_TOKEN"] # headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"} # # def search_issues(query): # results = [] # url = ("https://api.github.com/search/issues?" # + urllib.parse.urlencode({"q": query, "per_page": 100})) # while url: # req = urllib.request.Request(url, headers=headers) # with urllib.request.urlopen(req, timeout=30) as resp: # data = json.loads(resp.read()) # if data.get("incomplete_results"): # sys.exit("FATAL: GitHub search returned incomplete_results for the " # "re-quarantine issue query; failing closed rather than " # "unquarantining against a partial blocklist") # results.extend(data.get("items", [])) # link = resp.headers.get("Link", "") # url = None # for part in link.split(","): # if 'rel="next"' in part: # url = part.split("<")[1].split(">")[0] # return results # # # Any state — a re-quarantined issue may be closed by a later unquarantine PR but # # the test remains permanently blocked from automated unquarantining. # items = search_issues("repo:dotnet/aspnetcore is:issue label:re-quarantine") # numbers = sorted({it["number"] for it in items}) # # github_output = os.environ.get("GITHUB_OUTPUT", "") # if not github_output: # print("ERROR: GITHUB_OUTPUT is not set, cannot pass data to agent", file=sys.stderr) # sys.exit(1) # with open(github_output, "a") as gh_out: # gh_out.write(f"requarantine_issue_numbers={json.dumps(numbers)}\n") # # print(f"Found {len(numbers)} re-quarantine issue numbers, wrote to step output") # SCRIPT # - env: # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # id: closed_quarantine_prs # name: Fetch closed test-quarantine PRs # run: | # # Fetch closed-but-unmerged [test-quarantine] PRs from the last 30 days, plus any # # (open or closed) [test-quarantine] PR carrying the `no-quarantine-for-30-days` or # # `no-unquarantine-for-30-days` label, bypassing DIFC filtering. The agent's MCP # # search tools silently drop PRs authored by this workflow's own bot # # (app/github-actions), which hides maintainer "do not (un)quarantine" feedback and # # causes the workflow to re-create previously rejected PRs. This deterministic step # # runs with full token access so that signal always reaches the agent. # # # # We deliberately do NOT read PR/issue comment bodies as a trust signal: comments are # # free text anyone can post regardless of permission level, so treating them as # # authoritative would let an untrusted actor inject a fake "do not quarantine" # # instruction into the agent's prompt. Only two signals are used, and both are # # mechanically un-spoofable: # # 1. `trusted_closed` — a human (not the bot itself) closed the bot's own PR. Both # # queries below are restricted to PRs authored by this workflow's own bot # # (author:app/github-actions), so closing one of them requires the closer to be # # the bot itself or a repo collaborator with triage/write access — closing # # someone else's PR on GitHub is not possible without that permission. (An # # earlier revision also treated public dotnet org membership as trusted, but # # public org membership does not imply write access — a public member could open # # and close their own fake PR to spoof this signal — so that fallback was # # removed.) # # 2. `quarantine_label_added_at` / `unquarantine_label_added_at` — the PR carries # # the `no-quarantine-for-30-days` or `no-unquarantine-for-30-days` label, # # respectively. These are two distinct labels because "don't (re-)quarantine # # this test" and "don't unquarantine this test" are opposite actions — a label # # opting out of one must never suppress the other. GitHub only allows accounts # # with triage/write access to add labels, so a label's mere presence is itself # # proof of a privileged decision — no author-identity check is needed on top of # # it. # python3 << 'SCRIPT' # import json, os, secrets, sys, urllib.parse, urllib.request, urllib.error # from datetime import datetime, timedelta, timezone # # token = os.environ["GH_TOKEN"] # headers = {"Authorization": f"******", "Accept": "application/vnd.github+json"} # since = (datetime.now(timezone.utc) - timedelta(days=30)).strftime("%Y-%m-%d") # NO_QUARANTINE_LABEL = "no-quarantine-for-30-days" # NO_UNQUARANTINE_LABEL = "no-unquarantine-for-30-days" # BOT_AUTHOR = "app/github-actions" # # def search_prs(query): # results = [] # url = ("https://api.github.com/search/issues?" # + urllib.parse.urlencode({"q": query, "per_page": 100})) # while url: # req = urllib.request.Request(url, headers=headers) # with urllib.request.urlopen(req, timeout=30) as resp: # data = json.loads(resp.read()) # if data.get("incomplete_results"): # sys.exit("FATAL: GitHub search returned incomplete_results for the " # "closed test-quarantine PR query; failing closed rather than " # "risking re-creation of an already-rejected PR") # results.extend(data.get("items", [])) # link = resp.headers.get("Link", "") # url = None # for part in link.split(","): # if 'rel="next"' in part: # url = part.split("<")[1].split(">")[0] # return results # # def get_issue(number): # # closed_at is present on search results, but closed_by is not, so fetch the # # issue record to learn who closed our own rejected attempt. This drives the # # per-test "count only failures after the close" cutoff below. # url = f"https://api.github.com/repos/dotnet/aspnetcore/issues/{number}" # req = urllib.request.Request(url, headers=headers) # with urllib.request.urlopen(req, timeout=30) as resp: # d = json.loads(resp.read()) # return {"closed_at": d.get("closed_at"), # "closed_by": (d.get("closed_by") or {}).get("login")} # # def get_label_added_at(number, label_name): # # Returns the most recent time `label_name` was added to this issue/PR (via the # # issue timeline events), or None if it isn't currently applied or was never # # added via a "labeled" event we can see. Using the most recent add (rather than # # the first) means a maintainer can re-arm the 30-day window by removing and # # re-adding the label. # url = f"https://api.github.com/repos/dotnet/aspnetcore/issues/{number}/events?per_page=100" # latest = None # while url: # req = urllib.request.Request(url, headers=headers) # with urllib.request.urlopen(req, timeout=30) as resp: # for ev in json.loads(resp.read()): # if ev.get("event") == "labeled" and (ev.get("label") or {}).get("name") == label_name: # ts = ev.get("created_at") # if ts and (latest is None or ts > latest): # latest = ts # link = resp.headers.get("Link", "") # url = None # for part in link.split(","): # if 'rel="next"' in part: # url = part.split("<")[1].split(">")[0] # return latest # # closed_prs = search_prs( # 'repo:dotnet/aspnetcore is:pr is:closed is:unmerged ' # f'author:{BOT_AUTHOR} "test-quarantine" in:title closed:>={since}' # ) # # Any state: a maintainer may label a PR the workflow left open (without closing it) # # to opt a test out of automated action for 30 days. Restricted to bot-authored PRs, # # same as above — this label only has meaning on the workflow's own PRs. Two separate # # queries (one per label) rather than a single OR query, so each stays simple and # # explicit about which label it's fetching. # labeled_prs_quarantine = search_prs( # f'repo:dotnet/aspnetcore is:pr author:{BOT_AUTHOR} ' # f'"test-quarantine" in:title label:{NO_QUARANTINE_LABEL}' # ) # labeled_prs_unquarantine = search_prs( # f'repo:dotnet/aspnetcore is:pr author:{BOT_AUTHOR} ' # f'"test-quarantine" in:title label:{NO_UNQUARANTINE_LABEL}' # ) # by_number = {pr["number"]: pr for pr in closed_prs} # for pr in labeled_prs_quarantine: # by_number.setdefault(pr["number"], pr) # for pr in labeled_prs_unquarantine: # by_number.setdefault(pr["number"], pr) # # data = [] # for number, pr in by_number.items(): # # labeled_prs_quarantine/labeled_prs_unquarantine are queried across any PR # # state (open/closed/merged), since a maintainer may label a PR after it # # merges. But a merged PR represents a # # *successful* action, not a rejected attempt — it must never seed a failure # # cutoff below. Use pull_request.merged_at (present on search results) rather # # than an extra API call to tell "merged" apart from "closed without merging". # was_merged = bool((pr.get("pull_request") or {}).get("merged_at")) # issue_meta = (get_issue(number) if pr.get("closed_at") and not was_merged # else {"closed_at": None, "closed_by": None}) # closed_by = issue_meta.get("closed_by") # author_login = (pr.get("user") or {}).get("login") # # Both queries above are restricted to author:app/github-actions, so this PR is # # guaranteed bot-authored. On GitHub, closing a PR you did NOT author requires # # triage or write permission, so any human (non-bot) closer whose login differs # # from the bot author necessarily holds repo privileges — this is a deterministic, # # zero-API signal with no unauthenticated fallback. # closer_is_privileged_human = bool( # closed_by # and author_login # and closed_by != author_login # and not closed_by.endswith("[bot]") # ) # label_names = {lbl.get("name") for lbl in (pr.get("labels") or [])} # quarantine_label_added_at = ( # get_label_added_at(number, NO_QUARANTINE_LABEL) # if NO_QUARANTINE_LABEL in label_names else None # ) # unquarantine_label_added_at = ( # get_label_added_at(number, NO_UNQUARANTINE_LABEL) # if NO_UNQUARANTINE_LABEL in label_names else None # ) # data.append({ # "number": number, # "title": pr["title"], # # Grouped PRs often list only some tests in the title; the per-test # # fully-qualified name lives in the body, which the agent matches on. # "body": (pr.get("body") or "")[:2000], # # closed_at is the cutoff timestamp: when a trusted contributor closed our # # own rejected (re-)quarantine attempt, only failures AFTER this instant # # should count toward re-attempting it (see the "prior-attempt cutoff" rule). # # Null for merged PRs — a merge is a successful outcome, not a rejection, # # and must never seed this cutoff. # "closed_at": None if was_merged else (issue_meta.get("closed_at") or pr.get("closed_at")), # "closed_by": closed_by, # # "trusted_closed" is true only when a non-bot human closed this bot-authored # # PR (requires triage/write access). No comment text or org-membership check # # is consulted — public org membership does not imply write access. # "trusted_closed": bool(closed_by and closer_is_privileged_human), # # ISO-8601 timestamp of the most recent time `no-quarantine-for-30-days` was # # added to this PR, or null if never applied. Governs (re-)quarantine # # candidates only — see "Important Rules" for how these two label fields map # # to candidate type. Adding a label requires triage/write access, so its # # presence alone is sufficient proof of a privileged decision. # "quarantine_label_added_at": quarantine_label_added_at, # # Same as above, but for `no-unquarantine-for-30-days`, which governs # # unquarantine candidates only. # "unquarantine_label_added_at": unquarantine_label_added_at, # }) # # github_output = os.environ.get("GITHUB_OUTPUT", "") # if not github_output: # print("ERROR: GITHUB_OUTPUT is not set, cannot pass data to agent", file=sys.stderr) # sys.exit(1) # js = json.dumps(data) # # The value is injected into the agent prompt via an env var subject to the # # 131072-byte MAX_ARG_STRLEN limit; fail closed if it would exceed a safe cap # # rather than letting it be silently dropped later and starve the guardrail. # MAX_OUTPUT_BYTES = 120000 # if len(js) > MAX_OUTPUT_BYTES: # sys.exit(f"FATAL: closed_quarantine_prs is {len(js)} bytes, exceeds " # f"{MAX_OUTPUT_BYTES}; failing closed so the agent does not proceed " # "without the recently-rejected-PR data") # # Randomized, collision-checked heredoc delimiter: the value embeds user-controlled # # PR bodies, so a fixed delimiter could in principle be reproduced in the data and # # truncate the output. json.dumps already escapes newlines, but a random delimiter is # # the GitHub-recommended defense-in-depth. # delim = f"CLOSED_QUAR_EOF_{secrets.token_hex(16)}" # while delim in js: # delim = f"CLOSED_QUAR_EOF_{secrets.token_hex(16)}" # with open(github_output, "a") as gh_out: # gh_out.write(f"closed_quarantine_prs<<{delim}\n{js}\n{delim}\n") # # print(f"Found {len(data)} relevant test-quarantine PRs, wrote to step output") # SCRIPT # - env: # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # id: source_b_prs # name: Verify Source B PRs # run: | # # Source B looks for flaky tests in failed CI builds of PRs that were merged # # into main. Selecting those builds requires verifying each candidate PR # # (base==main, merged==true) and matching its head SHA — which needs a GitHub # # token. The agent sandbox has NO usable token, and its MCP search tool # # silently drops external-contributor PRs. So we do the ENTIRE selection here — # # outside the firewall, with full token access and no integrity filter — and # # hand the agent the exact Azure DevOps build IDs to collect results from. The # # agent makes ZERO GitHub calls and does NOT re-enumerate builds, which both # # eliminates the per-PR pull_request_read loop (the effective-token-budget # # sink) and avoids any snapshot skew between this step and the agent. # python3 << 'SCRIPT' # import json, os, sys, time, datetime, urllib.parse, urllib.request, urllib.error # # def fetch(url, data=None, headers=None, retries=3): # """GET (or POST if data) with small backoff. Re-raises HTTPError so the # caller can distinguish auth failures; retries only transient errors.""" # hdrs = {"User-Agent": "aspnetcore-test-quarantine"} # if headers: # hdrs.update(headers) # last = None # for attempt in range(retries): # try: # req = urllib.request.Request(url, data=data, headers=hdrs) # with urllib.request.urlopen(req, timeout=60) as r: # return json.loads(r.read()), r.headers # except urllib.error.HTTPError as e: # if e.code in (401, 403) or e.code == 404: # raise # last = e # except Exception as e: # last = e # time.sleep(2 * (attempt + 1)) # raise last # # # --- 1. Enumerate completed PR builds from the last 7 days (Azure DevOps, # # public project — no auth needed) for both CI pipelines. Record each # # failed/partial build's id, PR number and the commit it ran on. --- # BUILDS = "https://dev.azure.com/dnceng-public/public/_apis/build/builds" # DEFINITIONS = [83, 87] # 83 = aspnetcore-ci, 87 = components-e2e # min_time = (datetime.datetime.utcnow() - datetime.timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%SZ") # # failed_builds = [] # list of (build_id, pr_number, source_sha) # for d in DEFINITIONS: # token = None # while True: # params = {"definitions": d, "reasonFilter": "pullRequest", # "statusFilter": "completed", "minTime": min_time, # "$top": 200, "api-version": "7.1"} # if token: # params["continuationToken"] = token # data, hdrs = fetch(f"{BUILDS}?{urllib.parse.urlencode(params)}") # # ADO returns the continuation token in a response header. # token = hdrs.get("x-ms-continuationtoken") # for b in data.get("value", []): # if b.get("result") not in ("failed", "partiallySucceeded"): # continue # branch = b.get("sourceBranch", "") # refs/pull/{N}/merge # if not branch.startswith("refs/pull/"): # continue # try: # pr = int(branch.split("/")[2]) # except (IndexError, ValueError): # continue # sha = (b.get("triggerInfo") or {}).get("pr.sourceSha") # if sha: # failed_builds.append((b["id"], pr, sha)) # if not token: # break # # # (B4) Only PRs with >= 1 failed/partial build can ever yield a candidate. # candidates = sorted({pr for _, pr, _ in failed_builds}) # # # --- 2. Verify B2 (base == main) + B3 (merged) and capture head SHA via batched # # GraphQL. Fail LOUD on systemic failures (auth, rate-limit, every chunk # # failed, or no candidate could even be resolved) so the run aborts # # visibly instead of silently emitting an empty set. --- # gh_token = os.environ["GH_TOKEN"] # # def verify(pr_numbers, chunk=50): # verified = {} # str(pr_number) -> headRefOid # resolved = 0 # candidate PRs we positively read a node for # chunks_total = chunks_failed = 0 # for k in range(0, len(pr_numbers), chunk): # batch = pr_numbers[k:k + chunk] # chunks_total += 1 # aliases = "\n".join( # f'p{n}: pullRequest(number: {n}) {{ number baseRefName merged headRefOid }}' # for n in batch) # query = f'query {{ repository(owner: "dotnet", name: "aspnetcore") {{ {aliases} }} }}' # try: # body, _ = fetch( # "https://api.github.com/graphql", # data=json.dumps({"query": query}).encode(), # headers={"Authorization": f"bearer {gh_token}", # "Content-Type": "application/json"}) # except urllib.error.HTTPError as e: # if e.code in (401, 403): # sys.exit(f"FATAL: GitHub GraphQL {e.code} — aborting Source B verification") # chunks_failed += 1 # continue # except Exception: # chunks_failed += 1 # continue # errored, chunk_untrusted = set(), False # for err in body.get("errors") or []: # if err.get("type") == "RATE_LIMITED": # sys.exit("FATAL: GitHub GraphQL RATE_LIMITED — aborting Source B verification") # alias = next((p for p in (err.get("path") or []) # if isinstance(p, str) and len(p) > 1 and p[0] == "p" and p[1:].isdigit()), None) # if alias: # errored.add(alias) # else: # chunk_untrusted = True # repo = (body.get("data") or {}).get("repository") # if repo is None or chunk_untrusted: # chunks_failed += 1 # continue # for alias, pr in repo.items(): # if alias in errored or not pr or not pr.get("headRefOid"): # continue # resolved += 1 # if pr.get("baseRefName") == "main" and pr.get("merged") is True: # verified[str(pr["number"])] = pr["headRefOid"] # # Fail LOUD on ANY chunk that could not be conclusively read: a partially # # dropped chunk would silently omit up to `chunk` real candidate PRs from # # Source B. fetch() already retries transient blips, so a surviving failure # # is a real problem worth aborting the daily run over. # if chunks_failed: # sys.exit(f"FATAL: {chunks_failed}/{chunks_total} GraphQL verification " # "chunk(s) failed — aborting Source B verification") # if pr_numbers and resolved == 0: # sys.exit("FATAL: could not resolve any candidate PR via GraphQL — aborting Source B verification") # return verified # # verified = verify(candidates) if candidates else {} # # # --- 3. (B1) Keep failed/partial builds whose PR is merged into main AND whose # # commit matches that PR's head SHA. Emit only those build IDs. --- # build_ids = sorted({bid for bid, pr, sha in failed_builds # if verified.get(str(pr)) == sha}) # # github_output = os.environ.get("GITHUB_OUTPUT", "") # if not github_output: # print("ERROR: GITHUB_OUTPUT is not set, cannot pass data to agent", file=sys.stderr) # sys.exit(1) # json_str = json.dumps(build_ids) # with open(github_output, "a") as gh_out: # gh_out.write(f"source_b_build_ids<<SOURCE_B_EOF\n{json_str}\nSOURCE_B_EOF\n") # print(f"Source B: {len(failed_builds)} failed PR builds, {len(candidates)} candidate PRs, " # f"{len(verified)} merged-into-main, {len(build_ids)} builds selected (B1-B4), wrote to step output") # SCRIPT # - env: # SOURCE_B_BUILD_IDS: ${{ steps.source_b_prs.outputs.source_b_build_ids }} # id: part1_aggregate # name: Aggregate Part 1 failures # run: | # # Part 1 (Sources A/B/C) failure gathering is the dominant token sink of this # # workflow: it spans ~200 builds, many resultsbyBuild calls, and multi-MB Helix # # console logs. Surfacing that data into the metered agent loop is what exhausted # # the per-run effective-token budget mid-gathering -- the run repeatedly died # # before creating any output. Do ALL of it here, in the pre-activation job that # # runs OUTSIDE the firewall at zero effective-token cost, and inject a single # # compact JSON blob the agent consumes directly. The agent makes ZERO AzDO/Helix # # calls for Part 1. # # Source A: defs 83+87, refs/heads/main, failed/partial builds in the last 30 # # days -> resultsbyBuild(Failed) -> per-test failure counts (+assembly, # # up to 3 example build ids). # # Source B: resultsbyBuild(Failed) for the already-selected source_b_build_ids # # (the Verify Source B PRs step did the full B1-B4 selection). # # builds: compact metadata map (def, startedUtc, finishedUtc, sourceVersion, # # pr) for every referenced build, so the agent can do the Case B # # "failure after the unquarantine landed" timing check and the # # Source B "PR modified its own test" exclusion WITHOUT any AzDO call. # # Enrichment: each failing test is enriched from its representative result's # # detail with the Helix job id + work-item name (parsed from the result # # `comment` field -- reliable, no fragile build-timeline parsing) and, # # for individual tests, the real errorMessage/stackTrace (capped). # # Source C: for work items (names ending .WorkItemExecution) use those Helix # # coords to download the console log and extract only the [FAIL] blocks # # (capped), probing multiple builds until a [FAIL] block is found and # # bounded by a global download budget. Turns multi-MB logs into a few KB. # # emit() guarantees the output stays under the 1MB GITHUB_OUTPUT limit by shedding # # optional enrichment (never the per-test counts) and fails loud rather than letting # # GitHub silently truncate into corrupt JSON. Validated ~170KB on 30 days of data. # python3 << 'SCRIPT' # import json, os, sys, time, datetime, urllib.parse, urllib.request, urllib.error, re # # ADO = "https://dev.azure.com/dnceng-public/public/_apis" # VSTMR = "https://vstmr.dev.azure.com/dnceng-public/public/_apis" # HELIX = "https://helix.dot.net/api/2019-06-17" # DEFS = [83, 87] # DAYS = 30 # WI_SUFFIX = ".WorkItemExecution" # # ERROR_CAP = 1200 # STACK_CAP = 900 # OCC_CAP = 2 # occurrences tracked per test (for Source C multi-probe) # # BLOCK_CAP = 8000 # WORKITEM_CAP = 40000 # SOURCE_C_GLOBAL_CAP = 300000 # SAFE_OUTPUT = 950000 # hard ceiling under the 1MB GITHUB_OUTPUT limit # # Safety valve: cap total Helix console-log bytes downloaded. The first occurrence of # # every work item is always fetched; extra occurrences are only probed while under this # # budget. Stops a regression spell (dozens of multi-MB macOS-hang logs) from making the # # pre-activation step download gigabytes / run unbounded. # SOURCE_C_DOWNLOAD_BUDGET = 300_000_000 # # _ANSI = re.compile(r'\x1b\[[0-9;]*[A-Za-z]') # # # Redact token-shaped strings from captured CI failure text before emitting it. # # Helix work-item upload steps log a live Azure DevOps bearer JWT on failure, which # # ends up inside the test stackTrace / [FAIL] console blocks we capture. GitHub's # # GITHUB_OUTPUT secret detector then skips the ENTIRE part1_data output # # ("Skip output 'part1_data' since it may contain secret"), starving the agent of all # # Part 1 data and producing a false noop. Scrubbing removes the trigger and avoids # # surfacing live tokens in the prompt and uploaded artifacts. # _SECRET_PATTERNS = [ # re.compile(r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}'), # JWT (header.payload.signature) # re.compile(r'eyJ[A-Za-z0-9_-]{20,}'), # bare JWT segment # re.compile(r'\bgh[pousr]_[A-Za-z0-9]{20,}\b'), # GitHub token (ghp_/gho_/...) # re.compile(r'\bgithub_pat_[A-Za-z0-9_]{20,}\b'), # GitHub fine-grained PAT # re.compile(r'(?i)\bbearer\s+[A-Za-z0-9._~+/=-]{20,}'), # Authorization: Bearer <token> # re.compile(r'(?i)\bhttps?://[^/\s:@"]+:[^@\s/"]{6,}@'), # basic-auth credentials in URL # re.compile(r'(?i)[?&]sig=[A-Za-z0-9%/+_=-]{20,}'), # Azure SAS signature # re.compile(r'(?i)\b(?:AccountKey|SharedAccessKey|AccessKey|Password|Pwd)=[^;\s"\']{12,}'), # connection-string secret # re.compile(r'[A-Za-z0-9][A-Za-z0-9+/=_-]{51,}'), # long high-entropy run (AzDO PAT, base64); must start with an alnum so it skips ===/--- separators # ] # # def scrub_secrets(s): # # Replace token-shaped substrings with a placeholder. Patterns run in order; # # earlier, more specific rules win, and "[REDACTED]" is inert for later rules. # if not s: # return s # for _pat in _SECRET_PATTERNS: # s = _pat.sub("[REDACTED]", s) # return s # # # def fetch(url, headers=None, retries=3, raw=False, timeout=120): # hdrs = {"User-Agent": "aspnetcore-test-quarantine"} # if headers: # hdrs.update(headers) # last = None # for attempt in range(retries): # try: # req = urllib.request.Request(url, headers=hdrs) # with urllib.request.urlopen(req, timeout=timeout) as r: # data = r.read() # return (data if raw else json.loads(data)), r.headers # except urllib.error.HTTPError as e: # if e.code in (401, 403, 404): # raise # last = e # except Exception as e: # last = e # time.sleep(2 * (attempt + 1)) # raise last # # # def list_failed_builds(definition, branch=None): # """Return the failed/partial build objects (not just ids) so we can record metadata.""" # mt = (datetime.datetime.utcnow() - datetime.timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ") # tok, out = None, [] # while True: # p = {"definitions": definition, "statusFilter": "completed", # "resultFilter": "failed,partiallySucceeded", "$top": 200, # "minTime": mt, "api-version": "7.1"} # if branch: # p["branchName"] = branch # if tok: # p["continuationToken"] = tok # data, h = fetch(f"{ADO}/build/builds?{urllib.parse.urlencode(p)}") # out += data.get("value", []) # tok = h.get("x-ms-continuationtoken") # if not tok: # break # return out # # # def list_completed_builds(definition, branch=None): # """Return ALL completed build objects (any result) so we can reconstruct the # per-pipeline timeline and detect PASSING runs between failures. Lightweight: # build metadata only, no test-result calls.""" # mt = (datetime.datetime.utcnow() - datetime.timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ") # tok, out = None, [] # while True: # p = {"definitions": definition, "statusFilter": "completed", "$top": 200, # "minTime": mt, "api-version": "7.1"} # if branch: # p["branchName"] = branch # if tok: # p["continuationToken"] = tok # data, h = fetch(f"{ADO}/build/builds?{urllib.parse.urlencode(p)}") # out += data.get("value", []) # tok = h.get("x-ms-continuationtoken") # if not tok: # break # return out # # # def builds_by_ids(ids): # out = [] # for k in range(0, len(ids), 100): # chunk = ",".join(str(i) for i in ids[k:k + 100]) # data, _ = fetch(f"{ADO}/build/builds?buildIds={chunk}&api-version=7.1") # out += data.get("value", []) # return out # # # def pr_of(build): # br = build.get("sourceBranch", "") or "" # if br.startswith("refs/pull/"): # try: # return int(br.split("/")[2]) # except (IndexError, ValueError): # return None # return None # # # def build_meta(build): # return {"def": (build.get("definition") or {}).get("id"), # "startedUtc": build.get("startTime"), # "finishedUtc": build.get("finishTime"), # "sourceVersion": build.get("sourceVersion"), # "pr": pr_of(build)} # # # def failed_results(build_id): # tok = None # while True: # p = {"buildId": build_id, "outcomes": "Failed", "$top": 1000, "api-version": "7.1-preview.1"} # if tok: # p["continuationToken"] = tok # data, h = fetch(f"{VSTMR}/testresults/resultsbyBuild?{urllib.parse.urlencode(p)}") # for t in data.get("value", []): # yield t # tok = h.get("x-ms-continuationtoken") # if not tok: # break # # # def norm_name(t): # name = t.get("automatedTestName") or "" # if not name: # # testCaseTitle can carry parameterized args; strip them for stable dedup. # name = (t.get("testCaseTitle") or "").split("(")[0].strip() # return name # # # def aggregate(build_ids): # agg = {} # for bid in build_ids: # for t in failed_results(bid): # name = norm_name(t) # if not name: # continue # e = agg.setdefault(name, {"count": 0, "assembly": t.get("automatedTestStorage", ""), # "builds": [], "occ": []}) # e["count"] += 1 # if bid not in e["builds"]: # e["builds"].append(bid) # if t.get("runId") and t.get("id") and len(e["occ"]) < OCC_CAP: # e["occ"].append({"runId": t["runId"], "resultId": t["id"], "build": bid}) # return agg # # # def parse_helix(comment): # if not comment: # return None, None # try: # c = json.loads(comment) # except (json.JSONDecodeError, TypeError): # return None, None # return c.get("HelixJobId"), c.get("HelixWorkItemName") # # # def result_detail(run_id, result_id): # data, _ = fetch(f"{VSTMR}/testresults/runs/{run_id}/results/{result_id}?api-version=7.1-preview.1") # return data # # # def enrich(agg): # """Attach Helix coords (job+workitem, only when BOTH present) and, for individual # tests, real error/stack from the representative result detail. For work items, also # collect candidate (job, workitem, build) probes from every tracked occurrence so # Source C can try more than just the first build.""" # for name, e in agg.items(): # is_wi = name.endswith(WI_SUFFIX) # probes = [] # for idx, occ in enumerate(e.get("occ", [])): # # Individual tests only need the first occurrence (error/stack + coords). # if not is_wi and idx > 0: # break # try: # det = result_detail(occ["runId"], occ["resultId"]) # except Exception as ex: # if idx == 0: # e["detail_note"] = f"detail fetch failed: {type(ex).__name__}" # continue # job, wi_name = parse_helix(det.get("comment")) # if idx == 0 and job and wi_name: # e["helix"] = {"job": job, "workitem": wi_name} # if is_wi and job and wi_name: # probes.append({"job": job, "workitem": wi_name, "build": occ["build"]}) # if idx == 0 and not is_wi: # em, st = det.get("errorMessage"), det.get("stackTrace") # if em: # e["error"] = scrub_secrets(em)[:ERROR_CAP] # if st: # e["stack"] = scrub_secrets(st)[:STACK_CAP] # if is_wi: # e["probes"] = probes # return agg # # # _MARKER = re.compile(r'\[(?:PASS|FAIL|SKIP)\]\s*$') # _FAIL = re.compile(r'\[FAIL\]\s*$') # # # def extract_fail_blocks(text): # lines = [_ANSI.sub("", ln) for ln in text.splitlines()] # blocks, i = [], 0 # while i < len(lines): # if _FAIL.search(lines[i]): # j = i + 1 # while j < len(lines) and not _MARKER.search(lines[j]): # j += 1 # blocks.append(scrub_secrets("\n".join(lines[i:j]))[:BLOCK_CAP]) # i = j # else: # i += 1 # return blocks # # # def helix_console_blocks(job_id, wi_name): # files, _ = fetch(f"{HELIX}/jobs/{job_id}/workitems/{urllib.parse.quote(wi_name)}/files") # seq = files if isinstance(files, list) else files.get("Files", files.get("files", [])) # link = None # for f in seq: # nm = f.get("Name") or f.get("name") or "" # if nm.startswith("console."): # link = f.get("Link") or f.get("link") # break # if not link: # return None, 0 # raw, _ = fetch(link, raw=True, timeout=180) # text = raw.decode("utf-8", "replace") # # Return the raw byte length: it feeds the byte-denominated download budget # # and the reported log_bytes, whereas len(text) is a decoded character count. # return extract_fail_blocks(text), len(raw) # # # def sizeof(obj): # return len(json.dumps(obj, separators=(",", ":"))) # # # def emit(out): # """Serialize, but guarantee the result stays under SAFE_OUTPUT by progressively # shedding the largest optional payloads (never the core per-test counts). Fail loud # if even the trimmed core is too big, rather than letting GITHUB_OUTPUT silently # truncate into corrupt JSON.""" # if sizeof(out) <= SAFE_OUTPUT: # return json.dumps(out, separators=(",", ":")) # out["trim"] = [] # for src in ("source_a", "source_b"): # for e in out[src].values(): # e.pop("stack", None) # out["trim"].append("stack_dropped") # if sizeof(out) <= SAFE_OUTPUT: # return json.dumps(out, separators=(",", ":")) # for src in ("source_a", "source_b"): # for e in out[src].values(): # e.pop("error", None) # out["trim"].append("error_dropped") # if sizeof(out) <= SAFE_OUTPUT: # return json.dumps(out, separators=(",", ":")) # for c in out["source_c"]: # if "fail_blocks" in c: # c["fail_blocks"] = c["fail_blocks"][:2000] # out["trim"].append("source_c_blocks_trimmed") # js = json.dumps(out, separators=(",", ":")) # if len(js) > SAFE_OUTPUT: # sys.exit(f"FATAL: part1_data is {len(js)} bytes after trimming, exceeds the " # f"{SAFE_OUTPUT}-byte safe limit — aborting rather than emitting truncated JSON") # return js # # # def mark_intermittency(source_a, all_main_builds, bmeta): # """Set `is_consistent_regression` on every individual test in source_a. # # A test is a CONSISTENT REGRESSION (not flaky) when, on ANY `main` pipeline (def) # where it failed 2+ times, its two most recent failures on that pipeline were in # back-to-back runs with NO passing run in between. That is the signature of a real # regression, so such a test must NOT be auto-quarantined under Case A — quarantining # it would hide the regression. The check is conservative on purpose: a back-to-back # failure streak on EITHER pipeline blocks quarantine, even if the test happened to # look intermittent on the other pipeline (an intermittent pattern on one pipeline # must never mask a hard regression on another). # # A "pass" between two failures is a completed `main` build on the SAME def that # SUCCEEDED or PARTIALLY SUCCEEDED (so tests actually ran), started strictly between # the two failures, and in which this test did NOT fail (not in its failing-build # set). `failed`/`canceled` builds are excluded — a compile/infra break produces no # test results and must not be mistaken for a passing run. # # A test with fewer than two failures on every single pipeline has too little # evidence of consistency, so `is_consistent_regression` is False (the gate does not # block it; it is judged on the other Case A criteria — e.g. PR-only flakes).""" # PASS_RESULTS = ("succeeded", "partiallySucceeded") # # Per-def ascending (startedUtc, id) timeline + result/def lookup. Seed the # # start/def of every Source A failing build from `bmeta` first (it carries `def` # # and `startedUtc` for each), so a failure always has a timestamp + pipeline even # # if it falls outside the full-timeline window below; then layer the completed-build # # timeline (the only source of `result`, needed to spot passing runs) on top. # bstart, bdef, bresult, by_def = {}, {}, {}, {} # for sid, mv in bmeta.items(): # try: # bid = int(sid) # except (TypeError, ValueError): # continue # if mv.get("startedUtc") and mv.get("def") is not None: # bstart[bid] = mv["startedUtc"] # bdef[bid] = mv["def"] # for b in all_main_builds: # bid = b.get("id") # d = (b.get("definition") or {}).get("id") # st = b.get("startTime") # if bid is None or d is None or not st: # continue # bstart[bid] = st # bdef[bid] = d # bresult[bid] = b.get("result") # by_def.setdefault(d, []).append((st, bid)) # for d in by_def: # by_def[d].sort() # # for name, e in source_a.items(): # if name.endswith(WI_SUFFIX): # continue # failset = set(e.get("builds", [])) # # Group this test's timestamped failures by the pipeline they ran on. # fails_by_def = {} # for bid in failset: # if bid in bstart and bid in bdef: # fails_by_def.setdefault(bdef[bid], []).append(bstart[bid]) # regression = False # for d, fl in fails_by_def.items(): # if len(fl) < 2: # continue # fl.sort() # t2, t1 = fl[-2], fl[-1] # two most recent failures on this def # passed_here = False # for st, bid in by_def.get(d, []): # if st <= t2: # continue # if st >= t1: # break # if bid not in failset and bresult.get(bid) in PASS_RESULTS: # passed_here = True # break # if not passed_here: # # Back-to-back failures on this pipeline with no pass between -> regression. # regression = True # break # e["is_consistent_regression"] = regression # # # def main(): # # Source A: failed/partial builds on main, both pipelines, last 30 days. # a_builds = [b for d in DEFS for b in list_failed_builds(d, branch="refs/heads/main")] # bmeta = {} # for b in a_builds: # bmeta[str(b["id"])] = build_meta(b) # source_a = enrich(aggregate([b["id"] for b in a_builds])) # # Flakiness signal: needs the FULL main timeline (incl. succeeded builds), not just # # the failed/partial builds above, to spot a passing run between two failures. # all_main_builds = [b for d in DEFS for b in list_completed_builds(d, branch="refs/heads/main")] # mark_intermittency(source_a, all_main_builds, bmeta) # # # Source B: preselected merged-PR build ids (env from the Verify Source B PRs step). # raw_ids = os.environ.get("SOURCE_B_BUILD_IDS", "").strip() # if not raw_ids: # b_ids = [] # else: # try: # b_ids = json.loads(raw_ids) # if not isinstance(b_ids, list): # raise ValueError("not a list") # except (json.JSONDecodeError, ValueError) as ex: # sys.exit(f"FATAL: SOURCE_B_BUILD_IDS is set but not a valid JSON array ({ex}) — aborting") # if b_ids: # for b in builds_by_ids(b_ids): # bmeta[str(b["id"])] = build_meta(b) # source_b = enrich(aggregate(b_ids)) # # # Source C: work items (combined A+B) -> Helix console [FAIL] blocks. Probe each # # tracked occurrence until one yields [FAIL] blocks (the first build is often a # # macOS hang with none, while a later build has the real failure). # wi = {} # for src in (source_a, source_b): # for name, e in src.items(): # if name.endswith(WI_SUFFIX) and e.get("probes"): # lst = wi.setdefault(name, []) # seen = {(p["job"], p["workitem"], p["build"]) for p in lst} # for p in e["probes"]: # k = (p["job"], p["workitem"], p["build"]) # if k not in seen: # seen.add(k) # lst.append(p) # # source_c = [] # truncated = False # total = 0 # downloaded = [0] # mutable: total Helix log bytes pulled across all probes # # def probe(pr): # blocks, log_size = helix_console_blocks(pr["job"], pr["workitem"]) # downloaded[0] += log_size # return blocks, log_size # # for name in sorted(wi): # probes = wi[name] # if truncated: # source_c.append({"workitem": name, "build": probes[0]["build"], "job": probes[0]["job"], # "note": "omitted: Source C global size cap reached"}) # continue # chosen = None # last_err = None # for idx, pr in enumerate(probes): # # Always fetch the first occurrence; only probe further while under the # # download budget (degrades to first-occurrence-only during big regressions). # if idx > 0 and downloaded[0] >= SOURCE_C_DOWNLOAD_BUDGET: # break # try: # blocks, log_size = probe(pr) # except Exception as ex: # last_err = type(ex).__name__ # continue # if blocks is None: # chosen = chosen or {"build": pr["build"], "job": pr["job"], "blocks": None, "log": 0} # continue # chosen = {"build": pr["build"], "job": pr["job"], "blocks": blocks, "log": log_size} # if blocks: # break # found real [FAIL] content; stop probing # if chosen is None: # source_c.append({"workitem": name, "build": probes[0]["build"], "job": probes[0]["job"], # "note": f"investigation error: {last_err}" if last_err else "no probe succeeded"}) # continue # if chosen["blocks"] is None: # source_c.append({"workitem": name, "build": chosen["build"], "job": chosen["job"], # "note": "no console log file found"}) # continue # joined = "\n---\n".join(chosen["blocks"])[:WORKITEM_CAP] # if total + len(joined) > SOURCE_C_GLOBAL_CAP: # truncated = True # source_c.append({"workitem": name, "build": chosen["build"], "job": chosen["job"], # "note": "omitted: Source C global size cap reached"}) # continue # total += len(joined) # source_c.append({"workitem": name, "build": chosen["build"], "job": chosen["job"], # "log_bytes": chosen["log"], "fail_block_count": len(chosen["blocks"]), # "fail_blocks": joined}) # # # Drop internal-only fields from the per-test payload. # for src in (source_a, source_b): # for e in src.values(): # e.pop("occ", None) # e.pop("probes", None) # # out = { # "generated_utc": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), # "builds": bmeta, # "source_a": source_a, # "source_b": source_b, # "source_c": source_c, # "source_c_truncated": truncated, # } # js = emit(out) # regr = sum(1 for n, e in source_a.items() # if not n.endswith(WI_SUFFIX) and e.get("is_consistent_regression")) # sys.stderr.write(f"part1: A={len(source_a)} tests, B={len(source_b)} tests, " # f"C={len(source_c)} work items, builds={len(bmeta)}, " # f"main_builds={len(all_main_builds)}, regression_A={regr}, " # f"output {len(js)/1024:.0f} KB, source_c_truncated={truncated}, " # f"trim={out.get('trim')}\n") # return js # # # # The agent prompt receives this JSON through the part1_data_N job outputs, which gh-aw # # interpolates into the prompt and passes to the prompt-building steps as ENVIRONMENT # # VARIABLES. Linux caps a single env-var string at MAX_ARG_STRLEN (32 * 4 KiB page = # # 131072 bytes) when starting a process; a value larger than that makes the prompt step # # die with "Argument list too long" (E2BIG) before it even runs — which is exactly what # # happened once the data started flowing at full size. So the payload is split into a # # fixed number of byte-bounded chunks (each well under the limit) written to separate # # outputs; the prompt concatenates the chunks back together with no separator, # # reconstructing the JSON verbatim. # PART1_CHUNKS = 16 # MUST match the count of part1_data_N refs in the prompt body # PART1_CHUNK_BYTES = 80000 # << 131072 MAX_ARG_STRLEN, leaving ample room for the var name # # # def write_part1_chunks(f, js): # """Split `js` into <=PART1_CHUNKS pieces of <=PART1_CHUNK_BYTES bytes each, never cutting # a multi-byte UTF-8 sequence, and write them as part1_data_0..part1_data_{N-1} outputs. # Unused slots are emitted empty so the prompt's fixed set of placeholders always resolves.""" # data = js.encode("utf-8") # chunks = [] # i, n = 0, len(data) # while i < n: # end = min(i + PART1_CHUNK_BYTES, n) # # back off to a UTF-8 character boundary (continuation bytes are 0b10xxxxxx) # while end < n and (data[end] & 0xC0) == 0x80: # end -= 1 # chunks.append(data[i:end].decode("utf-8")) # i = end # if len(chunks) > PART1_CHUNKS: # sys.exit(f"FATAL: part1_data needs {len(chunks)} chunks but only {PART1_CHUNKS} " # f"output slots exist — raise PART1_CHUNKS and add matching " # f"part1_data_N references in the prompt body") # for k in range(PART1_CHUNKS): # chunk = chunks[k] if k < len(chunks) else "" # f.write(f"part1_data_{k}<<PART1_EOF\n{chunk}\nPART1_EOF\n") # # # if __name__ == "__main__": # # Final safety net: scrub the fully serialized payload as well. GitHub skips a # # part1_data_N output if any token pattern is detected, so a leaked secret in # # any field (not just the three scrubbed above) would starve the agent. Scrubbing # # only ever shrinks the payload, so it stays under the GITHUB_OUTPUT size cap. # js = scrub_secrets(main()) # gh_out = os.environ.get("GITHUB_OUTPUT") # if not gh_out: # sys.exit("ERROR: GITHUB_OUTPUT is not set, cannot pass Part 1 data to agent") # with open(gh_out, "a") as f: # write_part1_chunks(f, js) # SCRIPT workflow_dispatch: inputs: aw_context: default: "" description: "Agent caller context (used internally by Agentic Workflows)." required: false type: string permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}" cancel-in-progress: true run-name: "Daily Test Quarantine Management" jobs: activation: needs: - pat_pool - pre_activation - pre_activation if: > needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'workflow_dispatch' || !github.event.repository.fork) 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_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }} model: ${{ steps.generate_aw_info.outputs.model }} oauth_token_check_failed: ${{ steps.check-oauth-tokens.outputs.oauth_token_check_failed == 'true' }} 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 }} stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }} steps: - name: Setup Scripts id: setup uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 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: "Daily Test Quarantine Management" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/test-quarantine.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.77" GH_AW_INFO_AWF_VERSION: "v0.27.43" GH_AW_INFO_ENGINE_ID: "copilot" - name: Generate agentic run info id: generate_aw_info env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_INFO_VERSION: "1.0.77" GH_AW_INFO_AGENT_VERSION: "1.0.77" GH_AW_INFO_CLI_VERSION: "v0.84.3" GH_AW_INFO_WORKFLOW_NAME: "Daily Test Quarantine Management" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","dev.azure.com","vstmr.dev.azure.com","helix.dot.net","learn.microsoft.com","*.vsblob.vsassets.io","*.vssps.visualstudio.com","*.blob.core.windows.net"]' GH_AW_INFO_FIREWALL_ENABLED: "true" GH_AW_INFO_AWF_VERSION: "v0.27.43" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" GH_AW_COMPILED_STRICT: "true" uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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/generate_aw_info.cjs'); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} continue-on-error: true uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: agentic-workflow-usage-testquarantine-${{ github.run_id }} restore-keys: agentic-workflow-usage-testquarantine- path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl - name: Restore daily AIC usage cache (artifact fallback) id: restore-daily-aic-cache-fallback if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_RESTORE_DAILY_AIC_CACHE_HIT: ${{ steps.restore-daily-aic-cache.outputs.cache-hit }} GH_AW_RESTORE_DAILY_AIC_CACHE_MATCHED_KEY: ${{ steps.restore-daily-aic-cache.outputs.cache-matched-key }} 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/restore_aic_usage_cache_fallback.cjs'); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail if: ${{ env.GH_AW_MAX_DAILY_AI_CREDITS != '' }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_NAME: "Daily Test Quarantine Management" GH_AW_WORKFLOW_ID: "test-quarantine" 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_HAS_SLASH_COMMAND: "false" GH_AW_HAS_LABEL_COMMAND: "false" GH_AW_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_AW_MAX_DAILY_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_DAILY_AI_CREDITS || '5000' }} 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_daily_aic_workflow_guardrail.cjs'); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, 'NO COPILOT PAT AVAILABLE') }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents .antigravity .claude .codex .gemini .opencode .pi sparse-checkout-cone-mode: true fetch-depth: 1 - name: Save agent config folders for base branch restoration env: GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" # poutine:ignore untrusted_checkout_exec run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh" - name: Check workflow lock file id: check-lock-file uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_WORKFLOW_FILE: "test-quarantine.lock.yml" GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" 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/check_workflow_timestamp_api.cjs'); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_COMPILED_VERSION: "v0.84.3" 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/check_version_updates.cjs'); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_CLOSED_QUARANTINE_PRS: ${{ needs.pre_activation.outputs.closed_quarantine_prs }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_0: ${{ needs.pre_activation.outputs.part1_data_0 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_1: ${{ needs.pre_activation.outputs.part1_data_1 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_10: ${{ needs.pre_activation.outputs.part1_data_10 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_11: ${{ needs.pre_activation.outputs.part1_data_11 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_12: ${{ needs.pre_activation.outputs.part1_data_12 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_13: ${{ needs.pre_activation.outputs.part1_data_13 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_14: ${{ needs.pre_activation.outputs.part1_data_14 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_15: ${{ needs.pre_activation.outputs.part1_data_15 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_2: ${{ needs.pre_activation.outputs.part1_data_2 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_3: ${{ needs.pre_activation.outputs.part1_data_3 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_4: ${{ needs.pre_activation.outputs.part1_data_4 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_5: ${{ needs.pre_activation.outputs.part1_data_5 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_6: ${{ needs.pre_activation.outputs.part1_data_6 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_7: ${{ needs.pre_activation.outputs.part1_data_7 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_8: ${{ needs.pre_activation.outputs.part1_data_8 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_9: ${{ needs.pre_activation.outputs.part1_data_9 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_DATA: ${{ needs.pre_activation.outputs.requarantine_data }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_ISSUE_NUMBERS: ${{ needs.pre_activation.outputs.requarantine_issue_numbers }} # poutine:ignore untrusted_checkout_exec run: | bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" { cat << 'GH_AW_PROMPT_d72a60219b00cc4f_EOF' <system> GH_AW_PROMPT_d72a60219b00cc4f_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/safe_outputs_prompt.md" cat << 'GH_AW_PROMPT_d72a60219b00cc4f_EOF' <safe-output-tools> Tools: add_comment(max:10), create_issue(max:10), create_pull_request(max:10), add_labels, missing_tool, missing_data, noop GH_AW_PROMPT_d72a60219b00cc4f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" cat << 'GH_AW_PROMPT_d72a60219b00cc4f_EOF' </safe-output-tools> GH_AW_PROMPT_d72a60219b00cc4f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" cat << 'GH_AW_PROMPT_d72a60219b00cc4f_EOF' <github-context> The following GitHub context information is available for this workflow: {{#if github.actor}} - **actor**: __GH_AW_GITHUB_ACTOR__ {{/if}} {{#if github.repository}} - **repository**: __GH_AW_GITHUB_REPOSITORY__ {{/if}} {{#if github.workspace}} - **workspace**: __GH_AW_GITHUB_WORKSPACE__ {{/if}} {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}} - **issue-number**: #__GH_AW_EXPR_802A9F6A__ {{/if}} {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}} - **discussion-number**: #__GH_AW_EXPR_1A3A194A__ {{/if}} {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}} - **pull-request-number**: #__GH_AW_EXPR_463A214A__ {{/if}} {{#if github.event.comment.id || github.aw.context.comment_id}} - **comment-id**: __GH_AW_EXPR_FF1D34CE__ {{/if}} {{#if github.run_id}} - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__ {{/if}} - **checkouts**: The following repositories have been checked out and are available in the workspace: - repo `__GH_AW_GITHUB_REPOSITORY__` → `$GITHUB_WORKSPACE` (cwd) [full history, all branches available as remote-tracking refs] - **Note**: If a branch you need is not in the list above and is not listed as an additional fetched ref, it has NOT been checked out. For private repositories you cannot fetch it. If the branch is required and not available, exit with an error and ask the user to add it to the `fetch:` option of the `checkout:` configuration (e.g., `fetch: ["refs/pulls/open/*"]` for all open PR refs, or `fetch: ["main", "feature/my-branch"]` for specific branches). - **Warning: No git credentials are available to the agent.** Credentials are intentionally removed after the checkout step for security. This means any git operation that needs to authenticate to the remote will fail. In private repositories, that includes: - `git fetch`, `git pull`, `git clone`, and `git push` (direct push, not via safe-output tools) - Checking out or switching to a remote branch that is not already fetched - Deepening a shallow clone (`git fetch --unshallow`) - On-demand blob fetches in partial/blobless clones (operations on files not in the initial checkout) Do NOT attempt to configure credentials, run `git credential fill`, or modify `.gitconfig` — authentication will not succeed. If you encounter credential prompts or authentication errors, stop immediately and report the limitation rather than spending turns trying to work around it. </github-context> GH_AW_PROMPT_d72a60219b00cc4f_EOF cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" cat << 'GH_AW_PROMPT_d72a60219b00cc4f_EOF' </system> {{#runtime-import .github/workflows/test-quarantine.md}} GH_AW_PROMPT_d72a60219b00cc4f_EOF } > "$GH_AW_PROMPT" - name: Interpolate variables and render templates uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_CLOSED_QUARANTINE_PRS: ${{ needs.pre_activation.outputs.closed_quarantine_prs }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_0: ${{ needs.pre_activation.outputs.part1_data_0 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_1: ${{ needs.pre_activation.outputs.part1_data_1 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_10: ${{ needs.pre_activation.outputs.part1_data_10 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_11: ${{ needs.pre_activation.outputs.part1_data_11 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_12: ${{ needs.pre_activation.outputs.part1_data_12 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_13: ${{ needs.pre_activation.outputs.part1_data_13 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_14: ${{ needs.pre_activation.outputs.part1_data_14 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_15: ${{ needs.pre_activation.outputs.part1_data_15 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_2: ${{ needs.pre_activation.outputs.part1_data_2 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_3: ${{ needs.pre_activation.outputs.part1_data_3 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_4: ${{ needs.pre_activation.outputs.part1_data_4 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_5: ${{ needs.pre_activation.outputs.part1_data_5 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_6: ${{ needs.pre_activation.outputs.part1_data_6 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_7: ${{ needs.pre_activation.outputs.part1_data_7 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_8: ${{ needs.pre_activation.outputs.part1_data_8 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_9: ${{ needs.pre_activation.outputs.part1_data_9 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_DATA: ${{ needs.pre_activation.outputs.requarantine_data }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_ISSUE_NUMBERS: ${{ needs.pre_activation.outputs.requarantine_issue_numbers }} 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/interpolate_prompt.cjs'); await main(); - name: Substitute placeholders uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }} GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }} GH_AW_GITHUB_ACTOR: ${{ github.actor }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_CLOSED_QUARANTINE_PRS: ${{ needs.pre_activation.outputs.closed_quarantine_prs }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_0: ${{ needs.pre_activation.outputs.part1_data_0 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_1: ${{ needs.pre_activation.outputs.part1_data_1 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_10: ${{ needs.pre_activation.outputs.part1_data_10 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_11: ${{ needs.pre_activation.outputs.part1_data_11 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_12: ${{ needs.pre_activation.outputs.part1_data_12 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_13: ${{ needs.pre_activation.outputs.part1_data_13 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_14: ${{ needs.pre_activation.outputs.part1_data_14 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_15: ${{ needs.pre_activation.outputs.part1_data_15 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_2: ${{ needs.pre_activation.outputs.part1_data_2 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_3: ${{ needs.pre_activation.outputs.part1_data_3 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_4: ${{ needs.pre_activation.outputs.part1_data_4 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_5: ${{ needs.pre_activation.outputs.part1_data_5 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_6: ${{ needs.pre_activation.outputs.part1_data_6 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_7: ${{ needs.pre_activation.outputs.part1_data_7 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_8: ${{ needs.pre_activation.outputs.part1_data_8 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_9: ${{ needs.pre_activation.outputs.part1_data_9 }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_DATA: ${{ needs.pre_activation.outputs.requarantine_data }} GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_ISSUE_NUMBERS: ${{ needs.pre_activation.outputs.requarantine_issue_numbers }} with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); // Call the substitution function return await substitutePlaceholders({ file: process.env.GH_AW_PROMPT, substitutions: { GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A, GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A, GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A, GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE, GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR, GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY, GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID, GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_CLOSED_QUARANTINE_PRS: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_CLOSED_QUARANTINE_PRS, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_0: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_0, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_1: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_1, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_10: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_10, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_11: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_11, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_12: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_12, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_13: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_13, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_14: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_14, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_15: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_15, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_2: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_2, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_3: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_3, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_4: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_4, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_5: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_5, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_6: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_6, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_7: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_7, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_8: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_8, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_9: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_PART1_DATA_9, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_DATA: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_DATA, GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_ISSUE_NUMBERS: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_REQUARANTINE_ISSUE_NUMBERS } }); - name: Validate prompt placeholders env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt # poutine:ignore untrusted_checkout_exec run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh" - name: Print prompt env: GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt # poutine:ignore untrusted_checkout_exec run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" - name: Upload activation artifact if: success() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation include-hidden-files: true path: | /tmp/gh-aw/aw_info.json /tmp/gh-aw/models.json /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/aw-prompts/prompt-template.txt /tmp/gh-aw/aw-prompts/prompt-import-tree.json /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/base /tmp/gh-aw/.github/agents /tmp/gh-aw/.github/skills if-no-files-found: ignore retention-days: 1 agent: needs: - activation - pat_pool if: needs.activation.outputs.daily_ai_credits_exceeded != 'true' runs-on: ubuntu-latest environment: copilot-pat-pool permissions: actions: read contents: read issues: read pull-requests: read env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" GH_AW_ASSETS_BRANCH: "" GH_AW_ASSETS_MAX_SIZE_KB: 0 GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: testquarantine outputs: agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }} ai_credits_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.ai_credits_rate_limit_error || 'false' }} aic: ${{ steps.parse-mcp-gateway.outputs.aic }} ambient_context: ${{ steps.parse-mcp-gateway.outputs.ambient_context }} checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }} effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }} has_patch: ${{ steps.collect_output.outputs.has_patch }} http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} model: ${{ needs.activation.outputs.model }} model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }} output: ${{ steps.collect_output.outputs.output }} output_types: ${{ steps.collect_output.outputs.output_types }} 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 }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} 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: "Daily Test Quarantine Management" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/test-quarantine.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.77" GH_AW_INFO_AWF_VERSION: "v0.27.43" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths run: | { echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl" echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json" } >> "$GITHUB_OUTPUT" - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false fetch-depth: 0 - name: Create gh-aw temp directory run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh" - name: Configure gh CLI for GitHub Enterprise run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh" env: GH_TOKEN: ${{ github.token }} - name: Download activation artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: activation path: /tmp/gh-aw - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Checkout PR branch id: checkout-pr if: | github.event.pull_request || github.event.issue.pull_request || github.event_name == 'workflow_dispatch' && fromJSON(github.event.inputs.aw_context || '{}').item_type == 'pull_request' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io, getOctokit); const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); await main(); - name: Install GitHub Copilot CLI run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com GH_AW_COMPILED_VERSION: v0.84.3 - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.43 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9) env: GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} with: script: | const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: GH_AW_SUB_AGENT_DIR: ".github/agents" GH_AW_SUB_AGENT_EXT: ".agent.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh" - name: Restore inline skills from activation artifact env: 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.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1 ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d ghcr.io/github/gh-aw-mcpg:v0.4.7@sha256:7545220a9aca134b71e51193ee0eaf4c50756ebf8fbd25a63ae7556e62815c00 ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196 ghcr.io/github/github-mcp-server:v1.8.0@sha256:d5a18c04b92714c309eb46a2305087e91a4dbd80420f6e462656699f95093520 - 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_5b2be0200c705dba_EOF' {"add_comment":{"max":10,"target":"*"},"add_labels":{"allowed":["re-quarantine"]},"create_issue":{"labels":["test-failure"],"max":10,"title_prefix":"Quarantine "},"create_pull_request":{"allowed_files":["src/**/*.cs"],"base_branch":"main","draft":false,"labels":["test-failure"],"max":10,"max_patch_files":100,"max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[test-quarantine] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"report_incomplete":{}} GH_AW_SAFE_OUTPUTS_CONFIG_5b2be0200c705dba_EOF - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | { "description_suffixes": { "add_comment": " CONSTRAINTS: Maximum 10 comment(s) can be added. Target: *. Supports reply_to_id for discussion threading.", "add_labels": " CONSTRAINTS: Only these labels are allowed: [\"re-quarantine\"].", "create_issue": " CONSTRAINTS: Maximum 10 issue(s) can be created. Title will be prefixed with \"Quarantine \". Labels [\"test-failure\"] will be automatically added.", "create_pull_request": " CONSTRAINTS: Maximum 10 pull request(s) can be created. Title will be prefixed with \"[test-quarantine] \". Labels [\"test-failure\"] will be automatically added." }, "repo_params": {}, "dynamic_tools": [] } GH_AW_VALIDATION_JSON: | { "add_comment": { "defaultMax": 1, "fields": { "body": { "required": true, "type": "string", "sanitize": true, "maxLength": 65000 }, "item_number": { "issueOrPRNumber": true }, "reply_to_id": { "type": "string", "maxLength": 256 }, "repo": { "type": "string", "maxLength": 256 } } }, "add_labels": { "defaultMax": 5, "fields": { "item_number": { "issueNumberOrTemporaryId": true }, "labels": { "required": true, "type": "array" }, "repo": { "type": "string", "maxLength": 256 } } }, "create_issue": { "defaultMax": 1, "fields": { "body": { "required": true, "type": "string", "sanitize": true, "maxLength": 65000, "minLength": 20 }, "fields": { "type": "array" }, "labels": { "type": "array", "itemType": "string", "itemSanitize": true, "itemMaxLength": 128 }, "parent": { "issueOrPRNumber": true }, "repo": { "type": "string", "maxLength": 256 }, "temporary_id": { "type": "string" }, "title": { "required": true, "type": "string", "sanitize": true, "maxLength": 128 } } }, "create_pull_request": { "defaultMax": 1, "fields": { "base": { "type": "string", "sanitize": true, "maxLength": 128 }, "body": { "required": true, "type": "string", "sanitize": true, "maxLength": 65000 }, "branch": { "required": true, "type": "string", "sanitize": true, "maxLength": 256 }, "draft": { "type": "boolean" }, "labels": { "type": "array", "itemType": "string", "itemSanitize": true, "itemMaxLength": 128 }, "repo": { "type": "string", "maxLength": 256 }, "title": { "required": true, "type": "string", "sanitize": true, "maxLength": 128 } } }, "missing_data": { "defaultMax": 20, "fields": { "alternatives": { "type": "string", "sanitize": true, "maxLength": 256 }, "context": { "type": "string", "sanitize": true, "maxLength": 256 }, "data_type": { "type": "string", "sanitize": true, "maxLength": 128 }, "reason": { "type": "string", "sanitize": true, "maxLength": 256 } } }, "missing_tool": { "defaultMax": 20, "fields": { "alternatives": { "type": "string", "sanitize": true, "maxLength": 512 }, "reason": { "required": true, "type": "string", "sanitize": true, "maxLength": 256 }, "tool": { "type": "string", "sanitize": true, "maxLength": 128 } } }, "noop": { "defaultMax": 1, "fields": { "message": { "required": true, "type": "string", "sanitize": true, "maxLength": 65000 } } }, "report_incomplete": { "defaultMax": 5, "fields": { "details": { "type": "string", "sanitize": true, "maxLength": 65000 }, "reason": { "required": true, "type": "string", "sanitize": true, "maxLength": 1024 } } } } uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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/generate_safe_outputs_tools.cjs'); await main(); - name: Start MCP Gateway id: start-mcp-gateway env: GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST: ${{ vars.GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST || 'true' }} GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} 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 }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" export MCP_GATEWAY_HOST_DOMAIN="localhost" MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=') echo "::add-mask::${MCP_GATEWAY_API_KEY}" export MCP_GATEWAY_API_KEY export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" export DEBUG="*" export GH_AW_ENGINE="copilot" MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GH_AW_SINK_VISIBILITY -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e RUNNER_TEMP -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.7' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) cat << GH_AW_MCP_CONFIG_eda67c15876dba3e_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.8.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_MCP_SERVER_TOKEN}", "GITHUB_READ_ONLY": "1", "GITHUB_TOOLSETS": "repos,issues,pull_requests,search" }, "guard-policies": { "allow-only": { "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY", "repos": "$GITHUB_MCP_GUARD_REPOS" } } }, "safeoutputs": { "type": "stdio", "container": "ghcr.io/github/gh-aw-node", "mounts": ["\${GITHUB_WORKSPACE}:\${GITHUB_WORKSPACE}:rw", "${RUNNER_TEMP}/gh-aw/safeoutputs:${RUNNER_TEMP}/gh-aw/safeoutputs:rw", "/tmp/gh-aw:/tmp/gh-aw:rw"], "args": ["-w", "\${GITHUB_WORKSPACE}"], "entrypoint": "sh", "entrypointArgs": ["-c", "sh ${RUNNER_TEMP}/gh-aw/safeoutputs/start_safe_outputs_mcp.sh"], "env": { "DEBUG": "*", "DEFAULT_BRANCH": "\${DEFAULT_BRANCH}", "GH_AW_ASSETS_ALLOWED_EXTS": "\${GH_AW_ASSETS_ALLOWED_EXTS}", "GH_AW_ASSETS_BRANCH": "\${GH_AW_ASSETS_BRANCH}", "GH_AW_ASSETS_MAX_SIZE_KB": "\${GH_AW_ASSETS_MAX_SIZE_KB}", "GH_AW_MCP_LOG_DIR": "\${GH_AW_MCP_LOG_DIR}", "GH_AW_SAFE_OUTPUTS": "\${GH_AW_SAFE_OUTPUTS}", "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", "GITHUB_WORKSPACE": "\${GITHUB_WORKSPACE}", "RUNNER_TEMP": "\${RUNNER_TEMP}" }, "guard-policies": { "write-sink": { "accept": [ "*" ], "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } }, "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", "apiKey": "${MCP_GATEWAY_API_KEY}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", "startupTimeout": 120 } } GH_AW_MCP_CONFIG_eda67c15876dba3e_EOF - name: Mount MCP servers as CLIs id: mount-mcp-clis continue-on-error: true env: MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }} MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context, exec, io); const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); await main(); - name: Clean credentials continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh" - name: Audit pre-agent workspace id: pre_agent_audit continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh" - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): # --allow-tool github # --allow-tool safeoutputs # --allow-tool shell(cat) # --allow-tool shell(curl:*) # --allow-tool shell(date) # --allow-tool shell(echo) # --allow-tool shell(git add:*) # --allow-tool shell(git branch:*) # --allow-tool shell(git checkout:*) # --allow-tool shell(git commit:*) # --allow-tool shell(git merge:*) # --allow-tool shell(git rm:*) # --allow-tool shell(git status) # --allow-tool shell(git switch:*) # --allow-tool shell(git:*) # --allow-tool shell(github:*) # --allow-tool shell(grep) # --allow-tool shell(head) # --allow-tool shell(ls) # --allow-tool shell(printf) # --allow-tool shell(pwd) # --allow-tool shell(python3) # --allow-tool shell(safeoutputs:*) # --allow-tool shell(sort) # --allow-tool shell(tail) # --allow-tool shell(uniq) # --allow-tool shell(wc) # --allow-tool shell(yq) # --allow-tool web_fetch # --allow-tool write timeout-minutes: 90 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/agent-stdio.log) # shellcheck disable=SC2016 printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json","network":{"allowDomains":["*.blob.core.windows.net","*.vsblob.vsassets.io","*.vssps.visualstudio.com","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","dev.azure.com","github.com","helix.dot.net","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","learn.microsoft.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","vstmr.dev.azure.com","www.googleapis.com"],"isolation":true,"topologyAttach":["awmg-mcpg"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxCacheMisses":5,"maxAiCredits":2000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.5","gpt-5.6","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"auto":["copilot/auto","large"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex","kimi"],"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*"],"detection":["small"],"evals":["small"],"fable":["copilot/*fable*","anthropic/*fable*"],"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-3.6-flash":["copilot/gemini-3.6*flash*","google/gemini-3.6*flash*","gemini/gemini-3.6*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-omni":["copilot/gemini-omni*","google/gemini-omni*","gemini/gemini-omni*"],"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.1":["copilot/gpt-5.1*","openai/gpt-5.1*"],"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*"],"gpt-5.6":["copilot/gpt-5.6*","openai/gpt-5.6*"],"grok":["copilot/*grok*","openai/*grok*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"image-generation":["copilot/gpt-image*","openai/gpt-image*","openai/chatgpt-image*","copilot/gemini-*image*","google/gemini-*image*","gemini/gemini-*image*","google/imagen*"],"kimi":["copilot/kimi*","openai/kimi*"],"kiwi":["copilot/kiwi*","openai/kiwi*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"lyria":["google/lyria*","gemini/lyria*","copilot/lyria*"],"mai-code":["copilot/MAI-Code*","copilot/mai-code*","openai/MAI-Code*"],"mai-code-1-flash-picker":["copilot/MAI-Code-1-Flash-picker*","copilot/mai-code-1-flash-picker*","openai/MAI-Code-1-Flash-picker*"],"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"],"raptor-mini":["copilot/raptor*","openai/raptor*"],"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-5*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*","anthropic/*sonnet-5*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"veo":["google/veo*","gemini/veo*"],"vision":["copilot/gemini-*image*","google/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab"},"logging":{"proxyLogsDir":"/tmp/gh-aw/sandbox/firewall/logs","auditDir":"/tmp/gh-aw/sandbox/firewall/audit"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json" 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" GH_AW_DOCKER_HOST="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST="${DOCKER_HOST}" fi if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_CHROOT_BINARIES_SOURCE_PATH="${RUNNER_TEMP}/gh-aw" GH_AW_CHROOT_IDENTITY_HOME="${RUNNER_TEMP}/gh-aw/home" node "${RUNNER_TEMP}/gh-aw/actions/patch_awf_chroot_config.cjs" fi GH_AW_TOOL_CACHE_MOUNT="" GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" if [ -d "$GH_AW_TOOL_CACHE" ]; then if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" fi fi # shellcheck disable=SC1003,SC2016,SC2086 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(curl:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(git add:*)'\'' --allow-tool '\''shell(git branch:*)'\'' --allow-tool '\''shell(git checkout:*)'\'' --allow-tool '\''shell(git commit:*)'\'' --allow-tool '\''shell(git merge:*)'\'' --allow-tool '\''shell(git rm:*)'\'' --allow-tool '\''shell(git status)'\'' --allow-tool '\''shell(git switch:*)'\'' --allow-tool '\''shell(git:*)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(python3)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool web_fetch --allow-tool write --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 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, 'NO COPILOT PAT AVAILABLE') }} COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: agent GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_TIMEOUT_MINUTES: 90 GH_AW_VERSION: v0.84.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md GITHUB_WORKSPACE: ${{ github.workspace }} GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - name: Detect agent errors if: always() id: detect-agent-errors continue-on-error: true run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_TOKEN: ${{ github.token }} run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Copy Copilot session state files to logs if: always() continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh" - name: Stop MCP Gateway if: always() continue-on-error: true env: MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }} MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" - name: Redact secrets in logs if: always() uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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/redact_secrets.cjs'); await main(); env: GH_AW_SECRET_NAMES: 'COPILOT_PAT_0,COPILOT_PAT_1,COPILOT_PAT_2,COPILOT_PAT_3,COPILOT_PAT_4,COPILOT_PAT_5,COPILOT_PAT_6,COPILOT_PAT_7,COPILOT_PAT_8,COPILOT_PAT_9,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' SECRET_COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} SECRET_COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} SECRET_COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} SECRET_COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} SECRET_COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} SECRET_COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} SECRET_COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} SECRET_COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} SECRET_COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} SECRET_COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Append agent step summary if: always() run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh" - name: Copy Safe Outputs if: always() env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} run: | mkdir -p /tmp/gh-aw cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true - name: Ingest agent output id: collect_output if: always() uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_ALLOWED_DOMAINS: "*.blob.core.windows.net,*.vsblob.vsassets.io,*.vssps.visualstudio.com,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,dev.azure.com,github.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,learn.microsoft.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,vstmr.dev.azure.com,www.googleapis.com" GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_API_URL: ${{ github.api_url }} 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/collect_ndjson_output.cjs'); await main(); - name: Parse agent logs for step summary if: always() uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} 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/parse_copilot_log.cjs'); await main(); - name: Parse MCP Gateway logs for step summary if: always() id: parse-mcp-gateway uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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/parse_mcp_gateway_log.cjs'); await main(); - name: Print firewall logs if: always() continue-on-error: true env: AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs run: bash "${RUNNER_TEMP}/gh-aw/actions/print_firewall_logs.sh" --rootless - name: Parse token usage for step summary if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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/parse_token_usage.cjs'); await main(); - name: Print AWF reflect summary if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 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/awf_reflect_summary.cjs'); await main(); - name: Write agent output placeholder if missing if: always() run: | if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi - name: Upload agent artifacts if: always() continue-on-error: true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: agent path: | /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/sandbox/agent/logs/ /tmp/gh-aw/redacted-urls.log /tmp/gh-aw/mcp-logs/ /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent-stdio.log /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch /tmp/gh-aw/aw-*.bundle /tmp/gh-aw/awf-config.json /tmp/gh-aw/sandbox/firewall/logs/ /tmp/gh-aw/sandbox/firewall/audit/ /tmp/gh-aw/sandbox/firewall/awf-reflect.json if-no-files-found: ignore conclusion: needs: - activation - agent - detection - pat_pool - safe_outputs if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim environment: copilot-pat-pool permissions: contents: write issues: write pull-requests: write concurrency: group: "gh-aw-conclusion-test-quarantine" cancel-in-progress: false queue: max env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }} noop_message: ${{ steps.noop.outputs.noop_message }} tools_reported: ${{ steps.missing_tool.outputs.tools_reported }} total_count: ${{ steps.missing_tool.outputs.total_count }} steps: - name: Setup Scripts id: setup uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} 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: "Daily Test Quarantine Management" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/test-quarantine.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.77" GH_AW_INFO_AWF_VERSION: "v0.27.43" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: agent path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env if: steps.download-agent-output.outcome == 'success' run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Download safe outputs items manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: safe-outputs-items path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: usage path: | /tmp/gh-aw/usage/aw_info.json /tmp/gh-aw/usage/aw-info.jsonl /tmp/gh-aw/usage/agent_usage.json /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl /tmp/gh-aw/usage/activity/summary.json if-no-files-found: ignore - name: Restore daily AIC usage cache id: restore-daily-aic-cache-conclusion if: always() continue-on-error: true uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: agentic-workflow-usage-testquarantine-${{ github.run_id }} restore-keys: agentic-workflow-usage-testquarantine- path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl - name: Write daily AIC usage cache entry id: write-daily-aic-cache if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: github-token: ${{ github.token }} script: | const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); setupGlobals(core, github, context); const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache if: always() continue-on-error: true uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: key: agentic-workflow-usage-testquarantine-${{ github.run_id }} path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl - name: Upload daily AIC usage cache artifact id: upload-daily-aic-cache if: always() continue-on-error: true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: aic-usage-cache path: /tmp/gh-aw/agentic-workflow-usage-cache.jsonl if-no-files-found: ignore retention-days: 7 - name: Process no-op messages id: noop 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_NOOP_MAX: "1" GH_AW_WORKFLOW_NAME: "Daily Test Quarantine Management" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/test-quarantine.md" 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: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_WORKFLOW_ID: "test-quarantine" with: github-token: ${{ secrets.GH_AW_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/handle_noop_message.cjs'); await main(); - name: Log detection run id: detection_runs 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: "Daily Test Quarantine Management" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/test-quarantine.md" 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 }} with: github-token: ${{ secrets.GH_AW_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/handle_detection_runs.cjs'); await main(); - name: Record missing tool id: missing_tool 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_MISSING_TOOL_CREATE_ISSUE: "true" GH_AW_WORKFLOW_NAME: "Daily Test Quarantine Management" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/test-quarantine.md" with: github-token: ${{ secrets.GH_AW_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/missing_tool.cjs'); await main(); - name: Record incomplete id: report_incomplete 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_REPORT_INCOMPLETE_CREATE_ISSUE: "true" GH_AW_WORKFLOW_NAME: "Daily Test Quarantine Management" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/test-quarantine.md" with: github-token: ${{ secrets.GH_AW_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/report_incomplete_handler.cjs'); await main(); - name: Handle agent failure id: handle_agent_failure if: always() 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: "Daily Test Quarantine Management" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/test-quarantine.md" 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: "test-quarantine" GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} GH_AW_AI_CREDITS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.ai_credits_rate_limit_error || 'false' }} GH_AW_UNKNOWN_MODEL_AI_CREDITS: ${{ needs.agent.outputs.unknown_model_ai_credits || 'false' }} GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_MAX_AI_CREDITS: "2000" GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }} GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }} GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }} GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }} GH_AW_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }} GH_AW_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }} GH_AW_DAILY_AI_CREDITS_EXCEEDED: ${{ needs.activation.outputs.daily_ai_credits_exceeded }} GH_AW_DAILY_AI_CREDITS_TOTAL_EFFECTIVE_TOKENS: ${{ needs.activation.outputs.daily_ai_credits_total_effective_tokens }} GH_AW_DAILY_AI_CREDITS_THRESHOLD: ${{ needs.activation.outputs.daily_ai_credits_threshold }} GH_AW_GROUP_REPORTS: "false" GH_AW_FAILURE_REPORT_AS_ISSUE: "false" GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true" GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true" GH_AW_TIMEOUT_MINUTES: "90" with: github-token: ${{ secrets.GH_AW_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/handle_agent_failure.cjs'); await main(); detection: needs: - activation - agent - pat_pool if: always() && needs.agent.result != 'skipped' runs-on: ubuntu-latest environment: copilot-pat-pool permissions: contents: read env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: aic: ${{ steps.parse_detection_token_usage.outputs.aic }} detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }} detection_reason: ${{ steps.detection_conclusion.outputs.reason }} detection_success: ${{ steps.detection_conclusion.outputs.success }} steps: - name: Setup Scripts id: setup uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} 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: "Daily Test Quarantine Management" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/test-quarantine.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.77" GH_AW_INFO_AWF_VERSION: "v0.27.43" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: agent path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env if: steps.download-agent-output.outcome == 'success' run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- - name: Clean stale firewall files from agent artifact run: | rm -rf /tmp/gh-aw/sandbox/firewall/logs rm -rf /tmp/gh-aw/sandbox/firewall/audit - name: Download container images run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.27.43@sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.43@sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1 ghcr.io/github/gh-aw-firewall/squid:0.27.43@sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d - name: Check if detection needed id: detection_guard if: always() env: OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }} HAS_PATCH: ${{ needs.agent.outputs.has_patch }} run: | if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then echo "run_detection=true" >> "$GITHUB_OUTPUT" echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH" else echo "run_detection=false" >> "$GITHUB_OUTPUT" echo "Detection skipped: no agent outputs or patches to analyze" fi - name: Clear MCP Config for detection if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json" rm -f "$HOME/.copilot/mcp-config.json" rm -f "$GITHUB_WORKSPACE/.gemini/settings.json" - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection/aw-prompts rm -f /tmp/gh-aw/agent_usage.json cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context." fi cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true for f in /tmp/gh-aw/aw-*.patch; do if [ -f "$f" ]; then cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true fi done for f in /tmp/gh-aw/aw-*.bundle; do if [ -f "$f" ]; then cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true fi done echo "Prepared threat detection files:" ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Daily Test Quarantine Management" WORKFLOW_DESCRIPTION: "Daily quarantine/unquarantine flaky tests based on Azure DevOps pipeline analytics" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "false" 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/setup_threat_detection.cjs'); await main(); - name: Ensure threat-detection directory and log if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | mkdir -p /tmp/gh-aw/threat-detection touch /tmp/gh-aw/threat-detection/detection.log - name: Setup Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '24' package-manager-cache: false - name: Install GitHub Copilot CLI run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com GH_AW_COMPILED_VERSION: v0.84.3 - name: Install AWF binary run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.43 - name: Execute GitHub Copilot CLI if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true id: detection_agentic_execution # Copilot CLI tool arguments (sorted): timeout-minutes: 20 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" touch /tmp/gh-aw/agent-step-summary.md GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true) export GH_AW_NODE_BIN export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK" (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.43/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"api.business.githubcopilot.com\",\"api.enterprise.githubcopilot.com\",\"api.github.com\",\"api.githubcopilot.com\",\"api.individual.githubcopilot.com\",\"github.com\",\"host.docker.internal\",\"registry.npmjs.org\",\"telemetry.enterprise.githubcopilot.com\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"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\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"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-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.43,squid=sha256:26be5e0b8c8f4c41c8a59126b29bb5d80b07253597472ded2a16bdd75abcbf9d,agent=sha256:04e2d1987a565000a8f114b89d806ae7a3864dd4f944be65275b28c93d8690e6,api-proxy=sha256:d85f57975af5ea23af4996e41ed73fbc8f5b4a47402472bfe82e508f352cb0c1,cli-proxy=sha256:65c45ea2967984d0024f3df61bc71335658a77ede96c8d9665da7a5f33a795ab\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" 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" GH_AW_DOCKER_HOST="" if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then GH_AW_DOCKER_HOST="${DOCKER_HOST}" fi if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" fi GH_AW_TOOL_CACHE_MOUNT="" GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" if [ -d "$GH_AW_TOOL_CACHE" ]; then if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro" fi fi # shellcheck disable=SC1003,SC2016,SC2086 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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 --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ case(needs.pat_pool.outputs.pat_number == '0', secrets.COPILOT_PAT_0, needs.pat_pool.outputs.pat_number == '1', secrets.COPILOT_PAT_1, needs.pat_pool.outputs.pat_number == '2', secrets.COPILOT_PAT_2, needs.pat_pool.outputs.pat_number == '3', secrets.COPILOT_PAT_3, needs.pat_pool.outputs.pat_number == '4', secrets.COPILOT_PAT_4, needs.pat_pool.outputs.pat_number == '5', secrets.COPILOT_PAT_5, needs.pat_pool.outputs.pat_number == '6', secrets.COPILOT_PAT_6, needs.pat_pool.outputs.pat_number == '7', secrets.COPILOT_PAT_7, needs.pat_pool.outputs.pat_number == '8', secrets.COPILOT_PAT_8, needs.pat_pool.outputs.pat_number == '9', secrets.COPILOT_PAT_9, 'NO COPILOT PAT AVAILABLE') }} COPILOT_MODEL: detection GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt GH_AW_TIMEOUT_MINUTES: 20 GH_AW_VERSION: v0.84.3 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows GITHUB_HEAD_REF: ${{ github.head_ref }} GITHUB_REF_NAME: ${{ github.ref_name }} GITHUB_SERVER_URL: ${{ github.server_url }} GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md GITHUB_WORKSPACE: ${{ github.workspace }} GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com GIT_AUTHOR_NAME: github-actions[bot] GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com GIT_COMMITTER_NAME: github-actions[bot] RUNNER_TEMP: ${{ runner.temp }} TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - name: Parse threat detection token usage for step summary id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage 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/parse_token_usage.cjs'); await main(); - name: Upload threat detection log if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: detection path: /tmp/gh-aw/threat-detection/detection.log if-no-files-found: ignore - name: Parse and conclude threat detection id: detection_conclusion if: always() uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }} DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }} GH_AW_DETECTION_CONTINUE_ON_ERROR: "false" with: script: | try { 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/parse_threat_detection_results.cjs'); await main(); } catch (loadErr) { const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false'; const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure'; const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr)); core.error(msg); core.setOutput('reason', 'parse_error'); if (continueOnError && !detectionExecutionFailed) { core.warning('\u26A0\uFE0F ' + msg); core.setOutput('conclusion', 'warning'); core.setOutput('success', 'false'); } else { core.setOutput('conclusion', 'failure'); core.setOutput('success', 'false'); core.setFailed(msg); } } pat_pool: needs: pre_activation runs-on: ubuntu-slim environment: copilot-pat-pool outputs: pat_number: ${{ steps.select-pat-number.outputs.copilot_pat_number }} steps: - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" GH_HOST="${GH_HOST#http://}" echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Select Copilot token from pool id: select-pat-number run: | # Collect pool entries with non-empty secrets from COPILOT_PAT_0..COPILOT_PAT_9. PAT_NUMBERS=() POOL_INDICATORS=(➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖ ➖) for i in $(seq 0 9); do var="COPILOT_PAT_${i}" val="${!var}" if [ -n "$val" ]; then PAT_NUMBERS+=(${i}) POOL_INDICATORS[${i}]="🟪" fi done # If none of the entries in the pool have values, emit a warning # and do not set an output value. The consumer can fall back to # using COPILOT_GITHUB_TOKEN. if [ ${#PAT_NUMBERS[@]} -eq 0 ]; then warning_message="::warning::None of the PAT pool entries had values " warning_message+="(checked COPILOT_PAT_0 through COPILOT_PAT_9)" echo "$warning_message" exit 0 fi # Select a random index using the seed if specified if [ -n "$RANDOM_SEED" ]; then RANDOM=$RANDOM_SEED fi PAT_INDEX=$(( RANDOM % ${#PAT_NUMBERS[@]} )) PAT_NUMBER="${PAT_NUMBERS[$PAT_INDEX]}" POOL_INDICATORS[${PAT_NUMBER}]="✅" echo "Pool size: ${#PAT_NUMBERS[@]}" echo "Selected PAT number ${PAT_NUMBER} (index: ${PAT_INDEX})" # Emit a markdown table of the pool entries to the step summary echo "|0|1|2|3|4|5|6|7|8|9|" >> "$GITHUB_STEP_SUMMARY" echo "|-|-|-|-|-|-|-|-|-|-|" >> "$GITHUB_STEP_SUMMARY" (IFS='|'; printf '|%s' "${POOL_INDICATORS[@]}"; printf '|\n') >> "$GITHUB_STEP_SUMMARY" # Set the PAT number as the output echo "copilot_pat_number=${PAT_NUMBER}" >> "$GITHUB_OUTPUT" env: COPILOT_PAT_0: ${{ secrets.COPILOT_PAT_0 }} COPILOT_PAT_1: ${{ secrets.COPILOT_PAT_1 }} COPILOT_PAT_2: ${{ secrets.COPILOT_PAT_2 }} COPILOT_PAT_3: ${{ secrets.COPILOT_PAT_3 }} COPILOT_PAT_4: ${{ secrets.COPILOT_PAT_4 }} COPILOT_PAT_5: ${{ secrets.COPILOT_PAT_5 }} COPILOT_PAT_6: ${{ secrets.COPILOT_PAT_6 }} COPILOT_PAT_7: ${{ secrets.COPILOT_PAT_7 }} COPILOT_PAT_8: ${{ secrets.COPILOT_PAT_8 }} COPILOT_PAT_9: ${{ secrets.COPILOT_PAT_9 }} RANDOM_SEED: ${{ github.aw.import-inputs.random_seed }} shell: bash pre_activation: if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork runs-on: ubuntu-slim environment: copilot-pat-pool env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }} closed_quarantine_prs: ${{ steps.closed_quarantine_prs.outputs.closed_quarantine_prs }} closed_quarantine_prs_result: ${{ steps.closed_quarantine_prs.outcome }} matched_command: '' part1_aggregate_result: ${{ steps.part1_aggregate.outcome }} part1_data_0: ${{ steps.part1_aggregate.outputs.part1_data_0 }} part1_data_1: ${{ steps.part1_aggregate.outputs.part1_data_1 }} part1_data_10: ${{ steps.part1_aggregate.outputs.part1_data_10 }} part1_data_11: ${{ steps.part1_aggregate.outputs.part1_data_11 }} part1_data_12: ${{ steps.part1_aggregate.outputs.part1_data_12 }} part1_data_13: ${{ steps.part1_aggregate.outputs.part1_data_13 }} part1_data_14: ${{ steps.part1_aggregate.outputs.part1_data_14 }} part1_data_15: ${{ steps.part1_aggregate.outputs.part1_data_15 }} part1_data_2: ${{ steps.part1_aggregate.outputs.part1_data_2 }} part1_data_3: ${{ steps.part1_aggregate.outputs.part1_data_3 }} part1_data_4: ${{ steps.part1_aggregate.outputs.part1_data_4 }} part1_data_5: ${{ steps.part1_aggregate.outputs.part1_data_5 }} part1_data_6: ${{ steps.part1_aggregate.outputs.part1_data_6 }} part1_data_7: ${{ steps.part1_aggregate.outputs.part1_data_7 }} part1_data_8: ${{ steps.part1_aggregate.outputs.part1_data_8 }} part1_data_9: ${{ steps.part1_aggregate.outputs.part1_data_9 }} requarantine_data: ${{ steps.requarantine_prs.outputs.requarantine_data }} requarantine_issue_numbers: ${{ steps.requarantine_issues.outputs.requarantine_issue_numbers }} requarantine_issues_result: ${{ steps.requarantine_issues.outcome }} requarantine_prs_result: ${{ steps.requarantine_prs.outcome }} 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 }} source_b_build_ids: ${{ steps.source_b_prs.outputs.source_b_build_ids }} source_b_prs_result: ${{ steps.source_b_prs.outcome }} steps: - name: Setup Scripts id: setup uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "Daily Test Quarantine Management" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/test-quarantine.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.77" GH_AW_INFO_AWF_VERSION: "v0.27.43" 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(); - name: Fetch re-quarantine PRs id: requarantine_prs run: | # Fetch all merged PRs with re-quarantine label or title, bypassing DIFC filtering. # The agent's MCP search tools filter out PRs from external contributors, which # can hide legitimate re-quarantine PRs. This deterministic step runs with full # GitHub token access and writes results that get injected into the agent prompt. python3 << 'SCRIPT' import json, os, urllib.request token = os.environ["GH_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"} def search_prs(query): results = [] url = f"https://api.github.com/search/issues?q={query}&per_page=100" while url: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req) as resp: data = json.loads(resp.read()) results.extend(data.get("items", [])) # Follow pagination link = resp.headers.get("Link", "") url = None for part in link.split(","): if 'rel="next"' in part: url = part.split("<")[1].split(">")[0] return results def get_changed_files(pr_number): url = f"https://api.github.com/repos/dotnet/aspnetcore/pulls/{pr_number}/files?per_page=100" files = [] while url: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req) as resp: files.extend(json.loads(resp.read())) link = resp.headers.get("Link", "") url = None for part in link.split(","): if 'rel="next"' in part: url = part.split("<")[1].split(">")[0] return files # Search by label and by title by_label = search_prs("repo:dotnet/aspnetcore+is:pr+is:merged+label:re-quarantine") by_title = search_prs("repo:dotnet/aspnetcore+is:pr+is:merged+%22Re-quarantine%22+in:title") # Deduplicate by PR number seen = set() prs = [] for pr in by_label + by_title: if pr["number"] not in seen: seen.add(pr["number"]) prs.append(pr) # For each PR, get changed files and check for QuarantinedTest additions. # Store the added lines containing [QuarantinedTest so the agent can match at # method/class/assembly level, not just file level. requarantine_data = [] for pr in prs: files = get_changed_files(pr["number"]) quarantine_entries = [] for f in files: patch = f.get("patch", "") if not patch and f.get("status") in ("modified", "added"): # Patch may be omitted for large diffs — fail closed by # treating the whole file as potentially re-quarantined quarantine_entries.append({ "filename": f["filename"], "added_lines": [], "patch_truncated": True }) continue added = [line[1:] for line in patch.split("\n") if line.startswith("+") and "[QuarantinedTest" in line] if added: quarantine_entries.append({ "filename": f["filename"], "added_lines": added, "patch_truncated": False }) requarantine_data.append({ "number": pr["number"], "title": pr["title"], "quarantine_entries": quarantine_entries }) # Write JSON to GITHUB_OUTPUT so it flows through jobs.pre_activation.outputs # into the agent prompt. /tmp/ is NOT shared between pre_activation and agent jobs. import sys # Filter out PRs with no quarantine_entries — they're irrelevant and # keeping them wastes step output / prompt token budget. requarantine_data = [pr for pr in requarantine_data if pr["quarantine_entries"]] json_str = json.dumps(requarantine_data) github_output = os.environ.get("GITHUB_OUTPUT", "") if not github_output: print("ERROR: GITHUB_OUTPUT is not set, cannot pass data to agent", file=sys.stderr) sys.exit(1) with open(github_output, "a") as gh_out: gh_out.write(f"requarantine_data<<REQUARANTINE_EOF\n{json_str}\nREQUARANTINE_EOF\n") print(f"Found {len(requarantine_data)} re-quarantine PRs, wrote to step output") SCRIPT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Fetch re-quarantine issue numbers id: requarantine_issues run: | # Fetch the numbers of every issue carrying the `re-quarantine` label. A test # tracked by one of these issues has been deliberately re-quarantined and must # NEVER be auto-unquarantined. This is a deterministic complement to the # re-quarantine-PR diff check: matching a candidate's [QuarantinedTest] issue URL # against this set is exact and cannot be missed by fuzzy diff parsing. python3 << 'SCRIPT' import json, os, sys, urllib.parse, urllib.request token = os.environ["GH_TOKEN"] headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json"} def search_issues(query): results = [] url = ("https://api.github.com/search/issues?" + urllib.parse.urlencode({"q": query, "per_page": 100})) while url: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read()) if data.get("incomplete_results"): sys.exit("FATAL: GitHub search returned incomplete_results for the " "re-quarantine issue query; failing closed rather than " "unquarantining against a partial blocklist") results.extend(data.get("items", [])) link = resp.headers.get("Link", "") url = None for part in link.split(","): if 'rel="next"' in part: url = part.split("<")[1].split(">")[0] return results # Any state — a re-quarantined issue may be closed by a later unquarantine PR but # the test remains permanently blocked from automated unquarantining. items = search_issues("repo:dotnet/aspnetcore is:issue label:re-quarantine") numbers = sorted({it["number"] for it in items}) github_output = os.environ.get("GITHUB_OUTPUT", "") if not github_output: print("ERROR: GITHUB_OUTPUT is not set, cannot pass data to agent", file=sys.stderr) sys.exit(1) with open(github_output, "a") as gh_out: gh_out.write(f"requarantine_issue_numbers={json.dumps(numbers)}\n") print(f"Found {len(numbers)} re-quarantine issue numbers, wrote to step output") SCRIPT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Fetch closed test-quarantine PRs id: closed_quarantine_prs run: | # Fetch closed-but-unmerged [test-quarantine] PRs from the last 30 days, plus any # (open or closed) [test-quarantine] PR carrying the `no-quarantine-for-30-days` or # `no-unquarantine-for-30-days` label, bypassing DIFC filtering. The agent's MCP # search tools silently drop PRs authored by this workflow's own bot # (app/github-actions), which hides maintainer "do not (un)quarantine" feedback and # causes the workflow to re-create previously rejected PRs. This deterministic step # runs with full token access so that signal always reaches the agent. # # We deliberately do NOT read PR/issue comment bodies as a trust signal: comments are # free text anyone can post regardless of permission level, so treating them as # authoritative would let an untrusted actor inject a fake "do not quarantine" # instruction into the agent's prompt. Only two signals are used, and both are # mechanically un-spoofable: # 1. `trusted_closed` — a human (not the bot itself) closed the bot's own PR. Both # queries below are restricted to PRs authored by this workflow's own bot # (author:app/github-actions), so closing one of them requires the closer to be # the bot itself or a repo collaborator with triage/write access — closing # someone else's PR on GitHub is not possible without that permission. (An # earlier revision also treated public dotnet org membership as trusted, but # public org membership does not imply write access — a public member could open # and close their own fake PR to spoof this signal — so that fallback was # removed.) # 2. `quarantine_label_added_at` / `unquarantine_label_added_at` — the PR carries # the `no-quarantine-for-30-days` or `no-unquarantine-for-30-days` label, # respectively. These are two distinct labels because "don't (re-)quarantine # this test" and "don't unquarantine this test" are opposite actions — a label # opting out of one must never suppress the other. GitHub only allows accounts # with triage/write access to add labels, so a label's mere presence is itself # proof of a privileged decision — no author-identity check is needed on top of # it. python3 << 'SCRIPT' import json, os, secrets, sys, urllib.parse, urllib.request, urllib.error from datetime import datetime, timedelta, timezone token = os.environ["GH_TOKEN"] headers = {"Authorization": f"******", "Accept": "application/vnd.github+json"} since = (datetime.now(timezone.utc) - timedelta(days=30)).strftime("%Y-%m-%d") NO_QUARANTINE_LABEL = "no-quarantine-for-30-days" NO_UNQUARANTINE_LABEL = "no-unquarantine-for-30-days" BOT_AUTHOR = "app/github-actions" def search_prs(query): results = [] url = ("https://api.github.com/search/issues?" + urllib.parse.urlencode({"q": query, "per_page": 100})) while url: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read()) if data.get("incomplete_results"): sys.exit("FATAL: GitHub search returned incomplete_results for the " "closed test-quarantine PR query; failing closed rather than " "risking re-creation of an already-rejected PR") results.extend(data.get("items", [])) link = resp.headers.get("Link", "") url = None for part in link.split(","): if 'rel="next"' in part: url = part.split("<")[1].split(">")[0] return results def get_issue(number): # closed_at is present on search results, but closed_by is not, so fetch the # issue record to learn who closed our own rejected attempt. This drives the # per-test "count only failures after the close" cutoff below. url = f"https://api.github.com/repos/dotnet/aspnetcore/issues/{number}" req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=30) as resp: d = json.loads(resp.read()) return {"closed_at": d.get("closed_at"), "closed_by": (d.get("closed_by") or {}).get("login")} def get_label_added_at(number, label_name): # Returns the most recent time `label_name` was added to this issue/PR (via the # issue timeline events), or None if it isn't currently applied or was never # added via a "labeled" event we can see. Using the most recent add (rather than # the first) means a maintainer can re-arm the 30-day window by removing and # re-adding the label. url = f"https://api.github.com/repos/dotnet/aspnetcore/issues/{number}/events?per_page=100" latest = None while url: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=30) as resp: for ev in json.loads(resp.read()): if ev.get("event") == "labeled" and (ev.get("label") or {}).get("name") == label_name: ts = ev.get("created_at") if ts and (latest is None or ts > latest): latest = ts link = resp.headers.get("Link", "") url = None for part in link.split(","): if 'rel="next"' in part: url = part.split("<")[1].split(">")[0] return latest closed_prs = search_prs( 'repo:dotnet/aspnetcore is:pr is:closed is:unmerged ' f'author:{BOT_AUTHOR} "test-quarantine" in:title closed:>={since}' ) # Any state: a maintainer may label a PR the workflow left open (without closing it) # to opt a test out of automated action for 30 days. Restricted to bot-authored PRs, # same as above — this label only has meaning on the workflow's own PRs. Two separate # queries (one per label) rather than a single OR query, so each stays simple and # explicit about which label it's fetching. labeled_prs_quarantine = search_prs( f'repo:dotnet/aspnetcore is:pr author:{BOT_AUTHOR} ' f'"test-quarantine" in:title label:{NO_QUARANTINE_LABEL}' ) labeled_prs_unquarantine = search_prs( f'repo:dotnet/aspnetcore is:pr author:{BOT_AUTHOR} ' f'"test-quarantine" in:title label:{NO_UNQUARANTINE_LABEL}' ) by_number = {pr["number"]: pr for pr in closed_prs} for pr in labeled_prs_quarantine: by_number.setdefault(pr["number"], pr) for pr in labeled_prs_unquarantine: by_number.setdefault(pr["number"], pr) data = [] for number, pr in by_number.items(): # labeled_prs_quarantine/labeled_prs_unquarantine are queried across any PR # state (open/closed/merged), since a maintainer may label a PR after it # merges. But a merged PR represents a # *successful* action, not a rejected attempt — it must never seed a failure # cutoff below. Use pull_request.merged_at (present on search results) rather # than an extra API call to tell "merged" apart from "closed without merging". was_merged = bool((pr.get("pull_request") or {}).get("merged_at")) issue_meta = (get_issue(number) if pr.get("closed_at") and not was_merged else {"closed_at": None, "closed_by": None}) closed_by = issue_meta.get("closed_by") author_login = (pr.get("user") or {}).get("login") # Both queries above are restricted to author:app/github-actions, so this PR is # guaranteed bot-authored. On GitHub, closing a PR you did NOT author requires # triage or write permission, so any human (non-bot) closer whose login differs # from the bot author necessarily holds repo privileges — this is a deterministic, # zero-API signal with no unauthenticated fallback. closer_is_privileged_human = bool( closed_by and author_login and closed_by != author_login and not closed_by.endswith("[bot]") ) label_names = {lbl.get("name") for lbl in (pr.get("labels") or [])} quarantine_label_added_at = ( get_label_added_at(number, NO_QUARANTINE_LABEL) if NO_QUARANTINE_LABEL in label_names else None ) unquarantine_label_added_at = ( get_label_added_at(number, NO_UNQUARANTINE_LABEL) if NO_UNQUARANTINE_LABEL in label_names else None ) data.append({ "number": number, "title": pr["title"], # Grouped PRs often list only some tests in the title; the per-test # fully-qualified name lives in the body, which the agent matches on. "body": (pr.get("body") or "")[:2000], # closed_at is the cutoff timestamp: when a trusted contributor closed our # own rejected (re-)quarantine attempt, only failures AFTER this instant # should count toward re-attempting it (see the "prior-attempt cutoff" rule). # Null for merged PRs — a merge is a successful outcome, not a rejection, # and must never seed this cutoff. "closed_at": None if was_merged else (issue_meta.get("closed_at") or pr.get("closed_at")), "closed_by": closed_by, # "trusted_closed" is true only when a non-bot human closed this bot-authored # PR (requires triage/write access). No comment text or org-membership check # is consulted — public org membership does not imply write access. "trusted_closed": bool(closed_by and closer_is_privileged_human), # ISO-8601 timestamp of the most recent time `no-quarantine-for-30-days` was # added to this PR, or null if never applied. Governs (re-)quarantine # candidates only — see "Important Rules" for how these two label fields map # to candidate type. Adding a label requires triage/write access, so its # presence alone is sufficient proof of a privileged decision. "quarantine_label_added_at": quarantine_label_added_at, # Same as above, but for `no-unquarantine-for-30-days`, which governs # unquarantine candidates only. "unquarantine_label_added_at": unquarantine_label_added_at, }) github_output = os.environ.get("GITHUB_OUTPUT", "") if not github_output: print("ERROR: GITHUB_OUTPUT is not set, cannot pass data to agent", file=sys.stderr) sys.exit(1) js = json.dumps(data) # The value is injected into the agent prompt via an env var subject to the # 131072-byte MAX_ARG_STRLEN limit; fail closed if it would exceed a safe cap # rather than letting it be silently dropped later and starve the guardrail. MAX_OUTPUT_BYTES = 120000 if len(js) > MAX_OUTPUT_BYTES: sys.exit(f"FATAL: closed_quarantine_prs is {len(js)} bytes, exceeds " f"{MAX_OUTPUT_BYTES}; failing closed so the agent does not proceed " "without the recently-rejected-PR data") # Randomized, collision-checked heredoc delimiter: the value embeds user-controlled # PR bodies, so a fixed delimiter could in principle be reproduced in the data and # truncate the output. json.dumps already escapes newlines, but a random delimiter is # the GitHub-recommended defense-in-depth. delim = f"CLOSED_QUAR_EOF_{secrets.token_hex(16)}" while delim in js: delim = f"CLOSED_QUAR_EOF_{secrets.token_hex(16)}" with open(github_output, "a") as gh_out: gh_out.write(f"closed_quarantine_prs<<{delim}\n{js}\n{delim}\n") print(f"Found {len(data)} relevant test-quarantine PRs, wrote to step output") SCRIPT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Verify Source B PRs id: source_b_prs run: | # Source B looks for flaky tests in failed CI builds of PRs that were merged # into main. Selecting those builds requires verifying each candidate PR # (base==main, merged==true) and matching its head SHA — which needs a GitHub # token. The agent sandbox has NO usable token, and its MCP search tool # silently drops external-contributor PRs. So we do the ENTIRE selection here — # outside the firewall, with full token access and no integrity filter — and # hand the agent the exact Azure DevOps build IDs to collect results from. The # agent makes ZERO GitHub calls and does NOT re-enumerate builds, which both # eliminates the per-PR pull_request_read loop (the effective-token-budget # sink) and avoids any snapshot skew between this step and the agent. python3 << 'SCRIPT' import json, os, sys, time, datetime, urllib.parse, urllib.request, urllib.error def fetch(url, data=None, headers=None, retries=3): """GET (or POST if data) with small backoff. Re-raises HTTPError so the caller can distinguish auth failures; retries only transient errors.""" hdrs = {"User-Agent": "aspnetcore-test-quarantine"} if headers: hdrs.update(headers) last = None for attempt in range(retries): try: req = urllib.request.Request(url, data=data, headers=hdrs) with urllib.request.urlopen(req, timeout=60) as r: return json.loads(r.read()), r.headers except urllib.error.HTTPError as e: if e.code in (401, 403) or e.code == 404: raise last = e except Exception as e: last = e time.sleep(2 * (attempt + 1)) raise last # --- 1. Enumerate completed PR builds from the last 7 days (Azure DevOps, # public project — no auth needed) for both CI pipelines. Record each # failed/partial build's id, PR number and the commit it ran on. --- BUILDS = "https://dev.azure.com/dnceng-public/public/_apis/build/builds" DEFINITIONS = [83, 87] # 83 = aspnetcore-ci, 87 = components-e2e min_time = (datetime.datetime.utcnow() - datetime.timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%SZ") failed_builds = [] # list of (build_id, pr_number, source_sha) for d in DEFINITIONS: token = None while True: params = {"definitions": d, "reasonFilter": "pullRequest", "statusFilter": "completed", "minTime": min_time, "$top": 200, "api-version": "7.1"} if token: params["continuationToken"] = token data, hdrs = fetch(f"{BUILDS}?{urllib.parse.urlencode(params)}") # ADO returns the continuation token in a response header. token = hdrs.get("x-ms-continuationtoken") for b in data.get("value", []): if b.get("result") not in ("failed", "partiallySucceeded"): continue branch = b.get("sourceBranch", "") # refs/pull/{N}/merge if not branch.startswith("refs/pull/"): continue try: pr = int(branch.split("/")[2]) except (IndexError, ValueError): continue sha = (b.get("triggerInfo") or {}).get("pr.sourceSha") if sha: failed_builds.append((b["id"], pr, sha)) if not token: break # (B4) Only PRs with >= 1 failed/partial build can ever yield a candidate. candidates = sorted({pr for _, pr, _ in failed_builds}) # --- 2. Verify B2 (base == main) + B3 (merged) and capture head SHA via batched # GraphQL. Fail LOUD on systemic failures (auth, rate-limit, every chunk # failed, or no candidate could even be resolved) so the run aborts # visibly instead of silently emitting an empty set. --- gh_token = os.environ["GH_TOKEN"] def verify(pr_numbers, chunk=50): verified = {} # str(pr_number) -> headRefOid resolved = 0 # candidate PRs we positively read a node for chunks_total = chunks_failed = 0 for k in range(0, len(pr_numbers), chunk): batch = pr_numbers[k:k + chunk] chunks_total += 1 aliases = "\n".join( f'p{n}: pullRequest(number: {n}) {{ number baseRefName merged headRefOid }}' for n in batch) query = f'query {{ repository(owner: "dotnet", name: "aspnetcore") {{ {aliases} }} }}' try: body, _ = fetch( "https://api.github.com/graphql", data=json.dumps({"query": query}).encode(), headers={"Authorization": f"bearer {gh_token}", "Content-Type": "application/json"}) except urllib.error.HTTPError as e: if e.code in (401, 403): sys.exit(f"FATAL: GitHub GraphQL {e.code} — aborting Source B verification") chunks_failed += 1 continue except Exception: chunks_failed += 1 continue errored, chunk_untrusted = set(), False for err in body.get("errors") or []: if err.get("type") == "RATE_LIMITED": sys.exit("FATAL: GitHub GraphQL RATE_LIMITED — aborting Source B verification") alias = next((p for p in (err.get("path") or []) if isinstance(p, str) and len(p) > 1 and p[0] == "p" and p[1:].isdigit()), None) if alias: errored.add(alias) else: chunk_untrusted = True repo = (body.get("data") or {}).get("repository") if repo is None or chunk_untrusted: chunks_failed += 1 continue for alias, pr in repo.items(): if alias in errored or not pr or not pr.get("headRefOid"): continue resolved += 1 if pr.get("baseRefName") == "main" and pr.get("merged") is True: verified[str(pr["number"])] = pr["headRefOid"] # Fail LOUD on ANY chunk that could not be conclusively read: a partially # dropped chunk would silently omit up to `chunk` real candidate PRs from # Source B. fetch() already retries transient blips, so a surviving failure # is a real problem worth aborting the daily run over. if chunks_failed: sys.exit(f"FATAL: {chunks_failed}/{chunks_total} GraphQL verification " "chunk(s) failed — aborting Source B verification") if pr_numbers and resolved == 0: sys.exit("FATAL: could not resolve any candidate PR via GraphQL — aborting Source B verification") return verified verified = verify(candidates) if candidates else {} # --- 3. (B1) Keep failed/partial builds whose PR is merged into main AND whose # commit matches that PR's head SHA. Emit only those build IDs. --- build_ids = sorted({bid for bid, pr, sha in failed_builds if verified.get(str(pr)) == sha}) github_output = os.environ.get("GITHUB_OUTPUT", "") if not github_output: print("ERROR: GITHUB_OUTPUT is not set, cannot pass data to agent", file=sys.stderr) sys.exit(1) json_str = json.dumps(build_ids) with open(github_output, "a") as gh_out: gh_out.write(f"source_b_build_ids<<SOURCE_B_EOF\n{json_str}\nSOURCE_B_EOF\n") print(f"Source B: {len(failed_builds)} failed PR builds, {len(candidates)} candidate PRs, " f"{len(verified)} merged-into-main, {len(build_ids)} builds selected (B1-B4), wrote to step output") SCRIPT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Aggregate Part 1 failures id: part1_aggregate run: | # Part 1 (Sources A/B/C) failure gathering is the dominant token sink of this # workflow: it spans ~200 builds, many resultsbyBuild calls, and multi-MB Helix # console logs. Surfacing that data into the metered agent loop is what exhausted # the per-run effective-token budget mid-gathering -- the run repeatedly died # before creating any output. Do ALL of it here, in the pre-activation job that # runs OUTSIDE the firewall at zero effective-token cost, and inject a single # compact JSON blob the agent consumes directly. The agent makes ZERO AzDO/Helix # calls for Part 1. # Source A: defs 83+87, refs/heads/main, failed/partial builds in the last 30 # days -> resultsbyBuild(Failed) -> per-test failure counts (+assembly, # up to 3 example build ids). # Source B: resultsbyBuild(Failed) for the already-selected source_b_build_ids # (the Verify Source B PRs step did the full B1-B4 selection). # builds: compact metadata map (def, startedUtc, finishedUtc, sourceVersion, # pr) for every referenced build, so the agent can do the Case B # "failure after the unquarantine landed" timing check and the # Source B "PR modified its own test" exclusion WITHOUT any AzDO call. # Enrichment: each failing test is enriched from its representative result's # detail with the Helix job id + work-item name (parsed from the result # `comment` field -- reliable, no fragile build-timeline parsing) and, # for individual tests, the real errorMessage/stackTrace (capped). # Source C: for work items (names ending .WorkItemExecution) use those Helix # coords to download the console log and extract only the [FAIL] blocks # (capped), probing multiple builds until a [FAIL] block is found and # bounded by a global download budget. Turns multi-MB logs into a few KB. # emit() guarantees the output stays under the 1MB GITHUB_OUTPUT limit by shedding # optional enrichment (never the per-test counts) and fails loud rather than letting # GitHub silently truncate into corrupt JSON. Validated ~170KB on 30 days of data. python3 << 'SCRIPT' import json, os, sys, time, datetime, urllib.parse, urllib.request, urllib.error, re ADO = "https://dev.azure.com/dnceng-public/public/_apis" VSTMR = "https://vstmr.dev.azure.com/dnceng-public/public/_apis" HELIX = "https://helix.dot.net/api/2019-06-17" DEFS = [83, 87] DAYS = 30 WI_SUFFIX = ".WorkItemExecution" ERROR_CAP = 1200 STACK_CAP = 900 OCC_CAP = 2 # occurrences tracked per test (for Source C multi-probe) BLOCK_CAP = 8000 WORKITEM_CAP = 40000 SOURCE_C_GLOBAL_CAP = 300000 SAFE_OUTPUT = 950000 # hard ceiling under the 1MB GITHUB_OUTPUT limit # Safety valve: cap total Helix console-log bytes downloaded. The first occurrence of # every work item is always fetched; extra occurrences are only probed while under this # budget. Stops a regression spell (dozens of multi-MB macOS-hang logs) from making the # pre-activation step download gigabytes / run unbounded. SOURCE_C_DOWNLOAD_BUDGET = 300_000_000 _ANSI = re.compile(r'\x1b\[[0-9;]*[A-Za-z]') # Redact token-shaped strings from captured CI failure text before emitting it. # Helix work-item upload steps log a live Azure DevOps bearer JWT on failure, which # ends up inside the test stackTrace / [FAIL] console blocks we capture. GitHub's # GITHUB_OUTPUT secret detector then skips the ENTIRE part1_data output # ("Skip output 'part1_data' since it may contain secret"), starving the agent of all # Part 1 data and producing a false noop. Scrubbing removes the trigger and avoids # surfacing live tokens in the prompt and uploaded artifacts. _SECRET_PATTERNS = [ re.compile(r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}'), # JWT (header.payload.signature) re.compile(r'eyJ[A-Za-z0-9_-]{20,}'), # bare JWT segment re.compile(r'\bgh[pousr]_[A-Za-z0-9]{20,}\b'), # GitHub token (ghp_/gho_/...) re.compile(r'\bgithub_pat_[A-Za-z0-9_]{20,}\b'), # GitHub fine-grained PAT re.compile(r'(?i)\bbearer\s+[A-Za-z0-9._~+/=-]{20,}'), # Authorization: Bearer <token> re.compile(r'(?i)\bhttps?://[^/\s:@"]+:[^@\s/"]{6,}@'), # basic-auth credentials in URL re.compile(r'(?i)[?&]sig=[A-Za-z0-9%/+_=-]{20,}'), # Azure SAS signature re.compile(r'(?i)\b(?:AccountKey|SharedAccessKey|AccessKey|Password|Pwd)=[^;\s"\']{12,}'), # connection-string secret re.compile(r'[A-Za-z0-9][A-Za-z0-9+/=_-]{51,}'), # long high-entropy run (AzDO PAT, base64); must start with an alnum so it skips ===/--- separators ] def scrub_secrets(s): # Replace token-shaped substrings with a placeholder. Patterns run in order; # earlier, more specific rules win, and "[REDACTED]" is inert for later rules. if not s: return s for _pat in _SECRET_PATTERNS: s = _pat.sub("[REDACTED]", s) return s def fetch(url, headers=None, retries=3, raw=False, timeout=120): hdrs = {"User-Agent": "aspnetcore-test-quarantine"} if headers: hdrs.update(headers) last = None for attempt in range(retries): try: req = urllib.request.Request(url, headers=hdrs) with urllib.request.urlopen(req, timeout=timeout) as r: data = r.read() return (data if raw else json.loads(data)), r.headers except urllib.error.HTTPError as e: if e.code in (401, 403, 404): raise last = e except Exception as e: last = e time.sleep(2 * (attempt + 1)) raise last def list_failed_builds(definition, branch=None): """Return the failed/partial build objects (not just ids) so we can record metadata.""" mt = (datetime.datetime.utcnow() - datetime.timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ") tok, out = None, [] while True: p = {"definitions": definition, "statusFilter": "completed", "resultFilter": "failed,partiallySucceeded", "$top": 200, "minTime": mt, "api-version": "7.1"} if branch: p["branchName"] = branch if tok: p["continuationToken"] = tok data, h = fetch(f"{ADO}/build/builds?{urllib.parse.urlencode(p)}") out += data.get("value", []) tok = h.get("x-ms-continuationtoken") if not tok: break return out def list_completed_builds(definition, branch=None): """Return ALL completed build objects (any result) so we can reconstruct the per-pipeline timeline and detect PASSING runs between failures. Lightweight: build metadata only, no test-result calls.""" mt = (datetime.datetime.utcnow() - datetime.timedelta(days=DAYS)).strftime("%Y-%m-%dT%H:%M:%SZ") tok, out = None, [] while True: p = {"definitions": definition, "statusFilter": "completed", "$top": 200, "minTime": mt, "api-version": "7.1"} if branch: p["branchName"] = branch if tok: p["continuationToken"] = tok data, h = fetch(f"{ADO}/build/builds?{urllib.parse.urlencode(p)}") out += data.get("value", []) tok = h.get("x-ms-continuationtoken") if not tok: break return out def builds_by_ids(ids): out = [] for k in range(0, len(ids), 100): chunk = ",".join(str(i) for i in ids[k:k + 100]) data, _ = fetch(f"{ADO}/build/builds?buildIds={chunk}&api-version=7.1") out += data.get("value", []) return out def pr_of(build): br = build.get("sourceBranch", "") or "" if br.startswith("refs/pull/"): try: return int(br.split("/")[2]) except (IndexError, ValueError): return None return None def build_meta(build): return {"def": (build.get("definition") or {}).get("id"), "startedUtc": build.get("startTime"), "finishedUtc": build.get("finishTime"), "sourceVersion": build.get("sourceVersion"), "pr": pr_of(build)} def failed_results(build_id): tok = None while True: p = {"buildId": build_id, "outcomes": "Failed", "$top": 1000, "api-version": "7.1-preview.1"} if tok: p["continuationToken"] = tok data, h = fetch(f"{VSTMR}/testresults/resultsbyBuild?{urllib.parse.urlencode(p)}") for t in data.get("value", []): yield t tok = h.get("x-ms-continuationtoken") if not tok: break def norm_name(t): name = t.get("automatedTestName") or "" if not name: # testCaseTitle can carry parameterized args; strip them for stable dedup. name = (t.get("testCaseTitle") or "").split("(")[0].strip() return name def aggregate(build_ids): agg = {} for bid in build_ids: for t in failed_results(bid): name = norm_name(t) if not name: continue e = agg.setdefault(name, {"count": 0, "assembly": t.get("automatedTestStorage", ""), "builds": [], "occ": []}) e["count"] += 1 if bid not in e["builds"]: e["builds"].append(bid) if t.get("runId") and t.get("id") and len(e["occ"]) < OCC_CAP: e["occ"].append({"runId": t["runId"], "resultId": t["id"], "build": bid}) return agg def parse_helix(comment): if not comment: return None, None try: c = json.loads(comment) except (json.JSONDecodeError, TypeError): return None, None return c.get("HelixJobId"), c.get("HelixWorkItemName") def result_detail(run_id, result_id): data, _ = fetch(f"{VSTMR}/testresults/runs/{run_id}/results/{result_id}?api-version=7.1-preview.1") return data def enrich(agg): """Attach Helix coords (job+workitem, only when BOTH present) and, for individual tests, real error/stack from the representative result detail. For work items, also collect candidate (job, workitem, build) probes from every tracked occurrence so Source C can try more than just the first build.""" for name, e in agg.items(): is_wi = name.endswith(WI_SUFFIX) probes = [] for idx, occ in enumerate(e.get("occ", [])): # Individual tests only need the first occurrence (error/stack + coords). if not is_wi and idx > 0: break try: det = result_detail(occ["runId"], occ["resultId"]) except Exception as ex: if idx == 0: e["detail_note"] = f"detail fetch failed: {type(ex).__name__}" continue job, wi_name = parse_helix(det.get("comment")) if idx == 0 and job and wi_name: e["helix"] = {"job": job, "workitem": wi_name} if is_wi and job and wi_name: probes.append({"job": job, "workitem": wi_name, "build": occ["build"]}) if idx == 0 and not is_wi: em, st = det.get("errorMessage"), det.get("stackTrace") if em: e["error"] = scrub_secrets(em)[:ERROR_CAP] if st: e["stack"] = scrub_secrets(st)[:STACK_CAP] if is_wi: e["probes"] = probes return agg _MARKER = re.compile(r'\[(?:PASS|FAIL|SKIP)\]\s*$') _FAIL = re.compile(r'\[FAIL\]\s*$') def extract_fail_blocks(text): lines = [_ANSI.sub("", ln) for ln in text.splitlines()] blocks, i = [], 0 while i < len(lines): if _FAIL.search(lines[i]): j = i + 1 while j < len(lines) and not _MARKER.search(lines[j]): j += 1 blocks.append(scrub_secrets("\n".join(lines[i:j]))[:BLOCK_CAP]) i = j else: i += 1 return blocks def helix_console_blocks(job_id, wi_name): files, _ = fetch(f"{HELIX}/jobs/{job_id}/workitems/{urllib.parse.quote(wi_name)}/files") seq = files if isinstance(files, list) else files.get("Files", files.get("files", [])) link = None for f in seq: nm = f.get("Name") or f.get("name") or "" if nm.startswith("console."): link = f.get("Link") or f.get("link") break if not link: return None, 0 raw, _ = fetch(link, raw=True, timeout=180) text = raw.decode("utf-8", "replace") # Return the raw byte length: it feeds the byte-denominated download budget # and the reported log_bytes, whereas len(text) is a decoded character count. return extract_fail_blocks(text), len(raw) def sizeof(obj): return len(json.dumps(obj, separators=(",", ":"))) def emit(out): """Serialize, but guarantee the result stays under SAFE_OUTPUT by progressively shedding the largest optional payloads (never the core per-test counts). Fail loud if even the trimmed core is too big, rather than letting GITHUB_OUTPUT silently truncate into corrupt JSON.""" if sizeof(out) <= SAFE_OUTPUT: return json.dumps(out, separators=(",", ":")) out["trim"] = [] for src in ("source_a", "source_b"): for e in out[src].values(): e.pop("stack", None) out["trim"].append("stack_dropped") if sizeof(out) <= SAFE_OUTPUT: return json.dumps(out, separators=(",", ":")) for src in ("source_a", "source_b"): for e in out[src].values(): e.pop("error", None) out["trim"].append("error_dropped") if sizeof(out) <= SAFE_OUTPUT: return json.dumps(out, separators=(",", ":")) for c in out["source_c"]: if "fail_blocks" in c: c["fail_blocks"] = c["fail_blocks"][:2000] out["trim"].append("source_c_blocks_trimmed") js = json.dumps(out, separators=(",", ":")) if len(js) > SAFE_OUTPUT: sys.exit(f"FATAL: part1_data is {len(js)} bytes after trimming, exceeds the " f"{SAFE_OUTPUT}-byte safe limit — aborting rather than emitting truncated JSON") return js def mark_intermittency(source_a, all_main_builds, bmeta): """Set `is_consistent_regression` on every individual test in source_a. A test is a CONSISTENT REGRESSION (not flaky) when, on ANY `main` pipeline (def) where it failed 2+ times, its two most recent failures on that pipeline were in back-to-back runs with NO passing run in between. That is the signature of a real regression, so such a test must NOT be auto-quarantined under Case A — quarantining it would hide the regression. The check is conservative on purpose: a back-to-back failure streak on EITHER pipeline blocks quarantine, even if the test happened to look intermittent on the other pipeline (an intermittent pattern on one pipeline must never mask a hard regression on another). A "pass" between two failures is a completed `main` build on the SAME def that SUCCEEDED or PARTIALLY SUCCEEDED (so tests actually ran), started strictly between the two failures, and in which this test did NOT fail (not in its failing-build set). `failed`/`canceled` builds are excluded — a compile/infra break produces no test results and must not be mistaken for a passing run. A test with fewer than two failures on every single pipeline has too little evidence of consistency, so `is_consistent_regression` is False (the gate does not block it; it is judged on the other Case A criteria — e.g. PR-only flakes).""" PASS_RESULTS = ("succeeded", "partiallySucceeded") # Per-def ascending (startedUtc, id) timeline + result/def lookup. Seed the # start/def of every Source A failing build from `bmeta` first (it carries `def` # and `startedUtc` for each), so a failure always has a timestamp + pipeline even # if it falls outside the full-timeline window below; then layer the completed-build # timeline (the only source of `result`, needed to spot passing runs) on top. bstart, bdef, bresult, by_def = {}, {}, {}, {} for sid, mv in bmeta.items(): try: bid = int(sid) except (TypeError, ValueError): continue if mv.get("startedUtc") and mv.get("def") is not None: bstart[bid] = mv["startedUtc"] bdef[bid] = mv["def"] for b in all_main_builds: bid = b.get("id") d = (b.get("definition") or {}).get("id") st = b.get("startTime") if bid is None or d is None or not st: continue bstart[bid] = st bdef[bid] = d bresult[bid] = b.get("result") by_def.setdefault(d, []).append((st, bid)) for d in by_def: by_def[d].sort() for name, e in source_a.items(): if name.endswith(WI_SUFFIX): continue failset = set(e.get("builds", [])) # Group this test's timestamped failures by the pipeline they ran on. fails_by_def = {} for bid in failset: if bid in bstart and bid in bdef: fails_by_def.setdefault(bdef[bid], []).append(bstart[bid]) regression = False for d, fl in fails_by_def.items(): if len(fl) < 2: continue fl.sort() t2, t1 = fl[-2], fl[-1] # two most recent failures on this def passed_here = False for st, bid in by_def.get(d, []): if st <= t2: continue if st >= t1: break if bid not in failset and bresult.get(bid) in PASS_RESULTS: passed_here = True break if not passed_here: # Back-to-back failures on this pipeline with no pass between -> regression. regression = True break e["is_consistent_regression"] = regression def main(): # Source A: failed/partial builds on main, both pipelines, last 30 days. a_builds = [b for d in DEFS for b in list_failed_builds(d, branch="refs/heads/main")] bmeta = {} for b in a_builds: bmeta[str(b["id"])] = build_meta(b) source_a = enrich(aggregate([b["id"] for b in a_builds])) # Flakiness signal: needs the FULL main timeline (incl. succeeded builds), not just # the failed/partial builds above, to spot a passing run between two failures. all_main_builds = [b for d in DEFS for b in list_completed_builds(d, branch="refs/heads/main")] mark_intermittency(source_a, all_main_builds, bmeta) # Source B: preselected merged-PR build ids (env from the Verify Source B PRs step). raw_ids = os.environ.get("SOURCE_B_BUILD_IDS", "").strip() if not raw_ids: b_ids = [] else: try: b_ids = json.loads(raw_ids) if not isinstance(b_ids, list): raise ValueError("not a list") except (json.JSONDecodeError, ValueError) as ex: sys.exit(f"FATAL: SOURCE_B_BUILD_IDS is set but not a valid JSON array ({ex}) — aborting") if b_ids: for b in builds_by_ids(b_ids): bmeta[str(b["id"])] = build_meta(b) source_b = enrich(aggregate(b_ids)) # Source C: work items (combined A+B) -> Helix console [FAIL] blocks. Probe each # tracked occurrence until one yields [FAIL] blocks (the first build is often a # macOS hang with none, while a later build has the real failure). wi = {} for src in (source_a, source_b): for name, e in src.items(): if name.endswith(WI_SUFFIX) and e.get("probes"): lst = wi.setdefault(name, []) seen = {(p["job"], p["workitem"], p["build"]) for p in lst} for p in e["probes"]: k = (p["job"], p["workitem"], p["build"]) if k not in seen: seen.add(k) lst.append(p) source_c = [] truncated = False total = 0 downloaded = [0] # mutable: total Helix log bytes pulled across all probes def probe(pr): blocks, log_size = helix_console_blocks(pr["job"], pr["workitem"]) downloaded[0] += log_size return blocks, log_size for name in sorted(wi): probes = wi[name] if truncated: source_c.append({"workitem": name, "build": probes[0]["build"], "job": probes[0]["job"], "note": "omitted: Source C global size cap reached"}) continue chosen = None last_err = None for idx, pr in enumerate(probes): # Always fetch the first occurrence; only probe further while under the # download budget (degrades to first-occurrence-only during big regressions). if idx > 0 and downloaded[0] >= SOURCE_C_DOWNLOAD_BUDGET: break try: blocks, log_size = probe(pr) except Exception as ex: last_err = type(ex).__name__ continue if blocks is None: chosen = chosen or {"build": pr["build"], "job": pr["job"], "blocks": None, "log": 0} continue chosen = {"build": pr["build"], "job": pr["job"], "blocks": blocks, "log": log_size} if blocks: break # found real [FAIL] content; stop probing if chosen is None: source_c.append({"workitem": name, "build": probes[0]["build"], "job": probes[0]["job"], "note": f"investigation error: {last_err}" if last_err else "no probe succeeded"}) continue if chosen["blocks"] is None: source_c.append({"workitem": name, "build": chosen["build"], "job": chosen["job"], "note": "no console log file found"}) continue joined = "\n---\n".join(chosen["blocks"])[:WORKITEM_CAP] if total + len(joined) > SOURCE_C_GLOBAL_CAP: truncated = True source_c.append({"workitem": name, "build": chosen["build"], "job": chosen["job"], "note": "omitted: Source C global size cap reached"}) continue total += len(joined) source_c.append({"workitem": name, "build": chosen["build"], "job": chosen["job"], "log_bytes": chosen["log"], "fail_block_count": len(chosen["blocks"]), "fail_blocks": joined}) # Drop internal-only fields from the per-test payload. for src in (source_a, source_b): for e in src.values(): e.pop("occ", None) e.pop("probes", None) out = { "generated_utc": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), "builds": bmeta, "source_a": source_a, "source_b": source_b, "source_c": source_c, "source_c_truncated": truncated, } js = emit(out) regr = sum(1 for n, e in source_a.items() if not n.endswith(WI_SUFFIX) and e.get("is_consistent_regression")) sys.stderr.write(f"part1: A={len(source_a)} tests, B={len(source_b)} tests, " f"C={len(source_c)} work items, builds={len(bmeta)}, " f"main_builds={len(all_main_builds)}, regression_A={regr}, " f"output {len(js)/1024:.0f} KB, source_c_truncated={truncated}, " f"trim={out.get('trim')}\n") return js # The agent prompt receives this JSON through the part1_data_N job outputs, which gh-aw # interpolates into the prompt and passes to the prompt-building steps as ENVIRONMENT # VARIABLES. Linux caps a single env-var string at MAX_ARG_STRLEN (32 * 4 KiB page = # 131072 bytes) when starting a process; a value larger than that makes the prompt step # die with "Argument list too long" (E2BIG) before it even runs — which is exactly what # happened once the data started flowing at full size. So the payload is split into a # fixed number of byte-bounded chunks (each well under the limit) written to separate # outputs; the prompt concatenates the chunks back together with no separator, # reconstructing the JSON verbatim. PART1_CHUNKS = 16 # MUST match the count of part1_data_N refs in the prompt body PART1_CHUNK_BYTES = 80000 # << 131072 MAX_ARG_STRLEN, leaving ample room for the var name def write_part1_chunks(f, js): """Split `js` into <=PART1_CHUNKS pieces of <=PART1_CHUNK_BYTES bytes each, never cutting a multi-byte UTF-8 sequence, and write them as part1_data_0..part1_data_{N-1} outputs. Unused slots are emitted empty so the prompt's fixed set of placeholders always resolves.""" data = js.encode("utf-8") chunks = [] i, n = 0, len(data) while i < n: end = min(i + PART1_CHUNK_BYTES, n) # back off to a UTF-8 character boundary (continuation bytes are 0b10xxxxxx) while end < n and (data[end] & 0xC0) == 0x80: end -= 1 chunks.append(data[i:end].decode("utf-8")) i = end if len(chunks) > PART1_CHUNKS: sys.exit(f"FATAL: part1_data needs {len(chunks)} chunks but only {PART1_CHUNKS} " f"output slots exist — raise PART1_CHUNKS and add matching " f"part1_data_N references in the prompt body") for k in range(PART1_CHUNKS): chunk = chunks[k] if k < len(chunks) else "" f.write(f"part1_data_{k}<<PART1_EOF\n{chunk}\nPART1_EOF\n") if __name__ == "__main__": # Final safety net: scrub the fully serialized payload as well. GitHub skips a # part1_data_N output if any token pattern is detected, so a leaked secret in # any field (not just the three scrubbed above) would starve the agent. Scrubbing # only ever shrinks the payload, so it stays under the GITHUB_OUTPUT size cap. js = scrub_secrets(main()) gh_out = os.environ.get("GITHUB_OUTPUT") if not gh_out: sys.exit("ERROR: GITHUB_OUTPUT is not set, cannot pass Part 1 data to agent") with open(gh_out, "a") as f: write_part1_chunks(f, js) SCRIPT env: SOURCE_B_BUILD_IDS: ${{ steps.source_b_prs.outputs.source_b_build_ids }} safe_outputs: needs: - activation - agent - detection if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim environment: copilot-pat-pool permissions: contents: write issues: write pull-requests: write timeout-minutes: 45 env: GH_AW_AGENT_AIC: ${{ needs.agent.outputs.aic }} GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/test-quarantine" GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }} GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "test-quarantine" GH_AW_WORKFLOW_NAME: "Daily Test Quarantine Management" GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/test-quarantine.md" 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 }} comment_id: ${{ steps.process_safe_outputs.outputs.comment_id }} comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }} created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }} created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup uses: github/gh-aw-actions/setup@c863074b673419603d146aab585e2986ef08deec # v0.84.3 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} 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: "Daily Test Quarantine Management" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/test-quarantine.lock.yml@${{ github.ref }} GH_AW_INFO_VERSION: "1.0.77" GH_AW_INFO_AWF_VERSION: "v0.27.43" GH_AW_INFO_ENGINE_ID: "copilot" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: agent path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env if: steps.download-agent-output.outcome == 'success' run: | mkdir -p /tmp/gh-aw/ find "/tmp/gh-aw/" -type f -print echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" - name: Download patch artifact continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: agent path: /tmp/gh-aw/ - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true fetch-depth: 0 token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - name: Configure Git credentials if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') env: GITHUB_REPOSITORY: ${{ github.repository }} GITHUB_SERVER_URL: ${{ github.server_url }} GIT_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_git_credentials.sh" - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash run: | # zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input. # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op. GH_HOST="${GITHUB_SERVER_URL#https://}" GH_HOST="${GH_HOST#http://}" echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV" - name: Process Safe Outputs id: process_safe_outputs 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_COMMENT_ID: ${{ needs.activation.outputs.comment_id }} GH_AW_ALLOWED_DOMAINS: "*.blob.core.windows.net,*.vsblob.vsassets.io,*.vssps.visualstudio.com,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,dev.azure.com,github.com,helix.dot.net,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,learn.microsoft.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,vstmr.dev.azure.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\":10,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"re-quarantine\"]},\"create_issue\":{\"labels\":[\"test-failure\"],\"max\":10,\"title_prefix\":\"Quarantine \"},\"create_pull_request\":{\"allowed_files\":[\"src/**/*.cs\"],\"base_branch\":\"main\",\"draft\":false,\"labels\":[\"test-failure\"],\"max\":10,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[test-quarantine] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_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/process_safe_outputs.cjs'); await main(); - name: Upload Safe Outputs Items if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: safe-outputs-items path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json /tmp/gh-aw/process-safe-outputs.stdout.log /tmp/gh-aw/process-safe-outputs.stderr.log if-no-files-found: ignore