Reduce repeated CLI builds and isolate CI publication

This commit is contained in:
vorotamoroz
2026-10-14 10:08:35 +00:00
parent 0956663f7b
commit 8892f659f6
11 changed files with 547 additions and 109 deletions
+61 -42
View File
@@ -8,14 +8,34 @@ on:
paths:
- '.github/workflows/cli-deno-tests.yml'
- 'src/apps/cli/**'
- 'test/bench-network/**'
- '!src/apps/cli/**/*.md'
- 'src/LiveSyncBaseCore.ts'
- 'src/common/**'
- 'src/serviceFeatures/**'
- 'src/serviceModules/**'
- 'src/integrations/**'
- 'src/modules/AbstractModule.ts'
- 'src/modules/main/**'
- 'src/modules/coreFeatures/**'
- 'manifest.json'
- 'tsconfig.json'
- 'package.json'
- 'package-lock.json'
pull_request:
paths:
- '.github/workflows/cli-deno-tests.yml'
- 'src/apps/cli/**'
- 'test/bench-network/**'
- '!src/apps/cli/**/*.md'
- 'src/LiveSyncBaseCore.ts'
- 'src/common/**'
- 'src/serviceFeatures/**'
- 'src/serviceModules/**'
- 'src/integrations/**'
- 'src/modules/AbstractModule.ts'
- 'src/modules/main/**'
- 'src/modules/coreFeatures/**'
- 'manifest.json'
- 'tsconfig.json'
- 'package.json'
- 'package-lock.json'
workflow_dispatch:
@@ -40,23 +60,34 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
prepare:
runs-on: ubuntu-latest
timeout-minutes: 15
env:
NODE_OPTIONS: --max-old-space-size=3072
outputs:
task_matrix: ${{ steps.select.outputs.task_matrix }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Select task matrix
id: select
shell: bash
env:
SELECTED_TASK: ${{ github.event_name == 'workflow_dispatch' && inputs.test_task || 'test:ci' }}
run: |
set -euo pipefail
SELECTED_TASK="${{ github.event_name == 'workflow_dispatch' && inputs.test_task || 'test:ci' }}"
echo "[INFO] Selected task set: $SELECTED_TASK"
case "$SELECTED_TASK" in
test:ci)
TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon","test:daemon-startup","test:push-pull","test:decoupled-vault","test:sync-two-local","test:sync-locked-remote","test:remote-commands","test:e2e-matrix:couchdb-enc0","test:e2e-matrix:couchdb-enc1","test:e2e-matrix:minio-enc0","test:e2e-matrix:minio-enc1"]'
TASK_MATRIX=$(jq -c . src/apps/cli/testdeno/ci-tasks.json)
;;
test:local)
TASK_MATRIX='["test:setup-put-cat","test:mirror","test:daemon"]'
@@ -72,6 +103,27 @@ jobs:
echo "task_matrix=$TASK_MATRIX" >> "$GITHUB_OUTPUT"
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build CLI
working-directory: src/apps/cli
run: npm run build
- name: Upload CLI bundle
uses: actions/upload-artifact@v6
with:
name: cli-bundle
path: src/apps/cli/dist/
if-no-files-found: error
retention-days: 1
test:
needs: prepare
runs-on: ubuntu-latest
@@ -109,9 +161,11 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build CLI
working-directory: src/apps/cli
run: npm run build
- name: Download CLI bundle
uses: actions/download-artifact@v8
with:
name: cli-bundle
path: src/apps/cli/dist/
- name: Create .test.env
working-directory: src/apps/cli
@@ -145,38 +199,3 @@ jobs:
run: |
docker stop couchdb-test minio-test relay-test coturn-test >/dev/null 2>&1 || true
docker rm couchdb-test minio-test relay-test coturn-test >/dev/null 2>&1 || true
compose-p2p-e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Show Docker versions
run: |
docker --version
docker compose version
- name: Run Compose CLI P2P E2E
env:
CLI_E2E_TASK: test:p2p:ci
RELAY: ws://nostr-relay:7777/
PEERS_TIMEOUT: '20'
SYNC_TIMEOUT: '60'
LIVESYNC_USE_COTURN: '0'
TURN_SERVERS: none
LIVESYNC_P2P_PEERS_RETRY: '1'
LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: '60000'
BENCH_LIVESYNC_TEST_TEE: '0'
run: docker compose -f test/bench-network/compose.yml run --build --rm bench-runner run-livesync-cli-e2e
- name: Show Compose diagnostics
if: failure()
run: |
docker compose -f test/bench-network/compose.yml ps
docker compose -f test/bench-network/compose.yml logs --no-color couchdb nostr-relay || true
- name: Stop Compose services
if: always()
run: docker compose -f test/bench-network/compose.yml down -v --remove-orphans
@@ -0,0 +1,87 @@
name: CLI Docker validation
on:
pull_request:
paths:
- '.github/workflows/cli-docker.yml'
- '.github/workflows/cli-docker-validation.yml'
- 'src/apps/cli/**'
- '!src/apps/cli/**/*.md'
- 'src/LiveSyncBaseCore.ts'
- 'src/common/**'
- 'src/serviceFeatures/**'
- 'src/serviceModules/**'
- 'src/integrations/**'
- 'src/modules/AbstractModule.ts'
- 'src/modules/main/**'
- 'src/modules/coreFeatures/**'
- 'manifest.json'
- 'tsconfig.json'
- 'package.json'
- 'package-lock.json'
- '.dockerignore'
workflow_call:
inputs:
force:
type: boolean
default: false
outputs:
e2e_passed:
description: Whether the Docker CLI E2E suite passed
value: ${{ jobs.validate.outputs.e2e_passed }}
permissions:
contents: read
concurrency:
group: cli-docker-validation-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
validate:
runs-on: ubuntu-latest
timeout-minutes: 45
outputs:
e2e_passed: ${{ steps.e2e.outcome == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build Docker test image
uses: docker/build-push-action@v7
with:
context: .
file: src/apps/cli/Dockerfile
load: true
tags: livesync-cli
platforms: linux/amd64
cache-from: type=gha,scope=cli-image
cache-to: type=gha,scope=cli-image,mode=max
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run CLI E2E (docker)
id: e2e
continue-on-error: ${{ github.event_name == 'workflow_dispatch' && inputs.force }}
working-directory: src/apps/cli
env:
CI: true
run: npm --ignore-scripts run test:e2e:docker:all
- name: Stop test containers
if: always()
working-directory: src/apps/cli
run: |
bash ./util/couchdb-stop.sh >/dev/null 2>&1 || true
bash ./util/minio-stop.sh >/dev/null 2>&1 || true
bash ./util/p2p-stop.sh >/dev/null 2>&1 || true
+36 -42
View File
@@ -1,6 +1,6 @@
# Build and push the CLI Docker image to GitHub Container Registry (GHCR).#
# Image tag format: <manifest-version>-<unix-epoch>-cli
# Example: 0.25.56-1743500000-cli
# Build and push the CLI Docker image to GitHub Container Registry (GHCR).
# Image tag format: <manifest-version>-cli
# Example: 1.0.36-cli
#
# Stable releases are also tagged with their major-minor version and 'latest'.
# Pre-releases receive immutable version and SHA-qualified tags only.
@@ -13,16 +13,24 @@ on:
- main
tags:
- "*.*.*-cli"
paths-ignore:
- "docs/**"
- "*.md"
- "images/**"
- "assets/**"
- "instruction_images/**"
- "src/apps/webapp/**"
- "src/apps/webpeer/**"
- ".github/workflows/release.yml"
- ".github/workflows/unit-ci.yml"
paths:
- ".github/workflows/cli-docker.yml"
- ".github/workflows/cli-docker-validation.yml"
- "src/apps/cli/**"
- "!src/apps/cli/**/*.md"
- "src/LiveSyncBaseCore.ts"
- "src/common/**"
- "src/serviceFeatures/**"
- "src/serviceModules/**"
- "src/integrations/**"
- "src/modules/AbstractModule.ts"
- "src/modules/main/**"
- "src/modules/coreFeatures/**"
- "manifest.json"
- "tsconfig.json"
- "package.json"
- "package-lock.json"
- ".dockerignore"
workflow_dispatch:
inputs:
dry_run:
@@ -36,8 +44,21 @@ on:
type: boolean
default: false
concurrency:
group: cli-docker-publish-${{ github.ref }}
cancel-in-progress: false
jobs:
validate:
uses: ./.github/workflows/cli-docker-validation.yml
with:
force: ${{ github.event_name == 'workflow_dispatch' && inputs.force || false }}
permissions:
contents: read
build-and-push:
needs: validate
if: ${{ needs.validate.outputs.e2e_passed == 'true' || (github.event_name == 'workflow_dispatch' && inputs.force) }}
runs-on: ubuntu-latest
timeout-minutes: 90
permissions:
@@ -100,34 +121,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "24.x"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Run CLI E2E (docker)
id: e2e
continue-on-error: ${{ github.event_name == 'workflow_dispatch' && inputs.force }}
working-directory: src/apps/cli
env:
CI: true
run: npm run test:e2e:docker:all
- name: Stop test containers (safety net)
if: always()
working-directory: src/apps/cli
run: |
# Keep this as a safety net for future suites/steps that may leave containers running.
bash ./util/couchdb-stop.sh >/dev/null 2>&1 || true
bash ./util/minio-stop.sh >/dev/null 2>&1 || true
bash ./util/p2p-stop.sh >/dev/null 2>&1 || true
- name: Build and push
if: ${{ steps.e2e.outcome == 'success' || (github.event_name == 'workflow_dispatch' && inputs.force) }}
uses: docker/build-push-action@v7
with:
context: .
@@ -135,5 +129,5 @@ jobs:
push: ${{ steps.meta.outputs.push }}
tags: ${{ steps.meta.outputs.tags }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=cli-image
cache-to: type=gha,scope=cli-image,mode=max
+88
View File
@@ -0,0 +1,88 @@
name: CLI P2P Compose tests
on:
push:
branches:
- main
- beta
paths:
- '.github/workflows/cli-p2p-tests.yml'
- 'src/apps/cli/**'
- '!src/apps/cli/**/*.md'
- 'src/LiveSyncBaseCore.ts'
- 'src/common/**'
- 'src/serviceFeatures/**'
- 'src/serviceModules/**'
- 'src/integrations/**'
- 'src/modules/AbstractModule.ts'
- 'src/modules/main/**'
- 'src/modules/coreFeatures/**'
- 'manifest.json'
- 'tsconfig.json'
- 'package.json'
- 'package-lock.json'
- '.dockerignore'
- 'test/bench-network/**'
pull_request:
paths:
- '.github/workflows/cli-p2p-tests.yml'
- 'src/apps/cli/**'
- '!src/apps/cli/**/*.md'
- 'src/LiveSyncBaseCore.ts'
- 'src/common/**'
- 'src/serviceFeatures/**'
- 'src/serviceModules/**'
- 'src/integrations/**'
- 'src/modules/AbstractModule.ts'
- 'src/modules/main/**'
- 'src/modules/coreFeatures/**'
- 'manifest.json'
- 'tsconfig.json'
- 'package.json'
- 'package-lock.json'
- '.dockerignore'
- 'test/bench-network/**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
compose-p2p-e2e:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Show Docker versions
run: |
docker --version
docker compose version
- name: Run Compose CLI P2P E2E
env:
CLI_E2E_TASK: test:p2p:ci
RELAY: ws://nostr-relay:7777/
PEERS_TIMEOUT: '20'
SYNC_TIMEOUT: '60'
LIVESYNC_USE_COTURN: '0'
TURN_SERVERS: none
LIVESYNC_P2P_PEERS_RETRY: '1'
LIVESYNC_P2P_RELAY_READY_TIMEOUT_MS: '60000'
BENCH_LIVESYNC_TEST_TEE: '0'
run: docker compose -f test/bench-network/compose.yml run --build --rm bench-runner run-livesync-cli-e2e
- name: Show Compose diagnostics
if: failure()
run: |
docker compose -f test/bench-network/compose.yml ps
docker compose -f test/bench-network/compose.yml logs --no-color couchdb nostr-relay || true
- name: Stop Compose services
if: always()
run: docker compose -f test/bench-network/compose.yml down -v --remove-orphans
+13 -7
View File
@@ -9,6 +9,7 @@ on:
- 'aggregator.html'
- 'src/**'
- 'test/browser-apps/**'
- 'manifest.json'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/deploy-pages.yml'
@@ -17,18 +18,17 @@ on:
- 'aggregator.html'
- 'src/**'
- 'test/browser-apps/**'
- 'manifest.json'
- 'package.json'
- 'package-lock.json'
- '.github/workflows/deploy-pages.yml'
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
build:
@@ -84,9 +84,6 @@ jobs:
- name: Validate packaged Pages site
run: npm run test:browser-apps:pages
- name: Configure GitHub Pages
uses: actions/configure-pages@v6
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v5
with:
@@ -97,10 +94,19 @@ jobs:
if: github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
concurrency:
group: pages
cancel-in-progress: false
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Configure GitHub Pages
uses: actions/configure-pages@v6
- name: Deploy GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
+24
View File
@@ -8,8 +8,11 @@ on:
- main
- beta
paths:
- 'manifest.json'
- 'updates.md'
- 'src/**'
- 'test/**'
- '_tools/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
@@ -19,8 +22,13 @@ on:
- 'eslint.config.mjs'
- 'eslint.config.common.mjs'
- 'eslint.community.config.mjs'
- 'terser.config.mjs'
- 'update-workspaces.mjs'
- 'version-bump.mjs'
- 'utils/check-compatibility.js'
- 'utils/livesync-commonlib-version.test.ts'
- 'utils/livesync-commonlib-version.ts'
- 'utils/*ci*.unit.spec.ts'
- 'utils/release-*.mjs'
- 'utils/release-*.unit.spec.ts'
- 'utils/couchdb/**'
@@ -29,11 +37,18 @@ on:
- '.github/workflows/prepare-release.yml'
- '.github/workflows/finalise-release.yml'
- '.github/workflows/release.yml'
- '.github/workflows/cli-deno-tests.yml'
- '.github/workflows/cli-p2p-tests.yml'
- '.github/workflows/cli-docker-validation.yml'
- '.github/workflows/cli-docker.yml'
- '.github/workflows/unit-ci.yml'
pull_request:
paths:
- 'manifest.json'
- 'updates.md'
- 'src/**'
- 'test/**'
- '_tools/**'
- 'package.json'
- 'package-lock.json'
- 'tsconfig.json'
@@ -43,8 +58,13 @@ on:
- 'eslint.config.mjs'
- 'eslint.config.common.mjs'
- 'eslint.community.config.mjs'
- 'terser.config.mjs'
- 'update-workspaces.mjs'
- 'version-bump.mjs'
- 'utils/check-compatibility.js'
- 'utils/livesync-commonlib-version.test.ts'
- 'utils/livesync-commonlib-version.ts'
- 'utils/*ci*.unit.spec.ts'
- 'utils/release-*.mjs'
- 'utils/release-*.unit.spec.ts'
- 'utils/couchdb/**'
@@ -53,6 +73,10 @@ on:
- '.github/workflows/prepare-release.yml'
- '.github/workflows/finalise-release.yml'
- '.github/workflows/release.yml'
- '.github/workflows/cli-deno-tests.yml'
- '.github/workflows/cli-p2p-tests.yml'
- '.github/workflows/cli-docker-validation.yml'
- '.github/workflows/cli-docker.yml'
- '.github/workflows/unit-ci.yml'
permissions:
+17
View File
@@ -0,0 +1,17 @@
[
"test:settings-writeback",
"test:remote-administration-exit-codes",
"test:setup-put-cat",
"test:mirror",
"test:daemon",
"test:daemon-startup",
"test:push-pull",
"test:decoupled-vault",
"test:sync-two-local",
"test:sync-locked-remote",
"test:remote-commands",
"test:e2e-matrix:couchdb-enc0",
"test:e2e-matrix:couchdb-enc1",
"test:e2e-matrix:minio-enc0",
"test:e2e-matrix:minio-enc1"
]
+1 -17
View File
@@ -1,20 +1,4 @@
const TASKS = [
"test:settings-writeback",
"test:remote-administration-exit-codes",
"test:setup-put-cat",
"test:mirror",
"test:daemon",
"test:daemon-startup",
"test:push-pull",
"test:decoupled-vault",
"test:sync-two-local",
"test:sync-locked-remote",
"test:remote-commands",
"test:e2e-matrix:couchdb-enc0",
"test:e2e-matrix:couchdb-enc1",
"test:e2e-matrix:minio-enc0",
"test:e2e-matrix:minio-enc1",
] as const;
import TASKS from "./ci-tasks.json" with { type: "json" };
for (const [index, task] of TASKS.entries()) {
console.log(`\n[CLI E2E ${index + 1}/${TASKS.length}] ${task}`);
+5 -1
View File
@@ -323,7 +323,11 @@ deno task test:sync-two-local
## Continuous Integration
The GitHub Actions workflow `.github/workflows/cli-deno-tests.yml` runs automatically on pushes and pull requests affecting the CLI, executing the non-P2P test suite (`test:ci`). P2P tests (`test:p2p`) are excluded from automatic execution and must be run via manual dispatch (`workflow_dispatch`). You can optionally check the "Enable verbose and debug logging" checkbox during a manual dispatch to produce detailed trace logs for troubleshooting.
The GitHub Actions workflow `.github/workflows/cli-deno-tests.yml` runs automatically on pushes and pull requests affecting the CLI or its shared runtime inputs. It builds the CLI once, then distributes the bundle to the non-P2P test jobs. The local `test:ci` runner and the automatic matrix both read `ci-tasks.json`. During a manual dispatch, select a test subset and optionally enable 'Enable verbose and debug logging' for detailed troubleshooting traces.
The separate `.github/workflows/cli-p2p-tests.yml` workflow runs the canonical Compose P2P suite automatically for changes to the CLI, its shared runtime inputs, or `test/bench-network/`. A change confined to the Compose fixtures does not start the non-P2P matrix. Host-direct `test:p2p` tasks remain available for explicit diagnostics.
The `.github/workflows/cli-docker-validation.yml` workflow tests the production Docker image on relevant pull requests and is also called before container publication. Validation runs without registry credentials. The publishing workflow, `.github/workflows/cli-docker.yml`, reuses the Docker build cache and retains the existing main-branch, release-tag, and manual publication conditions.
---
+72
View File
@@ -0,0 +1,72 @@
import { readFileSync } from "node:fs";
import { minimatch } from "minimatch";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
type Trigger = "push" | "pull_request";
type Concurrency = {
group?: string;
"cancel-in-progress"?: boolean | string;
};
type Workflow = {
on?: Partial<Record<Trigger, { paths?: string[] }>>;
concurrency?: Concurrency;
jobs?: Record<string, { concurrency?: Concurrency }>;
};
function readWorkflow(filename: string): Workflow {
const source = readFileSync(new URL(`../.github/workflows/${filename}`, import.meta.url), "utf8");
return parse(source) as Workflow;
}
function matchesAnyPath(patterns: string[], filename: string): boolean {
return patterns.some((pattern) => minimatch(filename, pattern));
}
const pagesWorkflow = readWorkflow("deploy-pages.yml");
const unitWorkflow = readWorkflow("unit-ci.yml");
describe("CI workflow path filters", () => {
it.each(["push", "pull_request"] as const)("runs Pages checks for the manifest on %s", (trigger) => {
const patterns = pagesWorkflow.on?.[trigger]?.paths ?? [];
expect(matchesAnyPath(patterns, "manifest.json")).toBe(true);
});
it.each(["push", "pull_request"] as const)("runs unit CI for required inputs on %s", (trigger) => {
const patterns = unitWorkflow.on?.[trigger]?.paths ?? [];
const requiredInputs = [
"manifest.json",
"updates.md",
"_tools/bakei18n.ts",
"terser.config.mjs",
"utils/check-compatibility.js",
"utils/livesync-commonlib-version.test.ts",
"utils/livesync-commonlib-version.ts",
"utils/ci-pages.unit.spec.ts",
"utils/cli-ci.unit.spec.ts",
".github/workflows/cli-docker.yml",
".github/workflows/cli-deno-tests.yml",
".github/workflows/cli-p2p-tests.yml",
".github/workflows/cli-docker-validation.yml",
];
expect(requiredInputs.filter((filename) => !matchesAnyPath(patterns, filename))).toEqual([]);
expect(matchesAnyPath(patterns, "utils/unrelated.unit.spec.ts")).toBe(false);
});
});
describe("Pages workflow concurrency", () => {
it("isolates pull requests from publishing refs and cancels superseded pull request runs", () => {
expect(pagesWorkflow.concurrency).toEqual({
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}",
"cancel-in-progress": "${{ github.event_name == 'pull_request' }}",
});
});
it("serialises deployments without cancelling an active deployment", () => {
expect(pagesWorkflow.jobs?.deploy?.concurrency).toEqual({
group: "pages",
"cancel-in-progress": false,
});
});
});
+143
View File
@@ -0,0 +1,143 @@
import { readFileSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { minimatch } from "minimatch";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
const root = new URL("../", import.meta.url);
type Step = { id?: string; run?: string; uses?: string; with?: Record<string, unknown> };
type Workflow = {
on: Record<string, { paths?: string[]; [key: string]: unknown }>;
jobs: Record<
string,
{ steps?: Step[]; needs?: string | string[]; uses?: string; if?: string; permissions?: Record<string, string> }
>;
};
function workflow(name: string): Workflow {
return parse(readFileSync(new URL(`.github/workflows/${name}`, root), "utf8")) as Workflow;
}
function matches(paths: string[], file: string): boolean {
let included = false;
for (const pattern of paths) {
const negative = pattern.startsWith("!");
if (minimatch(file, negative ? pattern.slice(1) : pattern)) included = !negative;
}
return included;
}
function localTasks(): string[] {
const runner = readFileSync(new URL("src/apps/cli/testdeno/run-ci-suite.ts", root), "utf8");
expect(runner).toContain('import TASKS from "./ci-tasks.json" with { type: "json" }');
return JSON.parse(readFileSync(new URL("src/apps/cli/testdeno/ci-tasks.json", root), "utf8")) as string[];
}
function selectedTasks(task: string): string[] {
const select = workflow("cli-deno-tests.yml").jobs.prepare.steps?.find((step) => step.id === "select");
expect(select?.run).toBeDefined();
const directory = mkdtempSync(join(tmpdir(), "livesync-ci-matrix-"));
const output = join(directory, "output");
try {
const script = select!.run!.replace(/\$\{\{[^}]+\}\}/g, task);
const result = spawnSync("bash", ["-c", script], {
cwd: root,
env: { ...process.env, SELECTED_TASK: task, GITHUB_OUTPUT: output },
encoding: "utf8",
});
expect(result.status, result.stderr).toBe(0);
const line = readFileSync(output, "utf8")
.split("\n")
.find((line) => line.startsWith("task_matrix="));
return JSON.parse(line!.slice("task_matrix=".length)) as string[];
} finally {
rmSync(directory, { recursive: true, force: true });
}
}
describe("CLI CI selection", () => {
it("runs every task in the local standard suite automatically", () => {
expect(selectedTasks("test:ci")).toEqual(localTasks());
expect(selectedTasks("test:ci")).toEqual(
expect.arrayContaining(["test:settings-writeback", "test:remote-administration-exit-codes"])
);
const tasks = JSON.parse(readFileSync(new URL("src/apps/cli/testdeno/deno.json", root), "utf8")).tasks;
expect(localTasks().filter((task) => !(task in tasks))).toEqual([]);
expect(new Set(localTasks()).size).toBe(localTasks().length);
});
it("keeps the selected manual subsets", () => {
expect(selectedTasks("test:local")).toEqual(["test:setup-put-cat", "test:mirror", "test:daemon"]);
expect(selectedTasks("test:e2e-matrix")).toEqual(
localTasks().filter((task) => task.startsWith("test:e2e-matrix:"))
);
});
it.each([
["cli-deno-tests.yml", "push"],
["cli-deno-tests.yml", "pull_request"],
["cli-p2p-tests.yml", "push"],
["cli-p2p-tests.yml", "pull_request"],
["cli-docker.yml", "push"],
["cli-docker-validation.yml", "pull_request"],
])("validates shared CLI runtime inputs in %s on %s", (name, event) => {
const paths = workflow(name).on[event].paths ?? [];
const inputs = [
"src/apps/cli/main.ts",
"src/LiveSyncBaseCore.ts",
"src/serviceFeatures/replicationScheduling.ts",
"src/common/replicatorProviders.ts",
"src/common/messages/en.ts",
"src/modules/AbstractModule.ts",
"src/modules/main/ModuleLiveSyncMain.ts",
"src/modules/coreFeatures/ModuleResolveMismatchedTweaks.ts",
"src/serviceModules/FileHandler.ts",
"src/integrations/cloudflare/turnCredentials.ts",
"manifest.json",
"package.json",
"package-lock.json",
"tsconfig.json",
];
expect(inputs.filter((file) => !matches(paths, file))).toEqual([]);
expect(matches(paths, "src/modules/features/ModuleObsidianSettingTab.ts")).toBe(false);
expect(matches(paths, "docs/settings.md")).toBe(false);
expect(matches(paths, "src/apps/cli/testdeno/CONTRIBUTING_TESTS.md")).toBe(false);
});
it("runs Compose alone for a change to its fixture", () => {
const file = "test/bench-network/compose.yml";
expect(matches(workflow("cli-p2p-tests.yml").on.pull_request.paths ?? [], file)).toBe(true);
expect(matches(workflow("cli-deno-tests.yml").on.pull_request.paths ?? [], file)).toBe(false);
expect(matches(workflow("cli-docker-validation.yml").on.pull_request.paths ?? [], file)).toBe(false);
});
it("passes a single CLI build to every standard test job", () => {
const { prepare, test } = workflow("cli-deno-tests.yml").jobs;
expect(test.needs).toBe("prepare");
expect(prepare.steps?.filter((step) => step.run === "npm run build")).toHaveLength(1);
expect(test.steps?.some((step) => step.run?.includes("npm run build"))).toBe(false);
const upload = prepare.steps?.find((step) => step.uses?.startsWith("actions/upload-artifact@"));
const download = test.steps?.find((step) => step.uses?.startsWith("actions/download-artifact@"));
expect(upload?.with?.["if-no-files-found"]).toBe("error");
expect(download?.with?.name).toBe(upload?.with?.name);
expect(download?.with?.path).toBe(upload?.with?.path);
});
it("keeps registry credentials out of Docker validation and gates publication on it", () => {
const validation = workflow("cli-docker-validation.yml");
const publication = workflow("cli-docker.yml");
expect(validation.on.push).toBeUndefined();
const validationJob = validation.jobs.validate;
expect(validationJob.steps?.some((step) => step.uses?.startsWith("docker/login-action@"))).toBe(false);
const build = validationJob.steps?.find((step) => step.uses?.startsWith("docker/build-push-action@"));
expect(build?.with?.load).toBe(true);
expect(build?.with?.push).toBeUndefined();
expect(validationJob.steps?.find((step) => step.id === "e2e")?.run).toBe(
"npm --ignore-scripts run test:e2e:docker:all"
);
expect(publication.jobs.validate.uses).toBe("./.github/workflows/cli-docker-validation.yml");
expect(publication.jobs.validate.permissions).toEqual({ contents: "read" });
expect(publication.jobs["build-and-push"].needs).toBe("validate");
expect(publication.jobs["build-and-push"].if).toBe(
"${{ needs.validate.outputs.e2e_passed == 'true' || (github.event_name == 'workflow_dispatch' && inputs.force) }}"
);
const publishBuild = publication.jobs["build-and-push"].steps?.find((step) =>
step.uses?.startsWith("docker/build-push-action@")
);
expect(publishBuild?.with?.["cache-from"]).toBe(build?.with?.["cache-from"]);
expect(publishBuild?.with?.["cache-to"]).toBe(build?.with?.["cache-to"]);
});
});