Skip to content

Repository files navigation

🏷️ GitHub Issues Triage

Linux FoundationSource CodeLicenseOpenSSF Scorecard

Scheduled AI triage of GitHub issues. A reusable workflow scans an organisation's open issues, runs an agent session — Claude Code, the Gemini CLI, or the GitHub Copilot CLI, selected per run — that applies category labels per a versioned policy prompt, and attaches full run evidence to the workflow run: before/after snapshots, engine-specific session evidence (Claude: a turn-by-turn transcript; Gemini: telemetry plus the session summary, with transcript fidelity under verification — design doc §12; Copilot: CLI logs plus the shared session transcript), and a diff-based report.

📚 Documentation

https://lfreleng-actions.github.io/github-issues-triage/

Per-engine setup — credentials, permissions, and the checks that prove them — lives in docs/setup/. The design document covers the architecture, containment model, and rollout plan in full.

How it works

snapshot (before) -> agent session -> snapshot (after)
-> diff -> report + summary
  1. Capture the organisation's open-issue state as JSON
  2. Skip the agent session when zero unlabelled issues exist
  3. Run the selected engine with read/label gh verbs and a policy prompt (prompt/triage.md)
  4. Capture the state again, diff, and report observed label movement — never the agent's own claims — to the step summary
  5. Upload the artefact bundle (snapshots, transcript, prompt, report) with 90-day retention, even when the session fails

Consuming the reusable workflow

Other organisations and users can call the pipeline directly, supplying their own tokens:

jobs:
triage:
permissions:
# Snapshot reads; label writes use the App token insteadissues: readcontents: read# Pin to an immutable release commit SHA; the tag rides along# as a comment. Never reference a mutable branch here: the# workflow receives your API key and an issues:write token.# yamllint disable-line rule:line-lengthuses: lfreleng-actions/github-issues-triage/.github/workflows/issues-triage.yaml@<commit-sha> # vX.Y.Zwith:
org: 'your-org'dry_run: truegithub_app_client_id: ${{ vars.YOUR_APP_CLIENT_ID }}secrets:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}github_app_private_key: ${{ secrets.YOUR_APP_PRIVATE_KEY }}

Pinning the workflow pins its scripts and prompt too: the assets checkout defaults to the called workflow's own commit.

Using the Copilot engine

Warning

The Copilot engine holds a weaker containment boundary than the other two — see the safety model below and design doc §13.2. It never applies labels: pairing it with dry_run: false fails the run before the session starts.

The copilot engine needs no model API key. Grant the calling job copilot-requests: write and hand its GITHUB_TOKEN to the pipeline; usage meters to the organisation, gated on the "Allow use of Copilot CLI billed to the organization" policy.

jobs:
triage:
permissions:
issues: readcontents: read# Authorises Copilot model requests; carries no repository# access of its own.copilot-requests: write# yamllint disable-line rule:line-lengthuses: lfreleng-actions/github-issues-triage/.github/workflows/issues-triage.yaml@<commit-sha> # vX.Y.Zwith:
org: 'your-org'engine: 'copilot'dry_run: truesecrets:
copilot_token: ${{ secrets.GITHUB_TOKEN }}

Where that policy is unavailable, pass a fine-grained PAT holding the "Copilot Requests" permission as copilot_token instead and drop the copilot-requests grant. Both routes, with their trade-offs, are in docs/setup/GITHUB.md.

Labels travel over a repository-access token, and the Copilot credential is never that same token. What else the model credential can reach depends on the route. A PAT scoped to Copilot Requests reaches nothing but the model. The caller GITHUB_TOKEN carries whatever the calling job grants — issues: read and contents: read in the example above — so grant that job reads alone, or the model credential becomes write-capable too.

The reusable workflow does not declare copilot-requests itself: a called workflow can narrow the caller's permissions but never widen them, so declaring it there would fail every caller that runs a different engine. Passing the token in as a secret leaves that decision with you. Design doc §13.3 covers the reasoning, and §13.5 records that no run has yet confirmed the grant survives the hand-off — if it does not, the CLI rejects the token and the step fails with the evidence bundle intact.

Without a GitHub App the pipeline cannot write: dry-run reports work with the caller's github.token (issues: read), and live runs refuse to start. Applying labels needs an App installed across the target with issues: write and metadata: read; single-repository runs scope the App token to that repository at mint time. The copilot engine takes the same App token at issues: read, which keeps its scan organisation-wide while leaving it unable to label.

Inputs

InputDefaultPurpose
org(required)GitHub organisation or user to triage
engineclaudeAgent engine: claude, gemini, or copilot (refuses live runs)
modelengine defaultclaude-opus-5 / gemini-3.5-flash-lite / claude-sonnet-5
dry_runtrueReport intended labels; apply nothing
retriagefalseRe-examine issues that carry labels
skip_agentfalsePlumbing test: skip the agent session
repository''Restrict the scan to one repository
exclude_repos''Comma-separated repositories to skip
max_turns80Agent session turn ceiling (ignored by copilot)
egress_policyauditharden-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_repositorythis repoSource of the prompt and scripts
assets_refcalled workflow's commitRef of assets_repository to fetch
SecretRequiredPurpose
anthropic_api_keyclaude runs, unless skip_agentAnthropic API authentication
gemini_api_keygemini runs, unless skip_agentGemini API (AI Studio) authentication
copilot_tokencopilot runs, unless skip_agentCopilot model requests: caller GITHUB_TOKEN or a "Copilot Requests" PAT
github_app_private_keynoPairs with github_app_client_id

Workflows in this repository

WorkflowPurposeTrigger
issues-triage.yamlThe reusable pipelineworkflow_call
issues-triage-cron.yamlOrg triage caller07:00 UTC weekdays / dispatch
testing.yamlSecretless plumbing test / manual dry-runPull request / dispatch
documentation.yamlBuild and publish the docs sitePush to main / dispatch
release.yamlPromote draft release on tag pushTag push

Safety model

  • Dry-run by default: consumers opt in to live labelling
  • Tool containment: the agent receives read verbs of gh plus, in live mode, a constrained wrapper that validates repository, issue number, and label existence before a fixed --add-label operation — no gh issue edit, no git, no arbitrary shell
  • Copilot engine caveat: that engine restricts the model to shell tools, denies the mutating gh and git verbs, turns off built-in MCP servers and custom-instruction loading, and keeps both credentials out of the agent's own environment by seeding a gh configuration directory instead. Its allow-list is an approval policy rather than a filter, though, and the CLI keeps auto-approving shell commands it treats as reads. Those commands run unsandboxed as the same user, so one can reach the configuration file, or the CLI's own environment a process up. Value-based redaction is what protects the token there, and a command that encodes the value defeats it. Writes stay shut, and the workflow refuses live runs on this engine until the enforcement in design doc §13.4 lands
  • Token scope: App tokens carry metadata: read plus issues: write, or issues: read on the copilot engine, which cannot label. Down-scoped at mint time, expiring in an hour. The model credential is never the repository-access token this pipeline mints: the Anthropic and Gemini keys carry no GitHub permissions at all, and a Copilot PAT scoped to Copilot Requests reaches nothing but the model. The Copilot route that reuses a caller GITHUB_TOKEN carries whatever that job grants, so grant such a job reads alone
  • Prompt-injection defence: the policy prompt instructs the agent to treat issue text as data; containment limits the worst case to a wrong label
  • Egress control: harden-runner in audit or block mode, with the allow-list loaded from a tagged org baseline

Development

Run the linting suite before pushing:

uvx pre-commit run --all-files

Build and preview the documentation site locally:

uv venv --python 3.13
uv pip install --require-hashes --requirement docs/requirements.txt
.venv/bin/mkdocs serve

The 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.

About

Pipeline to perform an AI triage of GitHub Issues

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages