mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
ci(runner): reuse Daytona images by content
This commit is contained in:
1 parent
42e8ac9d6f
commit
c6c0a431bf
8 files changed
+309
-20
No files matched your search
@@ -46,6 +46,7 @@ jobs:
|
||||
matrix: ${{ steps.catalog.outputs.matrix }}
|
||||
needs_daytona: ${{ steps.catalog.outputs.needs_daytona }}
|
||||
execution_ids: ${{ steps.catalog.outputs.execution_ids }}
|
||||
daytona_image_content_id: ${{ steps.daytona_image_content.outputs.content_id }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -60,6 +61,10 @@ jobs:
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Compute Daytona image content ID
|
||||
id: daytona_image_content
|
||||
run: echo "content_id=$(pnpm --silent test:e2e:runner:image-id)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate selectors and emit matrix
|
||||
id: catalog
|
||||
env:
|
||||
@@ -128,6 +133,8 @@ jobs:
|
||||
id-token: write
|
||||
outputs:
|
||||
image: ${{ steps.image.outputs.image }}
|
||||
source_revision: ${{ steps.image.outputs.source_revision }}
|
||||
content_id: ${{ steps.image.outputs.content_id }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -156,19 +163,24 @@ jobs:
|
||||
id: image
|
||||
env:
|
||||
NEEDS_DAYTONA: ${{ needs.catalog.outputs.needs_daytona }}
|
||||
IMAGE_TAG: ghcr.io/paperclipai/paperclip-daytona-runner:e2e-git-${{ github.sha }}
|
||||
IMAGE_CONTENT_ID: ${{ needs.catalog.outputs.daytona_image_content_id }}
|
||||
IMAGE_TAG: ghcr.io/paperclipai/paperclip-daytona-runner:e2e-content-${{ needs.catalog.outputs.daytona_image_content_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$NEEDS_DAYTONA" != true ]; then
|
||||
echo "image=" >> "$GITHUB_OUTPUT"
|
||||
echo "source_revision=" >> "$GITHUB_OUTPUT"
|
||||
echo "content_id=" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
[[ "$IMAGE_CONTENT_ID" =~ ^[0-9a-f]{64}$ ]]
|
||||
identity="^https://github.com/${GITHUB_REPOSITORY}/.github/workflows/runner-full-stack-e2e.yml@"
|
||||
if docker buildx imagetools inspect "$IMAGE_TAG" >/dev/null 2>&1; then
|
||||
digest="$(docker buildx imagetools inspect "$IMAGE_TAG" --format '{{json .Manifest.Digest}}' | tr -d '"')"
|
||||
else
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg "PAPERCLIP_RUNNER_CONTENT_ID=${IMAGE_CONTENT_ID}" \
|
||||
--build-arg "PAPERCLIP_RUNNER_SOURCE_REVISION=${GITHUB_SHA}" \
|
||||
--file docker/daytona-runner/Dockerfile \
|
||||
--tag "$IMAGE_TAG" \
|
||||
@@ -198,6 +210,10 @@ jobs:
|
||||
# configuration here without creating a redundant container.
|
||||
image_config="$(docker image inspect "$immutable" \
|
||||
--format '{{json .}}')"
|
||||
published_content_id="$(jq -r '.Config.Labels["io.paperclip.runner.content-id"] // empty' <<< "$image_config")"
|
||||
source_revision="$(jq -r '.Config.Labels["org.opencontainers.image.revision"] // empty' <<< "$image_config")"
|
||||
test "$published_content_id" = "$IMAGE_CONTENT_ID"
|
||||
[[ "$source_revision" =~ ^[0-9a-f]{40}$ ]]
|
||||
jq -e \
|
||||
'.Architecture == "amd64" and
|
||||
.Os == "linux" and
|
||||
@@ -205,6 +221,8 @@ jobs:
|
||||
(.Config.Env | any(startswith("PAPERCLIP_RUNNER_PROVIDER_PACK_ROOT=")))' \
|
||||
<<< "$image_config" >/dev/null
|
||||
echo "image=$immutable" >> "$GITHUB_OUTPUT"
|
||||
echo "source_revision=$source_revision" >> "$GITHUB_OUTPUT"
|
||||
echo "content_id=$published_content_id" >> "$GITHUB_OUTPUT"
|
||||
|
||||
test:
|
||||
name: ${{ matrix.executionId }}
|
||||
@@ -236,6 +254,11 @@ jobs:
|
||||
|
||||
- name: Build native remote provider pack
|
||||
if: matrix.environmentId == 'daytona' && (matrix.profileId == 'runner-opencode' || startsWith(matrix.profileId, 'runner-acpx-'))
|
||||
env:
|
||||
# Reused images retain the source revision that was embedded in their
|
||||
# provider pack. Matching it here lets the server reuse that exact
|
||||
# preinstalled pack instead of uploading a duplicate to the lease.
|
||||
PAPERCLIP_RUNNER_SOURCE_REVISION: ${{ needs.daytona_image.outputs.source_revision }}
|
||||
run: pnpm --filter @paperclipai/paperclip-runner build:provider-pack
|
||||
|
||||
- name: Install pinned legacy Claude CLI
|
||||
|
||||
@@ -128,11 +128,16 @@ schedulable profile/environment/case jobs for a full run. The paid matrix uses
|
||||
`fail-fast:false`, `max-parallel:16`, and 25-minute local or 40-minute Daytona
|
||||
job limits that cover the scenario deadline plus one fresh-harness retry.
|
||||
|
||||
When Daytona is selected, one image job reuses a verified digest for the tested
|
||||
SHA or builds `linux/amd64` from `docker/daytona-runner/Dockerfile`, pushes
|
||||
`e2e-git-<sha>`, signs with Cosign/OIDC, verifies anonymous pull, checks runner
|
||||
contract/transport metadata, and exposes the immutable digest to every Daytona
|
||||
cell.
|
||||
When Daytona is selected, one image job computes an audited content ID from the
|
||||
`linux/amd64` platform, Dockerfile, root package/lock/build configuration,
|
||||
dependency patches, eval-kernel package, and runner package. It reuses the
|
||||
verified `e2e-content-<sha256>` image when that dependency closure is unchanged,
|
||||
or builds and publishes it otherwise. The tested Git SHA remains separate image
|
||||
provenance. Reuse reads that original revision back into the controller-side
|
||||
provider-pack build so its manifest remains byte-identical to the preinstalled
|
||||
pack. The job signs with Cosign/OIDC, verifies anonymous pull, checks the content
|
||||
label and runner contract/transport metadata, and exposes the immutable digest
|
||||
to every Daytona cell.
|
||||
|
||||
Every execution uploads a 30-day sanitized bundle. The final job always downloads
|
||||
evidence, merges Playwright blob reports into HTML/JUnit, stages the sanitized
|
||||
|
||||
@@ -32,6 +32,13 @@ RUN pnpm --filter @paperclipai/paperclip-runner build:typescript \
|
||||
# runner-specific addition is /usr/local/bin/paperclip-runnerd below.
|
||||
FROM daytonaio/sandbox:0.8.0
|
||||
|
||||
ARG PAPERCLIP_RUNNER_CONTENT_ID
|
||||
ARG PAPERCLIP_RUNNER_SOURCE_REVISION
|
||||
RUN test -n "${PAPERCLIP_RUNNER_CONTENT_ID}" \
|
||||
&& test -n "${PAPERCLIP_RUNNER_SOURCE_REVISION}"
|
||||
LABEL io.paperclip.runner.content-id="${PAPERCLIP_RUNNER_CONTENT_ID}" \
|
||||
org.opencontainers.image.revision="${PAPERCLIP_RUNNER_SOURCE_REVISION}"
|
||||
|
||||
USER root
|
||||
|
||||
ENV PAPERCLIP_RUNNER_PROVIDER_PACK_ROOT=/opt/paperclip-runner/provider-pack
|
||||
|
||||
@@ -18,17 +18,19 @@ The fleet image is currently amd64-only because the pinned Cursor and GitHub CLI
|
||||
checksums cover amd64.
|
||||
|
||||
```bash
|
||||
content_id="$(pnpm --silent test:e2e:runner:image-id)"
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg PAPERCLIP_RUNNER_CONTENT_ID="${content_id}" \
|
||||
--build-arg PAPERCLIP_RUNNER_SOURCE_REVISION="$(git rev-parse HEAD)" \
|
||||
--tag paperclip-daytona-runner:dev \
|
||||
--tag "paperclip-daytona-runner:e2e-content-${content_id}" \
|
||||
--load \
|
||||
--file docker/daytona-runner/Dockerfile \
|
||||
.
|
||||
|
||||
docker run --rm --platform linux/amd64 \
|
||||
--entrypoint paperclip-runnerd \
|
||||
paperclip-daytona-runner:dev \
|
||||
"paperclip-daytona-runner:e2e-content-${content_id}" \
|
||||
--build-metadata
|
||||
```
|
||||
|
||||
@@ -36,6 +38,12 @@ The metadata must advertise `dial_ws_loopback`, `dial_wss`, and `listen_ws`.
|
||||
The explicit entrypoint is needed only for this local probe because Daytona's
|
||||
base image uses its own long-running sandbox entrypoint.
|
||||
|
||||
`test:e2e:runner:image-id` hashes the audited Docker build dependency closure
|
||||
and target platform. Git commits that do not change those inputs reuse the same
|
||||
content tag. `PAPERCLIP_RUNNER_SOURCE_REVISION` remains the full Git SHA that
|
||||
built the first published copy and is retained as provenance rather than cache
|
||||
identity.
|
||||
|
||||
## Use in Paperclip
|
||||
|
||||
Publish the image to a registry Daytona can pull, or use the environment
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"test:storybook-visual:update": "node scripts/storybook-visual-baseline.mjs download && pnpm build-storybook && npx playwright test --config tests/storybook-visual/playwright.config.ts --update-snapshots && node scripts/storybook-visual-baseline.mjs pack",
|
||||
"test:e2e": "pnpm run preflight:workspace-links && npx playwright test --config tests/e2e/playwright.config.ts",
|
||||
"test:e2e:runner": "node cli/node_modules/tsx/dist/cli.mjs tests/runner-e2e/launch.ts",
|
||||
"test:e2e:runner:image-id": "node cli/node_modules/tsx/dist/cli.mjs tests/runner-e2e/daytona-image-content.ts",
|
||||
"test:e2e:runner:unit": "vitest run --config tests/runner-e2e/vitest.config.ts",
|
||||
"test:e2e:runner:typecheck": "tsc -p tests/runner-e2e/tsconfig.json",
|
||||
"test:e2e:runner:report": "node cli/node_modules/tsx/dist/cli.mjs tests/runner-e2e/report.ts",
|
||||
|
||||
@@ -104,23 +104,37 @@ Use the immutable digest printed by the `Publish verified Daytona image` job,
|
||||
or publish the current source locally:
|
||||
|
||||
```bash
|
||||
image="ghcr.io/paperclipai/paperclip-daytona-runner:e2e-git-$(git rev-parse HEAD)"
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg "PAPERCLIP_RUNNER_SOURCE_REVISION=$(git rev-parse HEAD)" \
|
||||
--file docker/daytona-runner/Dockerfile \
|
||||
--tag "$image" \
|
||||
--push \
|
||||
.
|
||||
content_id="$(pnpm --silent test:e2e:runner:image-id)"
|
||||
source_revision="$(git rev-parse HEAD)"
|
||||
image="ghcr.io/paperclipai/paperclip-daytona-runner:e2e-content-${content_id}"
|
||||
if ! docker buildx imagetools inspect "$image" >/dev/null 2>&1; then
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--build-arg "PAPERCLIP_RUNNER_CONTENT_ID=${content_id}" \
|
||||
--build-arg "PAPERCLIP_RUNNER_SOURCE_REVISION=${source_revision}" \
|
||||
--file docker/daytona-runner/Dockerfile \
|
||||
--tag "$image" \
|
||||
--push \
|
||||
.
|
||||
fi
|
||||
docker buildx imagetools inspect "$image"
|
||||
```
|
||||
|
||||
The content ID hashes the audited image inputs, including the Dockerfile,
|
||||
platform, root package/lock/build configuration, dependency patches,
|
||||
`paperclip-eval-kernel`, and `paperclip-runner`. Changes elsewhere in the
|
||||
repository keep the same tag and reuse the already signed image. The Git SHA is
|
||||
stored separately as image provenance. CI reads that provenance back from a
|
||||
reused image when it builds the controller-side provider pack, preserving the
|
||||
exact manifest match required to avoid restaging the pack into Daytona.
|
||||
|
||||
Resolve the manifest digest and set `PAPERCLIP_E2E_DAYTONA_IMAGE` to
|
||||
`ghcr.io/paperclipai/paperclip-daytona-runner@sha256:...`. The repository
|
||||
workflow signs that digest with Cosign/OIDC and verifies that it is publicly
|
||||
pullable, includes the provider pack, and advertises `dial_ws_loopback`,
|
||||
`dial_wss`, and `listen_ws`. The GHCR package must be configured as public;
|
||||
the image job deliberately fails its anonymous-pull check otherwise.
|
||||
the image job deliberately fails its anonymous-pull check otherwise. Existing
|
||||
content tags are never rebuilt or overwritten by the workflow.
|
||||
|
||||
## Evidence and cleanup
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { lstat, readdir, readFile, readlink } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const repositoryRoot = path.resolve(import.meta.dirname, "../..");
|
||||
|
||||
export const DAYTONA_IMAGE_CONTENT_SCHEMA =
|
||||
"paperclip-daytona-runner-image-content/v1";
|
||||
export const DAYTONA_IMAGE_PLATFORM = "linux/amd64";
|
||||
|
||||
// This is the audited dependency closure of docker/daytona-runner/Dockerfile.
|
||||
// Keep it conservative: a false positive only rebuilds the image, while a
|
||||
// missing input could incorrectly reuse an incompatible paid-test image.
|
||||
export const DAYTONA_IMAGE_INPUT_PATHS = [
|
||||
".dockerignore",
|
||||
".npmrc",
|
||||
"docker/daytona-runner/Dockerfile",
|
||||
"package.json",
|
||||
"patches",
|
||||
"pnpm-lock.yaml",
|
||||
"pnpm-workspace.yaml",
|
||||
"scripts/link-plugin-dev-sdk.mjs",
|
||||
"tsconfig.base.json",
|
||||
"packages/paperclip-eval-kernel",
|
||||
"packages/paperclip-runner",
|
||||
] as const;
|
||||
|
||||
const ignoredDirectoryPaths = new Set([
|
||||
"packages/paperclip-runner/dist",
|
||||
"packages/paperclip-runner/runner/target",
|
||||
]);
|
||||
|
||||
export interface DaytonaImageContentOptions {
|
||||
repositoryRoot?: string;
|
||||
inputPaths?: readonly string[];
|
||||
platform?: string;
|
||||
}
|
||||
|
||||
function compareNames(left: string, right: string): number {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
function normalizedRelativePath(value: string): string {
|
||||
return value.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function shouldIgnore(relativePath: string): boolean {
|
||||
if (ignoredDirectoryPaths.has(relativePath)) return true;
|
||||
return relativePath.split("/").includes("node_modules");
|
||||
}
|
||||
|
||||
function updateRecord(
|
||||
hash: ReturnType<typeof createHash>,
|
||||
kind: string,
|
||||
relativePath: string,
|
||||
payload = "",
|
||||
): void {
|
||||
hash.update(kind);
|
||||
hash.update("\0");
|
||||
hash.update(relativePath);
|
||||
hash.update("\0");
|
||||
hash.update(payload);
|
||||
hash.update("\0");
|
||||
}
|
||||
|
||||
async function hashEntry(
|
||||
hash: ReturnType<typeof createHash>,
|
||||
root: string,
|
||||
relativePath: string,
|
||||
): Promise<void> {
|
||||
const normalizedPath = normalizedRelativePath(relativePath);
|
||||
if (shouldIgnore(normalizedPath)) return;
|
||||
|
||||
const absolutePath = path.resolve(root, relativePath);
|
||||
const relativeFromRoot = path.relative(root, absolutePath);
|
||||
if (
|
||||
relativeFromRoot.startsWith(`..${path.sep}`) ||
|
||||
relativeFromRoot === ".." ||
|
||||
path.isAbsolute(relativeFromRoot)
|
||||
) {
|
||||
throw new Error(
|
||||
`Daytona image input escapes repository root: ${relativePath}`,
|
||||
);
|
||||
}
|
||||
|
||||
const stats = await lstat(absolutePath);
|
||||
if (stats.isDirectory()) {
|
||||
updateRecord(hash, "directory", normalizedPath);
|
||||
const entries = await readdir(absolutePath, { withFileTypes: true });
|
||||
entries.sort((left, right) => compareNames(left.name, right.name));
|
||||
for (const entry of entries) {
|
||||
await hashEntry(hash, root, path.join(relativePath, entry.name));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (stats.isSymbolicLink()) {
|
||||
updateRecord(hash, "symlink", normalizedPath, await readlink(absolutePath));
|
||||
return;
|
||||
}
|
||||
if (stats.isFile()) {
|
||||
const executable =
|
||||
(stats.mode & 0o111) === 0 ? "non-executable" : "executable";
|
||||
updateRecord(hash, "file", normalizedPath, executable);
|
||||
hash.update(await readFile(absolutePath));
|
||||
hash.update("\0");
|
||||
return;
|
||||
}
|
||||
throw new Error(`Unsupported Daytona image input type: ${relativePath}`);
|
||||
}
|
||||
|
||||
export async function computeDaytonaImageContentId(
|
||||
options: DaytonaImageContentOptions = {},
|
||||
): Promise<string> {
|
||||
const root = path.resolve(options.repositoryRoot ?? repositoryRoot);
|
||||
const inputPaths = [
|
||||
...(options.inputPaths ?? DAYTONA_IMAGE_INPUT_PATHS),
|
||||
].sort(compareNames);
|
||||
const hash = createHash("sha256");
|
||||
updateRecord(
|
||||
hash,
|
||||
"contract",
|
||||
DAYTONA_IMAGE_CONTENT_SCHEMA,
|
||||
options.platform ?? DAYTONA_IMAGE_PLATFORM,
|
||||
);
|
||||
for (const inputPath of inputPaths) {
|
||||
await hashEntry(hash, root, inputPath);
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
||||
if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
|
||||
computeDaytonaImageContentId()
|
||||
.then((contentId) => process.stdout.write(`${contentId}\n`))
|
||||
.catch((error: unknown) => {
|
||||
process.stderr.write(
|
||||
`${error instanceof Error ? error.message : String(error)}\n`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
|
||||
export const DAYTONA_IMAGE_CONTENT_SCRIPT = fileURLToPath(import.meta.url);
|
||||
@@ -1,16 +1,22 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
computeDaytonaImageContentId,
|
||||
DAYTONA_IMAGE_INPUT_PATHS,
|
||||
} from "./daytona-image-content.js";
|
||||
|
||||
const repositoryRoot = path.resolve(import.meta.dirname, "../..");
|
||||
|
||||
describe("runner E2E Daytona image contract", () => {
|
||||
it("builds runnerd and the provider pack and verifies every required transport", async () => {
|
||||
const [dockerfile, workflow] = await Promise.all([
|
||||
const [dockerfile, dockerignore, workflow] = await Promise.all([
|
||||
readFile(
|
||||
path.join(repositoryRoot, "docker/daytona-runner/Dockerfile"),
|
||||
"utf8",
|
||||
),
|
||||
readFile(path.join(repositoryRoot, ".dockerignore"), "utf8"),
|
||||
readFile(
|
||||
path.join(
|
||||
repositoryRoot,
|
||||
@@ -33,9 +39,23 @@ describe("runner E2E Daytona image contract", () => {
|
||||
for (const transport of ["dial_ws_loopback", "dial_wss", "listen_ws"]) {
|
||||
expect(dockerfile).toContain(transport);
|
||||
}
|
||||
expect(dockerfile).toContain('metadata="$(paperclip-runnerd --build-metadata)"');
|
||||
expect(dockerfile).toContain(
|
||||
'metadata="$(paperclip-runnerd --build-metadata)"',
|
||||
);
|
||||
expect(dockerfile).toContain("provider-pack.json");
|
||||
expect(dockerfile).toContain("io.paperclip.runner.content-id");
|
||||
expect(dockerfile).toContain("org.opencontainers.image.revision");
|
||||
expect(dockerignore).toContain("**/node_modules");
|
||||
expect(dockerignore).toContain("packages/paperclip-runner/dist");
|
||||
expect(dockerignore).toContain("packages/paperclip-runner/runner/target");
|
||||
expect(workflow).toContain("--platform linux/amd64");
|
||||
expect(workflow).toContain(
|
||||
"e2e-content-${{ needs.catalog.outputs.daytona_image_content_id }}",
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
'--build-arg "PAPERCLIP_RUNNER_CONTENT_ID=${IMAGE_CONTENT_ID}"',
|
||||
);
|
||||
expect(workflow).not.toContain("e2e-git-${{ github.sha }}");
|
||||
expect(workflow).toContain("cosign sign --yes");
|
||||
expect(workflow).toContain("docker image inspect");
|
||||
expect(workflow).toContain('.Config.User == "daytona"');
|
||||
@@ -46,6 +66,73 @@ describe("runner E2E Daytona image contract", () => {
|
||||
expect(workflow).toContain(
|
||||
"PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH: ${{ github.workspace }}/packages/paperclip-runner/provider-pack",
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
"PAPERCLIP_RUNNER_SOURCE_REVISION: ${{ needs.daytona_image.outputs.source_revision }}",
|
||||
);
|
||||
expect(workflow).toContain("anonymous_config");
|
||||
});
|
||||
|
||||
it("hashes the audited image dependency closure rather than the repository revision", async () => {
|
||||
for (const requiredPath of [
|
||||
".dockerignore",
|
||||
"docker/daytona-runner/Dockerfile",
|
||||
"pnpm-lock.yaml",
|
||||
"patches",
|
||||
"packages/paperclip-eval-kernel",
|
||||
"packages/paperclip-runner",
|
||||
]) {
|
||||
expect(DAYTONA_IMAGE_INPUT_PATHS).toContain(requiredPath);
|
||||
}
|
||||
|
||||
const contentId = await computeDaytonaImageContentId();
|
||||
expect(contentId).toMatch(/^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
it("changes only when a selected image input or target platform changes", async () => {
|
||||
const root = await mkdtemp(
|
||||
path.join(tmpdir(), "paperclip-daytona-image-id-"),
|
||||
);
|
||||
try {
|
||||
await mkdir(path.join(root, "image-input"));
|
||||
await writeFile(
|
||||
path.join(root, "image-input", "runner.ts"),
|
||||
"version one\n",
|
||||
);
|
||||
const baseline = await computeDaytonaImageContentId({
|
||||
repositoryRoot: root,
|
||||
inputPaths: ["image-input"],
|
||||
});
|
||||
|
||||
await writeFile(
|
||||
path.join(root, "unrelated.txt"),
|
||||
"does not enter the image\n",
|
||||
);
|
||||
expect(
|
||||
await computeDaytonaImageContentId({
|
||||
repositoryRoot: root,
|
||||
inputPaths: ["image-input"],
|
||||
}),
|
||||
).toBe(baseline);
|
||||
|
||||
await writeFile(
|
||||
path.join(root, "image-input", "runner.ts"),
|
||||
"version two\n",
|
||||
);
|
||||
expect(
|
||||
await computeDaytonaImageContentId({
|
||||
repositoryRoot: root,
|
||||
inputPaths: ["image-input"],
|
||||
}),
|
||||
).not.toBe(baseline);
|
||||
expect(
|
||||
await computeDaytonaImageContentId({
|
||||
repositoryRoot: root,
|
||||
inputPaths: ["image-input"],
|
||||
platform: "linux/arm64",
|
||||
}),
|
||||
).not.toBe(baseline);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user