fix(install): honor ambient GitHub tokens; e2e onboards before service test

- install --ref resolves refs via anonymous api.github.com, which 403s from
  rate-limited shared IPs (CI runners, corporate NAT). Use GH_TOKEN/GITHUB_TOKEN
  when present, passed through a 0600 curl --config file so the token never
  appears in process args.
- e2e: run the real quickstart (onboard --yes --install-service) before the
  service assertions, and capture install.sh guard output before grep so
  pipefail does not eat the expected nonzero exit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
DottaandClaude Fable 5 committed 2026-07-31 18:37:31 +00:00
1 parent a2394ce0d9
commit 8699e457e0
2 files changed
+31 -6

No files matched your search

+23 -2
View File
@@ -1,4 +1,5 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
@@ -136,8 +137,28 @@ export function resolveGitInstallRequest(options: InstallOptions): { repo: strin
return { repo, ref, pinned: /^[0-9a-f]{7,40}$/i.test(ref) };
}
async function runGitHubCurl(
args: string[],
runCommand: CommandRunner,
options?: Parameters<CommandRunner>[2],
): Promise<{ stdout: string; stderr: string }> {
// Anonymous GitHub requests are rate-limited per source IP (CI runners and
// corporate NAT exhaust the shared quota); honor an ambient token when present.
// The token travels via a curl --config file so it never appears in process args.
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN;
if (!token) return runCommand("curl", args, options);
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "paperclipai-gh-"));
const configFile = path.join(configDir, "headers");
try {
fs.writeFileSync(configFile, `header = "Authorization: Bearer ${token}"\n`, { mode: 0o600 });
return await runCommand("curl", ["--config", configFile, ...args], options);
} finally {
fs.rmSync(configDir, { recursive: true, force: true });
}
}
export async function resolveGitHubRef(repo: string, ref: string, runCommand: CommandRunner): Promise<string> {
const result = await runCommand("curl", ["--fail", "--silent", "--show-error", "--location", "--header", "Accept: application/vnd.github+json", "--header", "User-Agent: paperclipai-install", `https://api.github.com/repos/${repo}/commits/${encodeURIComponent(ref)}`], { maxBuffer: 4 * 1024 * 1024 });
const result = await runGitHubCurl(["--fail", "--silent", "--show-error", "--location", "--header", "Accept: application/vnd.github+json", "--header", "User-Agent: paperclipai-install", `https://api.github.com/repos/${repo}/commits/${encodeURIComponent(ref)}`], runCommand, { maxBuffer: 4 * 1024 * 1024 });
let sha: unknown;
try { sha = (JSON.parse(result.stdout) as { sha?: unknown }).sha; } catch { throw new Error(`GitHub returned an invalid response while resolving ${repo}@${ref}.`); }
if (typeof sha !== "string" || !/^[0-9a-f]{40}$/i.test(sha)) throw new Error(`GitHub did not return a full commit SHA for ${repo}@${ref}.`);
@@ -251,7 +272,7 @@ export async function installGitPayload(repo: string, sha: string, runCommand: C
const buildEnv = (extra: NodeJS.ProcessEnv = {}) =>
gitBuildEnv({ PATH: [pnpmShimDir, process.env.PATH].filter(Boolean).join(path.delimiter), ...extra });
try {
await runCommand("curl", ["--fail", "--silent", "--show-error", "--location", "--output", archivePath, `https://codeload.github.com/${repo}/tar.gz/${sha}`], { maxBuffer: 4 * 1024 * 1024 });
await runGitHubCurl(["--fail", "--silent", "--show-error", "--location", "--output", archivePath, `https://codeload.github.com/${repo}/tar.gz/${sha}`], runCommand, { maxBuffer: 4 * 1024 * 1024 });
await runCommand("tar", ["-xzf", archivePath, "--strip-components=1", "-C", checkoutPath], { maxBuffer: 4 * 1024 * 1024 });
await runCommand("corepack", ["enable", "pnpm", "--install-directory", pnpmShimDir], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 4 * 1024 * 1024 });
await runCommand("corepack", ["pnpm", "install", "--frozen-lockfile"], { cwd: checkoutPath, env: buildEnv(), maxBuffer: 32 * 1024 * 1024 });
+8 -4
View File
@@ -177,10 +177,11 @@ else
skip_ "8 service lifecycle" "no systemd user bus at /run/user/$(id -u)/bus"
else
note "8. service lifecycle ($(uname -s): systemd/launchd)"
if shim service install --json; then
pass "8a service install exits 0"
# Real quickstart path: onboard with defaults, then install + start the service.
if shim onboard --yes --install-service; then
pass "8a onboard --yes --install-service exits 0"
else
fail_ "8a service install exits 0"
fail_ "8a onboard --yes --install-service exits 0"
fi
DEADLINE=$(( $(date +%s) + E2E_SERVICE_TIMEOUT_SECS ))
ACTIVE=0
@@ -203,9 +204,12 @@ else
fi
note "9. installer script guardrails (from the bootstrap checkout)"
if bash "$BOOT/scripts/install.sh" --ref deadbeef 2>&1 | grep -qi "not supported"; then
# Capture first: under pipefail, install.sh's expected exit 1 would fail the pipeline.
GUARD_OUT="$(bash "$BOOT/scripts/install.sh" --ref deadbeef 2>&1 || true)"
if echo "$GUARD_OUT" | grep -qi "not supported"; then
pass "9a install.sh rejects --ref with guidance to npx path"
else
echo "$GUARD_OUT" | tail -3
fail_ "9a install.sh rejects --ref"
fi