vercel/turborepo · warning

GitHub auth failed (HTTP ${res.status}). Check GITHUB_TOKEN/

Error message

GitHub auth failed (HTTP ${res.status}). Check GITHUB_TOKEN/GH_TOKEN permissions.

What it means

isUrlOk() sends a HEAD request (10s timeout) to check whether an example URL exists. When the response is not ok AND a GitHub token is configured AND the status is 401 or 403, it warns that the authenticated request was rejected — the token exists but GitHub refused it. The function still returns res.ok (false), so callers treat the example as unavailable; the warning tells you the failure was token-related, not a missing example.

Source

Thrown at packages/turbo-utils/src/examples.ts:178

  }
}

export interface RepoInfo {
  username: string;
  name: string;
  branch: string;
  filePath: string;
}

export async function isUrlOk(url: string): Promise<boolean> {
  try {
    const res = await fetchWithTimeout(url, { method: "HEAD" });
    if (
      !res.ok &&
      getGitHubToken() &&
      (res.status === 401 || res.status === 403)
    ) {
      warn(
        `GitHub auth failed (HTTP ${res.status}). Check GITHUB_TOKEN/GH_TOKEN permissions.`
      );
    }
    return res.ok;
  } catch {
    return false;
  }
}

export async function getRepoInfo(
  url: URL,
  examplePath?: string
): Promise<RepoInfo | undefined> {
  const [, username, name, tree, sourceBranch, ...file] = url.pathname.split(
    "/"
  ) as Array<string | undefined>;
  const filePath = examplePath
    ? examplePath.replace(/^\//, "")

View on GitHub (pinned to f9245100cf)

Solutions

  1. Check the token's validity and scopes directly: `curl -i -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/vercel/turborepo` and inspect X-OAuth-Scopes / the response body.
  2. For a fine-grained PAT, edit it at github.com/settings/personal-access-tokens and grant `contents:read` on the target repo/org.
  3. For a classic PAT, ensure it has the `repo` scope (or at minimum `public_repo` for public repos); regenerate if expired.
  4. If the org uses SAML SSO, click 'Configure SSO' on the token page and authorize it for the org.
  5. If 403 comes with rate-limit headers (X-RateLimit-Remaining: 0), wait for the reset window or unexport the broken token so requests go out unauthenticated from CI with per-IP limits.

Example fix

# before: fine-grained PAT without contents:read -> HTTP 403
curl -i -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/vercel/turborepo
# HTTP/2 403

# after: re-create token with 'Contents: Read-only' permission
curl -i -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/vercel/turborepo
# HTTP/2 200
Defensive patterns

Strategy: validation

Validate before calling

async function tokenWorks(token: string): Promise<boolean> {
  const res = await fetch("https://api.github.com/repos/vercel/turborepo", {
    headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json" },
  });
  return res.ok; // 200 = token valid with sufficient read access
}
// run before scaffolding:
const t = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
if (t && !(await tokenWorks(t))) throw new Error("GitHub token rejected; check scopes/expiry");

Try / catch

// isUrlOk never throws — it returns false on network errors and on 401/403.
// Treat a false result as 'unavailable', and only surface auth advice when a token is set:
const ok = await isUrlOk(exampleUrl);
if (!ok) {
  const hasToken = !!(process.env.GITHUB_TOKEN || process.env.GH_TOKEN);
  console.error(hasToken ? "Example check failed — verify token scopes (repo / contents:read)" : "Example check failed — network or example name");
}

Prevention

When it happens

Trigger: fetchWithTimeout(url, {method: 'HEAD'}) on a github.com/codeload.github.com URL returns HTTP 401 (bad/expired/revoked PAT) or 403 (classic PAT missing `repo` scope, fine-grained PAT missing `contents:read`, no access to a private/SAML-enforced org, or secondary rate limit) while getGitHubToken() returns a syntactically valid token.

Common situations: Expired or revoked classic PAT still exported in the shell; fine-grained PAT created without the `contents:read` permission; classic PAT with only `public_repo` scope used for a private repo; token issued by an org with SAML enforcement that was never SSO-authorized; clock-skewed or IP-rate-limited CI runners getting 403 from api.github.com.

Related errors


AI-assisted analysis of vercel/turborepo@f9245100cf (2026-08-17). Data as JSON: /api/errors/0f08863081914c52. Report an issue: GitHub.