feat: release 2.7.0-rc2

This commit is contained in:
2026-10-06 15:31:51 +00:00 Unverified
parent cd9fb512cd
commit c8de9ddcba
42 changed files with 6127 additions and 593 deletions
+206 -2
View File
@@ -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 () => {