-
-
Notifications
You must be signed in to change notification settings - Fork 7
feat(ci): hard-gate PR merges on contributor star (Star Check workflow) #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -36,10 +36,10 @@ $ npm run build | |||||
|
|
||||||
| If this PR closes a `good first issue` or `help wanted` issue, please confirm: | ||||||
|
|
||||||
| - [ ] I starred the repo ⭐ — see [CONTRIBUTING.md → How to claim](../blob/main/.github/CONTRIBUTING.md#-how-to-claim-an-issue-required-before-opening-a-pr) (low-friction signal that you intend to follow through) | ||||||
| - [ ] I commented `I'll take this` (or similar) on the issue before starting work, so two people don't accidentally race on the same issue | ||||||
| - [ ] **I starred the repo ⭐** — see [CONTRIBUTING.md → How to claim](../blob/main/.github/CONTRIBUTING.md#-how-to-claim-an-issue-required-before-opening-a-pr). **This is now enforced by CI** ([`.github/workflows/star-check.yml`](../blob/main/.github/workflows/star-check.yml)) — the "Star Check" status will block merge until you star. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The relative links to both
Suggested change
|
||||||
| - [ ] I commented `I'll take this` (or similar) on the issue before starting work, so two people don't accidentally race on the same issue. (Honor-system; not CI-enforced.) | ||||||
|
|
||||||
| If this PR is from a maintainer or a follow-up to a tracked plan, both can be skipped — just delete this section. | ||||||
| If this PR is from a maintainer or a follow-up to a tracked plan, both can be skipped — just delete this section. Maintainer/bot/`tracked-plan`-labeled PRs are auto-exempted by the Star Check workflow. | ||||||
|
|
||||||
| ## Checklist | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| 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 → ❌ 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 --- | ||
| try { | ||
| await github.rest.activity.checkRepoIsStarredByAuthenticatedUserAtUsername({ | ||
| username: author, | ||
| owner, | ||
| repo, | ||
| }); | ||
| // No throw → starred. | ||
| core.notice(`⭐ @${author} has starred ${owner}/${repo}.`); | ||
| core.setOutput('result', 'starred'); | ||
| } catch (err) { | ||
| // Fallback: octokit doesn't expose the cross-user endpoint by name, so use raw request. | ||
| core.info(`Falling back to raw API request.`); | ||
| try { | ||
| const resp = await github.request('GET /users/{username}/starred/{owner}/{repo}', { | ||
| username: author, | ||
| owner, | ||
| repo, | ||
| }); | ||
| if (resp.status === 204) { | ||
| core.notice(`⭐ @${author} has starred ${owner}/${repo}.`); | ||
| core.setOutput('result', 'starred'); | ||
| return; | ||
| } | ||
| core.setOutput('result', 'unexpected-status'); | ||
| core.setFailed(`Unexpected response status ${resp.status}`); | ||
| } catch (innerErr) { | ||
| if (innerErr.status === 404) { | ||
| 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 believe this is an exemption case (maintainer / bot / tracked-plan / grandfathered),', | ||
| 'ping @hoainho and we will apply the appropriate label.', | ||
| '', | ||
| ].join('\n'); | ||
| core.error(msg); | ||
| core.setFailed(`@${author} has not starred ${owner}/${repo}.`); | ||
| } else { | ||
| core.setOutput('result', 'api-error'); | ||
| core.setFailed(`Star-check API error: ${innerErr.message}`); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| - 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. 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The relative link to the workflow file is broken. Since
CONTRIBUTING.mdis located inside the.github/directory, the relative path to the workflow is simplyworkflows/star-check.yml. The current path../blob/main/.github/workflows/star-check.ymlwill result in a 404 error when navigating on GitHub.