Files
plainleaf/deploy/nas/deployment.test.ts
T
wushenghuaandCodex 87def9b8a6
Plainleaf 自动发布 / release (push) Successful in 3m34s
ci: 启用 main 分支自动发布
Co-Authored-By: Codex <noreply@anthropic.com>
2026-08-13 12:19:33 +08:00

168 lines
7.4 KiB
TypeScript

import { readFileSync } from "node:fs";
import { expect, test } from "vitest";
const dockerfile = readFileSync("Dockerfile.nas", "utf8");
const compose = readFileSync("deploy/nas/compose.yaml", "utf8");
const updater = readFileSync("deploy/nas/update-plainleaf.sh", "utf8");
const updateService = readFileSync(
"deploy/nas/plainleaf-update.service",
"utf8",
);
const updateSudoers = readFileSync(
"deploy/nas/plainleaf-update.sudoers",
"utf8",
);
const updateInstaller = readFileSync(
"deploy/nas/install-update-service.sh",
"utf8",
);
const releaseWorkflow = readFileSync(
".gitea/workflows/plainleaf-release.yml",
"utf8",
);
const runnerCompose = readFileSync("deploy/runner/compose.yaml", "utf8");
const runnerDaemon = readFileSync("deploy/runner/daemon.json", "utf8");
const runnerConfig = readFileSync("deploy/runner/config.yaml", "utf8");
const runnerInstaller = readFileSync("deploy/runner/install-runner.sh", "utf8");
const clientBuild = readFileSync("build/build_client.ts", "utf8");
test("NAS image allows automatic setup, multi-space, and legacy mode detection", () => {
expect(dockerfile).toContain(
"docker.m.daocloud.io/library/node:24.13.0-bookworm-slim@sha256:",
);
expect(dockerfile).toContain(
"docker.m.daocloud.io/library/rust:bookworm@sha256:",
);
expect(dockerfile).toContain(
"docker.m.daocloud.io/library/debian:bookworm-slim@sha256:",
);
expect(dockerfile).toContain("FROM --platform=$BUILDPLATFORM ${NODE_IMAGE}");
expect(dockerfile).toContain("FROM --platform=$BUILDPLATFORM ${RUST_IMAGE}");
expect(dockerfile).toContain("FROM ${DEBIAN_IMAGE}");
expect(dockerfile).toContain('ENTRYPOINT ["/usr/local/bin/plainleaf"]');
expect(dockerfile).toContain("http://127.0.0.1:${SB_PORT}/.instance");
expect(compose).toContain("http://127.0.0.1:3000/.instance");
expect(compose).not.toContain("kill -0 1");
expect(dockerfile).not.toContain('CMD ["--single"]');
expect(dockerfile).not.toContain("SB_NAME=");
expect(dockerfile).not.toContain("SB_DESCRIPTION=");
});
test("account-managed NAS compose separates server data from Markdown", () => {
expect(compose).toContain("SB_FOLDER: /data");
expect(compose).not.toContain("SB_USER:");
expect(compose).toContain("PLAINLEAF_SERVER_PATH");
expect(compose).toContain("PLAINLEAF_SPACE_PATH");
expect(compose).toContain(":/data");
expect(compose).toContain(":/notes");
});
test("NAS login offers a 30 day remembered session", () => {
expect(compose).toContain("SB_REMEMBER_ME_HOURS: 720");
expect(dockerfile).toContain("SB_REMEMBER_ME_HOURS=720");
});
test("Cloudleaf skin assets are included in the client bundle", () => {
expect(clientBuild).toContain("cloudleaf-login-desktop.webp");
expect(clientBuild).toContain("cloudleaf-login-mobile.webp");
});
test("NAS deployment follows the Gitea edge image", () => {
expect(compose).toContain(
"gitea.aichickenfarm.cn/wushenghua/plainleaf:${PLAINLEAF_IMAGE_TAG:-edge}",
);
expect(releaseWorkflow).toContain("runs-on: plainleaf-release");
expect(releaseWorkflow).toContain("REGISTRY_TOKEN");
expect(releaseWorkflow).toContain("NAS_DEPLOY_KEY");
expect(releaseWorkflow).toContain("REGISTRY_ENDPOINT: 192.168.31.68:8092");
expect(releaseWorkflow).toContain(
"IMAGE_REPOSITORY: 192.168.31.68:8092/wushenghua/plainleaf",
);
expect(releaseWorkflow).not.toContain(
"IMAGE_REPOSITORY: gitea.aichickenfarm.cn",
);
expect(releaseWorkflow).toContain("--platform linux/amd64");
expect(releaseWorkflow).toContain('--tag "${IMAGE_REPOSITORY}:edge"');
expect(releaseWorkflow).toContain("push_with_retry");
expect(releaseWorkflow).toContain("for attempt in 1 2 3 4 5");
expect(releaseWorkflow).toContain("StrictHostKeyChecking=yes");
expect(releaseWorkflow).toContain(
"sudo -n /usr/bin/systemctl start plainleaf-update.service",
);
});
test("automatic updates only recreate Plainleaf and preserve persistent data", () => {
expect(updater).toContain("pull plainleaf");
expect(updater).toContain("up --detach --no-deps --force-recreate plainleaf");
expect(updater).toContain("restore_previous_image");
expect(updater).not.toMatch(/compose(?:\[.*?\])?[^\n]*\bdown\b/);
expect(updater).not.toContain("prune");
expect(updater).not.toMatch(/\bvolume\s+(?:rm|remove)\b/);
expect(compose).toContain(":/data");
expect(compose).toContain(":/notes");
});
test("root service only runs the Plainleaf updater when explicitly triggered", () => {
expect(updateService).toContain("User=root");
expect(updateService).toContain("PLAINLEAF_DEPLOY_DIR=/etc/plainleaf");
expect(updateService).toContain("ExecStart=/usr/local/sbin/plainleaf-update");
expect(updater).toContain(
'readonly DEPLOY_DIR="${PLAINLEAF_DEPLOY_DIR:-/etc/plainleaf}"',
);
expect(updateSudoers.trim()).toBe(
"13616066635 ALL=(root) NOPASSWD: /usr/bin/systemctl start plainleaf-update.service",
);
expect(updateSudoers).not.toMatch(/NOPASSWD:\s*ALL/);
expect(releaseWorkflow).not.toContain("StrictHostKeyChecking=no");
expect(releaseWorkflow).not.toContain("sshpass");
});
test("one-time installer validates sudoers and restricts the publishing key", () => {
expect(updateInstaller).toContain(
'/usr/sbin/visudo -cf "${SOURCE_DIR}/plainleaf-update.sudoers"',
);
expect(updateInstaller).toContain('restrict,command=\\"${FORCED_COMMAND}\\"');
expect(updateInstaller).toContain("systemctl daemon-reload");
expect(updateInstaller).not.toContain("systemctl enable");
expect(updateInstaller).not.toMatch(/^systemctl start/m);
});
test("dedicated Actions runner is isolated from the NAS Docker daemon", () => {
expect(runnerCompose).toContain("gitea/runner:3.1.0-dind-rootless");
expect(runnerCompose).toContain("plainleaf-release:docker://");
expect(runnerCompose).not.toContain("/var/run/docker.sock");
expect(runnerCompose).not.toContain("PLAINLEAF_SPACE_PATH");
expect(runnerCompose).not.toContain("PLAINLEAF_SERVER_PATH");
expect(runnerCompose).toContain("unix:///run/user/1000/docker.sock");
expect(runnerCompose).not.toContain("unix:///var/run/user/1000/docker.sock");
expect(runnerCompose).toContain(
"./daemon.json:/home/rootless/.config/docker/daemon.json:ro",
);
expect(JSON.parse(runnerDaemon)).toEqual({
"insecure-registries": ["192.168.31.68:8092"],
});
expect(runnerConfig).toContain("capacity: 1");
expect(runnerConfig).toContain("privileged: false");
expect(runnerConfig).toContain("valid_volumes: []");
expect(runnerInstaller).toContain('chmod 0644 "${SOURCE_DIR}/config.yaml"');
expect(runnerInstaller).toContain("read -r -s registration_token");
expect(runnerInstaller.indexOf("wait_for_internal_docker")).toBeLessThan(
runnerInstaller.indexOf("read -r -s registration_token"),
);
expect(runnerInstaller).toContain("--env GITEA_RUNNER_REGISTRATION_TOKEN");
expect(runnerInstaller).not.toContain(
'--env GITEA_RUNNER_REGISTRATION_TOKEN="$registration_token"',
);
});
test("release workflow runs for main pushes and manual retries only", () => {
expect(releaseWorkflow).not.toContain("pull_request:");
expect(releaseWorkflow).toContain("push:");
expect(releaseWorkflow).toContain("branches:");
expect(releaseWorkflow).toContain("- main");
expect(releaseWorkflow).not.toContain("runs-on: ubuntu-latest");
expect(releaseWorkflow).toContain("workflow_dispatch:");
expect(releaseWorkflow).toContain("cancel-in-progress: false");
expect(releaseWorkflow).not.toMatch(/GITEA_RUNNER_REGISTRATION_TOKEN:\s*\S+/);
});