Skip to content

fix(ci): lazy-load installer so mcp/help work without cli deps #54

fix(ci): lazy-load installer so mcp/help work without cli deps

fix(ci): lazy-load installer so mcp/help work without cli deps #54

Workflow file for this run

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();