diff --git a/.github/scripts/jira_sites.py b/.github/scripts/jira_sites.py deleted file mode 100644 index a5bd3cf..0000000 --- a/.github/scripts/jira_sites.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -"""Helpers for diagnosing JIRA API access in the analytics-review workflow. - -Parses the JSON that `GET https://api.atlassian.com/oauth/token/accessible-resources` -returns (the list of Atlassian sites a credential can reach) and answers two -questions for the workflow's JIRA pre-step: - - jira_sites.py cloud-id - Print the cloudId to use with the scoped-token endpoint - (https://api.atlassian.com/ex/jira/), chosen as: the site - whose `url` equals the configured base URL, else the cloudId already - embedded in an .../ex/jira/ base, else the only site when exactly - one is accessible. Prints nothing when no confident answer exists. - - jira_sites.py summary - Print a one-line "url (cloudId ...)" list for diagnostics. - -Site URLs and cloudIds are not credentials (cloudIds appear in every browser -request to a JIRA site); nothing secret is ever read or printed here. Exit -code is always 0 — this feeds a diagnostics path that must never fail the job. -""" - -import json -import sys - - -def load_sites(path): - try: - with open(path) as fh: - data = json.load(fh) - except Exception: - return [] - return [s for s in data if isinstance(s, dict)] if isinstance(data, list) else [] - - -def main(): - if len(sys.argv) < 3: - return - mode, path = sys.argv[1], sys.argv[2] - sites = load_sites(path) - if mode == "summary": - line = "; ".join( - "%s (cloudId %s)" % (s.get("url", "?"), s.get("id", "?")) for s in sites - ) - print(line or "no sites accessible to this token") - elif mode == "cloud-id": - base = (sys.argv[3] if len(sys.argv) > 3 else "").rstrip("/") - match = [s for s in sites if str(s.get("url", "")).rstrip("/") == base] - if not match and "/ex/jira/" in base: - cid = base.split("/ex/jira/", 1)[1].split("/")[0] - match = [s for s in sites if s.get("id") == cid] - if not match and len(sites) == 1: - match = sites - if match: - print(match[0].get("id", "")) - - -if __name__ == "__main__": - main() diff --git a/.github/workflows/analytics-review.lock.yml b/.github/workflows/analytics-review.lock.yml index 1185246..3fe62cf 100644 --- a/.github/workflows/analytics-review.lock.yml +++ b/.github/workflows/analytics-review.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a5aae95c31e04d094eb82b541158b7713878811afe0acba09faffd0513ea4798","body_hash":"0b8477f533a289d2aeef66568c4ced69e0535dc02c4547c8d4a2c48f85037557","compiler_version":"v0.86.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.79"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a57a7a44f1d7f755a3657770d628374a0c6d463ac9bf75cdb85dc8ad97806154","body_hash":"0b8477f533a289d2aeef66568c4ced69e0535dc02c4547c8d4a2c48f85037557","compiler_version":"v0.86.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.79"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","JIRA_API_TOKEN","JIRA_BASE_URL","JIRA_EMAIL"],"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/setup-python","sha":"5fda3b95a4ea91299a34e894583c3862153e4b97","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"6aab9e5b5c91c615506061f09bedd81a23babe3c","version":"v0.86.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44","digest":"sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.44@sha256:0d727725c737b58c7bdf51f640cffb928385ec46517e0917c7f1a02f1bada8b4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44","digest":"sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.44@sha256:b50fbadba138f6e9aba94aca09711335c489bb3b15861220cb66f6092e042dc7"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44","digest":"sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.44@sha256:83e48bbe12c634be8c228a576832fe45f66c529ac3659db92bddbcf2eeb6d627"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.9","digest":"sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.9@sha256:e5a1569aeaf41820fa7bdee3e94468cae448133cdbf00119ad24f5b74db1ab9f"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196","pinned_image":"ghcr.io/github/gh-aw-node@sha256:0d9f1fb5fd6610c0ac1f5194a38e45a8a1e81f8a390d5142d8e4e6f26a4b3196"},{"image":"ghcr.io/github/github-mcp-server:v1.9.0","digest":"sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e","pinned_image":"ghcr.io/github/github-mcp-server:v1.9.0@sha256:881b53d6f75f69bdbc1b5b10fc2f1361717c19054143b3a8529fb5c32061a50e"}],"has_pull_request_target":true} # This file was automatically generated by gh-aw (v0.86.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -493,13 +493,13 @@ jobs: REPO: ${{ github.repository }} id: resolve_pr name: Resolve PR context (number, head ref, head SHA) - run: "set -euo pipefail\nmkdir -p /tmp/gh-aw/context\nPR_NUMBER=\"\"\nfor candidate in \"${EVENT_PR_NUMBER:-}\" \"${EVENT_ISSUE_NUMBER:-}\" \"${INPUT_PR_NUMBER:-}\"; do\n # issue_comment events reach us only for comments on PRs (trigger filter), so the\n # issue number IS the PR number there. workflow_dispatch supplies its own input.\n if printf '%s' \"$candidate\" | grep -qE '^[0-9]+$'; then PR_NUMBER=\"$candidate\"; break; fi\ndone\nHEAD_REF=\"\"; HEAD_SHA=\"\"\nif [ -n \"$PR_NUMBER\" ]; then\n HEAD_REF=$(gh api \"repos/$REPO/pulls/$PR_NUMBER\" --jq .head.ref || true)\n HEAD_SHA=$(gh api \"repos/$REPO/pulls/$PR_NUMBER\" --jq .head.sha || true)\nfi\n# git ref names cannot contain whitespace or control characters, so single-line\n# GITHUB_ENV writes are safe here.\n{\n echo \"AW_PR_NUMBER=$PR_NUMBER\"\n echo \"AW_HEAD_REF=$HEAD_REF\"\n echo \"AW_HEAD_SHA=$HEAD_SHA\"\n} >> \"$GITHUB_ENV\"\n{\n echo \"pr_number=$PR_NUMBER\"\n echo \"head_ref=$HEAD_REF\"\n echo \"head_sha=$HEAD_SHA\"\n} >> \"$GITHUB_OUTPUT\"\n# The agent's source of truth for WHICH PR it is reviewing. Head ref is\n# author-controlled text, but git forbids whitespace/control characters in ref\n# names, so these single-line writes cannot be broken out of.\n{\n echo \"# Run context (resolved by a deterministic pre-step — trust this over the event payload)\"\n echo\n echo \"- Triggering event: $EVENT_NAME\"\n if [ -n \"$PR_NUMBER\" ]; then\n echo \"- PR under review: #$PR_NUMBER\"\n echo \"- Head ref (PR branch name): $HEAD_REF\"\n echo \"- Head SHA: $HEAD_SHA\"\n else\n echo \"- PR under review: NONE RESOLVED — the event payload contained no PR or issue number and no pr_number dispatch input was given. There is nothing to review.\"\n fi\n} > /tmp/gh-aw/context/run-context.md\necho \"PR=#${PR_NUMBER:-none} head=${HEAD_REF:-?}@${HEAD_SHA:-?}\"\n" + run: "# Deliberately STRICT (-e): if PR resolution itself breaks, we want a loud failure,\n# not a review of the wrong PR. (Contrast with the JIRA/lint steps below, which must\n# never fail the job and therefore explicitly clear the inherited -e.)\nset -euo pipefail\nmkdir -p /tmp/gh-aw/context\nPR_NUMBER=\"\"\nfor candidate in \"${EVENT_PR_NUMBER:-}\" \"${EVENT_ISSUE_NUMBER:-}\" \"${INPUT_PR_NUMBER:-}\"; do\n # issue_comment events reach us only for comments on PRs (trigger filter), so the\n # issue number IS the PR number there. workflow_dispatch supplies its own input.\n if printf '%s' \"$candidate\" | grep -qE '^[0-9]+$'; then PR_NUMBER=\"$candidate\"; break; fi\ndone\nHEAD_REF=\"\"; HEAD_SHA=\"\"\nif [ -n \"$PR_NUMBER\" ]; then\n HEAD_REF=$(gh api \"repos/$REPO/pulls/$PR_NUMBER\" --jq .head.ref || true)\n HEAD_SHA=$(gh api \"repos/$REPO/pulls/$PR_NUMBER\" --jq .head.sha || true)\nfi\n# git ref names cannot contain whitespace or control characters, so single-line\n# GITHUB_ENV writes are safe here.\n{\n echo \"AW_PR_NUMBER=$PR_NUMBER\"\n echo \"AW_HEAD_REF=$HEAD_REF\"\n echo \"AW_HEAD_SHA=$HEAD_SHA\"\n} >> \"$GITHUB_ENV\"\n{\n echo \"pr_number=$PR_NUMBER\"\n echo \"head_ref=$HEAD_REF\"\n echo \"head_sha=$HEAD_SHA\"\n} >> \"$GITHUB_OUTPUT\"\n# The agent's source of truth for WHICH PR it is reviewing. Head ref is\n# author-controlled text, but git forbids whitespace/control characters in ref\n# names, so these single-line writes cannot be broken out of.\n{\n echo \"# Run context (resolved by a deterministic pre-step — trust this over the event payload)\"\n echo\n echo \"- Triggering event: $EVENT_NAME\"\n if [ -n \"$PR_NUMBER\" ]; then\n echo \"- PR under review: #$PR_NUMBER\"\n echo \"- Head ref (PR branch name): $HEAD_REF\"\n echo \"- Head SHA: $HEAD_SHA\"\n else\n echo \"- PR under review: NONE RESOLVED — the event payload contained no PR or issue number and no pr_number dispatch input was given. There is nothing to review.\"\n fi\n} > /tmp/gh-aw/context/run-context.md\necho \"PR=#${PR_NUMBER:-none} head=${HEAD_REF:-?}@${HEAD_SHA:-?}\"\n" - env: JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} name: Fetch JIRA ticket context - run: "# Deliberately no `set -e`: this step must NEVER fail the job. Every failure mode\n# degrades into a marker file that tells the agent (and the humans reading the review)\n# exactly what was missing.\nset -uo pipefail\nOUT=/tmp/gh-aw/context/jira-ticket.md\nmkdir -p /tmp/gh-aw/context\nno_ticket() {\n printf 'NO TICKET FOUND: %s\\n' \"$1\" > \"$OUT\"\n echo \"jira-ticket.md marker written: $1\"\n exit 0\n}\n[ -n \"${AW_HEAD_REF:-}\" ] || no_ticket \"no pull request context, so no branch name to extract a ticket ID from\"\nKEY=$(printf '%s' \"$AW_HEAD_REF\" | grep -oiE 'ENG-[0-9]+' | head -1 | tr '[:lower:]' '[:upper:]')\n[ -n \"$KEY\" ] || no_ticket \"branch '$AW_HEAD_REF' does not contain a JIRA ticket ID (repo convention: branch name = ticket, e.g. ENG-909)\"\nif [ -z \"${JIRA_BASE_URL:-}\" ] || [ -z \"${JIRA_EMAIL:-}\" ] || [ -z \"${JIRA_API_TOKEN:-}\" ]; then\n no_ticket \"ticket $KEY detected in branch name, but the JIRA_BASE_URL / JIRA_EMAIL / JIRA_API_TOKEN repo secrets are not configured\"\nfi\nJIRA_BASE_URL=\"${JIRA_BASE_URL%/}\"\n# Classify the configured base URL's shape for diagnostics. Hosts only — never values.\ncase \"$JIRA_BASE_URL\" in\n https://api.atlassian.com/ex/jira/*) BASE_FORM=\"host api.atlassian.com — the scoped-token form\" ;;\n https://*.atlassian.net*) BASE_FORM=\"a *.atlassian.net host — works only with UNSCOPED tokens\" ;;\n *) BASE_FORM=\"a host that is neither api.atlassian.com nor *.atlassian.net\" ;;\nesac\nISSUE_JSON=$(mktemp); COMMENTS_JSON=$(mktemp)\nfetch_issue() {\n CODE=$(curl -sS -o \"$ISSUE_JSON\" -w '%{http_code}' --max-time 30 \\\n -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" -H 'Accept: application/json' \\\n \"$1/rest/api/3/issue/$KEY?fields=summary,description,labels,status\") || CODE=000\n}\nBASE=\"$JIRA_BASE_URL\"\nfetch_issue \"$BASE\"\nif [ \"$CODE\" != \"200\" ]; then\n ORIG_CODE=$CODE\n # Ask Atlassian which sites this credential can reach. Works for scoped AND\n # unscoped tokens, and each returned entry's `id` is the cloudId needed for the\n # scoped-token endpoint — one call both diagnoses and enables auto-recovery.\n RES_JSON=$(mktemp)\n PROBE=$(curl -sS -o \"$RES_JSON\" -w '%{http_code}' --max-time 15 \\\n -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" -H 'Accept: application/json' \\\n \"https://api.atlassian.com/oauth/token/accessible-resources\") || PROBE=000\n CLOUD_ID=\"\"; RETRIED=\"\"\n if [ \"$PROBE\" = \"200\" ]; then\n CLOUD_ID=$(python3 \"$GITHUB_WORKSPACE/.github/scripts/jira_sites.py\" cloud-id \"$RES_JSON\" \"$BASE\" || true)\n if [ -n \"$CLOUD_ID\" ] && [ \"$BASE\" != \"https://api.atlassian.com/ex/jira/$CLOUD_ID\" ]; then\n BASE=\"https://api.atlassian.com/ex/jira/$CLOUD_ID\"\n RETRIED=1\n echo \"issue fetch got HTTP $ORIG_CODE at the configured base URL ($BASE_FORM); retrying via the scoped-token endpoint\"\n fetch_issue \"$BASE\"\n fi\n fi\n if [ \"$CODE\" != \"200\" ]; then\n # Only HTTP codes, site URLs, and cloudIds appear below — never credential values.\n case \"$PROBE\" in\n 401|403)\n DIAG=\"JIRA credentials rejected (accessible-resources probe returned HTTP $PROBE) — check that JIRA_EMAIL matches the account that owns JIRA_API_TOKEN, and that the token has not expired (Atlassian scoped tokens expire within 365 days)\" ;;\n 200)\n SITES=$(python3 \"$GITHUB_WORKSPACE/.github/scripts/jira_sites.py\" summary \"$RES_JSON\" || echo \"unavailable\")\n if [ -n \"$RETRIED\" ]; then\n DIAG=\"authentication works, and the scoped-token endpoint https://api.atlassian.com/ex/jira/$CLOUD_ID was tried too (HTTP $CODE there) — $KEY is likely not visible to this account or does not exist (JIRA returns 404 rather than 403 for unviewable issues). Token can reach: $SITES\"\n elif [ -n \"$CLOUD_ID\" ]; then\n DIAG=\"authentication works and JIRA_BASE_URL is already well-formed ($BASE_FORM) — $KEY is likely not visible to this account or does not exist (JIRA returns 404 rather than 403 for unviewable issues). Token can reach: $SITES\"\n else\n DIAG=\"authentication works (accessible-resources returned 200) but the configured JIRA_BASE_URL ($BASE_FORM) matches none of the token's sites. Scoped API tokens must target https://api.atlassian.com/ex/jira/ — set JIRA_BASE_URL to exactly that (no trailing slash, no /rest suffix). Token can reach: $SITES\"\n fi ;;\n 000) DIAG=\"could not reach api.atlassian.com to validate the credentials (connection failed) — runner egress may be blocked\" ;;\n *) DIAG=\"accessible-resources probe returned unexpected HTTP $PROBE — the token may be of a type this workflow does not recognise\" ;;\n esac\n no_ticket \"JIRA returned HTTP $ORIG_CODE for $KEY at the configured base URL. Diagnosis: $DIAG.\"\n fi\n echo \"recovered: issue fetched via the scoped-token endpoint — consider setting JIRA_BASE_URL to https://api.atlassian.com/ex/jira/$CLOUD_ID\"\nfi\nCCODE=$(curl -sS -o \"$COMMENTS_JSON\" -w '%{http_code}' --max-time 30 \\\n -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" -H 'Accept: application/json' \\\n \"$BASE/rest/api/3/issue/$KEY/comment\") || CCODE=000\n[ \"$CCODE\" = \"200\" ] || printf '{\"comments\":[]}' > \"$COMMENTS_JSON\"\n# Render the ADF (Atlassian Document Format) JSON into readable markdown, using the\n# renderer from the TRUSTED BASE checkout. Imperfect rendering is fine; a failed render\n# is not — fall back to the marker.\npython3 \"$GITHUB_WORKSPACE/.github/scripts/render_jira_ticket.py\" \"$KEY\" \"$ISSUE_JSON\" \"$COMMENTS_JSON\" > \"$OUT\" \\\n || no_ticket \"failed to render the JIRA response for $KEY\"\necho \"jira-ticket.md written for $KEY\"\n" + run: "# This step must NEVER fail the job: every failure mode degrades into a marker file\n# that tells the agent (and the humans reading the review) exactly what was missing.\n#\n# CRITICAL: GitHub Actions invokes run: scripts as `bash -e {0}`, so errexit is ALREADY\n# ACTIVE at our first line. `set -uo pipefail` does NOT clear an inherited -e — only an\n# explicit `set +e` does. Without it, any pipeline that legitimately exits non-zero\n# (e.g. the ticket-ID grep below on a branch with no ENG-nnn) aborts the whole agent\n# job. Observed exactly so in run 32017183079 (branch amjithtitus09-analytics-review-bot:\n# grep → 1, pipefail propagated it, inherited -e killed the step before the no_ticket\n# fallback could run). Do NOT \"simplify\" the `set +e` away.\nset -uo pipefail\nset +e\nOUT=/tmp/gh-aw/context/jira-ticket.md\nmkdir -p /tmp/gh-aw/context\nno_ticket() {\n printf 'NO TICKET FOUND: %s\\n' \"$1\" > \"$OUT\"\n echo \"jira-ticket.md marker written: $1\"\n exit 0\n}\n[ -n \"${AW_HEAD_REF:-}\" ] || no_ticket \"no pull request context, so no branch name to extract a ticket ID from\"\n# `|| true`: grep exits 1 when the branch has no ticket ID — belt and braces with the\n# `set +e` above, so this pipeline can never take the job down again.\nKEY=$(printf '%s' \"$AW_HEAD_REF\" | grep -oiE 'ENG-[0-9]+' | head -1 | tr '[:lower:]' '[:upper:]' || true)\n[ -n \"$KEY\" ] || no_ticket \"branch '$AW_HEAD_REF' does not contain a JIRA ticket ID (repo convention: branch name = ticket, e.g. ENG-909)\"\nif [ -z \"${JIRA_BASE_URL:-}\" ] || [ -z \"${JIRA_EMAIL:-}\" ] || [ -z \"${JIRA_API_TOKEN:-}\" ]; then\n no_ticket \"ticket $KEY detected in branch name, but the JIRA_BASE_URL / JIRA_EMAIL / JIRA_API_TOKEN repo secrets are not configured\"\nfi\nJIRA_BASE_URL=\"${JIRA_BASE_URL%/}\"\n# Classify the configured base URL's shape for diagnostics. Hosts only — never values.\ncase \"$JIRA_BASE_URL\" in\n https://api.atlassian.com/ex/jira/*) BASE_FORM=\"host api.atlassian.com — the scoped-token form\" ;;\n https://*.atlassian.net*) BASE_FORM=\"a *.atlassian.net host — works only with UNSCOPED tokens\" ;;\n *) BASE_FORM=\"a host that is neither api.atlassian.com nor *.atlassian.net\" ;;\nesac\nISSUE_JSON=$(mktemp); COMMENTS_JSON=$(mktemp)\nfetch_issue() {\n CODE=$(curl -sS -o \"$ISSUE_JSON\" -w '%{http_code}' --max-time 30 \\\n -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" -H 'Accept: application/json' \\\n \"$1/rest/api/3/issue/$KEY?fields=summary,description,labels,status\") || CODE=000\n}\nBASE=\"$JIRA_BASE_URL\"\nfetch_issue \"$BASE\"\nif [ \"$CODE\" != \"200\" ]; then\n ORIG_CODE=$CODE\n CLOUD_ID=\"\"; RETRIED=\"\"; TCODE=\"\"\n SITE_ORIGIN=$(printf '%s' \"$JIRA_BASE_URL\" | grep -oE '^https?://[^/]+' || true)\n case \"$SITE_ORIGIN\" in\n *.atlassian.net)\n # Scoped tokens are ignored on *.atlassian.net hosts, so a failure here is most\n # often just the wrong base-URL form. Discover the site's cloudId via the PUBLIC\n # /_edge/tenant_info endpoint (no credentials → cannot be confounded by auth\n # problems) and retry via the scoped-token endpoint.\n TENANT_JSON=$(mktemp)\n TCODE=$(curl -sS -o \"$TENANT_JSON\" -w '%{http_code}' --max-time 15 \\\n -H 'Accept: application/json' \"$SITE_ORIGIN/_edge/tenant_info\") || TCODE=000\n if [ \"$TCODE\" = \"200\" ]; then\n CLOUD_ID=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get(\"cloudId\") or \"\")' \"$TENANT_JSON\" 2>/dev/null || true)\n fi\n if [ -n \"$CLOUD_ID\" ]; then\n BASE=\"https://api.atlassian.com/ex/jira/$CLOUD_ID\"\n RETRIED=1\n echo \"issue fetch got HTTP $ORIG_CODE at $SITE_ORIGIN (cloudId $CLOUD_ID via /_edge/tenant_info); retrying via the scoped-token endpoint\"\n fetch_issue \"$BASE\"\n fi\n ;;\n esac\n if [ \"$CODE\" != \"200\" ]; then\n # Only HTTP codes, hostnames, and cloudIds appear below — never credential values.\n # Decisive where the evidence is decisive (the two-URL rule is proven); hedged where\n # it genuinely cannot distinguish causes (JIRA 404s rather than 403s for unviewable\n # issues, so \"missing\" vs \"not visible\" is indistinguishable from outside).\n RULE=\"Two-URL rule (empirically confirmed): scoped API tokens authenticate ONLY against https://api.atlassian.com/ex/jira/ — on https://.atlassian.net they are ignored entirely (anonymous and authenticated requests return identical status codes). Unscoped tokens use https://.atlassian.net.\"\n if [ -n \"$RETRIED\" ]; then\n DIAG=\"The scoped-token endpoint https://api.atlassian.com/ex/jira/$CLOUD_ID (cloudId auto-discovered via /_edge/tenant_info) was tried too and returned HTTP $CODE. Likely causes: the token's account lacks access to the ${KEY%%-*} project, the ticket does not exist (JIRA returns 404 rather than 403 for unviewable issues), or the token has expired (Atlassian scoped tokens expire within 365 days). $RULE\"\n elif [ -n \"$TCODE\" ]; then\n DIAG=\"CloudId discovery via $SITE_ORIGIN/_edge/tenant_info did not yield a cloudId (HTTP $TCODE), so the scoped-endpoint retry could not be attempted. $RULE If JIRA_API_TOKEN was created with scopes, set JIRA_BASE_URL to https://api.atlassian.com/ex/jira/ (no trailing slash, no /rest suffix) — discover the cloudId with: curl -s $SITE_ORIGIN/_edge/tenant_info. Other possibilities: the token's account lacks access to the ${KEY%%-*} project, or the ticket does not exist.\"\n else\n case \"$JIRA_BASE_URL\" in\n https://api.atlassian.com/ex/jira/*)\n DIAG=\"JIRA_BASE_URL is already the scoped-token form. Likely causes: the token's account lacks access to the ${KEY%%-*} project, the ticket does not exist (JIRA returns 404 rather than 403 for unviewable issues), the token has expired (scoped tokens expire within 365 days), or the token is UNSCOPED (unscoped tokens need the https://.atlassian.net form instead). $RULE\" ;;\n *)\n DIAG=\"The configured base URL is $BASE_FORM. $RULE Set JIRA_BASE_URL to the form matching the token type; for a scoped token, discover the cloudId with: curl -s https://.atlassian.net/_edge/tenant_info. Other possibilities: the token's account lacks access to the ${KEY%%-*} project, or the ticket does not exist.\" ;;\n esac\n fi\n no_ticket \"JIRA returned HTTP $ORIG_CODE for $KEY at the configured base URL. $DIAG\"\n fi\n echo \"recovered: issue fetched via the scoped-token endpoint — set JIRA_BASE_URL to https://api.atlassian.com/ex/jira/$CLOUD_ID to skip this retry in future runs\"\nfi\nCCODE=$(curl -sS -o \"$COMMENTS_JSON\" -w '%{http_code}' --max-time 30 \\\n -u \"$JIRA_EMAIL:$JIRA_API_TOKEN\" -H 'Accept: application/json' \\\n \"$BASE/rest/api/3/issue/$KEY/comment\") || CCODE=000\n[ \"$CCODE\" = \"200\" ] || printf '{\"comments\":[]}' > \"$COMMENTS_JSON\"\n# Render the ADF (Atlassian Document Format) JSON into readable markdown, using the\n# renderer from the TRUSTED BASE checkout. Imperfect rendering is fine; a failed render\n# is not — fall back to the marker.\npython3 \"$GITHUB_WORKSPACE/.github/scripts/render_jira_ticket.py\" \"$KEY\" \"$ISSUE_JSON\" \"$COMMENTS_JSON\" > \"$OUT\" \\\n || no_ticket \"failed to render the JIRA response for $KEY\"\necho \"jira-ticket.md written for $KEY\"\n" - name: Check out the care-sql-code-review skill (pinned) uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 (source v7) with: @@ -522,7 +522,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} name: Lint changed SQL docs (sqlglot + template checks) - run: "set -uo pipefail\nOUT=/tmp/gh-aw/context/lint-report.md\nmkdir -p /tmp/gh-aw/context\nfallback() {\n { echo \"# SQL lint report\"; echo; echo \"$1\"; } > \"$OUT\"\n echo \"lint-report.md: $1\"\n exit 0\n}\n[ -n \"${AW_PR_NUMBER:-}\" ] && [ -n \"${AW_HEAD_SHA:-}\" ] || fallback \"No pull request context; nothing to lint.\"\npip install --quiet sqlglot || fallback \"Could not install sqlglot; lint skipped this run.\"\n# Query docs live under Care/, Care Apps/ and Internal/. Removed files have nothing to lint.\ngh api \"repos/$REPO/pulls/$AW_PR_NUMBER/files\" --paginate \\\n --jq '.[] | select(.status != \"removed\") | .filename' \\\n | grep -E '^(Care|Internal).*\\.md$' > /tmp/gh-aw/context/changed-files.txt || true\n[ -s /tmp/gh-aw/context/changed-files.txt ] || fallback \"No changed query docs (Care*/ or Internal/ *.md) in this PR; nothing to lint.\"\nSCRATCH=$(mktemp -d)\nlinted=()\nwhile IFS= read -r f; do\n dest=\"$SCRATCH/$f\"\n mkdir -p \"$(dirname \"$dest\")\"\n enc=$(python3 -c 'import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=\"/\"))' \"$f\")\n if gh api -H \"Accept: application/vnd.github.raw\" \\\n \"repos/$REPO/contents/$enc?ref=$AW_HEAD_SHA\" > \"$dest\" 2>/dev/null; then\n linted+=(\"$f\")\n else\n echo \"warning: could not fetch $f at $AW_HEAD_SHA\"\n fi\ndone < /tmp/gh-aw/context/changed-files.txt\n[ \"${#linted[@]}\" -gt 0 ] || fallback \"Changed query docs could not be fetched at the head SHA; lint skipped this run.\"\n# The lint script itself comes from the TRUSTED BASE checkout — only the .md files\n# being parsed come from the PR.\n(cd \"$SCRATCH\" && python3 \"$GITHUB_WORKSPACE/.github/scripts/lint_queries.py\" --out \"$OUT\" \"${linted[@]}\") \\\n || fallback \"lint_queries.py crashed; see the step log. Lint findings unavailable this run.\"" + run: "# Never fails the job — findings are agent input, and every abort path writes a\n# fallback marker instead. Actions injects `bash -e {0}`; clear it explicitly\n# (see the JIRA step's comment and run 32017183079 for the failure this prevents).\nset -uo pipefail\nset +e\nOUT=/tmp/gh-aw/context/lint-report.md\nmkdir -p /tmp/gh-aw/context\nfallback() {\n { echo \"# SQL lint report\"; echo; echo \"$1\"; } > \"$OUT\"\n echo \"lint-report.md: $1\"\n exit 0\n}\n[ -n \"${AW_PR_NUMBER:-}\" ] && [ -n \"${AW_HEAD_SHA:-}\" ] || fallback \"No pull request context; nothing to lint.\"\npip install --quiet sqlglot || fallback \"Could not install sqlglot; lint skipped this run.\"\n# Query docs live under Care/, Care Apps/ and Internal/. Removed files have nothing to lint.\ngh api \"repos/$REPO/pulls/$AW_PR_NUMBER/files\" --paginate \\\n --jq '.[] | select(.status != \"removed\") | .filename' \\\n | grep -E '^(Care|Internal).*\\.md$' > /tmp/gh-aw/context/changed-files.txt || true\n[ -s /tmp/gh-aw/context/changed-files.txt ] || fallback \"No changed query docs (Care*/ or Internal/ *.md) in this PR; nothing to lint.\"\nSCRATCH=$(mktemp -d)\nlinted=()\nwhile IFS= read -r f; do\n dest=\"$SCRATCH/$f\"\n mkdir -p \"$(dirname \"$dest\")\"\n enc=$(python3 -c 'import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1], safe=\"/\"))' \"$f\")\n if gh api -H \"Accept: application/vnd.github.raw\" \\\n \"repos/$REPO/contents/$enc?ref=$AW_HEAD_SHA\" > \"$dest\" 2>/dev/null; then\n linted+=(\"$f\")\n else\n echo \"warning: could not fetch $f at $AW_HEAD_SHA\"\n fi\ndone < /tmp/gh-aw/context/changed-files.txt\n[ \"${#linted[@]}\" -gt 0 ] || fallback \"Changed query docs could not be fetched at the head SHA; lint skipped this run.\"\n# The lint script itself comes from the TRUSTED BASE checkout — only the .md files\n# being parsed come from the PR.\n(cd \"$SCRATCH\" && python3 \"$GITHUB_WORKSPACE/.github/scripts/lint_queries.py\" --out \"$OUT\" \"${linted[@]}\") \\\n || fallback \"lint_queries.py crashed; see the step log. Lint findings unavailable this run.\"" - name: Configure Git credentials env: diff --git a/.github/workflows/analytics-review.md b/.github/workflows/analytics-review.md index 9b36968..29bf0db 100644 --- a/.github/workflows/analytics-review.md +++ b/.github/workflows/analytics-review.md @@ -109,6 +109,9 @@ steps: REPO: ${{ github.repository }} EVENT_NAME: ${{ github.event_name }} run: | + # Deliberately STRICT (-e): if PR resolution itself breaks, we want a loud failure, + # not a review of the wrong PR. (Contrast with the JIRA/lint steps below, which must + # never fail the job and therefore explicitly clear the inherited -e.) set -euo pipefail mkdir -p /tmp/gh-aw/context PR_NUMBER="" @@ -161,17 +164,24 @@ steps: # way — an LLM with a live credential in env is one prompt-injection away from leaking it. # # Atlassian has TWO kinds of API tokens with DIFFERENT base URLs (see "Manage API tokens for - # your Atlassian account" in Atlassian's docs): + # your Atlassian account" in Atlassian's docs) — CONFIRMED empirically against this site: # - unscoped tokens → https://.atlassian.net # - scoped tokens → https://api.atlassian.com/ex/jira/ - # A scoped token sent to the *.atlassian.net form gets HTTP 404 — same status JIRA uses for - # a missing/unviewable issue, so it is silently misleading. This step therefore (a) retries - # via the scoped-token endpoint when it can resolve the cloudId, making either token type - # work with either JIRA_BASE_URL form, and (b) on hard failure writes a self-diagnosing - # marker. The diagnostic probe is /oauth/token/accessible-resources, which works for BOTH - # token types. Do NOT probe /rest/api/3/myself (granular read:issue scopes are not allowed - # to call it — it false-reports valid credentials as broken) and do NOT use - # /_edgeAuth/tenantInfo for cloudId discovery (retired endpoint). + # A scoped token sent to the *.atlassian.net form is IGNORED ENTIRELY: anonymous and + # authenticated requests return identical status codes there (verified — /rest/api/3/myself + # → 401 and /rest/api/3/issue/ENG-909 → 404, with and without credentials). So JIRA answers + # 404, the same status it uses for a missing/unviewable issue — silently misleading. This + # step therefore (a) auto-recovers: when the configured base is a *.atlassian.net host and + # the issue fetch fails, it discovers the site's cloudId via the PUBLIC, unauthenticated + # GET /_edge/tenant_info endpoint and retries via the scoped-token endpoint, making + # either token type work; and (b) on hard failure writes a self-diagnosing marker. + # Endpoints deliberately NOT used: + # - /rest/api/3/myself — granular read:issue scopes may not call it; it false-reports + # valid credentials as broken. + # - /oauth/token/accessible-resources with basic auth — verified to return 401 uniformly + # (no auth, bogus basic, bogus bearer alike): it is OAuth-Bearer-only, so a 401 from it + # says NOTHING about the API-token credentials. + # - /_edgeAuth/tenantInfo — retired. /_edge/tenant_info is the live replacement. # ------------------------------------------------------------------------------------------ - name: Fetch JIRA ticket context env: @@ -179,10 +189,18 @@ steps: JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} run: | - # Deliberately no `set -e`: this step must NEVER fail the job. Every failure mode - # degrades into a marker file that tells the agent (and the humans reading the review) - # exactly what was missing. + # This step must NEVER fail the job: every failure mode degrades into a marker file + # that tells the agent (and the humans reading the review) exactly what was missing. + # + # CRITICAL: GitHub Actions invokes run: scripts as `bash -e {0}`, so errexit is ALREADY + # ACTIVE at our first line. `set -uo pipefail` does NOT clear an inherited -e — only an + # explicit `set +e` does. Without it, any pipeline that legitimately exits non-zero + # (e.g. the ticket-ID grep below on a branch with no ENG-nnn) aborts the whole agent + # job. Observed exactly so in run 32017183079 (branch amjithtitus09-analytics-review-bot: + # grep → 1, pipefail propagated it, inherited -e killed the step before the no_ticket + # fallback could run). Do NOT "simplify" the `set +e` away. set -uo pipefail + set +e OUT=/tmp/gh-aw/context/jira-ticket.md mkdir -p /tmp/gh-aw/context no_ticket() { @@ -191,7 +209,9 @@ steps: exit 0 } [ -n "${AW_HEAD_REF:-}" ] || no_ticket "no pull request context, so no branch name to extract a ticket ID from" - KEY=$(printf '%s' "$AW_HEAD_REF" | grep -oiE 'ENG-[0-9]+' | head -1 | tr '[:lower:]' '[:upper:]') + # `|| true`: grep exits 1 when the branch has no ticket ID — belt and braces with the + # `set +e` above, so this pipeline can never take the job down again. + KEY=$(printf '%s' "$AW_HEAD_REF" | grep -oiE 'ENG-[0-9]+' | head -1 | tr '[:lower:]' '[:upper:]' || true) [ -n "$KEY" ] || no_ticket "branch '$AW_HEAD_REF' does not contain a JIRA ticket ID (repo convention: branch name = ticket, e.g. ENG-909)" if [ -z "${JIRA_BASE_URL:-}" ] || [ -z "${JIRA_EMAIL:-}" ] || [ -z "${JIRA_API_TOKEN:-}" ]; then no_ticket "ticket $KEY detected in branch name, but the JIRA_BASE_URL / JIRA_EMAIL / JIRA_API_TOKEN repo secrets are not configured" @@ -213,43 +233,49 @@ steps: fetch_issue "$BASE" if [ "$CODE" != "200" ]; then ORIG_CODE=$CODE - # Ask Atlassian which sites this credential can reach. Works for scoped AND - # unscoped tokens, and each returned entry's `id` is the cloudId needed for the - # scoped-token endpoint — one call both diagnoses and enables auto-recovery. - RES_JSON=$(mktemp) - PROBE=$(curl -sS -o "$RES_JSON" -w '%{http_code}' --max-time 15 \ - -u "$JIRA_EMAIL:$JIRA_API_TOKEN" -H 'Accept: application/json' \ - "https://api.atlassian.com/oauth/token/accessible-resources") || PROBE=000 - CLOUD_ID=""; RETRIED="" - if [ "$PROBE" = "200" ]; then - CLOUD_ID=$(python3 "$GITHUB_WORKSPACE/.github/scripts/jira_sites.py" cloud-id "$RES_JSON" "$BASE" || true) - if [ -n "$CLOUD_ID" ] && [ "$BASE" != "https://api.atlassian.com/ex/jira/$CLOUD_ID" ]; then - BASE="https://api.atlassian.com/ex/jira/$CLOUD_ID" - RETRIED=1 - echo "issue fetch got HTTP $ORIG_CODE at the configured base URL ($BASE_FORM); retrying via the scoped-token endpoint" - fetch_issue "$BASE" - fi - fi + CLOUD_ID=""; RETRIED=""; TCODE="" + SITE_ORIGIN=$(printf '%s' "$JIRA_BASE_URL" | grep -oE '^https?://[^/]+' || true) + case "$SITE_ORIGIN" in + *.atlassian.net) + # Scoped tokens are ignored on *.atlassian.net hosts, so a failure here is most + # often just the wrong base-URL form. Discover the site's cloudId via the PUBLIC + # /_edge/tenant_info endpoint (no credentials → cannot be confounded by auth + # problems) and retry via the scoped-token endpoint. + TENANT_JSON=$(mktemp) + TCODE=$(curl -sS -o "$TENANT_JSON" -w '%{http_code}' --max-time 15 \ + -H 'Accept: application/json' "$SITE_ORIGIN/_edge/tenant_info") || TCODE=000 + if [ "$TCODE" = "200" ]; then + CLOUD_ID=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("cloudId") or "")' "$TENANT_JSON" 2>/dev/null || true) + fi + if [ -n "$CLOUD_ID" ]; then + BASE="https://api.atlassian.com/ex/jira/$CLOUD_ID" + RETRIED=1 + echo "issue fetch got HTTP $ORIG_CODE at $SITE_ORIGIN (cloudId $CLOUD_ID via /_edge/tenant_info); retrying via the scoped-token endpoint" + fetch_issue "$BASE" + fi + ;; + esac if [ "$CODE" != "200" ]; then - # Only HTTP codes, site URLs, and cloudIds appear below — never credential values. - case "$PROBE" in - 401|403) - DIAG="JIRA credentials rejected (accessible-resources probe returned HTTP $PROBE) — check that JIRA_EMAIL matches the account that owns JIRA_API_TOKEN, and that the token has not expired (Atlassian scoped tokens expire within 365 days)" ;; - 200) - SITES=$(python3 "$GITHUB_WORKSPACE/.github/scripts/jira_sites.py" summary "$RES_JSON" || echo "unavailable") - if [ -n "$RETRIED" ]; then - DIAG="authentication works, and the scoped-token endpoint https://api.atlassian.com/ex/jira/$CLOUD_ID was tried too (HTTP $CODE there) — $KEY is likely not visible to this account or does not exist (JIRA returns 404 rather than 403 for unviewable issues). Token can reach: $SITES" - elif [ -n "$CLOUD_ID" ]; then - DIAG="authentication works and JIRA_BASE_URL is already well-formed ($BASE_FORM) — $KEY is likely not visible to this account or does not exist (JIRA returns 404 rather than 403 for unviewable issues). Token can reach: $SITES" - else - DIAG="authentication works (accessible-resources returned 200) but the configured JIRA_BASE_URL ($BASE_FORM) matches none of the token's sites. Scoped API tokens must target https://api.atlassian.com/ex/jira/ — set JIRA_BASE_URL to exactly that (no trailing slash, no /rest suffix). Token can reach: $SITES" - fi ;; - 000) DIAG="could not reach api.atlassian.com to validate the credentials (connection failed) — runner egress may be blocked" ;; - *) DIAG="accessible-resources probe returned unexpected HTTP $PROBE — the token may be of a type this workflow does not recognise" ;; - esac - no_ticket "JIRA returned HTTP $ORIG_CODE for $KEY at the configured base URL. Diagnosis: $DIAG." + # Only HTTP codes, hostnames, and cloudIds appear below — never credential values. + # Decisive where the evidence is decisive (the two-URL rule is proven); hedged where + # it genuinely cannot distinguish causes (JIRA 404s rather than 403s for unviewable + # issues, so "missing" vs "not visible" is indistinguishable from outside). + RULE="Two-URL rule (empirically confirmed): scoped API tokens authenticate ONLY against https://api.atlassian.com/ex/jira/ — on https://.atlassian.net they are ignored entirely (anonymous and authenticated requests return identical status codes). Unscoped tokens use https://.atlassian.net." + if [ -n "$RETRIED" ]; then + DIAG="The scoped-token endpoint https://api.atlassian.com/ex/jira/$CLOUD_ID (cloudId auto-discovered via /_edge/tenant_info) was tried too and returned HTTP $CODE. Likely causes: the token's account lacks access to the ${KEY%%-*} project, the ticket does not exist (JIRA returns 404 rather than 403 for unviewable issues), or the token has expired (Atlassian scoped tokens expire within 365 days). $RULE" + elif [ -n "$TCODE" ]; then + DIAG="CloudId discovery via $SITE_ORIGIN/_edge/tenant_info did not yield a cloudId (HTTP $TCODE), so the scoped-endpoint retry could not be attempted. $RULE If JIRA_API_TOKEN was created with scopes, set JIRA_BASE_URL to https://api.atlassian.com/ex/jira/ (no trailing slash, no /rest suffix) — discover the cloudId with: curl -s $SITE_ORIGIN/_edge/tenant_info. Other possibilities: the token's account lacks access to the ${KEY%%-*} project, or the ticket does not exist." + else + case "$JIRA_BASE_URL" in + https://api.atlassian.com/ex/jira/*) + DIAG="JIRA_BASE_URL is already the scoped-token form. Likely causes: the token's account lacks access to the ${KEY%%-*} project, the ticket does not exist (JIRA returns 404 rather than 403 for unviewable issues), the token has expired (scoped tokens expire within 365 days), or the token is UNSCOPED (unscoped tokens need the https://.atlassian.net form instead). $RULE" ;; + *) + DIAG="The configured base URL is $BASE_FORM. $RULE Set JIRA_BASE_URL to the form matching the token type; for a scoped token, discover the cloudId with: curl -s https://.atlassian.net/_edge/tenant_info. Other possibilities: the token's account lacks access to the ${KEY%%-*} project, or the ticket does not exist." ;; + esac + fi + no_ticket "JIRA returned HTTP $ORIG_CODE for $KEY at the configured base URL. $DIAG" fi - echo "recovered: issue fetched via the scoped-token endpoint — consider setting JIRA_BASE_URL to https://api.atlassian.com/ex/jira/$CLOUD_ID" + echo "recovered: issue fetched via the scoped-token endpoint — set JIRA_BASE_URL to https://api.atlassian.com/ex/jira/$CLOUD_ID to skip this retry in future runs" fi CCODE=$(curl -sS -o "$COMMENTS_JSON" -w '%{http_code}' --max-time 30 \ -u "$JIRA_EMAIL:$JIRA_API_TOKEN" -H 'Accept: application/json' \ @@ -298,7 +324,11 @@ steps: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} run: | + # Never fails the job — findings are agent input, and every abort path writes a + # fallback marker instead. Actions injects `bash -e {0}`; clear it explicitly + # (see the JIRA step's comment and run 32017183079 for the failure this prevents). set -uo pipefail + set +e OUT=/tmp/gh-aw/context/lint-report.md mkdir -p /tmp/gh-aw/context fallback() { diff --git a/README.md b/README.md index eabc762..770ba26 100644 --- a/README.md +++ b/README.md @@ -50,5 +50,5 @@ Every pull request is reviewed automatically by an agentic workflow **Talking to it:** reply to any of its inline comments, or @-mention it anywhere on the PR, and it will answer. It stops re-reviewing after 6 rounds per PR unless summoned with an @-mention. To silence it for a PR, mark the PR as draft. **Prerequisites (admin setup):** -- Repo secrets `JIRA_BASE_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN` — for fetching ticket context. For Atlassian API tokens **with scopes**, `JIRA_BASE_URL` must be `https://api.atlassian.com/ex/jira/`; only unscoped tokens work against `https://.atlassian.net`. (The workflow auto-recovers from a mismatched base URL when it can resolve the cloudId, and otherwise writes a self-diagnosing note into the review context.) Without these secrets the bot still runs, minus the requirement-fidelity lens. +- Repo secrets `JIRA_BASE_URL`, `JIRA_EMAIL`, `JIRA_API_TOKEN` — for fetching ticket context. For Atlassian API tokens **with scopes**, `JIRA_BASE_URL` must be `https://api.atlassian.com/ex/jira/`; only unscoped tokens work against `https://.atlassian.net`. Discover the cloudId with `curl -s https://.atlassian.net/_edge/tenant_info` (public endpoint, no credentials needed). The workflow auto-recovers from a `*.atlassian.net` base URL by doing that lookup itself, and otherwise writes a self-diagnosing note into the review context. Without these secrets the bot still runs, minus the requirement-fidelity lens. - Org-level `COPILOT_GITHUB_TOKEN` — the Copilot engine credential already used by [care_fe's reviewer](https://github.com/ohcnetwork/care_fe/blob/develop/.github/workflows/care-review.md).