mirror of
https://github.com/dotnet/skills.git
synced 2026-09-20 09:49:54 +08:00
0c0f6f0263
The triage worker added the 'evaluate-now' label via GITHUB_TOKEN, but label events emitted by GITHUB_TOKEN do not start workflows (GitHub's recursion guard), so evaluation.yml's pull_request_target:[labeled] entry point never fired for the bot (repro: PR #745). workflow_dispatch and repository_dispatch are the only token-initiated events exempt from that guard. The worker now dispatches evaluation.yml directly via 'gh workflow run' with a pr_number input, routed through the existing gate job so the path is identical to /evaluate. A dispatched run's head_sha is the default branch (not the PR head), so idempotency now matches the deterministic run name 'Evaluate PR #<n> @ <sha7>'. The 'evaluate-now' label remains a valid human entry point. Worker granted actions:write for 'gh workflow run'.
95 lines
4.5 KiB
Markdown
95 lines
4.5 KiB
Markdown
# PR Triage Workflows
|
|
|
|
Three GitHub Actions workflows keep open PRs moving without manual nudging:
|
|
|
|
- **`pr-triage-batch.yml`** — hourly orchestrator (cron `17 * * * *`). Enumerates
|
|
open non-draft PRs, computes a deterministic state for each, and dispatches the
|
|
per-PR worker (or the malicious-code scanner). No comments, no labels, no model
|
|
calls.
|
|
- **`pr-triage.yml`** — per-PR worker (`workflow_dispatch`). Re-validates the
|
|
PR's state, reconciles a single `pr-state/*` label, and performs at most one
|
|
of: trigger evaluation (by dispatching `evaluation.yml`), ping the author, or
|
|
ping maintainers. Cool-down (default 4 days) is enforced via marker comments.
|
|
- **`pr-malicious-scan.agent.md`** — per-PR malicious-code scanner (gh-aw).
|
|
Static diff review for untrusted contributors. Reports findings as
|
|
code-scanning alerts and an optional comment; never executes PR head code.
|
|
|
|
## Architecture
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
Cron["cron: every hour"] --> Batch["pr-triage-batch.yml<br/>(orchestrator)"]
|
|
Batch -->|workflow_dispatch| Worker["pr-triage.yml<br/>(per-PR worker)"]
|
|
Batch -->|workflow_dispatch| Scan["pr-malicious-scan.agent.lock.yml<br/>(per-PR scanner)"]
|
|
Worker -->|workflow_dispatch: pr_number| Eval["evaluation.yml<br/>(existing)"]
|
|
Worker -->|adds pr-state/* label| PR[("PR")]
|
|
Worker -->|posts ping comment| PR
|
|
Scan -->|code-scanning alert + comment| PR
|
|
PR -.->|human adds label: evaluate-now| Eval
|
|
```
|
|
|
|
## Entry points into `evaluation.yml`
|
|
|
|
Three entry points feed the `gate` job, all sharing a per-PR concurrency group
|
|
so a race collapses to a single run:
|
|
|
|
1. The existing **`/evaluate`** slash command (`issue_comment`) — humans.
|
|
2. The **`evaluate-now`** label (`pull_request_target [labeled]`) — humans. The
|
|
`gate` job consumes (removes) the label so reapplying re-fires.
|
|
3. **`workflow_dispatch`** with a `pr_number` input — the triage worker. The
|
|
worker runs as `github-actions[bot]`, and label events emitted by
|
|
`GITHUB_TOKEN` do **not** start workflows (GitHub's recursion guard), so the
|
|
bot cannot use entry point 2. `workflow_dispatch` is exempt from that guard,
|
|
so the worker dispatches `evaluation.yml` directly. A dispatched run's
|
|
`head_sha` is the default branch (not the PR head), so the worker matches the
|
|
run by `evaluation.yml`'s run name (`Evaluate PR #<n> @ <sha7>`) for idempotency.
|
|
|
|
## State machine (worker)
|
|
|
|
Order of evaluation; first match wins:
|
|
|
|
| Order | Condition | State | Label | Action |
|
|
|---|---|---|---|---|
|
|
| 1 | draft, or `mergeable_state == unknown` | `skip` | — | none |
|
|
| 2 | non-bot && non-trusted && no malicious-scan marker on head | `needs-malicious-scan` | — | dispatch scanner |
|
|
| 3 | `CHANGES_REQUESTED` \|\| unresolved threads > 0 \|\| `mergeable_state == dirty` | `needs-author-attention` | `waiting-on-author` | author-ping |
|
|
| 4 | eval == success && `APPROVED` | `ready-for-merge` | `ready-to-merge` | maintainer-ping/C |
|
|
| 5 | eval == success && `REVIEW_REQUIRED`/none | `ready-for-review` | `waiting-on-review` | maintainer-ping/A |
|
|
| 6 | eval == success && other decision | `in-review` | `pr-state/in-review` | reconcile only |
|
|
| 7 | otherwise | `ready-for-eval` | `pr-state/ready-for-eval` | eval-trigger |
|
|
|
|
Trusted = `OWNER` / `MEMBER` / `COLLABORATOR`. Bots are short-circuited as trusted.
|
|
|
|
## Cool-down and idempotency
|
|
|
|
Each ping variant writes a hidden HTML marker into its comment. The worker
|
|
fetches prior bot comments and:
|
|
|
|
- If a marker for the same variant exists within `COOLDOWN_DAYS` (default 4),
|
|
the new comment is suppressed.
|
|
- A first-ping age gate (default 30 min after PR creation) prevents pings on
|
|
freshly opened PRs; it is bypassed once any prior ping marker exists.
|
|
|
|
Marker shapes:
|
|
|
|
- `<!-- pr-triage:fingerprint=author-ping:{sha7}:{yyyy-mm-dd} -->`
|
|
- `<!-- pr-triage:fingerprint=maintainer-ping/{A,B,C}:{sha7}:{yyyy-mm-dd} -->`
|
|
- `<!-- pr-malicious-scan:fingerprint={sha7}:{yyyy-mm-dd} -->`
|
|
|
|
## Labels owned by these workflows
|
|
|
|
State labels (exactly one is reconciled at a time). Where the existing label
|
|
taxonomy already covered a state, the workflow reuses it rather than introducing
|
|
a duplicate `pr-state/*` name:
|
|
|
|
- `pr-state/ready-for-eval` *(new)*
|
|
- `waiting-on-review` *(existing — reused for `ready-for-review`)*
|
|
- `ready-to-merge` *(existing — reused for `ready-for-merge`)*
|
|
- `waiting-on-author` *(existing — reused for `needs-author-attention`)*
|
|
- `pr-state/in-review` *(new)*
|
|
|
|
Triggers and opt-outs:
|
|
|
|
- `evaluate-now` — applied to fire evaluation; removed by the gate after consumption.
|
|
- `no-stale` — opt-out of stale-PR closure (consumed by `close-stale-prs`).
|