rea/tests/acceptance/setup/packageInstallWorkflow.test.ts

185 lines
6.3 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
import { z } from "zod";
import { packageHopperEnvironment } from "../../../scripts/verify-package-environment.mjs";
const workflowStepSchema = z
.object({
run: z.unknown().optional(),
name: z.unknown().optional(),
env: z.record(z.string(), z.unknown()).optional(),
})
.passthrough();
const workflowSchema = z
.object({
jobs: z.record(
z.string(),
z
.object({
steps: z.array(workflowStepSchema).default([]),
needs: z.union([z.array(z.string()), z.string()]).optional(),
})
.passthrough(),
),
})
.passthrough();
type Workflow = z.output<typeof workflowSchema>;
describe("package installation workflows", () => {
it("runs package E2E without the retired native rebuild script", async () => {
const continuousIntegration = await readWorkflow("ci.yml");
const realHopperLinux = await readWorkflow("real-hopper-linux.yml");
const realHopperMac = await readWorkflow("real-hopper.yml");
expect(workflowJob(continuousIntegration, "package-e2e")).toMatchObject({
strategy: { matrix: { os: ["ubuntu-latest", "macos-14"] } },
steps: expect.arrayContaining([{ run: "npm run verify:package" }]),
});
expect(
workflowJob(continuousIntegration, "static").steps.map(
({ run }: { readonly run?: unknown }) => run,
),
).toContain("npm run check:ci");
expect(workflowJob(continuousIntegration, "test-shard")).toMatchObject({
strategy: { matrix: { shard: ["1/4", "2/4", "3/4", "4/4"] } },
steps: expect.arrayContaining([
{ run: "npm run test:ci:shard:run -- --shard=${{ matrix.shard }}" },
]),
});
const testJob = workflowJob(continuousIntegration, "test");
expect(testJob.needs).toContain("test-shard");
expect(testJob.steps).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "Require successful test shards",
env: expect.objectContaining({
CODE_REQUIRED: "${{ needs.changes.outputs.code }}",
}),
}),
expect.objectContaining({
run: expect.stringContaining("npm run test:ci:merge"),
}),
]),
);
for (const workflow of [realHopperLinux, realHopperMac]) {
const verifyJob = workflowJob(workflow, "verify");
const verificationStep = verifyJob.steps.find(
({ run }: { readonly run?: unknown }) =>
typeof run === "string" && run.includes("npm run verify:hopper"),
);
expect(
verifyJob.steps.some(
({ run }: { readonly run?: unknown }) =>
typeof run === "string" &&
run.includes("npm install --global --ignore-scripts"),
),
).toBe(true);
expect(verificationStep).toBeDefined();
expect(verificationStep?.env).toMatchObject({
REA_HOPPER_CONFORMANCE_MANIFEST_PATH:
"${{ github.workspace }}/build/conformance/manifest.json",
});
}
for (const workflow of [
continuousIntegration,
realHopperLinux,
realHopperMac,
]) {
const commands = Object.values(workflow.jobs).flatMap((job) =>
job.steps.flatMap(({ run }: { readonly run?: unknown }) =>
typeof run === "string" ? [run] : [],
),
);
expect(commands.join("\n")).not.toContain("npm run rebuild:native");
}
});
it("verifies the package before publish and runs published canaries outside the checkout", async () => {
const release = await readWorkflow("release.yml");
const publishJob = workflowJob(release, "publish");
const publishSteps = publishJob.steps;
const verifyPackageIndex = publishSteps.findIndex(
({ run }: { readonly run?: unknown }) => run === "npm run verify:package",
);
const npmPublishIndex = publishSteps.findIndex(
({ run }: { readonly run?: unknown }) =>
run === "npm publish --access public",
);
expect(verifyPackageIndex).toBeGreaterThanOrEqual(0);
expect(npmPublishIndex).toBeGreaterThan(verifyPackageIndex);
const publishedCanary = publishSteps.find(
({ name }: { readonly name?: unknown }) =>
name === "Verify published CLI from npm",
);
expect(publishedCanary?.run).toContain(
"scripts/verify-published-package.mjs",
);
const publisherJob = workflowJob(release, "publish-mcp");
const publisherSteps = publisherJob.steps;
expect(publisherJob).toMatchObject({
needs: ["release-please", "publish"],
permissions: { contents: "read", "id-token": "write" },
});
expect(
publisherSteps.map(({ run }: { readonly run?: unknown }) => run),
).toEqual(
expect.arrayContaining([
"./mcp-publisher validate",
"./mcp-publisher login github-oidc",
"./mcp-publisher publish",
]),
);
expect(
publisherSteps.find(
({ name }: { readonly name?: unknown }) =>
name === "Install MCP Registry publisher",
)?.env,
).toMatchObject({
MCP_PUBLISHER_VERSION: "v1.8.0",
MCP_PUBLISHER_SHA256_LINUX_AMD64: expect.any(String),
});
});
it("uses direct Node ownership on macOS and Windows", () => {
const root = "/tmp/rea-package";
const expected = {
HOPPER_LAUNCHER_PATH: process.execPath,
HOPPER_LOADER_ARGS_JSON: JSON.stringify([
`${root}/tests/fixtures/fakeLauncher.mjs`,
]),
};
expect(packageHopperEnvironment(root, "darwin")).toEqual(expected);
expect(packageHopperEnvironment(root, "win32")).toEqual(expected);
expect(packageHopperEnvironment(root, "linux")).toEqual({
HOPPER_LAUNCHER_PATH: "/bin/sh",
HOPPER_LOADER_ARGS_JSON: JSON.stringify([
"-c",
'node_path=$1; shift; "$node_path" "$@"',
"rea-package-hopper",
process.execPath,
`${root}/tests/fixtures/fakeLauncher.mjs`,
]),
});
});
});
async function readWorkflow(path: string) {
const yaml = await readFile(
new URL(`../../../.github/workflows/${path}`, import.meta.url),
"utf8",
);
return workflowSchema.parse(parse(yaml));
}
const workflowJob = (workflow: Workflow, name: string) => {
const value = workflow.jobs[name];
if (value === undefined) throw new Error(`Missing workflow job: ${name}`);
return value;
};