Files
kandev/docs/specs/ci/system-design/pull-request-size-labels.md
T
2026-09-16 09:42:22 +01:00

8.3 KiB

status, system, requirements
status system requirements
draft ci
REQ-CI-PR-SIZE-001

Pull request size label system design

Purpose and boundaries

This design defines the trusted GitHub Actions workflow that maintains one review-size label on each pull request.

The CI system owns the event contract, file filter, size limits, label definitions, permissions, and contract tests. GitHub remains the label source of truth.

The workflow reads pull request metadata only. It does not read file contents, check out pull request code, or change other review automation.

Requirement mapping

Requirement Design sections
REQ-CI-PR-SIZE-001 Event contract, File classification, Control flow, Failure and recovery, Security

Components and responsibilities

Component Responsibility
.github/workflows/pr-size-label.yml Reads changed-file metadata and maintains the size labels.
actions/github-script Runs pinned API orchestration from the trusted workflow without a checkout.
GitHub pull request files API Returns the current changed-file paths with pagination.
GitHub labels API Creates missing label definitions and changes pull request labels.
.github/scripts/pr-size-label-workflow-contract_test.py Protects events, file rules, limits, mutations, concurrency, and permissions.
.github/workflows/lint-action-pinning.yml Runs the contract test and the action-pinning checks.

Event contract

The workflow uses this event contract:

on:
  pull_request_target:
    types: [opened, reopened, synchronize]

pull_request_target loads the workflow from the trusted base branch. It also gives fork pull requests the write permission required for labels.

The concurrency group contains the pull request number. The group uses cancel-in-progress: false.

The workflow serializes surviving runs for one pull request. cancel-in-progress: false keeps an active run, but GitHub can replace a pending run with a newer event and does not guarantee dispatch order. Each surviving run reads the current file list and label state instead of relying on event snapshots.

File classification

The workflow paginates github.rest.pulls.listFiles with 100 entries per page. It uses only each response item's normalized filename value.

A counted file must start with apps/. It must also meet one of these rules:

  • Its extension is c, cc, cpp, css, go, graphql, gql, h, html, js, jsx, mjs, mts, proto, rs, scss, sql, ts, or tsx.
  • Its path matches apps/web/src/locales/<locale>/<namespace>.json.

The filter rejects these repository tooling directories: apps/backend/internal/agentctl/server/api/scripts/, apps/backend/internal/webapp/embedded/generated/, apps/backend/scripts/, apps/web/e2e/scripts/, apps/web/generated/, and apps/web/scripts/. It rejects any path containing a node_modules directory.

The known linter directories are apps/backend/cmd/sqlguard/, apps/backend/internal/db/sqlguard/, and apps/web/eslint-rules/.

The filter rejects these exact tool setting base names: commitlint.config.mjs, eslint.config.mjs, eslint.e2e-sleeps.config.mjs, eslint.i18n.config.mjs, eslint.i18n.options.mjs, playwright.config.ts, postcss.config.mjs, prettier.config.mjs, stylelint.config.mjs, tailwind.config.ts, vite.config.ts, vitest.config.ts, vitest.monaco-editor.ts, and vitest.setup.ts. A source or test file with a similar prefix, such as vite-preload-recovery.ts, remains counted.

The filter excludes image, font, and binary files with known asset extensions and files under these asset directories: apps/backend/internal/notifications/providers/assets/, apps/desktop/src-tauri/icons/, apps/web/lib/assets/, apps/web/public/, and apps/web/src/assets/.

The explicit application root and extension list exclude these files without separate path rules:

  • docs/**, .github/**, and root scripts/** files.
  • Markdown guidance and public documentation.
  • Package manifests, dependency locks, and TypeScript settings.
  • Dockerfiles, shell scripts, image assets, and generated binaries.

Tests use the same path and extension rules as production source. The workflow does not inspect additions, deletions, or changed-line totals.

The count maps to labels as follows:

Counted files Label
0 through 10 small
11 through 50 medium
51 or more big

Label definitions

The workflow owns three repository label definitions:

Name Color Description
small 0E8A16 Pull request changes 0-10 application files
medium FBCA04 Pull request changes 11-50 application files
big D93F0B Pull request changes 51 or more application files

The workflow creates the selected definition only when applying that label shows that its repository definition is missing. It does not preflight all three definitions and does not replace the color or description of an existing label.

Control flow

  1. GitHub starts the trusted workflow for an allowed pull request event.
  2. The workflow reads all available changed-file pages for the current pull request.
  3. The workflow stops before mutations if the changed-file result can be incomplete.
  4. The workflow filters the paths and calculates the target size label.
  5. The workflow reads the current pull request labels once.
  6. If the target label is absent, the workflow applies it directly. A narrowly identified missing-definition response creates only the target definition and retries the apply. A concurrent already_exists creation converges on the same retry path.
  7. The workflow removes the other size labels if the pull request has them.
  8. The workflow writes the counted-file total and final label to the step summary.

The workflow adds the target label before it removes stale labels. An API error therefore does not remove the last valid size label first.

Failure and recovery

The GitHub files endpoint returns at most 3,000 files. A result with 3,000 files can be incomplete, so the workflow fails before a label mutation.

A changed-file API error also fails before a label mutation. The current size labels remain unchanged in both cases.

A label-definition or label-mutation error fails the job. A concurrent label-definition creation that returns 422 with already_exists is accepted before the target label apply is retried. A later workflow retry reads current GitHub state and converges on one target label.

The workflow adds the target before it removes stale labels. A partial mutation can temporarily leave two size labels, but it does not leave the pull request unlabeled.

Surviving serialized runs do not promise event order. Each run reads current file and label state, so a later completed run can converge on the current pull request diff even when GitHub replaces a pending run or dispatches events out of order.

Security

The job declares only pull-requests: write. GitHub permits this permission for file reads, label creation, and pull request label changes.

The workflow uses a full commit SHA for actions/github-script. It does not use actions/checkout, shell execution, eval, or pull request text in executable source.

The workflow passes repository and pull request values as API parameters. It does not interpolate untrusted metadata into executable workflow code.

Observability

The step summary shows the pull request number, counted-file total, and selected label. API errors remain visible in the workflow result.

The contract test covers the event list, permission block, pagination, file filter, limits, label definitions, mutation order, and trusted execution boundary. It also rejects unconditional repository label-definition reads and verifies the missing-target recovery path.

External contracts