Plainleaf 自动发布 / release (push) Successful in 3m34s
Co-Authored-By: Codex <noreply@anthropic.com>
168 lines
7.4 KiB
TypeScript
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+/);
|
|
});
|