paperclipai/paperclip · error · Error

GitHub Actions read failed

Error message

GitHub Actions read failed (HTTP ${response.status}).

What it means

When GitHub returns a non-OK status that is neither transient (408/425/429/500/502/503/504) nor a rate-limited 403 — or when retries are exhausted — the reader fails immediately with the HTTP status. A wrong token or a permissions problem should fail at once instead of waiting out retries.

Solutions

  1. Regenerate/export GITHUB_TOKEN as a classic PAT with the repo scope or a fine-grained PAT with Actions: read on paperclipai/paperclip.
  2. Read the status in the message: 401/403 -> token validity/permissions; 404 -> token cannot see the repo/workflow.
  3. Confirm with: curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/repos/paperclipai/paperclip/actions/workflows/cloud-readiness.yml
  4. If it was a 403 that looked like rate limiting but lacked headers, wait for the rate-limit window and check x-ratelimit-reset.
  5. Rerun the release poll once the token/status issue is fixed.

Example fix

// before: token without Actions read
export GITHUB_TOKEN=ghp_only_contents_read
# after: classic PAT with repo scope or fine-grained PAT with Actions: read
export GITHUB_TOKEN=ghp_with_actions_read
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: confirm the token can read Actions for the repo
const res = await fetch('https://api.github.com/repos/paperclipai/paperclip/actions/workflows/cloud-readiness.yml', { headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } });
if (res.status === 401 || res.status === 403 || res.status === 404) throw new Error(`Token cannot read Actions (HTTP ${res.status})`);

Try / catch

try {
  const proof = await waitForSourceVerification(sha, { api, timeoutMs });
} catch (e) {
  const m = e.message.match(/GitHub Actions read failed \(HTTP (\d+)\)/);
  if (m) {
    const hints = { 401: 'invalid/expired token', 403: 'token lacks Actions read or is blocked', 404: 'token cannot see the repo' };
    console.error(`Fix token: ${hints[m[1]] ?? 'unexpected status ' + m[1]}`);
  }
}

Prevention

When it happens

Trigger: Any GET to api.github.com from createActionsReader returning e.g. 401 (bad/expired GITHUB_TOKEN), 403 without retry-after/x-ratelimit-remaining:0 headers (token lacks Actions read), 404 (repository or workflow not visible to the token), when retryable is false, or a transient status on the final attempt.

Common situations: GITHUB_TOKEN set to a PAT without the Actions:read permission; token from the wrong account/org; fine-grained PAT not granted to paperclipai/paperclip; token revoked or expired; secondary 403 that lacks rate-limit headers being treated as fatal.

Related errors


AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18). Data as JSON: /api/errors/b9ecefa148ebe62b. Report an issue: GitHub.

Appendix: source

Thrown at scripts/cloud-source-verification.mjs:130

        if (!retryable) throw new Error(`GitHub Actions read failed: ${cause.message}`);
        log(`GitHub Actions read failed (${cause.message}); retrying (${attempt}/${attempts - 1}).`);
        await pause();
        continue;
      }
      if (response.ok) {
        try {
          // A 200 whose body is truncated or undecodable is a transport
          // failure like any other, so it belongs inside the retry.
          return await response.json();
        } catch (cause) {
          if (!retryable) throw new Error(`GitHub Actions read failed: ${cause.message}`);
          log(`GitHub Actions read body failed (${cause.message}); retrying (${attempt}/${attempts - 1}).`);
          await pause();
          continue;
        }
      }
      if (!retryable || !(TRANSIENT_READ_STATUSES.has(response.status) || rateLimited(response))) {
        throw new Error(`GitHub Actions read failed (HTTP ${response.status}).`);
      }
      log(`GitHub Actions read failed (HTTP ${response.status}); retrying (${attempt}/${attempts - 1}).`);
      await pause(response);
    }
  };
}

export async function waitForSourceVerification(sha, {
  api, now = Date.now, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
  timeoutMs = 45 * 60_000, intervalMs = 30_000, log = console.log,
} = {}) {
  assertSha(sha);
  const deadline = now() + timeoutMs;
  log(`Waiting for ${sourceVerificationJob} for ${sha}.`);
  while (now() < deadline) {
    const result = await readSourceVerification(sha, api);
    if (result) return result;
    const remaining = deadline - now();

View on GitHub (pinned to 3f1d897a7c)