Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The relative link to the workflow file is broken. Since CONTRIBUTING.md is located inside the .github/ directory, the relative path to the workflow is simply workflows/star-check.yml. The current path ../blob/main/.github/workflows/star-check.yml will result in a 404 error when navigating on GitHub.

Suggested change
**⚠️ 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).
**⚠️ This is now enforced by CI.** A workflow ([.github/workflows/star-check.yml](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)

Expand All @@ -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?

Expand Down
6 changes: 3 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The relative links to both CONTRIBUTING.md and the workflow file are broken. Since PULL_REQUEST_TEMPLATE.md is located inside the .github/ directory, the relative path to CONTRIBUTING.md is simply CONTRIBUTING.md, and the relative path to the workflow is workflows/star-check.yml. The current paths using ../blob/main/.github/... will result in 404 errors on GitHub.

Suggested change
- [ ] **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 starred the repo ⭐** — see [CONTRIBUTING.md → How to claim](CONTRIBUTING.md#-how-to-claim-an-issue-required-before-opening-a-pr). **This is now enforced by CI** ([.github/workflows/star-check.yml](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

Expand Down
161 changes: 161 additions & 0 deletions .github/workflows/star-check.yml
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();
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading