Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 26 additions & 60 deletions .github/workflows/tend-ci-failures.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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"]
Expand Down Expand Up @@ -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
Expand All @@ -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'
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tend-mention-relay.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-mention.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-nightly.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/tend-notifications.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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 }}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-review-runs.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-review.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 7 additions & 18 deletions .github/workflows/tend-triage.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/tend-weekly.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/security-ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion scripts/spec-word-budgets.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading