184 lines
5.6 KiB
TypeScript
184 lines
5.6 KiB
TypeScript
import { readdirSync, readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const repoRoot = resolve(__dirname, "../../..");
|
|
|
|
function readRepoFile(path: string): string {
|
|
return readFileSync(resolve(repoRoot, path), "utf8");
|
|
}
|
|
|
|
function workflowFiles(): string[] {
|
|
const workflowDir = resolve(repoRoot, ".github/workflows");
|
|
return readdirSync(workflowDir)
|
|
.filter((name) => name.endsWith(".yml") || name.endsWith(".yaml"))
|
|
.map((name) => `.github/workflows/${name}`);
|
|
}
|
|
|
|
function extractDockerPnpmVersion(dockerfile: string): string | undefined {
|
|
return dockerfile.match(/^ARG PNPM_VERSION=([0-9]+\.[0-9]+\.[0-9]+)$/m)?.[1];
|
|
}
|
|
|
|
function extractPackageManagerPnpmVersion(packageJSON: string): string {
|
|
const packageManager = (JSON.parse(packageJSON) as { packageManager?: string }).packageManager;
|
|
const version = packageManager?.match(/^pnpm@([0-9]+\.[0-9]+\.[0-9]+)$/)?.[1];
|
|
expect(version, "apps/package.json: packageManager must pin pnpm").toBeDefined();
|
|
if (version === undefined) {
|
|
throw new Error("apps/package.json: packageManager must pin pnpm");
|
|
}
|
|
return version;
|
|
}
|
|
|
|
function indentation(line: string): number {
|
|
return line.search(/\S/);
|
|
}
|
|
|
|
function isBlankOrComment(line: string): boolean {
|
|
const trimmed = line.trim();
|
|
return trimmed.length === 0 || trimmed.startsWith("#");
|
|
}
|
|
|
|
function parseVersionLine(line: string): string | undefined {
|
|
const match = line.match(/^\s*version:\s*(?:"([^"]+)"|'([^']+)'|([^"'\s#]+))\s*(?:#.*)?$/);
|
|
return match?.[1] ?? match?.[2] ?? match?.[3];
|
|
}
|
|
|
|
function findVersionInWithBlock(
|
|
lines: string[],
|
|
start: number,
|
|
withIndent: number,
|
|
): string | undefined {
|
|
for (let index = start; index < lines.length; index += 1) {
|
|
const line = lines[index];
|
|
if (isBlankOrComment(line)) {
|
|
continue;
|
|
}
|
|
if (indentation(line) <= withIndent) {
|
|
return undefined;
|
|
}
|
|
|
|
const version = parseVersionLine(line);
|
|
if (version !== undefined) {
|
|
return version;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function findPnpmSetupVersion(
|
|
lines: string[],
|
|
start: number,
|
|
stepIndent: number,
|
|
): string | undefined {
|
|
for (let index = start; index < lines.length; index += 1) {
|
|
const line = lines[index];
|
|
if (isBlankOrComment(line)) {
|
|
continue;
|
|
}
|
|
if (indentation(line) <= stepIndent) {
|
|
return undefined;
|
|
}
|
|
|
|
const withMatch = line.match(/^(\s*)with:\s*(?:#.*)?$/);
|
|
if (withMatch !== null) {
|
|
return findVersionInWithBlock(lines, index + 1, withMatch[1].length);
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function matchPnpmSetupUsesLine(line: string): RegExpMatchArray | null {
|
|
return line.match(/^(\s*)(?:-\s*)?uses:\s*["']?pnpm\/action-setup@v\d+["']?\s*(?:#.*)?$/);
|
|
}
|
|
|
|
function findStepIndent(lines: string[], usesLineIndex: number, usesIndent: number): number {
|
|
if (lines[usesLineIndex].trimStart().startsWith("- ")) {
|
|
return usesIndent;
|
|
}
|
|
|
|
for (let index = usesLineIndex - 1; index >= 0; index -= 1) {
|
|
const line = lines[index];
|
|
if (isBlankOrComment(line)) {
|
|
continue;
|
|
}
|
|
|
|
const lineIndent = indentation(line);
|
|
if (lineIndent < usesIndent && line.slice(lineIndent).startsWith("- ")) {
|
|
return lineIndent;
|
|
}
|
|
}
|
|
|
|
return Math.max(0, usesIndent - 2);
|
|
}
|
|
|
|
function extractWorkflowPnpmVersions(workflow: string): Array<string | undefined> {
|
|
const lines = workflow.split(/\r?\n/);
|
|
const versions: Array<string | undefined> = [];
|
|
|
|
for (let index = 0; index < lines.length; index += 1) {
|
|
const line = lines[index];
|
|
const setupMatch = matchPnpmSetupUsesLine(line);
|
|
if (setupMatch !== null) {
|
|
const stepIndent = findStepIndent(lines, index, setupMatch[1].length);
|
|
versions.push(findPnpmSetupVersion(lines, index + 1, stepIndent));
|
|
}
|
|
}
|
|
|
|
return versions;
|
|
}
|
|
|
|
function assertWorkflowPnpmVersions(file: string, expectedVersion: string): number {
|
|
const versions = extractWorkflowPnpmVersions(readRepoFile(file));
|
|
for (const version of versions) {
|
|
if (version === undefined) {
|
|
expect(version, `${file}: pnpm/action-setup step is missing a version pin`).toBeDefined();
|
|
continue;
|
|
}
|
|
|
|
expect(version, `${file}: pnpm/action-setup version must match apps/package.json`).toBe(
|
|
expectedVersion,
|
|
);
|
|
}
|
|
|
|
return versions.length;
|
|
}
|
|
|
|
describe("release package manager version", () => {
|
|
it("pins pnpm consistently for Docker and GitHub Actions", () => {
|
|
const packagePnpmVersion = extractPackageManagerPnpmVersion(readRepoFile("apps/package.json"));
|
|
const dockerfile = readRepoFile("Dockerfile");
|
|
const dockerPnpmVersion = extractDockerPnpmVersion(dockerfile);
|
|
|
|
expect(dockerfile).not.toContain("pnpm@latest");
|
|
if (dockerPnpmVersion !== undefined) {
|
|
expect(dockerPnpmVersion, "Dockerfile: PNPM_VERSION must match apps/package.json").toBe(
|
|
packagePnpmVersion,
|
|
);
|
|
}
|
|
|
|
const workflowSetupCount = workflowFiles().reduce(
|
|
(count, file) => count + assertWorkflowPnpmVersions(file, packagePnpmVersion),
|
|
0,
|
|
);
|
|
expect(workflowSetupCount).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("release npm publishing", () => {
|
|
it("skips packages that are already published on npm", () => {
|
|
const script = readRepoFile("scripts/release/publish-npm.sh");
|
|
|
|
expect(script).toContain('npm view "${pkg}@${VERSION}" version --silent');
|
|
expect(script).toMatch(
|
|
/if package_already_published "\$pkg"; then\s+record_already_published "\$pkg"\s+continue\s+fi/,
|
|
);
|
|
expect(script).toMatch(
|
|
/if package_already_published "kandev"; then\s+record_already_published "kandev"/,
|
|
);
|
|
expect(script).toContain("EPUBLISHCONFLICT");
|
|
expect(script).toContain("treated as idempotent success");
|
|
});
|
|
});
|