fix(ci): lazy-load installer so mcp/help work without cli deps #54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); |