Scheduled AI triage of GitHub issues. A reusable workflow prepares an offline issue packet, runs an agent to propose labels, priority and type, then validates and applies those proposals on a separate runner. Before/after snapshots record observed label changes; session logs remain separate from the trusted report.
The weekday schedule runs live, applying validated labels, Priority and Type. Manual dispatch and reusable-workflow defaults remain dry-run.
Copilot is the active validation target. Claude and Gemini remain selectable but unverified in the three-job layout.
https://lfreleng-actions.github.io/github-issues-triage/
Credential setup and validation steps live in
docs/setup/. The
design document covers the
architecture, containment model, and rollout plan in full.
prepare (trusted) -> propose (untrusted) -> apply (trusted)
snapshot + packet offline proposal verify + check
SHA + ID + digests ---------------------> write + report
Each job uses a separate runner:
- Prepare holds the App key, mints a read token, captures the
snapshot and resolves exclusions. It builds
issue-packet.jsonwith issue titles, bodies, existing labels, types and priorities, plus each repository's label vocabulary. - Propose reads the packet instead of querying GitHub issues.
It receives no App key or installation token; its job-native
GITHUB_TOKENgrantscontents: readand no other permissions. The session skips whenskip_agentis true, or when no unlabelled issues exist andretriageis false. - Apply gets the assets commit SHA, evidence artefact ID and
snapshot/exclusion digests directly from Prepare. It checks out
that commit, downloads evidence by ID and verifies its bytes
before minting an App token. It extracts the session into a
separate directory and copies a bounded, regular
session-summary.mdfor proposal validation, excluding other files. - Writes require a successful proposal job and accepted summary. After evidence verification, Apply can still report a skipped or failed proposal job. Cancellation gates apply and reporting; the report requires snapshot-step success before using after-state.
Prepared evidence and session artefacts have 7-day retention; final results have 90-day retention. Raw session logs and the prompt stay in the session artefact, not the trusted report bundle. Producer artefact IDs permit rerunning Apply without its producers while their artefacts remain available. A run/attempt/UUID namespace avoids collisions between reusable invocations and matrix calls; result names add the current attempt. See Design §13.7.
Workflow-level concurrency serializes the whole pipeline per caller repository and target owner. GitHub may supersede pending runs; this is not a FIFO queue. Different caller repositories targeting the same organisation need external coordination.
Other organisations can call the pipeline with their own credentials. Public user-owned targets require dry-run:
jobs:
triage:
permissions:
# Snapshot reads; label writes use the App token instead
issues: read
contents: read
# Pin to an immutable release commit SHA; the tag rides along
# as a comment. Never reference a mutable branch here: the
# trusted jobs receive your App private key.
# yamllint disable-line rule:line-length
uses: lfreleng-actions/github-issues-triage/.github/workflows/issues-triage.yaml@<commit-sha> # vX.Y.Z
with:
org: 'your-org'
engine: 'copilot'
dry_run: true
github_app_client_id: ${{ vars.YOUR_APP_CLIENT_ID }}
secrets:
copilot_token: ${{ secrets.COPILOT_CLI_TOKEN }}
github_app_private_key: ${{ secrets.YOUR_APP_PRIVATE_KEY }}The assets checkout defaults to the called workflow's commit.
Prepare resolves any trusted assets_ref override once and pins
both downstream checkouts to the resulting SHA.
Copilot requires a personal fine-grained PAT with the
Copilot Requests account permission and no repository
permissions. Store it as COPILOT_CLI_TOKEN. Caller-native
GITHUB_TOKEN authentication is no longer supported; do not pass
that token as copilot_token or grant copilot-requests.
jobs:
triage:
permissions:
issues: read
contents: read
# yamllint disable-line rule:line-length
uses: lfreleng-actions/github-issues-triage/.github/workflows/issues-triage.yaml@<commit-sha> # vX.Y.Z
with:
org: 'your-org'
engine: 'copilot'
dry_run: true
secrets:
copilot_token: ${{ secrets.COPILOT_CLI_TOKEN }}The workflow checks for the github_pat_ prefix. That check
neither validates the token remotely nor proves it lacks extra
permissions. The caller must provision and review its grants.
See Copilot setup for expiry, entitlement
and validation requirements.
Without App credentials, trusted jobs use their job-native token
for dry-run reads within its access. Live runs require an
organisation App with repository issues: write, metadata: read
and organisation issue_fields: read, issue_types: read.
Single-repository runs scope the installation token at mint time;
Propose never receives it.
| Input | Default | Purpose |
|---|---|---|
org |
(required) | GitHub organisation or user to triage |
engine |
claude |
Agent engine: claude, gemini, or copilot |
model |
engine default | claude-opus-5 / gemini-3.5-flash-lite / claude-sonnet-5 |
dry_run |
true |
Report intended labels; apply nothing |
retriage |
false |
Re-examine issues that carry labels |
skip_agent |
false |
Plumbing test: skip the agent session |
repository |
'' |
Restrict the scan to one repository |
exclude_repos |
'' |
Comma-separated repositories to skip |
max_turns |
80 |
Claude turn ceiling; Copilot and current Gemini use the step timeout |
egress_policy |
audit |
harden-runner mode (audit/block) |
egress_allow_config |
'' |
harden-runner-block-action config coordinate |
github_app_client_id |
'' |
App auth; empty limits runs to dry-run |
assets_repository |
this repo | Source of the prompt and scripts |
assets_ref |
called workflow's commit | Ref of assets_repository to fetch |
| Secret | Required | Purpose |
|---|---|---|
anthropic_api_key |
claude runs, unless skip_agent |
Anthropic API authentication |
gemini_api_key |
gemini runs, unless skip_agent |
Gemini API (AI Studio) authentication |
copilot_token |
copilot sessions | Fine-grained PAT (github_pat_) for Copilot Requests; no repository permissions |
github_app_private_key |
no | Pairs with github_app_client_id |
| Workflow | Purpose | Trigger |
|---|---|---|
issues-triage.yaml |
The reusable pipeline | workflow_call |
issues-triage-cron.yaml |
Org triage caller | 07:00 UTC weekdays / dispatch |
testing.yaml |
Secretless plumbing test / manual dry-run | Pull request / dispatch |
documentation.yaml |
Build and publish the docs site | Push to main / dispatch |
release.yaml |
Promote draft release on tag push | Tag push |
- Dry-run for manual and reusable defaults: callers can check proposals without writing; the weekday schedule opts into live mode. The App token has issue-read access during dry-run or reporting without application; issue-write access requires live mode and proposal success.
- App credentials stay in trusted jobs: neither an App-token action nor its private-key-bearing post action runs in Propose. Job separation alone does not protect artefacts: Apply verifies evidence by producer ID and Prepare's digests, not by name or hashes supplied by the session.
- Live checks before writes: the applier checks scope, normalized
exclusions, snapshot membership, current issue state and labels,
repository label vocabulary, and organisation field/type options.
An existing Priority takes the whole issue out of scope, even in
retriage mode.
Urgentbelongs to humans. - Configuration failures are visible: live organisation configuration read failures are fatal. Dry-run alone permits known endpoint absence or permission denial; outages, rate limits and unknown errors still fail. A search reaching 1,000 results fails before exclusions can hide truncation; restrict the scan.
- Bounded processing: at most 100 eligible issues per packet and 100 entries per proposal. Larger batches fail before detail reads or writes rather than sampling. Narrow the repository or exclusions; this processing cap is separate from the search limit. Helper commands have a 30-second timeout.
- No transaction or race guarantee: writes can succeed in part,
and humans or other callers can change issues after validation.
Recovery needs inspection and targeted action, often
retriage: trueafter a partial label write. Never overwrite a human Priority to force replay; the workflow has no automatic rollback. - Untrusted sessions remain a risk: tool approval rules and secret redaction are not a sandbox. The session may expose issue text, packet contents, model credentials and runtime credentials. Review PAT grants, artefact access and model data handling; a wrong label is not the worst possible outcome.
- Egress control: each runner uses harden-runner in audit or block mode. Audit records traffic; it does not block it.
Run the offline tests and linting suite:
uv run python -B -m unittest discover -s tests -v
prek run --all-filesThe offline suite covers policy, GitHub adapters, evidence, snapshots, report rendering and workflow contracts; workflow tests use the PyYAML development dependency. The three-job Copilot dry-run, both secretless PR invocations and the first live run all passed; see Design §11 for run evidence and remaining gaps. Claude and Gemini are outside active validation.
Build and preview the documentation site locally:
uv venv --python 3.13
uv pip install --require-hashes --requirement docs/requirements.txt
.venv/bin/mkdocs serveThe report generator has a strict type-checking gate
(basedpyright, see pyproject.toml) and the prompt document
passes the same prose linting as the rest of the repository.