-
-
Notifications
You must be signed in to change notification settings - Fork 9
183 lines (165 loc) · 7.9 KB
/
Copy pathstar-check.yml
File metadata and controls
183 lines (165 loc) · 7.9 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
name: Star Check
# Enforces the "must star the repo to merge" contributor policy.
# See .github/CONTRIBUTING.md → "How to claim an issue".
#
# Failure modes:
# - Author hasn't starred the repo (verified after 3 retries) → ❌ fail
# - Author is the maintainer (hoainho) → ⏭ skip
# - Author is in the bot allowlist (Dependabot etc.) → ⏭ skip
# - PR has 'tracked-plan' label → ⏭ skip (maintainer-driven milestones)
# - PR has 'pre-star-rule' label → ⏭ skip (grandfathered before policy)
#
# Privacy note: this check uses a public GitHub API endpoint
# (GET /users/{login}/starred/{owner}/{repo}) which returns 204 if starred,
# 404 if not. It does NOT require any auth scope beyond the default
# GITHUB_TOKEN provided by Actions.
on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled]
branches: [main]
permissions:
contents: read
pull-requests: read
jobs:
check-star:
name: Verify contributor starred the repo
runs-on: ubuntu-latest
steps:
- name: Inspect PR metadata
id: inspect
uses: actions/github-script@v7
with:
script: |
const author = context.payload.pull_request.user.login;
const labels = context.payload.pull_request.labels.map(l => l.name);
const owner = context.repo.owner;
const repo = context.repo.repo;
core.info(`PR author: @${author}`);
core.info(`Labels: ${labels.join(', ') || '(none)'}`);
// --- Exemption 1: maintainer ---
const MAINTAINERS = ['hoainho'];
if (MAINTAINERS.includes(author)) {
core.notice(`✅ Skipping — @${author} is the maintainer.`);
core.setOutput('result', 'exempt-maintainer');
return;
}
// --- Exemption 2: known bots ---
const BOTS = [
'dependabot[bot]',
'dependabot',
'gemini-code-assist[bot]',
'gemini-code-assist',
'google-cla[bot]',
'github-actions[bot]',
'renovate[bot]',
];
if (BOTS.includes(author) || author.endsWith('[bot]')) {
core.notice(`✅ Skipping — @${author} is a bot.`);
core.setOutput('result', 'exempt-bot');
return;
}
// --- Exemption 3: tracked-plan label (maintainer-driven milestones) ---
if (labels.includes('tracked-plan')) {
core.notice(`✅ Skipping — PR carries 'tracked-plan' label.`);
core.setOutput('result', 'exempt-tracked-plan');
return;
}
// --- Exemption 4: pre-star-rule label (grandfathered) ---
if (labels.includes('pre-star-rule')) {
core.notice(`✅ Skipping — PR is grandfathered ('pre-star-rule' label).`);
core.setOutput('result', 'exempt-grandfathered');
return;
}
// --- Star check (with retry for read-replica lag) ---
const MAX_ATTEMPTS = 3;
const BACKOFF_BASE_MS = 3000;
let starred = false;
let lastStatus = null;
let lastErrMsg = null;
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
try {
const resp = await github.request(
'GET /users/{username}/starred/{owner}/{repo}',
{ username: author, owner, repo },
);
lastStatus = resp.status;
if (resp.status === 204) {
core.notice(`⭐ @${author} has starred ${owner}/${repo} (attempt ${attempt}/${MAX_ATTEMPTS}).`);
starred = true;
break;
}
core.warning(`Attempt ${attempt}: unexpected status ${resp.status}.`);
} catch (err) {
lastStatus = err.status;
lastErrMsg = err.message;
if (err.status === 404) {
core.info(
`Attempt ${attempt}/${MAX_ATTEMPTS}: 404. ` +
(attempt < MAX_ATTEMPTS
? `Retrying in ${(BACKOFF_BASE_MS * attempt) / 1000}s (read-replica lag)...`
: 'Giving up.'),
);
} else if (err.status === 401 || err.status === 403) {
core.setOutput('result', 'api-error');
core.setFailed(`Star-check API auth/permission error (${err.status}): ${err.message}`);
return;
} else {
core.info(`Attempt ${attempt}: API error (${err.status || 'unknown'}): ${err.message}.`);
}
}
if (attempt < MAX_ATTEMPTS) {
await new Promise((r) => setTimeout(r, BACKOFF_BASE_MS * attempt));
}
}
if (starred) {
core.setOutput('result', 'starred');
return;
}
core.setOutput('result', 'not-starred');
const msg = [
'',
'❌ This PR cannot be merged until the author stars the repository.',
'',
`@${author}, please:`,
'',
`1. ⭐ Star this repository (https://github.com/${owner}/${repo}) — single click at top of repo`,
`2. Re-run this workflow (no need to re-push) — GitHub will detect the star and pass this check`,
'',
'Full policy: .github/CONTRIBUTING.md → "How to claim an issue"',
'',
'If you already starred and the check still fails after 30 seconds,',
'this may be a GitHub read-replica lag edge case — ping @hoainho and',
"we'll either re-run the check or apply a one-time 'pre-star-rule' label.",
'',
'If your case is an exemption (maintainer / bot / tracked-plan / grandfathered),',
'ping @hoainho and we will apply the appropriate label.',
'',
`Diagnostic: lastStatus=${lastStatus}, lastErr=${lastErrMsg || '(none)'}, attempts=${MAX_ATTEMPTS}.`,
'',
].join('\n');
core.error(msg);
core.setFailed(`@${author} has not starred ${owner}/${repo} (verified after ${MAX_ATTEMPTS} retries).`);
- name: Write check summary
if: always()
uses: actions/github-script@v7
with:
script: |
const result = '${{ steps.inspect.outputs.result }}' || 'unknown';
const author = context.payload.pull_request.user.login;
const friendly = {
'starred': '⭐ Author has starred the repo. Check passes.',
'exempt-maintainer': '⏭ Maintainer PR — check skipped.',
'exempt-bot': '⏭ Bot PR — check skipped.',
'exempt-tracked-plan': '⏭ Tracked-plan PR — check skipped.',
'exempt-grandfathered': '⏭ Grandfathered PR (pre-policy) — check skipped.',
'not-starred': '❌ Author has NOT starred (verified after 3 retries). PR cannot merge until they do.',
'api-error': '⚠️ API error — see logs.',
'unexpected-status': '⚠️ Unexpected API response — see logs.',
'unknown': '⚠️ Step did not produce a result — see logs.',
};
await core.summary
.addHeading('Star Check Result')
.addRaw(`**Author:** @${author}\n\n`)
.addRaw(`**Result:** ${friendly[result] || result}\n\n`)
.addRaw('Policy: [.github/CONTRIBUTING.md → How to claim](../blob/main/.github/CONTRIBUTING.md#-how-to-claim-an-issue-required-before-opening-a-pr)\n')
.write();