Files
edgeever/scripts/docker-release.test.mjs
tianma-if 91498c475f fix(docker): build the web bundle on the runner architecture
QEMU arm64 hangs inside Vite and the publish job is cancelled at 60 minutes. The bundle is plain JavaScript, so the build stage uses the runner platform and only the Bun runtime image stays per architecture.
2026-10-25 13:14:29 +08:00

261 lines
13 KiB
JavaScript

import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { resolveSelfHostedConfig } from "./self-hosted-config.mjs";
import { loadSelfHostedEnvironment } from "./self-hosted-secrets.mjs";
const readProjectFile = (path) =>
readFileSync(resolve(import.meta.dir, "..", path), "utf8");
describe("self-hosted runtime configuration", () => {
test("uses the portable single-volume defaults", () => {
const config = resolveSelfHostedConfig({}, "/opt/edgeever");
expect(config.dataDirectory).toBe("/opt/edgeever/.edgeever-data");
expect(config.databaseFile).toBe(
"/opt/edgeever/.edgeever-data/edgeever.sqlite",
);
expect(config.resourcesDirectory).toBe(
"/opt/edgeever/.edgeever-data/resources",
);
expect(config.port).toBe(8787);
expect(config.storageBackend).toBe("local");
});
test("loads Docker secrets without retaining the file indirection", async () => {
const directory = await mkdtemp(`${tmpdir()}/edgeever-docker-secret-`);
const secretPath = resolve(directory, "password");
try {
await writeFile(secretPath, "secret-from-file\n");
const environment = await loadSelfHostedEnvironment({
EDGE_EVER_AUTH_PASSWORD_FILE: secretPath,
});
expect(environment.EDGE_EVER_AUTH_PASSWORD).toBe("secret-from-file");
expect(environment.EDGE_EVER_AUTH_PASSWORD_FILE).toBeUndefined();
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("rejects invalid ports and storage backends", () => {
expect(() => resolveSelfHostedConfig({ EDGE_EVER_PORT: "0" })).toThrow(
"EDGE_EVER_PORT",
);
expect(() =>
resolveSelfHostedConfig({ EDGE_EVER_STORAGE_BACKEND: "r2" }),
).toThrow("either local or s3");
expect(() =>
resolveSelfHostedConfig({ EDGE_EVER_STORAGE_BACKEND: "s3" }),
).toThrow("EDGE_EVER_S3_BUCKET");
});
});
describe("Docker release contract", () => {
test("runs the production image as a non-root user with one persistent volume", () => {
const dockerfile = readProjectFile("Dockerfile");
const runtimeStage = dockerfile.slice(
dockerfile.indexOf("FROM oven/bun:1.3.14-alpine AS runtime"),
);
const packageMetadata = JSON.parse(readProjectFile("package.json"));
expect(dockerfile).toContain("FROM --platform=$BUILDPLATFORM oven/bun:1.3.14-alpine AS manifests");
expect(dockerfile).toContain("FROM oven/bun:1.3.14-alpine AS runtime");
expect(dockerfile).toContain("COPY patches patches");
expect(dockerfile).toContain("COPY docs docs");
expect(dockerfile).toContain(
"COPY packages/wrangler/package.json packages/wrangler/package.json",
);
expect(dockerfile).toContain(
"COPY packages/public-network/package.json packages/public-network/package.json",
);
expect(dockerfile).toContain(
"COPY release-summary.json release-summary.json",
);
expect(dockerfile).toContain("--filter @edgeever/web");
expect(dockerfile).toContain("--filter @edgeever/public-network");
expect(dockerfile).toContain(
"RUN bun install --frozen-lockfile \\\n --filter edgeever \\\n --filter @edgeever/api",
);
expect(packageMetadata.scripts["build:self-hosted"]).toBe(
"bun build scripts/self-hosted-server.mjs --target=bun --format=esm --outdir=dist/self-hosted --sourcemap=none",
);
expect(dockerfile).toContain("RUN bun run build:web && bun run build:self-hosted");
expect(dockerfile).toContain("scripts/prepare-poster-thumbnails.mjs scripts/prepare-poster-thumbnails.d.mts scripts/");
expect(dockerfile).toContain("RUN apk add --no-cache chromium nodejs");
expect(dockerfile).toContain("EDGE_EVER_POSTER_CHROME_EXECUTABLE=/usr/bin/chromium");
expect(dockerfile).toContain("COPY tests/browser/poster-thumbnail-generator.html tests/browser/poster-thumbnail-generator.ts tests/browser/");
expect(runtimeStage).toContain(
"COPY --from=build /app/dist/self-hosted/self-hosted-server.js ./scripts/self-hosted-server.js",
);
expect(runtimeStage).toContain(
"ln -s self-hosted-server.js /app/scripts/self-hosted-server.mjs",
);
expect(runtimeStage).toContain('org.opencontainers.image.title="EdgeEver"');
expect(runtimeStage).toContain('org.opencontainers.image.licenses="AGPL-3.0-only"');
expect(runtimeStage).not.toContain("node_modules");
expect(runtimeStage).not.toContain("/app/apps/api");
expect(runtimeStage).not.toContain("/app/packages");
expect(runtimeStage).not.toContain("./package.json");
expect(runtimeStage).not.toContain("./release-summary.json");
expect(runtimeStage).not.toContain("./docs/openapi.json");
expect(dockerfile).toContain("USER bun");
expect(dockerfile).toContain('VOLUME ["/data"]');
expect(dockerfile).toContain("HEALTHCHECK");
expect(dockerfile).toContain("ARG EDGE_EVER_BUILD_ID=unknown");
expect(dockerfile).toContain("EDGE_EVER_BUILD_ID=${EDGE_EVER_BUILD_ID}");
for (const name of ["EDGE_EVER_BUILD_CREATED_AT", "EDGE_EVER_BUILD_TRIGGER", "EDGE_EVER_BUILD_METHOD"]) {
// Empty by default so self-built images report unknown build metadata.
expect(runtimeStage).toContain(`ARG ${name}=\n`);
expect(runtimeStage).toContain(`${name}=\${${name}}`);
}
expect(runtimeStage).toContain('CMD ["bun", "scripts/self-hosted-server.js"]');
expect(readProjectFile("docs/deploy-docker.md")).toContain(
'Module not found "scripts/self-hosted-server.mjs"',
);
expect(readProjectFile("docs/deploy-docker.zh-CN.md")).toContain(
'Module not found "scripts/self-hosted-server.mjs"',
);
expect(readProjectFile("docs/self-hosting-architecture.md")).toContain(
"scripts/self-hosted-server.mjs",
);
expect(readProjectFile("docs/self-hosting-architecture.zh-CN.md")).toContain(
"scripts/self-hosted-server.mjs",
);
});
test("keeps authentication explicit in Compose", () => {
const compose = readProjectFile("compose.yaml");
expect(compose).toContain(
'EDGE_EVER_AUTH_PASSWORD: "${EDGE_EVER_AUTH_PASSWORD:?',
);
expect(compose).toContain(
'EDGE_EVER_CONTAINER_IMAGE: "${EDGE_EVER_IMAGE:-ghcr.io/tianma-if/edgeever}"',
);
expect(compose).toContain("edgeever-data:/data");
expect(compose).toContain("no-new-privileges:true");
});
test("keeps both runtime entrypoints on the shared application", () => {
const application = readProjectFile("apps/api/src/index.ts");
const selfHosted = readProjectFile("scripts/self-hosted-server.mjs");
expect(application).toContain("export const fetchEdgeEverApp");
expect(application).toContain(
"storage: createCloudflareStorageAdapter(env)",
);
expect(selfHosted).toContain(
"fetchEdgeEverApp(request, env, executionContext)",
);
expect(selfHosted).toContain(
'await import("../apps/api/src/s3-compatible-storage-adapter.ts")',
);
expect(selfHosted).not.toContain(
'import { createS3CompatibleStorageAdapter } from',
);
expect(selfHosted).not.toContain("worker.fetch(");
expect(selfHosted).toContain("ensureSelfHostedCredentialSecrets");
});
test("persists credential encryption secrets on the data volume", () => {
const secrets = readProjectFile("scripts/self-hosted-secrets.mjs");
expect(secrets).toContain("edgeever-secrets.json");
expect(secrets).not.toContain("edgeever.sqlite");
expect(readProjectFile("docs/deploy-docker.md")).toContain("edgeever-secrets.json");
expect(readProjectFile("docs/deploy-docker.zh-CN.md")).toContain("edgeever-secrets.json");
expect(readProjectFile("docs/self-hosting-architecture.md")).toContain(
"NAS/GUI upgrades that drop container environment variables",
);
expect(readProjectFile("docs/self-hosting-architecture.zh-CN.md")).toContain(
"NAS/GUI 升级丢掉容器环境变量",
);
});
test("gates official image publishing and release auditing", () => {
const workflow = readProjectFile(".github/workflows/docker-image.yml");
const mirrorWorkflow = readProjectFile(
".github/workflows/docker-tcr-mirror.yml",
);
const cnbWorkflow = readProjectFile(".cnb.yml");
const tcrPublisher = readProjectFile("scripts/publish-tcr-image.sh");
expect(
workflow.match(/github\.repository == 'tianma-if\/edgeever'/g)?.length,
).toBeGreaterThanOrEqual(2);
expect(workflow).toContain("release_tag");
expect(workflow).toContain("contents: write\n packages: write");
expect(workflow).toContain(
"name: Publish official multi-platform image\n runs-on: ubuntu-latest\n timeout-minutes: 60",
);
expect(workflow).toContain('gh release view "${RELEASE_TAG}"');
expect(workflow).not.toContain("releases/tags/${RELEASE_TAG}");
expect(workflow).toContain("docker logout ghcr.io");
expect(workflow).toContain("docker buildx imagetools inspect");
expect(workflow).toContain('docker pull "${GHCR_IMAGE_NAME}:${{ steps.image.outputs.audit_tag }}"');
expect(workflow).toContain("container_name=edgeever-published-audit");
expect(workflow).toContain("test -s /app/scripts/self-hosted-server.js");
expect(workflow).toContain("test -s /app/scripts/self-hosted-server.mjs");
expect(workflow).toContain(
"cmp -s /app/scripts/self-hosted-server.js /app/scripts/self-hosted-server.mjs",
);
expect(workflow).toContain("test ! -e /app/node_modules");
expect(workflow).toContain("test ! -e /app/apps/api");
expect(workflow).toContain("test ! -e /app/packages");
expect(workflow).toContain("curl --fail --silent --show-error http://127.0.0.1:8787/");
expect(workflow).toContain('docker build \\\n --build-arg EDGE_EVER_BUILD_ID="${GITHUB_SHA}"');
expect(workflow).toContain("EDGE_EVER_BUILD_ID=${{ github.sha }}");
expect(workflow).toContain("EDGE_EVER_BUILD_CREATED_AT=${{ steps.image.outputs.created_at }}");
expect(workflow).toContain("EDGE_EVER_BUILD_TRIGGER=${{ steps.image.outputs.build_trigger }}");
expect(workflow).toContain("EDGE_EVER_BUILD_METHOD=github_actions");
expect(workflow).toContain("org.opencontainers.image.created=${{ steps.image.outputs.created_at }}");
// Draft releases are prepared by workflow_dispatch with release_tag; published only audits.
const resolveTags = workflow.slice(workflow.indexOf("- name: Resolve image tags"), workflow.indexOf("- name: Set up QEMU"));
const releaseBranch = resolveTags.slice(resolveTags.indexOf('if [[ -n "${RELEASE_TAG}" ]]'), resolveTags.indexOf(" else\n"));
expect(releaseBranch).toContain('echo "build_trigger=github_release"');
expect(resolveTags).toContain('if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then\n build_trigger=main_push');
expect(resolveTags).toContain("build_trigger=manual");
expect(workflow).toContain("docker build --tag edgeever:ci-default .");
expect(workflow).toContain('.buildProvenance == {"trigger": "main_push", "method": "github_actions"}');
expect(workflow).toContain('.deployment == {"trigger": "manual", "method": "docker"}');
expect(workflow).toContain('.build == "unknown" and .buildCreatedAt == null and .buildProvenance == {"trigger": "unknown", "method": "unknown"}');
expect(workflow).toContain('.buildProvenance == {"trigger": $trigger, "method": "github_actions"}');
expect(workflow).not.toContain("TCR_IMAGE_NAME");
expect(workflow).not.toContain("TENCENT_TCR_USERNAME");
expect(mirrorWorkflow).toContain(
"name: Build Tencent TCR image through CNB",
);
expect(mirrorWorkflow).not.toContain("workflow_run:");
expect(mirrorWorkflow).toContain("release:");
expect(mirrorWorkflow).toContain("types:\n - published");
expect(mirrorWorkflow).toContain("workflow_dispatch:");
expect(mirrorWorkflow).toContain("secrets.CNB_TCR_BUILD_PUSH_TOKEN");
expect(mirrorWorkflow).toContain("https://cnb.cool/tianma-if/edgeever");
expect(mirrorWorkflow).toContain('"HEAD:${destination}"');
expect(mirrorWorkflow).not.toContain("skopeo");
expect(mirrorWorkflow).not.toContain("ghcr.io");
expect(cnbWorkflow).toContain("main:\n push:");
expect(cnbWorkflow).toContain('"v*":\n tag_push:');
expect(cnbWorkflow).toContain("runner:\n cpus: 2");
expect(cnbWorkflow).toContain("tianma-if/edgeever-secrets");
expect(cnbWorkflow).toContain("bash scripts/publish-tcr-image.sh");
expect(tcrPublisher).toContain(
'readonly TCR_IMAGE="${TCR_REGISTRY}/edgeever/edgeever"',
);
expect(tcrPublisher).toContain("--platform linux/amd64,linux/arm64");
expect(tcrPublisher).toContain('primary_tag="sha-${short_sha}"');
expect(tcrPublisher).toContain('promotion_tags=("${version}" latest)');
expect(tcrPublisher).toContain(
'--label "org.opencontainers.image.revision=${CNB_COMMIT}"',
);
expect(tcrPublisher).toContain("readonly build_attempts=3");
expect(tcrPublisher).toContain("attempt <= build_attempts");
expect(tcrPublisher).toContain("timeout --signal=TERM --kill-after=1m 15m");
expect(tcrPublisher).toContain("BuildKit can reuse completed layers");
expect(tcrPublisher).toContain("docker buildx imagetools create \\");
expect(tcrPublisher).toContain("org.opencontainers.image.revision");
expect(tcrPublisher).toContain("--format '{{json .Image}}'");
expect(tcrPublisher).toContain("TCR_SHA_TAGS_TO_KEEP=20");
expect(tcrPublisher).not.toContain("ghcr.io");
});
});