-
Notifications
You must be signed in to change notification settings - Fork 394
117 lines (105 loc) · 5.07 KB
/
Copy pathpr-title.yml
File metadata and controls
117 lines (105 loc) · 5.07 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
name: Pull Request Title
on:
pull_request_target:
types: [opened, edited, reopened]
branches:
- "master"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
conventional-commit:
runs-on: ubuntu-latest
permissions:
pull-requests: write
env:
# Shared between both steps. Must stay portable across bash ERE and JS
# regex (no lookarounds, named groups, or other JS-only features).
# Revert PRs nest the reverted commit's type, e.g. `revert: feat(api): undo X`,
# so the semver label can track the magnitude of the original change.
PR_TITLE_PATTERN: '^(revert(!)?: )?(feat|fix|docs|style|refactor|perf|test|bench|build|ci|chore)(\(([^)]+)\))?(!)?: .+'
steps:
- name: Validate PR title against Conventional Commits
if: github.event.action != 'edited' || github.event.changes.title != null
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
if [[ ! "$PR_TITLE" =~ $PR_TITLE_PATTERN ]]; then
echo "::error::PR title does not follow Conventional Commits format."
echo "Got: $PR_TITLE"
echo "Expected: <type>(<scope>)?(!)?: <subject>"
echo " revert(!)?: <type>(<scope>)?(!)?: <subject> (for reverts)"
echo "Allowed types: feat, fix, docs, style, refactor, perf, test, bench, build, ci, chore"
echo "Revert PRs must embed the original commit's type so the semver impact can"
echo "be determined (e.g. 'revert: feat(scope): original title')."
echo "Reverts of reverts re-apply the original change, so use the original"
echo "title (e.g. 'feat: x', not 'revert: revert: feat: x')."
exit 1
fi
echo "PR title OK: $PR_TITLE"
- name: Sync labels with PR title
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pattern = new RegExp(process.env.PR_TITLE_PATTERN)
const parse = (title) => {
const m = (title || '').match(pattern)
if (!m) return {}
const isRevert = !!m[1]
return {
type: isRevert ? 'revert' : m[3],
revertedType: isRevert ? m[3] : undefined,
scope: m[5],
breaking: m[2] === '!' || m[6] === '!',
}
}
// For reverts, the semver bump tracks the magnitude of the change
// being undone, parsed from the nested type in the title.
const semverFor = ({ type, revertedType, breaking }) => {
if (!type) return undefined
if (breaking) return 'semver-major'
const effective = type === 'revert' ? revertedType : type
if (effective === 'feat') return 'semver-minor'
return 'semver-patch'
}
const pr = context.payload.pull_request
const next = parse(pr.title)
// Prefetch all existing repo labels once to avoid per-label API calls.
const repoLabels = new Set()
for await (const page of github.paginate.iterator(github.rest.issues.listLabelsForRepo, { ...context.repo, per_page: 100 })) {
for (const label of page.data) repoLabels.add(label.name)
}
// Returns the set of labels derived from a parsed title.
// Type and scope are only added if they exist in the repo.
const titledLabels = (parsed) => {
const labels = new Set()
if (!parsed.type) return labels
if (repoLabels.has(parsed.type)) labels.add(parsed.type)
if (parsed.scope && repoLabels.has(parsed.scope)) labels.add(parsed.scope)
const semver = semverFor(parsed)
if (semver) labels.add(semver)
return labels
}
const nextLabels = titledLabels(next)
const current = new Set((pr.labels || []).map(l => l.name))
// When the title changes, remove labels from the old title that no
// longer apply so stale type/scope/semver labels don't linger.
const titleChanged = context.payload.action === 'edited' &&
context.payload.changes?.title?.from != null
const toRemove = new Set()
if (titleChanged) {
const prev = parse(context.payload.changes.title.from)
for (const label of titledLabels(prev)) {
if (!nextLabels.has(label)) toRemove.add(label)
}
}
const desired = new Set([...current].filter(l => !toRemove.has(l)))
for (const label of nextLabels) desired.add(label)
const unchanged = current.size === desired.size && [...current].every(l => desired.has(l))
if (!unchanged) {
await github.rest.issues.setLabels({
...context.repo,
issue_number: pr.number,
labels: [...desired],
})
}