-
Notifications
You must be signed in to change notification settings - Fork 461
141 lines (128 loc) · 6.19 KB
/
Copy pathmajor-version-check.yml
File metadata and controls
141 lines (128 loc) · 6.19 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
name: Major Version Check
on:
pull_request:
types: [opened, edited, synchronize, reopened]
issue_comment:
types: [created, edited, deleted]
permissions:
contents: read
issues: read
pull-requests: read
statuses: write
jobs:
check-major-bump:
if: ${{ github.event_name == 'pull_request' || github.event.issue.pull_request }}
runs-on: ubuntu-latest
steps:
- name: Check for unapproved major changesets
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const STATUS_CONTEXT = 'Major Version Check';
const prNumber = context.payload?.pull_request?.number || context.payload?.issue?.number;
// Resolve the PR so we have its head SHA. issue_comment runs are tied to the
// default branch, so the implicit job check-run lands on main rather than the PR
// head. We post an explicit commit status to the PR head instead, so the required
// context updates correctly no matter which event triggered the run (e.g. the
// re-run after an "!allow-major" comment).
let pullRequest = context.payload.pull_request;
if (!pullRequest) {
const { data } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
});
pullRequest = data;
}
const headSha = pullRequest.head.sha;
const setStatus = async (state, description) => {
try {
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: headSha,
context: STATUS_CONTEXT,
state,
description,
target_url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`,
});
} catch (error) {
// Fork PRs get a read-only token and can't post statuses; don't fail the job over it
// (such PRs need an internal re-run or admin bypass). Re-throw anything else.
if (error.status === 403) {
core.warning(`Could not post the "${STATUS_CONTEXT}" status (read-only token, likely a fork PR): ${error.message}`);
return;
}
throw error;
}
};
// Check if any changeset files indicate a major bump.
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
per_page: 100,
});
let hasMajorChangeset = false;
for (const file of files) {
if (file.filename.startsWith('.changeset/') && file.status !== 'removed') {
const { data: changesetContent } = await github.rest.repos.getContent({
owner: context.repo.owner,
repo: context.repo.repo,
path: file.filename,
ref: headSha,
});
const content = Buffer.from(changesetContent.content, changesetContent.encoding).toString();
// Only check for "major" in the YAML frontmatter section (between --- markers)
// This avoids false positives from descriptive text mentioning "major"
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\s*$/m);
if (frontmatterMatch && /:\s*["']?major["']?/.test(frontmatterMatch[1])) {
hasMajorChangeset = true;
break;
}
}
}
if (!hasMajorChangeset) {
await setStatus('success', 'No major version bump detected.');
console.log('No major changeset detected.');
return;
}
// A major bump is present: require an "!allow-major" comment from an org member.
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
per_page: 100,
});
// The GITHUB_TOKEN (github-actions[bot]) isn't an org member, so checkMembershipForUser
// only confirms PUBLIC members; approvers must have public "clerk" org membership.
let approvalFound = false;
for (const comment of comments) {
if (comment.body?.trim().toLowerCase() === '!allow-major') {
try {
const { status } = await github.rest.orgs.checkMembershipForUser({
org: context.repo.owner,
username: comment.user.login,
});
if (status === 204) {
approvalFound = true;
break;
}
} catch (error) {
// 404 = not a public org member; rethrow transient 403/5xx so a hiccup can't drop a valid approval.
if (error?.status === 404) continue;
throw error;
}
}
}
if (approvalFound) {
await setStatus('success', 'Major version bump approved by an organization member.');
console.log('Major version bump approved by an organization member.');
} else {
// The red "Major Version Check" commit status on the PR head is the gate. We keep
// the job itself green so that the success status posted by a later "!allow-major"
// re-run (an issue_comment run, whose implicit check-run lands on main, not the PR
// head) isn't shadowed by a stale red job check-run that never clears.
await setStatus('failure', 'Major bump needs an "!allow-major" comment from an org member.');
core.warning('Major version bump requires approval from an organization member by commenting "!allow-major" on the PR.');
}