Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/1_EXTENSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export default defineConfig({
- `repos` lists `owner/name` repositories. The default branch is resolved at
snapshot time unless `branch` is set.
- `gitCredentials` maps each Git host to a username and the environment
variable containing its token. An unset token means clone anonymously.
variable containing its token. A server on a nondefault HTTPS port is keyed
as `host:port`. An unset token means clone anonymously.
- `snapshotName` is the stable name built by the cron and restored by the
workflow. It must match `[A-Za-z0-9_.-]+` and be unique in the workspace.

Expand Down
3 changes: 2 additions & 1 deletion src/sandbox/render/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export async function resolveHeads(profile: RepoProfile, hostCreds: RenderConfig
return Promise.all(
profile.repos.map(async ({ repository, branch, url: repoUrl, browse }) => {
const url = repoUrl ?? `https://github.com/${repository}.git`;
const host = new URL(url).hostname;
// `host` keeps a nondefault port, matching the host:port key the staged clone helper looks up.
const host = new URL(url).host;
const cred = hostCreds[host];
// The token reaches git only through the environment of this trusted process, never argv or a URL.
const envVar = `RESEARCHER_TOKEN_${host.replace(/\W/g, "_").toUpperCase()}`;
Expand Down
42 changes: 40 additions & 2 deletions test/sandbox-render.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import type { RepoManifest } from "../src/manifest.ts";
import { AbortError, RenderError, SandboxSnapshotNotFoundError, SandboxSnapshotNotReadyError } from "@renderinc/sdk";
import type { SandboxExecEvent } from "@renderinc/sdk/experimental";
import { beforeEach, describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { configureSandboxes } from "../src/sandbox/index.ts";
import { SnapshotUnavailableError } from "../src/sandbox/interface.ts";
import { ClientError } from "@renderinc/sdk";
import { SandboxClient, type SandboxApi } from "../src/sandbox/render/client.ts";
import { loadRenderConfig, type NetworkPolicy, type RenderConfig } from "../src/sandbox/render/config.ts";
import { openRenderSandbox, runInvestigation } from "../src/sandbox/render/sandbox.ts";
import { buildSnapshot, pruneSnapshots } from "../src/sandbox/render/snapshot.ts";
import { buildSnapshot, pruneSnapshots, resolveHeads } from "../src/sandbox/render/snapshot.ts";
import { runnerInput } from "./helpers.ts";
import { createLog } from "../src/log.ts";

Expand Down Expand Up @@ -238,6 +241,41 @@ describe("buildSnapshot", () => {
});
});

describe("resolveHeads", () => {
const sha = "a".repeat(40);

async function lsRemote(url: string, hostCreds: RenderConfig["hostCredentials"]) {
const dir = await mkdtemp(join(tmpdir(), "resolve-heads-"));
try {
const record = join(dir, "record");
await writeFile(join(dir, "git"), `#!/bin/sh
env | grep '^RESEARCHER_TOKEN_' > ${record}
printf 'ref: refs/heads/main\\tHEAD\\n${sha}\\tHEAD\\n'
`, { mode: 0o755 });
vi.stubEnv("PATH", `${dir}:${process.env.PATH}`);
const heads = await resolveHeads({ name: "test", repos: [{ repository: "acme/repo", url }] }, hostCreds);
return { heads, tokens: (await readFile(record, "utf8")).trim() };
} finally {
vi.unstubAllEnvs();
await rm(dir, { recursive: true, force: true });
}
}

it("selects the credential for a nondefault HTTPS port, matching the staged clone helper's host:port key", async () => {
const { heads, tokens } = await lsRemote("https://git.example:8443/acme/repo.git", {
"git.example": { username: "x", token: "default-port-token" },
"git.example:8443": { username: "x", token: "port-token" },
});
expect(heads[0]).toMatchObject({ commit: sha, branch: "main" });
expect(tokens).toBe("RESEARCHER_TOKEN_GIT_EXAMPLE_8443=port-token");
});

it("does not reuse a default-port credential for another port", async () => {
const { tokens } = await lsRemote("https://git.example:8443/acme/repo.git", { "git.example": { username: "x", token: "t" } });
expect(tokens).toBe("");
});
});

describe("pruneSnapshots", () => {
const built = { id: "snp-new", sandboxGroupId: "sbg-1" };

Expand Down
Loading