ci(runner): reuse Daytona images for test-only changes
This commit is contained in:
parent
5b71c4c49a
commit
6b03239e16
|
|
@ -6,7 +6,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|||
const repositoryRoot = path.resolve(import.meta.dirname, "../..");
|
||||
|
||||
export const DAYTONA_IMAGE_CONTENT_SCHEMA =
|
||||
"paperclip-daytona-runner-image-content/v3";
|
||||
"paperclip-daytona-runner-image-content/v4";
|
||||
export const DAYTONA_IMAGE_PLATFORM = "linux/amd64";
|
||||
export const DAYTONA_IMAGE_DOCKERFILE_PATH = "docker/daytona-runner/Dockerfile";
|
||||
|
||||
|
|
@ -27,11 +27,30 @@ export const DAYTONA_IMAGE_INPUT_PATHS = [
|
|||
"packages/paperclip-runner",
|
||||
] as const;
|
||||
|
||||
const ignoredDirectoryPaths = new Set([
|
||||
const ignoredGeneratedDirectoryPaths = new Set([
|
||||
"packages/paperclip-runner/dist",
|
||||
"packages/paperclip-runner/runner/target",
|
||||
]);
|
||||
|
||||
// These paths may be copied into Docker's provider-build context, but they do
|
||||
// not contribute to the release runnerd binary or the executable/digested
|
||||
// provider-pack runtime payload. Keep the exclusions narrow and package-scoped:
|
||||
// unknown non-documentation files remain hashed by default, so a newly
|
||||
// introduced runtime input fails safe by invalidating the image key.
|
||||
const ignoredRunnerDevelopmentDirectoryPaths = new Set([
|
||||
"packages/paperclip-runner/devtools",
|
||||
"packages/paperclip-runner/docs",
|
||||
"packages/paperclip-runner/examples",
|
||||
"packages/paperclip-runner/test",
|
||||
"packages/paperclip-runner/test-fixtures",
|
||||
"packages/paperclip-runner/test-support",
|
||||
]);
|
||||
|
||||
const runnerDocumentationFilePattern = /\.md$/;
|
||||
const runnerTestFilePattern = /\.(?:spec|test)\.(?:[cm]?[jt]sx?)$/;
|
||||
const runnerRustIntegrationTestPathPattern =
|
||||
/^packages\/paperclip-runner\/runner\/crates\/[^/]+\/tests(?:\/|$)/;
|
||||
|
||||
export interface DaytonaImageContentOptions {
|
||||
repositoryRoot?: string;
|
||||
inputPaths?: readonly string[];
|
||||
|
|
@ -49,10 +68,20 @@ function normalizedRelativePath(value: string): string {
|
|||
}
|
||||
|
||||
function shouldIgnore(relativePath: string): boolean {
|
||||
if (ignoredDirectoryPaths.has(relativePath)) return true;
|
||||
if (ignoredGeneratedDirectoryPaths.has(relativePath)) return true;
|
||||
return relativePath.split("/").includes("node_modules");
|
||||
}
|
||||
|
||||
function shouldIgnoreRunnerDevelopmentInput(relativePath: string): boolean {
|
||||
if (!relativePath.startsWith("packages/paperclip-runner/")) return false;
|
||||
if (ignoredRunnerDevelopmentDirectoryPaths.has(relativePath)) return true;
|
||||
return (
|
||||
runnerDocumentationFilePattern.test(relativePath) ||
|
||||
runnerTestFilePattern.test(relativePath) ||
|
||||
runnerRustIntegrationTestPathPattern.test(relativePath)
|
||||
);
|
||||
}
|
||||
|
||||
function updateRecord(
|
||||
hash: ReturnType<typeof createHash>,
|
||||
kind: string,
|
||||
|
|
@ -160,7 +189,12 @@ async function hashEntry(
|
|||
relativePath: string,
|
||||
): Promise<void> {
|
||||
const normalizedPath = normalizedRelativePath(relativePath);
|
||||
if (shouldIgnore(normalizedPath)) return;
|
||||
if (
|
||||
shouldIgnore(normalizedPath) ||
|
||||
shouldIgnoreRunnerDevelopmentInput(normalizedPath)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(root, relativePath);
|
||||
const relativeFromRoot = path.relative(root, absolutePath);
|
||||
|
|
|
|||
|
|
@ -177,6 +177,102 @@ describe("runner E2E Daytona image contract", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("reuses the image for runner-only tests and documentation", async () => {
|
||||
const root = await mkdtemp(
|
||||
path.join(tmpdir(), "paperclip-daytona-runner-development-inputs-"),
|
||||
);
|
||||
const options = {
|
||||
repositoryRoot: root,
|
||||
inputPaths: ["packages/paperclip-runner"],
|
||||
baseImages: [`example.test/base:1@sha256:${"a".repeat(64)}`],
|
||||
frontendDigest: `sha256:${"c".repeat(64)}`,
|
||||
} as const;
|
||||
try {
|
||||
const runnerRoot = path.join(root, "packages/paperclip-runner");
|
||||
await mkdir(path.join(runnerRoot, "src/live"), { recursive: true });
|
||||
await mkdir(path.join(runnerRoot, "docs"), { recursive: true });
|
||||
await mkdir(path.join(runnerRoot, "spec"), { recursive: true });
|
||||
await mkdir(path.join(runnerRoot, "test-fixtures"), { recursive: true });
|
||||
await mkdir(path.join(runnerRoot, "runner/crates/runner-core/src"), {
|
||||
recursive: true,
|
||||
});
|
||||
await mkdir(path.join(runnerRoot, "runner/crates/runner-core/tests"), {
|
||||
recursive: true,
|
||||
});
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "src/live/transport.ts"),
|
||||
"export const runtime = 'one';\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "runner/crates/runner-core/src/lib.rs"),
|
||||
'pub const RUNTIME: &str = "one";\n',
|
||||
);
|
||||
await writeFile(path.join(runnerRoot, "README.md"), "first readme\n");
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "docs/local-runner.md"),
|
||||
"first documentation\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "spec/architecture.md"),
|
||||
"first architecture note\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "src/live/transport.test.ts"),
|
||||
"first TypeScript test\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "test-fixtures/provider.json"),
|
||||
'{"fixture":"one"}\n',
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "runner/crates/runner-core/tests/recovery.rs"),
|
||||
"// first Rust integration test\n",
|
||||
);
|
||||
|
||||
const baseline = await computeDaytonaImageContentId(options);
|
||||
await writeFile(path.join(runnerRoot, "README.md"), "second readme\n");
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "docs/local-runner.md"),
|
||||
"second documentation\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "spec/architecture.md"),
|
||||
"second architecture note\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "src/live/transport.test.ts"),
|
||||
"second TypeScript test\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "test-fixtures/provider.json"),
|
||||
'{"fixture":"two"}\n',
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "runner/crates/runner-core/tests/recovery.rs"),
|
||||
"// second Rust integration test\n",
|
||||
);
|
||||
expect(await computeDaytonaImageContentId(options)).toBe(baseline);
|
||||
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "src/live/transport.ts"),
|
||||
"export const runtime = 'two';\n",
|
||||
);
|
||||
expect(await computeDaytonaImageContentId(options)).not.toBe(baseline);
|
||||
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "src/live/transport.ts"),
|
||||
"export const runtime = 'one';\n",
|
||||
);
|
||||
await writeFile(
|
||||
path.join(runnerRoot, "runner/crates/runner-core/src/lib.rs"),
|
||||
'pub const RUNTIME: &str = "two";\n',
|
||||
);
|
||||
expect(await computeDaytonaImageContentId(options)).not.toBe(baseline);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects mutable Docker base references", () => {
|
||||
expect(() => extractDaytonaBaseImages("FROM node:24-bookworm\n")).toThrow(
|
||||
"must use an immutable sha256 digest",
|
||||
|
|
|
|||
Loading…
Reference in New Issue