pr-size-labeler is a fully open-source GitHub App service that applies size/* labels to pull requests based on effective code change size.
👉 Install via GitHub App directly 👉 Install via GitHub marketplace
It is inspired by noqcks/pull-request-size, but built around a transparent workflow, free use, and easy setup for both public repositories and private self-hosted setups.
This project is intentionally transparent:
- no billing
- no marketplace plan logic
- no hidden hosted-app behavior
- reproducible Go 1.26 binary builds attached to GitHub Releases
- Standardized behavior across repositories: one app deployment can apply the same size-label logic everywhere instead of copying and maintaining separate workflow files per repo.
- No-code repository onboarding: maintainers install the GitHub App and optionally tune
.github/labels.ymlinstead of authoring and debugging Actions YAML. - Instant updates: fixes and improvements ship once in the app deployment, so you do not need to wait for every repository to update its workflow.
- Proactive relabeling: optionally relabel existing open pull requests on app install or after merged
.github/labels.ymlchanges instead of waiting for each PR to receive a new event. - Startup recovery for missed failed deliveries: optionally recover recent failed webhook deliveries after downtime or restarts so missed labeling events are less likely to stay missed.
- No GitHub Actions quota usage: labeling runs in the app service, not in GitHub-hosted runners, so it does not consume Actions minutes or compete with CI jobs.
Live repo
For normal pull request labeling, pr-size-labeler:
- loads
.github/labels.ymlfrom the repository default branch when present, otherwise uses the built-in default label set - reads pull request file additions/deletions and patch hunks
- computes effective changed lines and effective changed symbols
- subtracts files matched by
.gitattributesentries markedlinguist-generated=true - chooses the largest eligible size label when either that label's
linesthreshold or itssymbolsthreshold is met - removes older configured size labels
- creates the selected repository label if needed
- applies exactly one configured size label
- optionally adds one configured comment
Normal labeling runs for:
pull_request.openedpull_request.reopenedpull_request.synchronizepull_request.edited
Proactive relabeling runs only when .github/labels.yml explicitly enables it:
- on repository connect (
installation.created,installation_repositories.added) - on merged
pull_request.closedevents into the default branch when that PR changed.github/labels.yml
Config behavior:
- if
.github/labels.ymlis missing, normal PR labeling uses built-in defaults and backfill stays disabled - if
.github/labels.ymlcontains unknown keys, they are ignored and the triggering PR gets a warning comment - if
.github/labels.ymlhas invalid values, the triggering PR gets a warning comment and that run skips label changes - if the selected repository label does not exist yet, the app creates it
The thresholds intentionally follow the same sizing model as the original pull-request-size project, but the default colors are slightly different.
| Key | Label | Lines | Color |
|---|---|---|---|
| XS | size/XS |
0 | 2FBF6B |
| S | size/S |
10 | 55A84B |
| M | size/M |
30 | 7A9135 |
| L | size/L |
100 | 9F6A27 |
| XL | size/XL |
500 | C44319 |
| XXL | size/XXL |
1000 | E91C0B |
pr-size-labeler uses the same basic idea as the reference project: patterns marked linguist-generated=true are excluded from size totals.
Example:
generated/* linguist-generated=true
vendor/** linguist-generated=trueIn this implementation, .gitattributes is read from the pull request base branch.
labels.yml is now the single repository-owned source of truth for both label selection and optional proactive relabeling.
Top-level keys:
backfill(optional): proactive relabel settings; if omitted, backfill defaults to disabledbackfill.enabled(optional): controls whether install-time and merged-config relabeling run; defaultfalsebackfill.lookback(optional): proactive relabel age window; default720h(30 days)labels(optional): override map for size keys (XS,S,M,L,XL,XXL); if omitted, the built-in label set is used unchanged
Each label supports:
namelinessymbols(optional)comment(optional)color(optional; used when the app creates a missing repository label)
If symbols is omitted, it defaults to lines * 100.
You only need to include the sections and keys you want to override.
Unknown keys are ignored. On pull_request-driven runs, the app leaves a warning comment on the triggering PR so the config problem is visible without checking server logs.
Example:
backfill:
enabled: true
lookback: 168h
labels:
XS:
name: size/XS
lines: 0
symbols: 0
S:
name: size/S
lines: 10
symbols: 800
color: 55A84B
comment: |
This PR is still in the small range.
M:
name: size/M
lines: 30
L:
name: size/L
lines: 100
symbols: 10000
XL:
name: size/XL
lines: 500
XXL:
name: size/XXL
lines: 1000
comment: |
This PR is very large. Consider splitting it up.labels.yml is always read from the repository default branch.
That is different from .gitattributes, which is still read from the pull request base branch.
For selection, a label is eligible when either its lines threshold or its symbols threshold is met.
Effective changed lines are counted as additions + deletions, matching GitHub diff totals. That means one removed line counts as one changed line, and one modified line counts as two changed lines.
If you enable backfill, lookback is a pull request age window. The app relabels only open pull requests whose created_at is inside that window.
Duration format note:
- use standard Go duration syntax with
h,m, andsunits - valid examples:
100h,72h30m,720h - invalid examples:
1y,30d
Use the checked-in app.yml manifest or configure the app manually with:
- events:
pull_requestinstallationinstallation_repositories
- permissions:
pull_requests: writemetadata: readsingle_file: read
- single-file paths:
.gitattributes.github/labels.yml
app.yml is only a transparent reference. GitHub App settings are still applied and updated manually in the GitHub UI.
More detail: docs/github-app.md
Copy .env.example and set:
APP_IDPRIVATE_KEYWEBHOOK_SECRET- optional
LISTEN_ADDR - optional
GITHUB_API_BASE_URL - optional
LOG_PRIVATE_DETAILS - optional
STARTUP_FAILED_DELIVERY_RECOVERY_ENABLED - optional
STARTUP_FAILED_DELIVERY_RECOVERY_LOOKBACK
See docs/github-app.md for where each value comes from.
Optionally add .github/labels.yml on the default branch if you want custom thresholds, custom names/comments/colors, or proactive backfill. If the file is absent, normal pull request labeling still works with the built-in defaults.
If you deploy on Hugging Face Spaces, rebuilds and restarts can make the webhook endpoint temporarily unavailable. pr-size-labeler can optionally try to recover from that on process startup by listing recent failed GitHub App webhook deliveries and asking GitHub to redeliver them.
For this repository's default GitHub Actions → Hugging Face deployment, startup recovery is enabled automatically. The code-level default is still false, but the deploy workflow sets STARTUP_FAILED_DELIVERY_RECOVERY_ENABLED=true for the Space.
Environment variables:
LOG_PRIVATE_DETAILS— set totrueto include request IP/header details, installation IDs, and detailed startup/startup-recovery diagnostics in logs; defaultfalseSTARTUP_FAILED_DELIVERY_RECOVERY_ENABLED— set totrueto enable startup recoverySTARTUP_FAILED_DELIVERY_RECOVERY_LOOKBACK— Go duration string for how far back to inspect deliveries, default2h
Example:
STARTUP_FAILED_DELIVERY_RECOVERY_ENABLED=true
STARTUP_FAILED_DELIVERY_RECOVERY_LOOKBACK=2hBehavior notes:
- this runs once on startup, not on a schedule
- the HTTP server starts first, then recovery runs in the background so redeliveries can reach a live process
- it only looks at deliveries inside the configured lookback window
- it only redelivers deliveries whose GitHub delivery
statusis notOK - repeated restarts inside the same lookback window can cause the same failed original delivery to be redelivered again
- GitHub only allows webhook redelivery for recent deliveries, so this is a best-effort recovery tool, not a durable queue
When backfill.enabled: true, the app proactively relabels already-open pull requests:
- when repositories are connected to the app
- when a merged PR updates
.github/labels.ymlon the default branch
Behavior notes:
- only open pull requests are relabeled
- only pull requests created inside
backfill.lookbackare relabeled - direct pushes do not trigger relabeling
- merged PR relabeling runs only when the merged PR touched exact path
.github/labels.yml - if
.github/labels.ymlalready exists on the default branch before installation, the installation event uses that existing file immediately - normal pull request labeling does not require
backfill.enabled
How to backfill existing open pull requests:
- Add or update
.github/labels.ymlon the default branch so backfill is enabled. Minimal example:
backfill:
enabled: true
lookback: 720h- If the app is not installed yet, make sure that config already exists on the default branch and then install the app. The installation event will use that pre-existing file and relabel open pull requests inside the lookback window.
- If the app is already installed, merge a pull request that changes
.github/labels.ymlon the default branch withbackfill.enabled: true. That merge event will relabel open pull requests inside the lookback window. - The app will create any missing size labels as it applies them.
- If you only wanted a one-time backfill, merge a follow-up config change that sets
backfill.enabled: falseagain after the relabel run you wanted has already happened.
go run ./cmd/pr-size-labelergo test ./...mkdir -p dist && go build -o dist/pr-size-labeler ./cmd/pr-size-labelerDeployment guidance lives in docs/deployment.md.
That includes:
- the default public Hugging Face deployment workflow for this repo
- the private Hugging Face setup path
- the private-Space plus public-proxy pattern for keeping execution logs private while still exposing a public endpoint when needed
- the GitHub-side and Hugging Face-side secrets and variables required for the app to run
Pushing a canonical full semver tag in the form vMAJOR.MINOR.PATCH runs the separate release.yml workflow. That workflow first runs the shared local CI action for test/build validation, then creates the GitHub Release for the tagged commit, builds platform-specific Go binaries, and uploads them as Release assets. The GitHub App runtime binary should be taken from Releases rather than rebuilt manually if you want a published artifact.
Users should create releases with full tags such as:
v0.1.2v0.1.3v1.0.0
After the release workflow starts from a successful CI run for the tagged commit, it creates or moves these shortcut tags to the same commit as that release:
vMAJOR.MINORvMAJORMAJOR.MINOR.PATCHMAJOR.MINORMAJOR
For example, pushing v0.1.2 creates or updates v0.1, v0, 0.1.2, 0.1, and 0 to point at that same commit. Pushing v0.1.3 later creates 0.1.3 and moves the shared aliases v0.1, v0, 0.1, and 0 forward to the new release commit while the GitHub Release itself remains v0.1.3.
Contribution setup and workflow are in CONTRIBUTING.md.
This repository includes AGENTS.md and .sisyphus/plans/ so future automated development has explicit project context instead of hidden conventions.
MIT.
