feat: release 2.7.0-rc2
This commit is contained in:
+206
-2
@@ -1,5 +1,6 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
readlink,
|
||||
rm,
|
||||
stat,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -51,6 +53,105 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe("workspace snapshots", () => {
|
||||
test("auto uses Git ignore rules even for tracked files, not .dockerignore", async () => {
|
||||
const root = await repository();
|
||||
await mkdir(join(root, "nested"));
|
||||
await writeFile(join(root, ".gitignore"), "*.secret\n.env*\nignored/\n");
|
||||
await writeFile(join(root, "nested/.gitignore"), "*.log\n!important.log\n");
|
||||
await writeFile(join(root, ".dockerignore"), "Dockerfile\nvisible.txt\n");
|
||||
await writeFile(join(root, "Dockerfile"), "FROM scratch\n");
|
||||
await writeFile(join(root, "visible.txt"), "visible");
|
||||
await writeFile(join(root, "tracked.secret"), "tracked secret");
|
||||
await writeFile(join(root, ".env.tracked"), "tracked dotenv");
|
||||
await run("git", [
|
||||
"-C",
|
||||
root,
|
||||
"add",
|
||||
"-f",
|
||||
"tracked.secret",
|
||||
".env.tracked",
|
||||
]);
|
||||
await writeFile(join(root, "untracked.secret"), "untracked secret");
|
||||
await writeFile(join(root, ".env.local"), "untracked dotenv");
|
||||
await writeFile(join(root, "nested/debug.log"), "ignored");
|
||||
await run("git", ["-C", root, "add", "-f", "nested/debug.log"]);
|
||||
await writeFile(join(root, "nested/important.log"), "included");
|
||||
|
||||
const regular = await enumerateWorkspace(root);
|
||||
const auto = await enumerateWorkspace(root, "auto");
|
||||
expect(regular.manifest.files.map((file) => file.path)).toContain(
|
||||
"tracked.secret",
|
||||
);
|
||||
expect(regular.manifest.files.map((file) => file.path)).toContain(
|
||||
".env.local",
|
||||
);
|
||||
expect(auto.manifest.files.map((file) => file.path)).toEqual([
|
||||
".dockerignore",
|
||||
".gitignore",
|
||||
"Dockerfile",
|
||||
"nested/.gitignore",
|
||||
"nested/important.log",
|
||||
"visible.txt",
|
||||
]);
|
||||
expect(auto.digest).not.toBe(regular.digest);
|
||||
expect(await enumerateWorkspace(root, "auto")).toEqual(auto);
|
||||
});
|
||||
|
||||
test("auto rejects selected dotenv and conventional credential paths", async () => {
|
||||
for (const [path, tracked] of [
|
||||
[".env", true],
|
||||
[".env.local", false],
|
||||
[".npmrc", false],
|
||||
["nested/.ssh/id_ed25519", false],
|
||||
["services/secrets/production.yaml", false],
|
||||
] as const) {
|
||||
const root = await repository();
|
||||
const file = join(root, path);
|
||||
await mkdir(join(file, ".."), { recursive: true });
|
||||
await writeFile(file, "credential");
|
||||
if (tracked) await run("git", ["-C", root, "add", "-f", path]);
|
||||
await expect(enumerateWorkspace(root, "auto")).rejects.toThrow(
|
||||
`refuses to snapshot potential credentials at ${path}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("auto does not reject ignored dotenv or safe sample files", async () => {
|
||||
const root = await repository();
|
||||
await writeFile(join(root, ".gitignore"), ".env\nignored/.env.local\n");
|
||||
await writeFile(join(root, ".env"), "ignored secret");
|
||||
await mkdir(join(root, "ignored"));
|
||||
await writeFile(join(root, "ignored/.env.local"), "ignored secret");
|
||||
await writeFile(join(root, ".env.example"), "TOKEN=replace-me");
|
||||
await writeFile(join(root, "secretary-notes.txt"), "ordinary source");
|
||||
|
||||
const snapshot = await enumerateWorkspace(root, "auto");
|
||||
expect(snapshot.manifest.files.map((file) => file.path)).toEqual([
|
||||
".env.example",
|
||||
".gitignore",
|
||||
"secretary-notes.txt",
|
||||
]);
|
||||
});
|
||||
|
||||
test("auto fails closed without a Git repository or executable", async () => {
|
||||
const root = await temporaryDirectory("kuber-auto-no-git-");
|
||||
await writeFile(join(root, "app.txt"), "app");
|
||||
await expect(enumerateWorkspace(root, "auto")).rejects.toThrow(
|
||||
/requires Git and a Git repository/,
|
||||
);
|
||||
await run("git", ["init", "-q", root]);
|
||||
const previousPath = process.env.PATH;
|
||||
try {
|
||||
process.env.PATH = "";
|
||||
await expect(enumerateWorkspace(root, "auto")).rejects.toThrow(
|
||||
/requires Git and a Git repository/,
|
||||
);
|
||||
} finally {
|
||||
if (previousPath === undefined) delete process.env.PATH;
|
||||
else process.env.PATH = previousPath;
|
||||
}
|
||||
});
|
||||
|
||||
test("snapshots Gitless directories deterministically without secrets", async () => {
|
||||
const root = await temporaryDirectory("kuber-workspace-filesystem-");
|
||||
await writeFile(join(root, "Dockerfile"), "FROM scratch\n");
|
||||
@@ -69,7 +170,10 @@ describe("workspace snapshots", () => {
|
||||
|
||||
test("works without Git and conservatively prunes ignored/generated and credential files", async () => {
|
||||
const root = await temporaryDirectory("kuber-workspace-no-git-");
|
||||
await writeFile(join(root, ".gitignore"), "local-only/\n*.generated\nsecrets/\n!secrets/keep.txt\n");
|
||||
await writeFile(
|
||||
join(root, ".gitignore"),
|
||||
"local-only/\n*.generated\nsecrets/\n!secrets/keep.txt\n",
|
||||
);
|
||||
await writeFile(join(root, "app.ts"), "source");
|
||||
await mkdir(join(root, "local-only"));
|
||||
await writeFile(join(root, "local-only", "hidden"), "secret");
|
||||
@@ -117,7 +221,8 @@ describe("workspace snapshots", () => {
|
||||
const second = await enumerateWorkspace(root);
|
||||
expect(first).toEqual(second);
|
||||
expect(first.manifest.files.map((file) => file.path)).toEqual([
|
||||
".gitignore", "source.ts",
|
||||
".gitignore",
|
||||
"source.ts",
|
||||
]);
|
||||
} finally {
|
||||
if (previousPath === undefined) delete process.env.PATH;
|
||||
@@ -212,6 +317,105 @@ describe("workspace snapshots", () => {
|
||||
await expect(enumerateWorkspace(specialRoot)).rejects.toThrow(
|
||||
"Special files",
|
||||
);
|
||||
|
||||
const autoRoot = await repository();
|
||||
await run("ln", ["-s", "../outside", join(autoRoot, "escape")]);
|
||||
await expect(enumerateWorkspace(autoRoot, "auto")).rejects.toThrow(
|
||||
"Symlink escapes workspace",
|
||||
);
|
||||
});
|
||||
|
||||
test("does not read Git-tracked files through replaced parent symlinks in either mode", async () => {
|
||||
const root = await repository();
|
||||
const outside = await temporaryDirectory("kuber-workspace-outside-");
|
||||
await mkdir(join(root, "sub"));
|
||||
await writeFile(join(root, "sub/visible.txt"), "original");
|
||||
await run("git", ["-C", root, "add", "sub/visible.txt"]);
|
||||
await rm(join(root, "sub"), { recursive: true });
|
||||
await writeFile(join(outside, "visible.txt"), "outside content");
|
||||
await symlink(outside, join(root, "sub"));
|
||||
|
||||
// Git itself may reject a tracked path beneath a symlink before the
|
||||
// parent guard runs; both outcomes must fail closed.
|
||||
for (const mode of ["default", "auto"] as const)
|
||||
await expect(enumerateWorkspace(root, mode)).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("rejects snapshot symlink chains before normalizing target components", async () => {
|
||||
const root = await repository();
|
||||
await symlink(".", join(root, "sub"));
|
||||
await symlink("sub/..", join(root, "chain"));
|
||||
|
||||
for (const mode of ["default", "auto"] as const) {
|
||||
await expect(enumerateWorkspace(root, mode)).rejects.toThrow(
|
||||
"Symlink traverses snapshot symlink: chain -> sub/..",
|
||||
);
|
||||
}
|
||||
|
||||
const blobs = new Map(
|
||||
["sub/..", "."].map((target) => {
|
||||
const data = Buffer.from(target);
|
||||
const digest =
|
||||
`sha256:${createHash("sha256").update(data).digest("hex")}` as const;
|
||||
return [digest, data] as const;
|
||||
}),
|
||||
);
|
||||
const manifest: WorkspaceManifest = {
|
||||
version: BUILD_PROTOCOL_VERSION,
|
||||
files: [
|
||||
{
|
||||
path: "chain",
|
||||
type: "symlink",
|
||||
digest: [...blobs.keys()][0]!,
|
||||
size: Buffer.byteLength("sub/.."),
|
||||
mode: 0o777,
|
||||
},
|
||||
{
|
||||
path: "sub",
|
||||
type: "symlink",
|
||||
digest: [...blobs.keys()][1]!,
|
||||
size: Buffer.byteLength("."),
|
||||
mode: 0o777,
|
||||
},
|
||||
],
|
||||
};
|
||||
const destination = join(await temporaryDirectory("kuber-chain-"), "tree");
|
||||
await expect(
|
||||
materializeWorkspace(destination, manifest, async (digest) =>
|
||||
blobs.get(digest)!,
|
||||
),
|
||||
).rejects.toThrow("Symlink traverses snapshot symlink: chain -> sub/..");
|
||||
await expect(stat(destination)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
|
||||
test("retains safe symlinks in both snapshot modes and materialization", async () => {
|
||||
const root = await repository();
|
||||
await writeFile(join(root, "app.txt"), "safe");
|
||||
await symlink(".", join(root, "sub"));
|
||||
await symlink("./app.txt", join(root, "alias"));
|
||||
|
||||
for (const mode of ["default", "auto"] as const) {
|
||||
const snapshot = await enumerateWorkspace(root, mode);
|
||||
expect(snapshot.manifest.files.map((file) => file.path)).toEqual([
|
||||
"alias",
|
||||
"app.txt",
|
||||
"sub",
|
||||
]);
|
||||
const blobs = new Map(
|
||||
snapshot.blobs.map((blob) => [blob.digest, blob.data]),
|
||||
);
|
||||
const destination = join(
|
||||
await temporaryDirectory("kuber-safe-links-"),
|
||||
"tree",
|
||||
);
|
||||
await materializeWorkspace(
|
||||
destination,
|
||||
snapshot.manifest,
|
||||
async (digest) => blobs.get(digest)!,
|
||||
);
|
||||
expect(await readlink(join(destination, "sub"))).toBe(".");
|
||||
expect(await readFile(join(destination, "alias"), "utf8")).toBe("safe");
|
||||
}
|
||||
});
|
||||
|
||||
test("allows ignored special files and prunes ignored directories", async () => {
|
||||
|
||||
Reference in New Issue
Block a user