From 2b4703de90c47340abf78e5be55a81dda1002064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ho=C3=A0i=20Nh=E1=BB=9B?= Date: Mon, 1 Jun 2026 06:42:38 +0000 Subject: [PATCH] feat(ci): hard-gate PR merges on contributor star (Star Check workflow) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promotes the 'star the repo' rule from soft enforcement to a CI-blocking gate on PR merges. PRs from contributors who haven't starred the repo cannot merge until they do (single click + Re-run job). Mechanism: - New workflow .github/workflows/star-check.yml runs on every PR (opened, reopened, synchronize, ready_for_review, labeled, unlabeled). - Uses the public GitHub REST API 'GET /users/{login}/starred/{owner}/{repo}' via github-script. Returns 204 if starred, 404 if not. - No extra auth scope required beyond the default GITHUB_TOKEN. - Single job, 2 steps: the check + a summary write to the Actions UI. Auto-exemptions (no human action needed): - Maintainer PRs (@hoainho): hard-coded in the workflow's MAINTAINERS list. - Bot PRs: matches *[bot] suffix + an allowlist (dependabot, gemini-code-assist, google-cla, github-actions, renovate). - PRs labeled 'tracked-plan': for maintainer-driven milestone work (M-A, M-B, future Self-Roadmap milestones). - PRs labeled 'pre-star-rule': grandfathered PRs that were open before this policy landed (2026-06-01). Grandfathering (applied before this commit, separately via gh CLI): - 4 in-flight contributor PRs (#17, #36, #37, #38) labeled 'pre-star-rule' so the new check skips them. - 2 in-flight maintainer PRs (#39 M-A, #41 M-B) labeled 'tracked-plan' so the new check skips them. Failure UX: - When star check fails, the workflow writes a clear error message with: 'star the repo', 'click Re-run failed jobs', and a link to CONTRIBUTING.md. No re-push required to re-run after starring. Doc surface updated: - CONTRIBUTING.md: 'How to claim' section now flags the hard gate with a clear ⚠️ notice; new 'Exemptions' subsection documents the 4 bypass categories. - PULL_REQUEST_TEMPLATE.md: 'Claim confirmation' section updated to note the CI enforcement. - CHANGELOG.md: new Unreleased entries under Added (workflow) + Changed (policy hardening) + Migration (grandfathering). Privacy + safety notes: - The API endpoint is public + read-only (no PII beyond GitHub's own public star list). - Workflow permissions are minimal: 'contents: read' + 'pull-requests: read'. No write access requested. - The check is idempotent and safe to re-run. Self-referential note: this PR itself will trigger the new workflow once merged. Author is @hoainho (in MAINTAINERS), so it auto-passes. --- .github/CONTRIBUTING.md | 21 +++- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/workflows/star-check.yml | 161 +++++++++++++++++++++++++++++++ CHANGELOG.md | 12 ++- 4 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/star-check.yml diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4300647..0cde52d 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -60,9 +60,13 @@ Each issue includes: Before opening a PR that claims a `good first issue` or `help wanted` issue, please do **both** of the following: -### 1. Star the repository ⭐ (required) +### 1. Star the repository ⭐ (required — hard gate) -Click the **Star** button at the top of the repo. This isn't a vanity gate — it's a low-friction signal that you've actually looked at the project and intend to follow through, not just farm a PR for a profile stat. Maintainers prioritize claims from users who star first. +Click the **Star** button at the top of the repo. This isn't a vanity gate — it's a low-friction signal that you've actually looked at the project and intend to follow through, not just farm a PR for a profile stat. + +**⚠️ This is now enforced by CI.** A workflow ([`.github/workflows/star-check.yml`](../blob/main/.github/workflows/star-check.yml)) runs on every PR. If the author hasn't starred the repo, the **Star Check** status will fail and the PR cannot be merged until you star and re-run the check (or push a new commit, which automatically re-runs). + +If you've already starred and the check is failing, click "Re-run failed jobs" in the GitHub Actions tab — the API takes a few seconds to propagate. ### 2. Comment on the issue with **"I'll take this"** (or similar) @@ -75,11 +79,22 @@ A short comment claiming the issue before you start coding. Examples that work: This prevents two contributors from working on the same issue in parallel, and lets the maintainer mentally assign it to you. +This step is honor-system + reviewer-checked, not CI-enforced — but PRs that skip it usually get a request to add the claim comment retroactively before review. + ### Why these two conditions We've been burned by drive-by PRs that didn't read the issue description, didn't read the linked spec, and broke other things in the process. The star + claim combo is a 30-second filter that selects for contributors who'll actually engage with the project. -**PRs that don't follow these rules will get a friendly comment asking you to do them retroactively, then we'll proceed with review.** It's never a hard block — we're not jerks about it — but consistency matters for the contributor pipeline. +### Exemptions (auto-detected by the Star Check workflow) + +The CI check skips automatically in these cases: + +- **Maintainer PRs** (`@hoainho`) — the maintainer doesn't need to star their own repo +- **Bot PRs** — Dependabot, gemini-code-assist, google-cla, github-actions, renovate +- **Tracked-plan PRs** — PRs labeled `tracked-plan` (used for maintainer-driven milestone work like M-A / M-B / future Self-Roadmap milestones) +- **Grandfathered PRs** — PRs labeled `pre-star-rule` (used for PRs that were already open when this policy landed on 2026-06-01) + +If your PR fits one of these and is being blocked, ping `@hoainho` and we'll apply the appropriate label. ### What if someone else claimed it but hasn't opened a PR? diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 43a4435..eb4c83e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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. +- [ ] 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 diff --git a/.github/workflows/star-check.yml b/.github/workflows/star-check.yml new file mode 100644 index 0000000..a7c90a0 --- /dev/null +++ b/.github/workflows/star-check.yml @@ -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(); diff --git a/CHANGELOG.md b/CHANGELOG.md index f5344c7..495a69a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **`Star Check` CI workflow** (`.github/workflows/star-check.yml`) — runs on every PR and blocks merge if the author hasn't starred the repository. Auto-exempts maintainer (`@hoainho`), bots (Dependabot, gemini-code-assist, google-cla, github-actions, renovate), `tracked-plan`-labeled PRs (maintainer-driven milestone work), and `pre-star-rule`-labeled PRs (grandfathered pre-policy). Uses the public `GET /users/{login}/starred/{owner}/{repo}` API — no extra auth scope. + ### Changed -- **Contributor claim policy** — contributors must now (1) star the repo and (2) comment `"I'll take this"` (or similar) before opening a PR that claims a `good first issue` or `help wanted` issue. PRs that skip these steps get a friendly retroactive request, not a hard block. See [CONTRIBUTING.md → How to claim](.github/CONTRIBUTING.md#-how-to-claim-an-issue-required-before-opening-a-pr). -- PR template adds a "Claim confirmation" section with checkboxes for the two required steps. Maintainer / tracked-plan PRs can delete this section. +- **Contributor claim policy hardened** — starring the repo is now a **hard precondition for merge**, enforced by CI (see Star Check workflow above). The previous "comment `I'll take this`" rule stays honor-system + reviewer-checked. See [CONTRIBUTING.md → How to claim](.github/CONTRIBUTING.md#-how-to-claim-an-issue-required-before-opening-a-pr). +- PR template "Claim confirmation" section updated to flag the star as CI-enforced. + +### Migration + +- 4 PRs that were already open when this policy landed (#17, #36, #37, #38) labeled `pre-star-rule` and grandfathered through the check. ## [2.0.3] - 2026-02-28