-
Notifications
You must be signed in to change notification settings - Fork 1
235 lines (210 loc) · 12 KB
/
commitlint.yml
File metadata and controls
235 lines (210 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# SPDX-FileCopyright (c) 2025-2026 SKY, LLC.
# SPDX-License-Identifier: MPL-2.0
# ─────────────────────────────────────────────────────────────────────────────
# Commitlint — Conventional Commits advisory check on PR titles
#
# Phase R1a of `docs/architecture/release-automation-plan.md`.
#
# Why this exists:
#
# UFFS is migrating to release-plz + git-cliff + Conventional Commits to
# automate version bumping and changelog generation (see plan §4 and §5).
# Release-plz infers the next version and the changelog category from the
# PR title's `type:` prefix. Non-conforming titles silently produce no
# changelog entry — release-plz treats them as if they didn't merge.
# That's a footgun: a fix shipped under "Improve search performance" gets
# no version bump and no changelog mention.
#
# This workflow surfaces non-conforming titles at PR-open time so the
# author can fix them before merge.
#
# Why advisory (not enforcing):
#
# Phase R1a is the OBSERVATION period. We're at ~100% adherence over
# the last 3 days (24/24 PRs) but only 83.3% over the last 30 days
# (75/90 PRs). The 15 non-conforming PRs use project-internal prefixes
# like `security:`, `bench:`, `shmem:`, `stream-stress:`,
# `cross-tool-benchmark:`, `gitignore:` (see
# `docs/architecture/release-automation-baseline.md` §4). Some of those
# may be re-tagged as `chore(security):`, `bench:` (if we choose to
# accept it as a real type), etc. Phase R1a gathers the data; Phase
# R1b (≥1 month later) makes the gate mandatory using the refined type
# list informed by the observation.
#
# In advisory mode this workflow ALWAYS exits 0. Non-conforming titles
# produce a sticky PR comment, never a merge block.
#
# Sticky-comment design:
#
# The job re-runs on every `synchronize` event (force-pushes, additional
# commits to the PR branch). Naïvely posting a comment on every run
# spams the PR. Instead, we tag managed comments with a unique HTML
# marker `<!-- commitlint-advisory: managed by ... -->` and use the
# GitHub REST API to either edit the existing managed comment in-place
# or delete it once the title becomes conformant. Result: at most ONE
# advisory comment per PR, auto-pruned when fixed.
#
# What this does NOT do:
#
# • Block merge. Always exits 0. R1b will flip a single line to enforce.
# • Validate individual commit messages on the feature branch. UFFS
# uses squash-merge exclusively, so the PR title (which becomes the
# squash subject) is the only commit message that lands on `main`.
# • Validate the PR description / body. Only the title is parsed for
# release-plz's version inference.
#
# References:
#
# • CONTRIBUTING.md → "Commit message conventions"
# (the user-facing list of allowed types and examples)
# • https://www.conventionalcommits.org/en/v1.0.0/
# (the spec that defines the type list and breaking-change syntax)
# • plan §2.8 (current adherence baseline) and §3 (R1a → R1b transition)
name: "📝 Commitlint (advisory)"
on:
pull_request:
# `edited` catches title edits after open. `synchronize` catches
# additional commits / force-pushes (so a stale advisory comment
# gets pruned promptly when the author updates the title).
# `reopened` covers the reopen-after-close case so a closed-then-
# reopened PR re-evaluates fresh.
types: [opened, edited, synchronize, reopened]
# Comment posting requires write access to the PR (which is the
# `pull-requests` scope, not `contents`). Read on contents is implicit
# and unused — we never check out code in this workflow.
permissions:
contents: read
pull-requests: write
# Cancel any in-flight commitlint run on the same PR when a new event
# arrives. Title edits land in bursts (typo → realize → fix); we only
# care about the latest state.
concurrency:
group: commitlint-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
pr-title:
name: "PR title — Conventional Commits"
runs-on: ubuntu-latest
timeout-minutes: 2
# Skip on PRs from forks: forked PRs don't have write tokens, so
# `gh pr comment` would fail. Forks are advised separately via the
# CONTRIBUTING.md docs; they get a hint from the PR template instead.
if: github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Validate PR title against Conventional Commits
env:
# `pull_request.title` is the canonical squash-merge subject.
# We deliberately do NOT inspect commits on the branch — those
# get squashed away on merge and are noise for release-plz.
TITLE: ${{ github.event.pull_request.title }}
PR_NUMBER: ${{ github.event.pull_request.number }}
# `github.token` is the workflow's ephemeral GITHUB_TOKEN.
# The `gh` CLI auto-detects this env var.
GH_TOKEN: ${{ github.token }}
# Marker embedded in the comment body so the same comment can
# be located across re-runs. Changing this string is a one-
# time migration: bump the marker AND delete any pre-bump
# advisory comments by hand on open PRs.
COMMENT_MARKER: "<!-- commitlint-advisory: managed by .github/workflows/commitlint.yml -->"
run: |
set -euo pipefail
# ── Allowed Conventional Commits types ─────────────────────
# Mirrors CONTRIBUTING.md → "Commit message conventions".
# Order matters only for readability; the regex is OR-of-all.
# Adding a new type means updating both this regex AND
# CONTRIBUTING.md in the same PR (and ideally cliff.toml's
# commit_parsers in Phase R2).
PATTERN='^(feat|fix|perf|refactor|docs|test|build|ci|chore|style|revert)(\([a-z0-9-]+\))?!?: .{1,}$'
# Group redirects to avoid the SC2129 shellcheck warning
# and minimise filesystem churn on the GitHub Actions side.
{
echo "## 📝 Commitlint — PR title check"
echo ""
echo "| Field | Value |"
echo "| --- | --- |"
echo "| Title | \`${TITLE}\` |"
echo "| Pattern | \`${PATTERN}\` |"
echo "| Mode | **advisory** (Phase R1a) — never blocks merge |"
echo ""
} >> "$GITHUB_STEP_SUMMARY"
# ── Locate any existing managed advisory comment ───────────
# `gh api` paginates automatically. We use the raw issues
# comments endpoint (PR comments live there) and jq-filter
# by marker presence. `head -1` defends against the
# (impossible-but-cheap-to-guard) double-managed-comment case.
EXISTING_ID=$(
gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
--jq ".[] | select(.body | contains(\"${COMMENT_MARKER}\")) | .id" \
| head -1 || true
)
if echo "$TITLE" | grep -qE "$PATTERN"; then
# ── Conforming title ───────────────────────────────────
echo "::notice::PR title matches Conventional Commits: ${TITLE}"
echo "✅ **Conforms** to Conventional Commits." >> "$GITHUB_STEP_SUMMARY"
if [ -n "${EXISTING_ID}" ]; then
# Title was previously non-conforming, now fixed —
# remove the stale advisory comment so the PR isn't
# cluttered with a resolved warning.
echo "::notice::Removing stale advisory comment ${EXISTING_ID}"
gh api -X DELETE "repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING_ID}"
echo "🧹 Removed stale advisory comment from earlier non-conforming title." >> "$GITHUB_STEP_SUMMARY"
fi
exit 0
fi
# ── Non-conforming title ───────────────────────────────────
echo "::warning::PR title does not match Conventional Commits: ${TITLE}"
echo "⚠️ **Does not conform** — advisory comment posted/updated below." >> "$GITHUB_STEP_SUMMARY"
# Compose the advisory body. The marker MUST be the first
# line for trivially-greppable detection. Keep the body
# informative but not punishing: contributors who hit this
# check probably haven't read CONTRIBUTING.md yet.
#
# NOTE on indentation: YAML's literal block scalar (`run: |`)
# strips the least common leading indentation from every
# content line. All heredoc body lines below sit at the
# same column as `set -euo pipefail` at the top of this
# block, so YAML strips them flush before bash sees them.
# No post-hoc `sed` indent-stripping required.
BODY=$(cat <<EOF
${COMMENT_MARKER}
## ⚠️ PR title is not in Conventional Commits format
**Your title:** \`${TITLE}\`
**Expected pattern:** \`type(optional-scope): subject\` where \`type\` is one of:
\`feat\`, \`fix\`, \`perf\`, \`refactor\`, \`docs\`, \`test\`, \`build\`, \`ci\`, \`chore\`, \`style\`, \`revert\`
**Examples** (from recent UFFS merges):
- \`fix(release): re-codesign macOS binaries after strip — v0.5.73\`
- \`refactor(uffs-mft): eliminate most shadow_reuse / shadow_unrelated via renames & let-else\`
- \`docs(architecture): add Windows-clippy + Linux-native-cross plan with W0 baseline\`
- \`feat(cli)!: drop deprecated --q shorthand\` (the trailing \`!\` marks a breaking change)
See [CONTRIBUTING.md → Commit message conventions](https://github.com/${GITHUB_REPOSITORY}/blob/main/CONTRIBUTING.md#commit-message-conventions) for the full rationale and the table of which types trigger releases.
---
**🟡 This check is ADVISORY during release-automation Phase R1a** — non-conforming titles do **not** block merge. Phase R1b (scheduled after ≥1 month of observation) will make this a required gate. Until then, please update the title when convenient; this comment will auto-delete on the next workflow run once the title conforms.
*(Workflow: \`.github/workflows/commitlint.yml\` · Plan: [\`docs/architecture/release-automation-plan.md\`](https://github.com/${GITHUB_REPOSITORY}/blob/main/docs/architecture/release-automation-plan.md) Phase R1a)*
EOF
)
if [ -n "${EXISTING_ID}" ]; then
# Edit in place — preserves comment threading and avoids
# spamming the PR with duplicate advisories on every push.
echo "::notice::Updating existing advisory comment ${EXISTING_ID}"
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${EXISTING_ID}" \
-f body="${BODY}" >/dev/null
else
# Pass --repo explicitly so `gh` does not try to infer the
# repo slug from the working directory's git remote — this
# job runs without an `actions/checkout` step, so a remote-
# less invocation crashes with `fatal: not a git repository`
# and the script's `set -euo pipefail` propagates the
# failure, defeating the advisory-mode `exit 0` below.
# (Discovered while diagnosing the stalled Dependabot tokio
# PR #125 — the workflow was supposed to be advisory but
# this single bug turned every non-conforming title into a
# red required-check.)
echo "::notice::Posting new advisory comment"
gh pr comment "${PR_NUMBER}" \
--repo "${GITHUB_REPOSITORY}" \
--body "${BODY}"
fi
# ADVISORY MODE: exit 0 even on non-conformance.
# Phase R1b will replace this single line with `exit 1` to
# promote the check from advisory to required.
exit 0