✨ feat(ci): tier-based change classification workflow (labels only)#8251
✨ feat(ci): tier-based change classification workflow (labels only)#8251clubanderson merged 1 commit intomainfrom
Conversation
Second of four PRs from the fullsend-ai/fullsend automation
evaluation. Labels every PR with exactly one tier/* label based on
the paths touched, so reviewers can prioritize and (in a follow-up
PR) tier/0 PRs can auto-merge.
Adds:
- .github/workflows/tier-classifier.yml — pull_request_target trigger,
reads the rules file, calls pulls.listFiles, classifies, applies
the label. Uses actions/github-script@v8 — no new dependencies.
Hand-parses the rules YAML (tiny subset) to avoid pulling in a
yaml dep.
- .github/tier-classifier-rules.yml — path globs per tier. Data file
so the rules can be tweaked without editing the workflow itself.
Covers:
tier/0-automatic lockfiles, docs, *.md, i18n, snapshots
tier/1-lightweight test-only, editor config
tier/2-standard (implicit default — no patterns)
tier/3-restricted CODEOWNERS, .github/workflows/**, pkg/auth,
pkg/api/middleware, Helm RBAC, docs/security,
GoReleaser config
Classification logic: tier/3 wins if ANY file matches (a single
dangerous file pulls the whole PR up). tier/0 and tier/1 require
EVERY file to match (conservative — any unclassified file bumps
the PR to tier 2).
Also:
- CONTRIBUTING.md — new "Change Tiers" section explaining the four
labels and where the rules live.
This PR is labels-only. A follow-up will wire auto-merge for
tier/0-automatic once the rules have proven accurate on real PRs.
The label itself is auto-created on first use with a sensible color
(green for tier/0, light blue for tier/1, yellow for tier/2, red
for tier/3) so maintainers don't need to pre-seed them.
Signed-off-by: Andrew Anderson <[email protected]>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
✅ Deploy Preview for kubestellarconsole canceled.
|
|
👋 Hey @clubanderson — thanks for opening this PR!
This is an automated message. |
There was a problem hiding this comment.
Pull request overview
Adds an automated “tier-based” PR labeling system so every pull request is classified into exactly one tier/* label based on touched paths, helping reviewers triage risk and review priority.
Changes:
- Introduces a new
pull_request_targetGitHub Actions workflow that computes and applies a singletier/*label per PR. - Adds a rules data file (
.github/tier-classifier-rules.yml) defining tier glob patterns without requiring extra workflow dependencies. - Documents the tier system and classification semantics in
CONTRIBUTING.md.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
CONTRIBUTING.md |
Documents tier meanings and how classification works. |
.github/workflows/tier-classifier.yml |
Implements the classifier + label application/auto-creation logic. |
.github/tier-classifier-rules.yml |
Defines tier glob patterns consumed by the workflow. |
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const fs = require('fs'); | ||
| const path = require('path'); |
There was a problem hiding this comment.
const path = require('path'); is unused in this script. Removing it will keep the workflow script minimal and avoid suggesting there’s path manipulation happening.
| const path = require('path'); |
| // A PR is classified to the HIGHEST tier any of its files matches. | ||
| // Default (no match anywhere) = tier/2-standard. | ||
| // ----------------------------------------------------------------- | ||
| const tierOrder = [ | ||
| 'tier/3-restricted', | ||
| 'tier/2-standard', | ||
| 'tier/1-lightweight', | ||
| 'tier/0-automatic', | ||
| ]; |
There was a problem hiding this comment.
The classification header comment says the PR is classified to the “highest tier any file matches”, but the actual logic uses special “ALL files must match” rules for tier/0 and tier/1. Also tierOrder is declared but never used. Update the comment to reflect the real logic and remove tierOrder (or use it) to avoid future drift.
| // A PR is classified to the HIGHEST tier any of its files matches. | |
| // Default (no match anywhere) = tier/2-standard. | |
| // ----------------------------------------------------------------- | |
| const tierOrder = [ | |
| 'tier/3-restricted', | |
| 'tier/2-standard', | |
| 'tier/1-lightweight', | |
| 'tier/0-automatic', | |
| ]; | |
| // tier/3-restricted applies when ANY changed file matches a tier 3 | |
| // pattern. Otherwise, tier/0-automatic applies only when EVERY | |
| // changed file matches a tier 0 pattern, and tier/1-lightweight | |
| // applies only when EVERY changed file matches either a tier 0 or | |
| // tier 1 pattern. If none of those conditions are met, fall back to | |
| // tier/2-standard. | |
| // ----------------------------------------------------------------- |
| await github.rest.issues.createLabel({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| name: classified, | ||
| color: colors[classified] || 'ededed', | ||
| description: descriptions[classified] || '', | ||
| }).catch(() => {}); // Ignore if it was created by another run in parallel. |
There was a problem hiding this comment.
createLabel(...).catch(() => {}) swallows all errors, not just the expected “already exists” race. This can hide real failures (e.g., insufficient permissions, invalid color/description) and makes troubleshooting hard. Consider only ignoring the specific 422/"already exists" case (or checking e.status) and logging/rethrowing unexpected errors.
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: classified, | |
| color: colors[classified] || 'ededed', | |
| description: descriptions[classified] || '', | |
| }).catch(() => {}); // Ignore if it was created by another run in parallel. | |
| try { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: classified, | |
| color: colors[classified] || 'ededed', | |
| description: descriptions[classified] || '', | |
| }); | |
| } catch (createLabelError) { | |
| if (createLabelError.status === 422) { | |
| core.info(`Label ${classified} already exists; continuing.`); | |
| } else { | |
| core.warning(`createLabel failed: ${createLabelError.message}`); | |
| throw createLabelError; | |
| } | |
| } |
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: write |
There was a problem hiding this comment.
Workflow uses github.rest.issues.addLabels/removeLabel/createLabel, which requires issues: write permission. With only pull-requests: write, these API calls can fail with 403, preventing tier labels from being applied. Add issues: write (and consider reducing pull-requests to read since this job only reads PR files and writes labels).
| pull-requests: write | |
| pull-requests: read | |
| issues: write |
| jobs: | ||
| classify: | ||
| runs-on: ubuntu-latest | ||
| if: github.event.pull_request.head.repo.full_name != '' |
There was a problem hiding this comment.
The if: github.event.pull_request.head.repo.full_name != '' condition is always true for real PR events, so it adds confusion without changing behavior. Consider removing it, or replace it with a concrete guard (e.g., skip drafts/bots) if that was the intent.
| if: github.event.pull_request.head.repo.full_name != '' |
|
Thank you for your contribution! Your PR has been merged. Check out what's new:
Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey |
|
Post-merge build verification passed ✅ Both Go and frontend builds compiled successfully against merge commit |
Summary
Second of four PRs from the fullsend-ai/fullsend automation evaluation. Adds a
tier-classifierworkflow that labels every PR with exactly onetier/*label based on the file paths touched, so reviewers can prioritize and (in a follow-up PR) tier/0 PRs can auto-merge.Adapted from fullsend-ai/fullsend's tier-based change classification pattern — graduated autonomy where safe changes (dep bumps, docs, generated files) can fast-track and risky changes (workflows, auth, RBAC) require multi-maintainer sign-off.
What lands here
.github/workflows/tier-classifier.yml—pull_request_targettrigger, reads the rules file, callspulls.listFiles, classifies, applies the label. Usesactions/github-script@v8— no new dependencies. Hand-parses the rules YAML (tiny subset — three top-level keys with lists) to avoid pulling in a yaml dep..github/tier-classifier-rules.yml— path globs per tier as a data file, so rules can be tweaked without editing the workflow.CONTRIBUTING.md— new## Change Tierssection explaining what each label means and where the rules live.The four tiers
tier/0-automaticpackage-lock.json,go.sum,go.mod,docs/**,README.md,**/*.md,web/src/locales/**/*.json, snapshotstier/1-lightweight**/__tests__/**,**/*.test.ts[x],**/*.spec.ts[x],**/*_test.go,web/e2e/**, editor configtier/2-standardtier/3-restrictedCODEOWNERS,.github/workflows/**,.github/actions/**,pkg/auth/**,pkg/api/middleware/**,pkg/api/handlers/auth*, Helm RBAC/ClusterRole/values,docs/security/**,.goreleaser*Classification logic
Label auto-creation
The labels are created on first use by the workflow itself (green for tier/0, light blue for tier/1, yellow for tier/2, red for tier/3) so maintainers don't need to pre-seed them.
Scope: labels only, no enforcement yet
This PR only labels. A follow-up PR will wire auto-merge for
tier/0-automaticonce the rules have proven accurate on real PRs for a week. Rolling it out blind is risky — wait for empirical validation of the allowlist.Test plan
npm run build— passespython3 -c "import yaml; yaml.safe_load(...)"— both YAML files parsetier/0-automaticlabel appliedtier/1-lightweightlabel appliedweb/src/components/cards/...→tier/2-standardlabel applied.github/workflows/*.yml→tier/3-restrictedlabel appliedtier/3-restrictedwins🤖 Generated with Claude Code