diff --git a/docs/1_EXTENSION.md b/docs/1_EXTENSION.md index e659609..4c08829 100644 --- a/docs/1_EXTENSION.md +++ b/docs/1_EXTENSION.md @@ -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. diff --git a/src/sandbox/render/snapshot.ts b/src/sandbox/render/snapshot.ts index 53301ce..6cea91c 100644 --- a/src/sandbox/render/snapshot.ts +++ b/src/sandbox/render/snapshot.ts @@ -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()}`; diff --git a/test/sandbox-render.test.ts b/test/sandbox-render.test.ts index 350b6c8..a4603a9 100644 --- a/test/sandbox-render.test.ts +++ b/test/sandbox-render.test.ts @@ -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"; @@ -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" };