fix(release): attest release plan parser tree

This commit is contained in:
Vincent Koc
2026-08-21 07:53:32 -07:00
parent 449b8c5c5a
commit 242d85732d
2 changed files with 221 additions and 11 deletions
+106 -9
View File
@@ -1,7 +1,16 @@
#!/usr/bin/env node
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync } from "node:fs";
import {
existsSync,
lstatSync,
mkdtempSync,
mkdirSync,
readFileSync,
readdirSync,
realpathSync,
rmSync,
} from "node:fs";
import { builtinModules, createRequire } from "node:module";
import { tmpdir } from "node:os";
import { dirname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
@@ -81,7 +90,10 @@ const TOOLING_LOCKFILE_PATH = "pnpm-lock.yaml";
const YAML_PACKAGE_VERSION = "2.9.0";
const YAML_PACKAGE_INTEGRITY =
"sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==";
const YAML_PACKAGE_JSON_SHA256 = "20b8b197cbd10dad245d45e463dfe58e4c8c25a47e24bc4256ad9ab58bf35683";
const YAML_PACKAGE_TREE_SHA256 = "610ccacfe592d226ac1eb04842d1f591c5381f2a68b9f785643101d10db52c27";
const YAML_PACKAGE_MAX_FILES = 512;
const YAML_PACKAGE_MAX_ENTRIES = 1024;
const YAML_PACKAGE_MAX_BYTES = 4 * 1024 * 1024;
const BUILTIN_IMPORTS = new Set([
...builtinModules,
...builtinModules.map((specifier) => `node:${specifier}`),
@@ -313,6 +325,87 @@ function verifyYamlLockfile(lockfileText: string) {
}
}
function assertYamlPackagePath(path: string) {
if (
!path ||
!/^[\x20-\x7e]+$/u.test(path) ||
path.includes("\\") ||
posix.isAbsolute(path) ||
path.split("/").some((component) => component === "." || component === "..")
) {
throw new Error(`installed yaml package contains an unsafe path: ${JSON.stringify(path)}`);
}
}
function verifyInstalledYamlPackageTree(packageRoot: string) {
const rootStat = lstatSync(packageRoot);
if (!rootStat.isDirectory()) {
throw new Error("installed yaml package root must be a directory");
}
const records: string[] = [];
let entryCount = 0;
let fileCount = 0;
let totalBytes = 0;
const walk = (directory: string, relativeDirectory = "") => {
for (const name of readdirSync(directory).toSorted(compareAscii)) {
const relativePath = relativeDirectory ? `${relativeDirectory}/${name}` : name;
assertYamlPackagePath(relativePath);
entryCount += 1;
if (entryCount > YAML_PACKAGE_MAX_ENTRIES) {
throw new Error(
`installed yaml package exceeds ${YAML_PACKAGE_MAX_ENTRIES} filesystem entries`,
);
}
const absolutePath = join(directory, name);
const stat = lstatSync(absolutePath);
if (stat.isSymbolicLink()) {
throw new Error(`installed yaml package must not contain symbolic links: ${relativePath}`);
}
if (stat.isDirectory()) {
records.push(JSON.stringify(["directory", relativePath]));
walk(absolutePath, relativePath);
continue;
}
if (!stat.isFile()) {
throw new Error(`installed yaml package must contain only directories and files`);
}
if (stat.nlink !== 1) {
throw new Error(`installed yaml package files must have one link: ${relativePath}`);
}
fileCount += 1;
if (fileCount > YAML_PACKAGE_MAX_FILES) {
throw new Error(`installed yaml package exceeds ${YAML_PACKAGE_MAX_FILES} files`);
}
totalBytes += stat.size;
if (totalBytes > YAML_PACKAGE_MAX_BYTES) {
throw new Error(`installed yaml package exceeds ${YAML_PACKAGE_MAX_BYTES} bytes`);
}
const bytes = readFileSync(absolutePath);
if (bytes.byteLength !== stat.size) {
throw new Error(`installed yaml package file changed while being read: ${relativePath}`);
}
records.push(
JSON.stringify([
"file",
relativePath,
bytes.byteLength,
createHash("sha256").update(bytes).digest("hex"),
]),
);
}
};
walk(packageRoot);
const manifest = `${records.toSorted(compareAscii).join("\n")}\n`;
const digest = createHash("sha256").update(manifest, "ascii").digest("hex");
if (digest !== YAML_PACKAGE_TREE_SHA256) {
throw new Error(`installed yaml package tree must match yaml@${YAML_PACKAGE_VERSION}`);
}
}
function loadVerifiedYamlParser(repoRoot: string, toolingSha: string): ParseYaml {
const packageJsonBytes = readVerifiedToolingRootBytes(
repoRoot,
@@ -335,7 +428,7 @@ function loadVerifiedYamlParser(repoRoot: string, toolingSha: string): ParseYaml
const toolingRequire = createRequire(resolve(EXECUTION_ROOT, TOOLING_PACKAGE_JSON_PATH));
const packageJsonPath = realpathSync(toolingRequire.resolve("yaml/package.json"));
const packageRoot = dirname(packageJsonPath);
const packageRoot = realpathSync(dirname(packageJsonPath));
const modulePath = realpathSync(toolingRequire.resolve("yaml"));
const moduleRelativePath = relative(packageRoot, modulePath);
if (
@@ -345,13 +438,17 @@ function loadVerifiedYamlParser(repoRoot: string, toolingSha: string): ParseYaml
) {
throw new Error("resolved yaml module must be owned by its installed package");
}
const installedPackageJsonBytes = readFileSync(packageJsonPath);
const installedPackageJsonSha = createHash("sha256")
.update(installedPackageJsonBytes)
.digest("hex");
if (installedPackageJsonSha !== YAML_PACKAGE_JSON_SHA256) {
throw new Error(`installed yaml package.json must match yaml@${YAML_PACKAGE_VERSION}`);
const packageJsonRelativePath = relative(packageRoot, packageJsonPath);
if (
packageJsonRelativePath === ".." ||
packageJsonRelativePath.startsWith(`..${sep}`) ||
isAbsolute(packageJsonRelativePath)
) {
throw new Error("resolved yaml package.json must be owned by its installed package");
}
verifyInstalledYamlPackageTree(packageRoot);
const installedPackageJsonBytes = readFileSync(packageJsonPath);
const installedPackageJson = JSON.parse(installedPackageJsonBytes.toString("utf8")) as {
name?: unknown;
version?: unknown;
+115 -2
View File
@@ -1,5 +1,15 @@
import { execFileSync, spawnSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import {
cpSync,
existsSync,
mkdirSync,
readFileSync,
realpathSync,
rmSync,
symlinkSync,
unlinkSync,
writeFileSync,
} from "node:fs";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
@@ -266,6 +276,50 @@ function trustedToolingGh(toolingFullRef: string, toolingSha: string) {
};
}
function runYamlPackageSubprocess(
mutate?: (params: { packageRoot: string; sentinelPath: string }) => void,
) {
const fixture = createFixtureRepo();
const packageRoot = join(fixture.root, "node_modules/yaml");
cpSync(realpathSync(resolve("node_modules/yaml")), packageRoot, { recursive: true });
const sentinelPath = join(fixture.root, "yaml-executed");
mutate?.({ packageRoot, sentinelPath });
writeFixture(
fixture.root,
"yaml-package-harness.mts",
`
import { produceReleasePlan } from "./scripts/release-plan-producer.mts";
const toolingFullRef = ${JSON.stringify(fixture.toolingFullRef)};
const toolingSha = ${JSON.stringify(fixture.toolingSha)};
produceReleasePlan({
repoRoot: ${JSON.stringify(fixture.root)},
intent: "publish",
candidateSha: ${JSON.stringify(fixture.candidateSha)},
candidateRef: ${JSON.stringify(fixture.candidateRef)},
toolingSha,
toolingFullRef,
runGh: () => JSON.stringify({
ref: toolingFullRef,
object: { type: "commit", sha: toolingSha },
}),
});
`,
);
const tsxImport = pathToFileURL(createRequire(import.meta.url).resolve("tsx")).href;
return {
result: spawnSync(
process.execPath,
["--import", tsxImport, join(fixture.root, "yaml-package-harness.mts")],
{
cwd: fixture.root,
encoding: "utf8",
},
),
sentinelPath,
};
}
describe("release plan producer", () => {
it("derives purpose, profile, tag, and soak from the canonical version parser", () => {
expect(deriveReleasePlanPolicy("publish", "2026.8.1-beta.2")).toEqual({
@@ -603,7 +657,66 @@ produceReleasePlan({
);
expect(result.status).toBe(1);
expect(result.stderr).toContain("installed yaml package.json must match yaml@2.9.0");
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
});
it("accepts the complete installed yaml package tree", () => {
const { result } = runYamlPackageSubprocess();
expect(result.stderr).toBe("");
expect(result.status).toBe(0);
});
it("rejects a changed yaml entry before executing it", () => {
const { result, sentinelPath } = runYamlPackageSubprocess(({ packageRoot, sentinelPath }) => {
const entryPath = join(packageRoot, "dist/index.js");
writeFileSync(
entryPath,
`require("node:fs").writeFileSync(${JSON.stringify(sentinelPath)}, "executed");\n${readFileSync(entryPath, "utf8")}`,
);
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
expect(existsSync(sentinelPath)).toBe(false);
});
it("rejects a changed yaml transitive module before execution", () => {
const { result, sentinelPath } = runYamlPackageSubprocess(({ packageRoot, sentinelPath }) => {
const transitivePath = join(packageRoot, "dist/public-api.js");
writeFileSync(
transitivePath,
`require("node:fs").writeFileSync(${JSON.stringify(sentinelPath)}, "executed");\n${readFileSync(transitivePath, "utf8")}`,
);
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
expect(existsSync(sentinelPath)).toBe(false);
});
it("rejects internal yaml package symlinks", () => {
const { result } = runYamlPackageSubprocess(({ packageRoot }) => {
const transitivePath = join(packageRoot, "dist/public-api.js");
unlinkSync(transitivePath);
symlinkSync("index.js", transitivePath);
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("installed yaml package must not contain symbolic links");
});
it("rejects extra yaml package files", () => {
const { result } = runYamlPackageSubprocess(({ packageRoot }) => {
writeFileSync(join(packageRoot, "unexpected.js"), "module.exports = {};\n");
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
});
it("rejects missing yaml transitive files through tree attestation", () => {
const { result } = runYamlPackageSubprocess(({ packageRoot }) => {
rmSync(join(packageRoot, "dist/public-api.js"));
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("installed yaml package tree must match yaml@2.9.0");
expect(result.stderr).not.toContain("MODULE_NOT_FOUND");
});
it("rejects malformed publishable plugins while producing the plan", () => {