Skip to content

Commit 990baea

Browse files
fhacloidclaude
andcommitted
ci: add commitlint + PR-lint gate for Linear-key discipline
Conventional Commits + a Linear issue key required on release-note types (feat/fix/perf); maintenance types exempt. Feeds Linear releases as changie is retired (OPS-1541). Same gate as youdeploy-http-api #5981. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FAv8ZjMKaAk7v2hF3Vf4j1
1 parent b252ff5 commit 990baea

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

.github/workflows/pr-lint.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: PR Lint
2+
3+
# Commit convention gate feeding Linear releases (changie retirement).
4+
# Validates PR commits AND the PR title (the PR title becomes the commit once we
5+
# squash-merge). Conventional Commits + a Linear issue key on release-note types
6+
# (see commitlint.config.js). No secrets, no self-hosted runner needed — runs on
7+
# GitHub-hosted ubuntu-latest (free for this trust-safe lint).
8+
9+
on:
10+
pull_request:
11+
types: [opened, edited, synchronize, reopened]
12+
13+
permissions:
14+
contents: read
15+
pull-requests: read
16+
17+
concurrency:
18+
group: pr-lint-${{ github.workflow }}-${{ github.event.pull_request.number }}
19+
cancel-in-progress: true
20+
21+
jobs:
22+
commitlint:
23+
name: Commit convention
24+
runs-on: ubuntu-latest
25+
# Same-repo PRs only; skip dependency bots (they carry no Linear key and are
26+
# exempt "Maintenance" changes). Fork PRs are skipped here (contributors have no
27+
# Linear access); a maintainer re-runs from an internal branch if needed.
28+
if: >-
29+
github.event.pull_request.head.repo.full_name == github.repository &&
30+
github.actor != 'dependabot[bot]' &&
31+
github.actor != 'renovate[bot]'
32+
steps:
33+
- uses: actions/checkout@v4
34+
with:
35+
fetch-depth: 0
36+
37+
- name: Lint PR commits
38+
uses: wagoid/commitlint-github-action@v6
39+
with:
40+
configFile: commitlint.config.js
41+
42+
- name: Lint PR title (becomes the squash commit)
43+
env:
44+
PR_TITLE: ${{ github.event.pull_request.title }}
45+
run: |
46+
printf '%s\n' "$PR_TITLE" | npx --yes \
47+
-p @commitlint/cli -p @commitlint/config-conventional \
48+
commitlint --config commitlint.config.js

commitlint.config.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Cycloid commit convention — feeds Linear releases (changie is being retired).
2+
//
3+
// Conventional Commits + a Linear issue key on release-note-bearing commits.
4+
// Release-note types (feat/fix/perf) MUST reference a Linear issue key (e.g. BE-123)
5+
// in the subject or body, so the release pipeline can link the issue to the Linear
6+
// release. Maintenance types (chore/build/ci/docs/style/test/refactor/revert) are
7+
// exempt — they land in the "Maintenance" bucket and need no key.
8+
//
9+
// NOTE: intended to be extracted into a shared @cycloid/commitlint-config package
10+
// once the monorepo lands; kept inline per-repo for now.
11+
12+
const RELEASE_NOTE_TYPES = ["feat", "fix", "perf"];
13+
const LINEAR_KEY = /\b[A-Z]{2,}-\d+\b/;
14+
15+
module.exports = {
16+
extends: ["@commitlint/config-conventional"],
17+
plugins: [
18+
{
19+
rules: {
20+
"linear-key-on-release-types": (parsed) => {
21+
const { type, header, body } = parsed;
22+
if (!type || !RELEASE_NOTE_TYPES.includes(type)) return [true];
23+
const hasKey = LINEAR_KEY.test(`${header || ""}\n${body || ""}`);
24+
return [
25+
hasKey,
26+
`a "${type}" commit must reference a Linear issue key (e.g. BE-123) ` +
27+
`in the subject or body so it can be linked to the Linear release`,
28+
];
29+
},
30+
},
31+
},
32+
],
33+
rules: {
34+
"linear-key-on-release-types": [2, "always"],
35+
},
36+
};

0 commit comments

Comments
 (0)