fix(projects): authenticate GitHub fetch and fail builds loudly - #73
Merged
Merged
Conversation
Live /api/projects.json was shipping with repos: [] because Cloudflare Pages' shared build IPs hit GitHub's 60/hr unauthenticated rate limit, and fetchAllRepos silently swallowed the 403 into an empty list. Result: no dynamically-fetched repos appeared on cortech.online — only the manual school-calendar fallback. Two paired changes: - Authenticate the build-time fetch when GITHUB_TOKEN is present (process.env, server-only — never bundled to the client). Lifts the rate limit to 5000/hr authenticated. Token is optional in dev so local builds still work when below the unauth quota. - Replace the silent-failure contract with a hard throw on any non-OK response or network error, so a transient GitHub hiccup blocks the deploy instead of shipping a degraded site. Error message includes the GITHUB_TOKEN hint when unauthenticated. Defense in depth: explicitly filter r.private at the source so a future endpoint or token-scope change can't accidentally leak private repos into the static JSON. CI build step now passes the auto-injected secrets.GITHUB_TOKEN. README documents the new required env var for Cloudflare Pages — fine-grained PAT, public-repo read-only, no other scopes. Tests inverted: the silent-failure contract is replaced with throw assertions, plus new coverage for Authorization header presence/absence and the private-repo filter.
7 tasks
schmug
added a commit
that referenced
this pull request
Apr 30, 2026
Two related guardrails so the build can't silently ship a half-broken deploy: - Add scripts/check-deps.mjs wired as predev/prebuild. Runs `npm install` only when package-lock.json is newer than node_modules/.package-lock.json. Skipped on CI (which uses `npm ci` explicitly) and via SKIP_DEP_CHECK=1. Closes #23 — fixes the @astrojs/rss "Rollup failed to resolve" error that bites worktree switches when a dep was added on main. - Throw in /api/projects.json when fetchAllRepos returns 0 repos. Defense-in-depth on top of #73: even if a future change ever lets the fetcher return an empty list cleanly, the build now fails instead of publishing a blank desktop. Add a regression test against the live featuredRepos config locking in which manual-fallback entries survive an empty API response. Closes the remaining items on #27. Co-authored-by: Claude <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Live
/api/projects.jsonwas shipping withrepos: [], so cortech.online's Projects view showed only the manualschool-calendarfallback. Cloudflare Pages' shared build IPs were hitting GitHub's 60/hr unauthenticated rate limit, andfetchAllRepossilently swallowed the 403 into an empty list — the build succeeded, the deploy went out, and the regression was invisible.Two paired changes:
GITHUB_TOKENis present. Read viaprocess.env(server-only — never bundled to the client) and sent asAuthorization: Bearer …. Lifts the limit to 5000/hr authenticated. Token is optional in dev/local builds.GITHUB_TOKENhint when the request was unauthenticated.Defense in depth: explicitly filter
r.privateat the source infetchAllReposso a future endpoint or token-scope change can't accidentally leak private repos into the static JSON.CI's
npm run buildstep now passes the auto-injectedsecrets.GITHUB_TOKENso verify jobs don't get rate-limited. README documents the new required env var for Cloudflare Pages: fine-grained PAT (or classic PAT withpublic_reporead), no broader scopes.Tests inverted from the old silent-failure contract: now assert
throwson 403 / network error, plus new coverage for Authorization header presence/absence and the private-repo filter.Test plan
npm run format:checknpm run lintnpm run typechecknpm test— 89/89 passing, including 6 new/updated tests insrc/lib/github.test.tsnpm run builddoes now hard-fail with the actionable error message when no token is set and GitHub responds 403 (was previously the silent regression)https://cortech.online/api/projects.jsonreturns a non-emptyreposarray with current reposOperator action required
Add
GITHUB_TOKENas an encrypted env var in Cloudflare Pages → Settings → Environment variables. (User confirmed this is now done — classic PAT with public-repo read scoped to cortech-online.)https://claude.ai/code/session_01QAwJ34JC732rN13zxJHzuk
Generated by Claude Code