From 152f09795361e8fc34a2faea22af617d140c3e12 Mon Sep 17 00:00:00 2001 From: dormouse-bot <287024035+dormouse-bot@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:15:28 +0000 Subject: [PATCH] =?UTF-8?q?chore:=20update=20tend=20workflows=20(0.3.16=20?= =?UTF-8?q?=E2=86=92=200.3.17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/tend-ci-failures.yaml | 86 +++++++---------------- .github/workflows/tend-mention-relay.yaml | 2 +- .github/workflows/tend-mention.yaml | 4 +- .github/workflows/tend-nightly.yaml | 4 +- .github/workflows/tend-notifications.yaml | 6 +- .github/workflows/tend-review-runs.yaml | 4 +- .github/workflows/tend-review.yaml | 4 +- .github/workflows/tend-triage.yaml | 25 ++----- .github/workflows/tend-weekly.yaml | 4 +- docs/specs/security-ci.md | 2 +- scripts/spec-word-budgets.json | 2 +- scripts/workflow-lint.mjs | 17 +---- scripts/workflow-lint.test.mjs | 5 -- 13 files changed, 50 insertions(+), 115 deletions(-) diff --git a/.github/workflows/tend-ci-failures.yaml b/.github/workflows/tend-ci-failures.yaml index 0a15d2823..a9f32f96b 100644 --- a/.github/workflows/tend-ci-failures.yaml +++ b/.github/workflows/tend-ci-failures.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.3.16. Regenerate with: uvx tend@latest init +# Generated by tend 0.3.17. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To change this workflow, edit .config/tend.yaml. To change what the agent @@ -9,10 +9,8 @@ name: tend-ci-failures # Turns a watched workflow's verdict on a watched branch into its CI failure -# report, with no agent: a failure files the report or comments on it and -# dispatches the report's triage, and a pass closes it. `ci_failure_report.py` -# documents the report, and why it writes with the job's own token rather than -# the bot's. +# report, with no agent: a failure files the report or comments on it, and a +# pass closes it. The bot token makes a new report trigger ordinary issue triage. on: workflow_run: workflows: ["CI", "Argos"] @@ -44,11 +42,10 @@ jobs: group: ${{ github.workflow }}-${{ github.event.workflow_run.name }}-${{ github.event.workflow_run.event }}-${{ github.event.workflow_run.head_branch }} cancel-in-progress: false runs-on: ubuntu-24.04 - # `actions: write` is what the triage dispatch needs; the job holds no - # secret, and runs only the pinned script below. - permissions: - actions: write - issues: write + environment: + name: tend + deployment: false + permissions: {} steps: - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0 id: tend_uv @@ -61,8 +58,7 @@ jobs: ignore-empty-workdir: true - name: File, update or close the CI failure report env: - GH_TOKEN: ${{ github.token }} - TRIAGE_WORKFLOW: "tend-triage.yaml" + GH_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} TEND_UV: ${{ steps.tend_uv.outputs.uv-path }} run: | "$TEND_UV" run --script - <<'TEND_PY' @@ -77,25 +73,20 @@ jobs: per workflow, branch and triggering event, the unit a run's verdict speaks for: a scheduled suite can fail on tests that push runs never reach. - - A failed run files the report, or comments on the one already open, and - dispatches `tend-triage` for it. + - A failed run files the report, or comments on the one already open. - A passing run closes the open report. The cause issues filed under it stay as they are: a pass doesn't show that a flaky cause is gone, and a cause's fix may still be in flight, so each cause's own triage closes it. - A run that a newer finished run of the same lane supersedes changes nothing. Runs can finish out of order, and the newer verdict is the lane's state. - The job's own `GITHUB_TOKEN` writes, as `github-actions[bot]`, rather than the - bot's token, so the bot is notified of the report and of each later comment; - GitHub notifies nobody of their own activity. `GITHUB_TOKEN` starts no workflow - by opening an issue, but it may dispatch one, and the dispatched triage run is - the fast path. The notification is the fallback, for a dispatch that failed or - a run the rate limit refused: the run marks it read only once it succeeds, and - the notifications poll re-dispatches whatever is left unread. + The bot's token writes the report, so creating it triggers ordinary issue + triage. Later failure comments add evidence without starting another session. + The daily `review-runs` sweep revisits reports with unhandled failures and + causes whose repair stalled; the bot gets no notification for its own writes. - Inputs (env): ``GH_TOKEN``, ``GITHUB_REPOSITORY``, ``GITHUB_EVENT_PATH`` (a - `workflow_run` payload), and ``TRIAGE_WORKFLOW``, the triage workflow's file, - empty where triage is not generated. + Inputs (env): ``GH_TOKEN`` (the bot's token), ``GITHUB_REPOSITORY``, + ``GITHUB_EVENT_PATH`` (a `workflow_run` payload). """ from __future__ import annotations @@ -111,7 +102,6 @@ jobs: LABEL = "tend-ci-failure" LABEL_DESCRIPTION = "A watched CI workflow is failing; Tend tracks its causes here" LABEL_COLOR = "b60205" - REPORTER = "github-actions[bot]" # The verdicts that say something about the lane. The generated job's `if:` # admits only these, so a cancelled run never takes the lane's pending slot # from a verdict; everything but `success` reaching here is a failure. @@ -183,16 +173,15 @@ jobs: def open_report(repo: str, run: dict[str, Any]) -> int | None: - """The lane's open report: the lowest open issue by `github-actions[bot]` - under the label and the lane's title. Any workflow's `GITHUB_TOKEN` writes - as that account, so this matches what no person and not the bot filed.""" + """The lane's report is the lowest open issue with its label and title. + + The label assigns report ownership independently of who created the issue. + """ issues = paginated(f"repos/{repo}/issues?labels={LABEL}&state=open&per_page=100") numbers = sorted( issue["number"] for issue in issues - if "pull_request" not in issue - and issue["user"]["login"] == REPORTER - and issue["title"] == title(run) + if "pull_request" not in issue and issue["title"] == title(run) ) return numbers[0] if numbers else None @@ -201,7 +190,7 @@ jobs: api(f"repos/{repo}/issues/{number}/comments", method="POST", body={"body": body}) - def file_report(repo: str, run: dict[str, Any]) -> int: + def file_report(repo: str, run: dict[str, Any]) -> None: # Fails once the label exists, which is every run after the first. try: api( @@ -220,37 +209,15 @@ jobs: f"triggered by `{run['event']}`.\n\n" f"This issue tracks `{run['name']}`'s `{run['event']}` runs on " f"`{run['head_branch']}` until one passes. Each later failed run adds a " - "comment, and the next passing run closes this issue. Tend triages each " - "failure, filing every distinct cause as a sub-issue and working it " - "there.\n" + "comment, and the next passing run closes this issue. Tend triages this " + "report when it opens, filing every distinct cause as a sub-issue and " + "working it there. The daily review revisits unhandled failures.\n" ) - issue = api( + api( f"repos/{repo}/issues", method="POST", body={"title": title(run), "body": body, "labels": [LABEL]}, ) - return issue["number"] - - - def dispatch_triage(repo: str, run: dict[str, Any], number: int, ref: str) -> None: - """Start the report's triage; a failure leaves it to the notification.""" - workflow = os.environ.get("TRIAGE_WORKFLOW", "") - if not workflow: - return - try: - api( - f"repos/{repo}/actions/workflows/{workflow}/dispatches", - method="POST", - body={ - "ref": ref, - "inputs": {"issue": str(number), "title": title(run), "report": "true"}, - }, - ) - except subprocess.CalledProcessError: - print( - f"::warning::could not dispatch triage for #{number}; the " - "notifications poll picks it up" - ) def main() -> int: @@ -263,10 +230,9 @@ jobs: number = open_report(repo, run) if run["conclusion"] != "success": if number is None: - number = file_report(repo, run) + file_report(repo, run) else: comment(repo, number, f"Failed again: {run_link(run)}.") - dispatch_triage(repo, run, number, event["repository"]["default_branch"]) elif number is not None: comment(repo, number, f"{run_link(run)} passed. Closing.") api( diff --git a/.github/workflows/tend-mention-relay.yaml b/.github/workflows/tend-mention-relay.yaml index aae8f0412..029998f0d 100644 --- a/.github/workflows/tend-mention-relay.yaml +++ b/.github/workflows/tend-mention-relay.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.3.16. Regenerate with: uvx tend@latest init +# Generated by tend 0.3.17. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To change this workflow, edit .config/tend.yaml. To change what the agent diff --git a/.github/workflows/tend-mention.yaml b/.github/workflows/tend-mention.yaml index 0b7783c70..a06d423fa 100644 --- a/.github/workflows/tend-mention.yaml +++ b/.github/workflows/tend-mention.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.3.16. Regenerate with: uvx tend@latest init +# Generated by tend 0.3.17. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To change this workflow, edit .config/tend.yaml. To change what the agent @@ -340,7 +340,7 @@ jobs: - run: node scripts/setup-git.mjs name: Define the markdown merge driver - - uses: max-sixty/tend/claude@0.3.16 + - uses: max-sixty/tend/claude@0.3.17 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} merge: restricted diff --git a/.github/workflows/tend-nightly.yaml b/.github/workflows/tend-nightly.yaml index 2e5ea2f87..b0bbe3019 100644 --- a/.github/workflows/tend-nightly.yaml +++ b/.github/workflows/tend-nightly.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.3.16. Regenerate with: uvx tend@latest init +# Generated by tend 0.3.17. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To change this workflow, edit .config/tend.yaml. To change what the agent @@ -35,7 +35,7 @@ jobs: - run: node scripts/setup-git.mjs name: Define the markdown merge driver - - uses: max-sixty/tend/claude@0.3.16 + - uses: max-sixty/tend/claude@0.3.17 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} merge: restricted diff --git a/.github/workflows/tend-notifications.yaml b/.github/workflows/tend-notifications.yaml index 506af5511..1e59a9966 100644 --- a/.github/workflows/tend-notifications.yaml +++ b/.github/workflows/tend-notifications.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.3.16. Regenerate with: uvx tend@latest init +# Generated by tend 0.3.17. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To change this workflow, edit .config/tend.yaml. To change what the agent @@ -38,7 +38,7 @@ jobs: checksum: ${{ runner.arch == 'ARM64' && '4bb69cf95ed13f23713bb7bc09f241896ea0c75b7c557e615a20604b82f9c403' || 'f68d291c75783122dad58ab77def0e13f3233f358731db5d6d7e8715603b928f' }} enable-cache: false ignore-empty-workdir: true - - name: Check for unread notifications and conflicted PRs + - name: Check notifications and conflicts id: check env: GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} @@ -234,7 +234,7 @@ jobs: - run: node scripts/setup-git.mjs name: Define the markdown merge driver if: steps.check.outputs.count != '0' || steps.check.outputs.conflict_count != '0' || github.event_name == 'workflow_dispatch' - - uses: max-sixty/tend/claude@0.3.16 + - uses: max-sixty/tend/claude@0.3.17 if: steps.check.outputs.count != '0' || steps.check.outputs.conflict_count != '0' || github.event_name == 'workflow_dispatch' with: github_token: ${{ secrets.TEND_BOT_TOKEN }} diff --git a/.github/workflows/tend-review-runs.yaml b/.github/workflows/tend-review-runs.yaml index e57ed3643..27cc6e232 100644 --- a/.github/workflows/tend-review-runs.yaml +++ b/.github/workflows/tend-review-runs.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.3.16. Regenerate with: uvx tend@latest init +# Generated by tend 0.3.17. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To change this workflow, edit .config/tend.yaml. To change what the agent @@ -35,7 +35,7 @@ jobs: - run: node scripts/setup-git.mjs name: Define the markdown merge driver - - uses: max-sixty/tend/claude@0.3.16 + - uses: max-sixty/tend/claude@0.3.17 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} merge: restricted diff --git a/.github/workflows/tend-review.yaml b/.github/workflows/tend-review.yaml index 2bf89de9a..a6a7f6903 100644 --- a/.github/workflows/tend-review.yaml +++ b/.github/workflows/tend-review.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.3.16. Regenerate with: uvx tend@latest init +# Generated by tend 0.3.17. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To change this workflow, edit .config/tend.yaml. To change what the agent @@ -53,7 +53,7 @@ jobs: - run: node scripts/setup-git.mjs name: Define the markdown merge driver - - uses: max-sixty/tend/claude@0.3.16 + - uses: max-sixty/tend/claude@0.3.17 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} merge: restricted diff --git a/.github/workflows/tend-triage.yaml b/.github/workflows/tend-triage.yaml index 6e71ddcea..e507c0a95 100644 --- a/.github/workflows/tend-triage.yaml +++ b/.github/workflows/tend-triage.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.3.16. Regenerate with: uvx tend@latest init +# Generated by tend 0.3.17. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To change this workflow, edit .config/tend.yaml. To change what the agent @@ -14,11 +14,8 @@ name: tend-triage run-name: ${{ inputs.title || github.event.issue.title }} on: issues: - types: [opened] - # A CI failure report, dispatched by tend-ci-failures for each failed run, - # and an issue a later run hands back, such as a stalled cause issue. - # GitHub creates the run even when a `GITHUB_TOKEN` dispatched it, and it - # carries the default branch, which the environment admits. + types: [opened, reopened] + # An issue a later run hands back, such as a stalled CI report or cause. workflow_dispatch: inputs: issue: @@ -29,23 +26,15 @@ on: description: The issue's title, which names the run type: string default: "" - report: - description: Whether the issue is a CI failure report - type: boolean - default: false jobs: triage: if: github.repository_owner == 'diffplug' && vars.TEND_ENABLED != 'false' && contains(github.event.issue.labels.*.name, 'tend-outage') == false && contains(github.event.issue.labels.*.name, 'tend-rate-limit') == false - # Reports share one group, so no two report sessions file the same cause - # at once. Each finished failure dispatches one, and the newest pending one - # replaces an older: it reads every run its report holds since the bot's - # last word, and a replaced one's report keeps its notification. Every - # other issue is a group of its own. Never cancelled: a session may already - # have filed an issue or opened a PR. + # New issues and recovery share one worker lane. concurrency: - group: ${{ inputs.report && 'tend-triage-ci-reports' || format('{0}-{1}', github.workflow, github.event.issue.number || inputs.issue) }} + group: tend-triage cancel-in-progress: false + queue: max runs-on: ubuntu-24.04 environment: name: tend @@ -74,7 +63,7 @@ jobs: - run: node scripts/setup-git.mjs name: Define the markdown merge driver - - uses: max-sixty/tend/claude@0.3.16 + - uses: max-sixty/tend/claude@0.3.17 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} merge: restricted diff --git a/.github/workflows/tend-weekly.yaml b/.github/workflows/tend-weekly.yaml index c5ce79e17..ed593c493 100644 --- a/.github/workflows/tend-weekly.yaml +++ b/.github/workflows/tend-weekly.yaml @@ -1,4 +1,4 @@ -# Generated by tend 0.3.16. Regenerate with: uvx tend@latest init +# Generated by tend 0.3.17. Regenerate with: uvx tend@latest init # # Do not edit this file directly — it will be overwritten on regeneration. # To change this workflow, edit .config/tend.yaml. To change what the agent @@ -35,7 +35,7 @@ jobs: - run: node scripts/setup-git.mjs name: Define the markdown merge driver - - uses: max-sixty/tend/claude@0.3.16 + - uses: max-sixty/tend/claude@0.3.17 with: github_token: ${{ secrets.TEND_BOT_TOKEN }} merge: restricted diff --git a/docs/specs/security-ci.md b/docs/specs/security-ci.md index a5fce6df8..eff0619b2 100644 --- a/docs/specs/security-ci.md +++ b/docs/specs/security-ci.md @@ -99,7 +99,7 @@ The diff window's evasions are `docs/specs/security.md` -> "Known gaps". - **FAIL IF** `S60` any `tend-*.yaml` pins `max-sixty/tend` below `0.1.19`, the release that pins instruction files by glob at any depth (rationale). Pinned by `scripts/workflow-lint.mjs`. - **FAIL IF** `S61` the pinned `max-sixty/tend/claude` starts the agent on a workflow's fork-PR checkout before reverting the project-instruction paths, or hands the agent's environment, disk, or `.git/config` the real `TEND_BOT_TOKEN` or an Anthropic credential; or a `tend-*.yaml` sets workflow- or job-level `env:`, which the harness forwards to the agent, or writes a secret to `$GITHUB_ENV`. Read the action at the tag the workflows pin: `claude/action.yaml` and the shared steps it runs for checkout ordering, instruction pinning, and sandbox setup (rationale). A fork PR the agent checks out itself is `docs/specs/security.md` -> "Known gaps". The `env:` and `$GITHUB_ENV` halves pinned by `scripts/workflow-lint.mjs`. - **FAIL IF** `S62` any `tend-*.yaml` references `max-sixty/tend/claude` unpinned (e.g. `@main`, no version). Every other action reference is "GitHub Actions Policies"'s. Pinned by `scripts/workflow-lint.mjs`. -- **FAIL IF** `S63` any job in an agent-managed workflow has **effective** `GITHUB_TOKEN` permissions beyond `contents: write`, `pull-requests: write`, `issues: write`, `actions: read`, or any `read` permission, except `actions: write` on the agentless `report` job of `tend-ci-failures.yaml`, which dispatches `tend-triage.yaml`; or runs `anthropics/claude-code-action` without `github_token`, which hands its agent the Claude GitHub App's token. Effective, not declared: apply job permissions over workflow permissions over the repository default; omitted scopes in an explicit block become `none` (rationale). Pinned by `scripts/workflow-lint.mjs`. +- **FAIL IF** `S63` any job in an agent-managed workflow has **effective** `GITHUB_TOKEN` permissions beyond `contents: write`, `pull-requests: write`, `issues: write`, `actions: read`, or any `read` permission; or runs `anthropics/claude-code-action` without `github_token`, which hands its agent the Claude GitHub App's token. Effective, not declared: apply job permissions over workflow permissions over the repository default; omitted scopes in an explicit block become `none` (rationale). Pinned by `scripts/workflow-lint.mjs`. - **FAIL IF** `S64` `default_workflow_permissions` for this repository is not `read`, or `can_approve_pull_request_reviews` is not `false` (`gh api repos/diffplug/dormouse/actions/permissions/workflow`) — the backstop for every permission bullet in this spec (rationale). Pinned by `scripts/github-state-check.mjs`. Source of truth: `packageRules` in `.github/renovate.json`; `.github/workflows/workflow-audit.yaml`. diff --git a/scripts/spec-word-budgets.json b/scripts/spec-word-budgets.json index 846c0ba1d..fa95dd77a 100644 --- a/scripts/spec-word-budgets.json +++ b/scripts/spec-word-budgets.json @@ -25,7 +25,7 @@ "docs/specs/remote-security-model.md": 5700, "docs/specs/reopen.md": 1200, "docs/specs/security-audit.md": 5400, - "docs/specs/security-ci.md": 3450, + "docs/specs/security-ci.md": 3400, "docs/specs/security-hosted.md": 3150, "docs/specs/security-local.md": 6300, "docs/specs/security-remote.md": 7200, diff --git a/scripts/workflow-lint.mjs b/scripts/workflow-lint.mjs index 7d505eb4c..8602ca9d0 100644 --- a/scripts/workflow-lint.mjs +++ b/scripts/workflow-lint.mjs @@ -127,10 +127,7 @@ export function check(inputs) { const declared = job?.permissions !== undefined ? job.permissions : doc.permissions; const held = declared === undefined ? [] : writes(declared); if (agent) { - // The CI failure reporter runs no agent and reads no secret; its - // `actions: write` dispatches `tend-triage.yaml` for a failed run. - const allowed = file === 'tend-ci-failures.yaml' && jobId === 'report' ? [...AGENT_WRITES, 'actions'] : AGENT_WRITES; - const extra = held.filter((s) => !allowed.includes(s)); + const extra = held.filter((s) => !AGENT_WRITES.includes(s)); if (extra.length) fail(TEND, `${file} job \`${jobId}\` holds write on ${extra.join(', ')}`); } else { const allowed = file === 'release.yml' && jobId === 'security-audit' ? [...RELEASE_WRITES, 'actions'] @@ -193,18 +190,6 @@ export function check(inputs) { if (!ephemeral) fail(VSCODE, 'release.yml `build-standalone` no longer generates an ephemeral Tauri updater key'); } - // tend-ci-failures.yaml `report`: its `actions: write` exception holds only while the job runs - // no action but uv's setup, reads no secret, and dispatches `tend-triage.yaml`. - const report = docs['tend-ci-failures.yaml']?.jobs?.report; - if (report) { - const actions = (report.steps ?? []).filter((s) => s.uses && !s.uses.startsWith('astral-sh/setup-uv@')); - if (actions.length) fail(TEND, `tend-ci-failures.yaml \`report\` runs \`${actions.map((s) => s.uses).join('`, `')}\` with its \`actions: write\``); - const secrets = secretsOf(docs['tend-ci-failures.yaml'], report); - if (secrets.size) fail(TEND, `tend-ci-failures.yaml \`report\` reads ${[...secrets].map((s) => `\`${s}\``).join(', ')}`); - const targets = (report.steps ?? []).filter((s) => s.env?.TRIAGE_WORKFLOW !== undefined).map((s) => s.env.TRIAGE_WORKFLOW); - if (targets.some((t) => t !== 'tend-triage.yaml')) fail(TEND, `tend-ci-failures.yaml \`report\` dispatches \`${targets.join('`, `')}\`, not \`tend-triage.yaml\``); - } - // hosted-production.yml `tag`: the minted token is the only credential, scoped to contents on this repository. const tag = docs['hosted-production.yml']?.jobs?.tag; const mint = tag?.steps?.find((s) => s.uses?.startsWith('actions/create-github-app-token@')); diff --git a/scripts/workflow-lint.test.mjs b/scripts/workflow-lint.test.mjs index e6bf0407a..79d46fbc7 100644 --- a/scripts/workflow-lint.test.mjs +++ b/scripts/workflow-lint.test.mjs @@ -42,11 +42,6 @@ const cases = [ ['issues: write on another job of the red-team issue workflow', edit('red-team-issue.yaml', ' open:\n', ' other:\n'), 'red-team-issue.yaml job `other` holds write on issues'], ['issues: write on the release red-team note', edit('release.yml', ' contents: read\n issues: read', ' contents: read\n issues: write'), 'release.yml job `red-team-note` holds write on issues'], ['an agent-managed job with actions: write', edit('tend-review.yaml', ' actions: read\n', ' actions: write\n'), 'tend-review.yaml job `review` holds write on actions'], - ['actions: write on another job of the CI failure reporter', edit('tend-ci-failures.yaml', ' report:\n', ' other:\n'), 'tend-ci-failures.yaml job `other` holds write on actions'], - ['the CI failure reporter checks out', edit('tend-ci-failures.yaml', ' steps:\n', ` steps:\n - uses: ${CHECKOUT}\n`), 'tend-ci-failures.yaml `report` runs `actions/checkout@'], - ['the CI failure reporter reads a secret', edit('tend-ci-failures.yaml', 'GH_TOKEN: ${{ github.token }}', 'GH_TOKEN: ${{ secrets.TEND_BOT_TOKEN }}'), 'tend-ci-failures.yaml `report` reads `TEND_BOT_TOKEN`'], - ['the CI failure reporter dispatches another workflow', edit('tend-ci-failures.yaml', 'TRIAGE_WORKFLOW: "tend-triage.yaml"', 'TRIAGE_WORKFLOW: "release.yml"'), 'dispatches `release.yml`, not `tend-triage.yaml`'], - ['the CI failure reporter writes checks', edit('tend-ci-failures.yaml', ' actions: write\n issues: write', ' actions: write\n checks: write\n issues: write'), 'tend-ci-failures.yaml job `report` holds write on checks'], ['an agent-managed job with checks: write', edit('security-audit.yaml', ' actions: read\n issues: write', ' actions: read\n checks: write\n issues: write'), 'security-audit.yaml job `audit` holds write on checks'], ['an agent-managed job with id-token: write', edit('security-audit.yaml', ' actions: read\n issues: write', ' actions: read\n issues: write\n id-token: write'), 'security-audit.yaml job `audit` holds write on id-token'], ['the audit agent on the Claude App token', edit('security-audit.yaml', ' github_token: ${{ github.token }}\n', ''), 'job `audit` runs `anthropics/claude-code-action` without `github_token`'],