mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-15 17:16:01 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d70ac8064 | |||
| f44dc76087 | |||
| 962589a40f | |||
| d8bbab6bed | |||
| b475ea64e2 | |||
| 908acc4a84 | |||
| 31e9186930 |
@@ -57,7 +57,7 @@ jobs:
|
|||||||
MAJOR_MINOR=$(echo "${VERSION}" | cut -d. -f1,2)
|
MAJOR_MINOR=$(echo "${VERSION}" | cut -d. -f1,2)
|
||||||
SHORT_SHA=$(git rev-parse --short HEAD)
|
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||||
IMAGE="ghcr.io/${{ github.repository_owner }}/livesync-cli"
|
IMAGE="ghcr.io/${{ github.repository_owner }}/livesync-cli"
|
||||||
|
|
||||||
# Build tag list based on the event and git ref
|
# Build tag list based on the event and git ref
|
||||||
TAGS=""
|
TAGS=""
|
||||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
# Other branches / manual run fallback
|
# Other branches / manual run fallback
|
||||||
TAGS="${IMAGE}:${VERSION}-dev-sha-${SHORT_SHA}-cli"
|
TAGS="${IMAGE}:${VERSION}-dev-sha-${SHORT_SHA}-cli"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Determine if the image should be pushed
|
# Determine if the image should be pushed
|
||||||
PUSH="true"
|
PUSH="true"
|
||||||
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
PUSH="false"
|
PUSH="false"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
||||||
echo "push=${PUSH}" >> $GITHUB_OUTPUT
|
echo "push=${PUSH}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
@@ -89,6 +89,11 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
with:
|
||||||
|
platforms: arm64
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
@@ -126,5 +131,6 @@ jobs:
|
|||||||
file: src/apps/cli/Dockerfile
|
file: src/apps/cli/Dockerfile
|
||||||
push: ${{ steps.meta.outputs.push }}
|
push: ${{ steps.meta.outputs.push }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ jobs:
|
|||||||
node-version: "24.x"
|
node-version: "24.x"
|
||||||
cache: npm
|
cache: npm
|
||||||
|
|
||||||
|
- name: Use Deno
|
||||||
|
uses: denoland/setup-deno@v2
|
||||||
|
with:
|
||||||
|
deno-version: v2.x
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "obsidian-livesync",
|
"id": "obsidian-livesync",
|
||||||
"name": "Self-hosted LiveSync",
|
"name": "Self-hosted LiveSync",
|
||||||
"version": "0.25.81",
|
"version": "0.25.82",
|
||||||
"minAppVersion": "1.7.2",
|
"minAppVersion": "1.7.2",
|
||||||
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Community implementation of self-hosted livesync. Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"author": "vorotamoroz",
|
"author": "vorotamoroz",
|
||||||
|
|||||||
Generated
+8
-8
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.25.81",
|
"version": "0.25.82",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.25.81",
|
"version": "0.25.82",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"src/apps/cli",
|
"src/apps/cli",
|
||||||
@@ -16226,11 +16226,11 @@
|
|||||||
},
|
},
|
||||||
"src/apps/cli": {
|
"src/apps/cli": {
|
||||||
"name": "self-hosted-livesync-cli",
|
"name": "self-hosted-livesync-cli",
|
||||||
"version": "0.25.81-cli",
|
"version": "0.25.82-cli",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"minimatch": "^10.2.5",
|
"minimatch": "^10.2.5",
|
||||||
"octagonal-wheels": "^0.1.50",
|
"octagonal-wheels": "^0.1.51",
|
||||||
"pouchdb-adapter-http": "^9.0.0",
|
"pouchdb-adapter-http": "^9.0.0",
|
||||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||||
"pouchdb-core": "^9.0.0",
|
"pouchdb-core": "^9.0.0",
|
||||||
@@ -16252,9 +16252,9 @@
|
|||||||
},
|
},
|
||||||
"src/apps/webapp": {
|
"src/apps/webapp": {
|
||||||
"name": "livesync-webapp",
|
"name": "livesync-webapp",
|
||||||
"version": "0.25.81-webapp",
|
"version": "0.25.82-webapp",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"octagonal-wheels": "^0.1.50"
|
"octagonal-wheels": "^0.1.51"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
@@ -16267,9 +16267,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/apps/webpeer": {
|
"src/apps/webpeer": {
|
||||||
"version": "0.25.81-webpeer",
|
"version": "0.25.82-webpeer",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"octagonal-wheels": "^0.1.50"
|
"octagonal-wheels": "^0.1.51"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-livesync",
|
"name": "obsidian-livesync",
|
||||||
"version": "0.25.81",
|
"version": "0.25.82",
|
||||||
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
"description": "Reflect your vault changes to some other devices immediately. Please make sure to disable other synchronize solutions to avoid content corruption or duplication.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
"test:e2e:obsidian:smoke": "tsx test/e2e-obsidian/scripts/smoke.ts",
|
"test:e2e:obsidian:smoke": "tsx test/e2e-obsidian/scripts/smoke.ts",
|
||||||
"test:e2e:obsidian:vault-reflection": "tsx test/e2e-obsidian/scripts/vault-reflection.ts",
|
"test:e2e:obsidian:vault-reflection": "tsx test/e2e-obsidian/scripts/vault-reflection.ts",
|
||||||
"test:e2e:obsidian:couchdb-upload": "tsx test/e2e-obsidian/scripts/couchdb-upload.ts",
|
"test:e2e:obsidian:couchdb-upload": "tsx test/e2e-obsidian/scripts/couchdb-upload.ts",
|
||||||
|
"test:e2e:obsidian:cli-to-obsidian-sync": "tsx test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts",
|
||||||
"test:e2e:obsidian:minio-upload": "tsx test/e2e-obsidian/scripts/minio-upload.ts",
|
"test:e2e:obsidian:minio-upload": "tsx test/e2e-obsidian/scripts/minio-upload.ts",
|
||||||
"test:e2e:obsidian:startup-scan": "tsx test/e2e-obsidian/scripts/startup-scan.ts",
|
"test:e2e:obsidian:startup-scan": "tsx test/e2e-obsidian/scripts/startup-scan.ts",
|
||||||
"test:e2e:obsidian:two-vault-sync": "tsx test/e2e-obsidian/scripts/two-vault-sync.ts",
|
"test:e2e:obsidian:two-vault-sync": "tsx test/e2e-obsidian/scripts/two-vault-sync.ts",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "self-hosted-livesync-cli",
|
"name": "self-hosted-livesync-cli",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.25.81-cli",
|
"version": "0.25.82-cli",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
"minimatch": "^10.2.5",
|
"minimatch": "^10.2.5",
|
||||||
"octagonal-wheels": "^0.1.50",
|
"octagonal-wheels": "^0.1.51",
|
||||||
"pouchdb-adapter-http": "^9.0.0",
|
"pouchdb-adapter-http": "^9.0.0",
|
||||||
"pouchdb-adapter-leveldb": "^9.0.0",
|
"pouchdb-adapter-leveldb": "^9.0.0",
|
||||||
"pouchdb-core": "^9.0.0",
|
"pouchdb-core": "^9.0.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "livesync-webapp",
|
"name": "livesync-webapp",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.25.81-webapp",
|
"version": "0.25.82-webapp",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"octagonal-wheels": "^0.1.50"
|
"octagonal-wheels": "^0.1.51"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "webpeer",
|
"name": "webpeer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.25.81-webpeer",
|
"version": "0.25.82-webpeer",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"octagonal-wheels": "^0.1.50"
|
"octagonal-wheels": "^0.1.51"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-plugin-svelte": "^3.19.0",
|
"eslint-plugin-svelte": "^3.19.0",
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ npm run test:e2e:obsidian:cli-help -- vaults verbose
|
|||||||
npm run test:e2e:obsidian:smoke
|
npm run test:e2e:obsidian:smoke
|
||||||
npm run test:e2e:obsidian:vault-reflection
|
npm run test:e2e:obsidian:vault-reflection
|
||||||
npm run test:e2e:obsidian:couchdb-upload
|
npm run test:e2e:obsidian:couchdb-upload
|
||||||
|
npm run test:e2e:obsidian:cli-to-obsidian-sync
|
||||||
npm run test:e2e:obsidian:minio-upload
|
npm run test:e2e:obsidian:minio-upload
|
||||||
npm run test:e2e:obsidian:startup-scan
|
npm run test:e2e:obsidian:startup-scan
|
||||||
npm run test:e2e:obsidian:two-vault-sync
|
npm run test:e2e:obsidian:two-vault-sync
|
||||||
@@ -61,10 +62,27 @@ npm run test:e2e:obsidian:local-suite
|
|||||||
npm run test:e2e:obsidian:local-suite:services
|
npm run test:e2e:obsidian:local-suite:services
|
||||||
```
|
```
|
||||||
|
|
||||||
`test:e2e:obsidian:local-suite` runs `npm run build`, discovery, smoke, vault reflection, CouchDB upload, Object Storage upload, startup scan, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB and MinIO fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run.
|
`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, vault reflection, CouchDB upload, CLI-to-Obsidian synchronisation, Object Storage upload, startup scan, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB and MinIO fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run.
|
||||||
|
|
||||||
`test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, configures Self-hosted LiveSync through `obsidian-cli eval`, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents.
|
`test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, configures Self-hosted LiveSync through `obsidian-cli eval`, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents.
|
||||||
|
|
||||||
|
`test:e2e:obsidian:cli-to-obsidian-sync` is the cross-runtime compatibility check for the official LiveSync CLI and the real Obsidian plug-in. Build the plug-in first, and build the local CLI too when no external CLI command is selected. The script uses E2EE, Path Obfuscation, and the current preferred chunk settings to create and synchronise a note through the CLI, starts real Obsidian with an isolated Vault and profile, synchronises the same CouchDB database, and verifies that the plug-in materialises identical note content. This covers the boundary that CLI-only and plug-in-only round trips do not exercise.
|
||||||
|
|
||||||
|
By default, the compatibility check runs `node src/apps/cli/dist/index.cjs`. Set `LIVESYNC_CLI_COMMAND` to test another CLI build or distribution. The value may be a quoted command line or a JSON array of executable and prefix arguments; the scenario arguments are appended without going through a shell.
|
||||||
|
|
||||||
|
For example, to test an executable on `PATH`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LIVESYNC_CLI_COMMAND='livesync-cli' npm run test:e2e:obsidian:cli-to-obsidian-sync
|
||||||
|
```
|
||||||
|
|
||||||
|
On Linux, a multi-architecture published Docker image can run against the local CouchDB fixture by sharing the temporary directory, using host networking, preserving the host user's file ownership, and overriding the image entrypoint so that the runner can supply its explicit database path. Images published before ARM64 support remain AMD64-only and require configured Docker emulation on an ARM host.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LIVESYNC_CLI_COMMAND="docker run --rm --network host --user $(id -u):$(id -g) --volume /tmp:/tmp --entrypoint node ghcr.io/vrtmrz/livesync-cli:edge /app/dist/index.cjs" \
|
||||||
|
npm run test:e2e:obsidian:cli-to-obsidian-sync
|
||||||
|
```
|
||||||
|
|
||||||
`test:e2e:obsidian:minio-upload` reuses the Object Storage variables from `.test.env` or the process environment. It expects a reachable S3-compatible service, configures Self-hosted LiveSync for Object Storage through `obsidian-cli eval`, creates a note in real Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written under a unique bucket prefix.
|
`test:e2e:obsidian:minio-upload` reuses the Object Storage variables from `.test.env` or the process environment. It expects a reachable S3-compatible service, configures Self-hosted LiveSync for Object Storage through `obsidian-cli eval`, creates a note in real Obsidian, runs one-shot Journal Sync, and verifies through the AWS SDK that objects were written under a unique bucket prefix.
|
||||||
|
|
||||||
`test:e2e:obsidian:startup-scan` configures a temporary CouchDB database, stops Obsidian, writes a note directly into the vault, restarts Obsidian, and verifies from CouchDB that the boot-time scan picked up the offline file.
|
`test:e2e:obsidian:startup-scan` configures a temporary CouchDB database, stops Obsidian, writes a note directly into the vault, restarts Obsidian, and verifies from CouchDB that the boot-time scan picked up the offline file.
|
||||||
@@ -104,6 +122,8 @@ Useful environment variables:
|
|||||||
- `E2E_OBSIDIAN_READY_TIMEOUT_MS`: plug-in readiness timeout in milliseconds.
|
- `E2E_OBSIDIAN_READY_TIMEOUT_MS`: plug-in readiness timeout in milliseconds.
|
||||||
- `E2E_OBSIDIAN_CLI_READY_TIMEOUT_MS`: timeout for waiting until the vault-side Obsidian CLI exposes the plug-in catalogue.
|
- `E2E_OBSIDIAN_CLI_READY_TIMEOUT_MS`: timeout for waiting until the vault-side Obsidian CLI exposes the plug-in catalogue.
|
||||||
- `E2E_OBSIDIAN_CLI_TIMEOUT_MS`: timeout for each `obsidian-cli` invocation.
|
- `E2E_OBSIDIAN_CLI_TIMEOUT_MS`: timeout for each `obsidian-cli` invocation.
|
||||||
|
- `E2E_LIVESYNC_CLI_TIMEOUT_MS`: timeout for each official LiveSync CLI invocation in the CLI-to-Obsidian compatibility check; default is 60 seconds.
|
||||||
|
- `LIVESYNC_CLI_COMMAND`: optional LiveSync CLI executable and prefix arguments used by the CLI-to-Obsidian compatibility check. The default is the locally built CLI.
|
||||||
- `E2E_OBSIDIAN_FILE_TIMEOUT_MS`: timeout for waiting until a note created through Obsidian's vault API is reflected to disk.
|
- `E2E_OBSIDIAN_FILE_TIMEOUT_MS`: timeout for waiting until a note created through Obsidian's vault API is reflected to disk.
|
||||||
- `E2E_OBSIDIAN_CORE_READY_TIMEOUT_MS`: timeout for waiting until Self-hosted LiveSync reports that its core lifecycle and local database are ready.
|
- `E2E_OBSIDIAN_CORE_READY_TIMEOUT_MS`: timeout for waiting until Self-hosted LiveSync reports that its core lifecycle and local database are ready.
|
||||||
- `E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS`: timeout for waiting until a file appears in Self-hosted LiveSync's local database.
|
- `E2E_OBSIDIAN_LOCAL_DB_TIMEOUT_MS`: timeout for waiting until a file appears in Self-hosted LiveSync's local database.
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import {
|
||||||
|
assertCouchDbReachable,
|
||||||
|
createCouchDbDatabase,
|
||||||
|
deleteCouchDbDatabase,
|
||||||
|
loadCouchDbConfig,
|
||||||
|
makeUniqueDatabaseName,
|
||||||
|
waitForCouchDbDocs,
|
||||||
|
} from "../runner/couchdb.ts";
|
||||||
|
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||||
|
import {
|
||||||
|
assertEqual,
|
||||||
|
configureCouchDb,
|
||||||
|
prepareRemote,
|
||||||
|
pushLocalChanges,
|
||||||
|
waitForLiveSyncCoreReady,
|
||||||
|
} from "../runner/liveSyncWorkflow.ts";
|
||||||
|
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||||
|
import { createTemporaryVault } from "../runner/vault.ts";
|
||||||
|
|
||||||
|
process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "30000";
|
||||||
|
process.env.E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS ??= "30000";
|
||||||
|
process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ??= "30000";
|
||||||
|
|
||||||
|
const liveSyncCli = resolve("src/apps/cli/dist/index.cjs");
|
||||||
|
const notePath = "E2E/cli-to-obsidian.md";
|
||||||
|
const noteContent = [
|
||||||
|
"# CLI to real Obsidian",
|
||||||
|
"",
|
||||||
|
"This note was created by the Self-hosted LiveSync CLI.",
|
||||||
|
"The real Obsidian plug-in must retrieve the same content from CouchDB.",
|
||||||
|
"0123456789 abcdefghijklmnopqrstuvwxyz 0123456789 abcdefghijklmnopqrstuvwxyz",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
const e2eePassphrase = "real-obsidian-cli-compatibility-e2e";
|
||||||
|
|
||||||
|
type LiveSyncCliCommand = {
|
||||||
|
executable: string;
|
||||||
|
prefixArgs: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CliResult = {
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CliFileInfo = {
|
||||||
|
id: string;
|
||||||
|
children: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseCommandLine(value: string): string[] {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.startsWith("[")) {
|
||||||
|
const parsed = JSON.parse(trimmed) as unknown;
|
||||||
|
if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((part) => typeof part !== "string")) {
|
||||||
|
throw new Error("LIVESYNC_CLI_COMMAND JSON form must be a non-empty array of strings.");
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: string[] = [];
|
||||||
|
let current = "";
|
||||||
|
let quote: "'" | '"' | undefined;
|
||||||
|
let tokenStarted = false;
|
||||||
|
for (let index = 0; index < trimmed.length; index++) {
|
||||||
|
const character = trimmed[index];
|
||||||
|
if (quote) {
|
||||||
|
if (character === quote) {
|
||||||
|
quote = undefined;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character === "\\" && quote === '"' && ['"', "\\"].includes(trimmed[index + 1] ?? "")) {
|
||||||
|
current += trimmed[++index];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += character;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character === "'" || character === '"') {
|
||||||
|
quote = character;
|
||||||
|
tokenStarted = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (character === "\\" && ["'", '"', "\\", " ", "\t"].includes(trimmed[index + 1] ?? "")) {
|
||||||
|
current += trimmed[++index];
|
||||||
|
tokenStarted = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/\s/u.test(character)) {
|
||||||
|
if (tokenStarted) {
|
||||||
|
parts.push(current);
|
||||||
|
current = "";
|
||||||
|
tokenStarted = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += character;
|
||||||
|
tokenStarted = true;
|
||||||
|
}
|
||||||
|
if (quote) {
|
||||||
|
throw new Error("LIVESYNC_CLI_COMMAND contains an unterminated quoted value.");
|
||||||
|
}
|
||||||
|
if (tokenStarted) {
|
||||||
|
parts.push(current);
|
||||||
|
}
|
||||||
|
if (parts.length === 0) {
|
||||||
|
throw new Error("LIVESYNC_CLI_COMMAND must not be empty.");
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLiveSyncCliCommand(): LiveSyncCliCommand {
|
||||||
|
const override = process.env.LIVESYNC_CLI_COMMAND;
|
||||||
|
if (override !== undefined) {
|
||||||
|
const [executable, ...prefixArgs] = parseCommandLine(override);
|
||||||
|
return { executable, prefixArgs };
|
||||||
|
}
|
||||||
|
return { executable: process.execPath, prefixArgs: [liveSyncCli] };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runLiveSyncCli(command: LiveSyncCliCommand, args: string[]): Promise<CliResult> {
|
||||||
|
return await new Promise((resolvePromise, reject) => {
|
||||||
|
const timeoutMs = Number(process.env.E2E_LIVESYNC_CLI_TIMEOUT_MS ?? 60000);
|
||||||
|
const child = spawn(command.executable, [...command.prefixArgs, ...args], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: process.env,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
child.stdout.on("data", (chunk: Buffer) => {
|
||||||
|
stdout += chunk.toString("utf-8");
|
||||||
|
});
|
||||||
|
child.stderr.on("data", (chunk: Buffer) => {
|
||||||
|
stderr += chunk.toString("utf-8");
|
||||||
|
});
|
||||||
|
let timedOut = false;
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
timedOut = true;
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
}, timeoutMs);
|
||||||
|
child.on("error", (error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
child.on("exit", (code, signal) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
const result = {
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
};
|
||||||
|
if (timedOut) {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`LiveSync CLI timed out after ${timeoutMs} ms\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (code === 0) {
|
||||||
|
resolvePromise(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`LiveSync CLI failed with ${signal ? `signal ${signal}` : `exit code ${String(code)}`}\n` +
|
||||||
|
`stdout:\n${result.stdout}\nstderr:\n${result.stderr}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function configureLiveSyncCli(
|
||||||
|
command: LiveSyncCliCommand,
|
||||||
|
settingsPath: string,
|
||||||
|
couchDb: Awaited<ReturnType<typeof loadCouchDbConfig>>,
|
||||||
|
dbName: string
|
||||||
|
): Promise<void> {
|
||||||
|
await runLiveSyncCli(command, ["init-settings", "--force", settingsPath]);
|
||||||
|
const settings = JSON.parse(await readFile(settingsPath, "utf-8")) as Record<string, unknown>;
|
||||||
|
Object.assign(settings, {
|
||||||
|
couchDB_URI: couchDb.uri,
|
||||||
|
couchDB_USER: couchDb.username,
|
||||||
|
couchDB_PASSWORD: couchDb.password,
|
||||||
|
couchDB_DBNAME: dbName,
|
||||||
|
remoteType: "",
|
||||||
|
liveSync: false,
|
||||||
|
syncOnStart: false,
|
||||||
|
syncOnSave: false,
|
||||||
|
usePluginSync: false,
|
||||||
|
usePluginSyncV2: true,
|
||||||
|
useEden: false,
|
||||||
|
customChunkSize: 60,
|
||||||
|
sendChunksBulk: false,
|
||||||
|
sendChunksBulkMaxSize: 1,
|
||||||
|
chunkSplitterVersion: "v3-rabin-karp",
|
||||||
|
readChunksOnline: true,
|
||||||
|
disableCheckingConfigMismatch: false,
|
||||||
|
enableCompression: false,
|
||||||
|
hashAlg: "xxhash64",
|
||||||
|
handleFilenameCaseSensitive: false,
|
||||||
|
doNotUseFixedRevisionForChunks: true,
|
||||||
|
E2EEAlgorithm: "v2",
|
||||||
|
encrypt: true,
|
||||||
|
passphrase: e2eePassphrase,
|
||||||
|
usePathObfuscation: true,
|
||||||
|
doctorProcessedVersion: "0.25.27",
|
||||||
|
isConfigured: true,
|
||||||
|
});
|
||||||
|
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeCliNote(
|
||||||
|
command: LiveSyncCliCommand,
|
||||||
|
databasePath: string,
|
||||||
|
settingsPath: string,
|
||||||
|
sourcePath: string
|
||||||
|
): Promise<CliFileInfo> {
|
||||||
|
await mkdir(dirname(sourcePath), { recursive: true });
|
||||||
|
await writeFile(sourcePath, noteContent, "utf-8");
|
||||||
|
await runLiveSyncCli(command, [databasePath, "--settings", settingsPath, "push", sourcePath, notePath]);
|
||||||
|
const info = await runLiveSyncCli(command, [databasePath, "--settings", settingsPath, "info", notePath]);
|
||||||
|
const fileInfo = JSON.parse(info.stdout) as CliFileInfo;
|
||||||
|
if (!fileInfo.id || !Array.isArray(fileInfo.children) || fileInfo.children.length === 0) {
|
||||||
|
throw new Error(`LiveSync CLI did not create complete metadata for ${notePath}: ${info.stdout}`);
|
||||||
|
}
|
||||||
|
await runLiveSyncCli(command, [databasePath, "--settings", settingsPath, "sync"]);
|
||||||
|
return fileInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForVaultContent(
|
||||||
|
vaultPath: string,
|
||||||
|
path: string,
|
||||||
|
timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS)
|
||||||
|
): Promise<string> {
|
||||||
|
const fullPath = join(vaultPath, path);
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
let lastContent = "";
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
try {
|
||||||
|
lastContent = await readFile(fullPath, "utf-8");
|
||||||
|
if (lastContent === noteContent) {
|
||||||
|
return lastContent;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await new Promise((resolvePromise) => setTimeout(resolvePromise, 250));
|
||||||
|
}
|
||||||
|
throw new Error(`Timed out waiting for CLI-created note at ${fullPath}. Last content:\n${lastContent}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const liveSyncCliCommand = resolveLiveSyncCliCommand();
|
||||||
|
if (process.env.LIVESYNC_CLI_COMMAND === undefined) {
|
||||||
|
await access(liveSyncCli).catch(() => {
|
||||||
|
throw new Error(
|
||||||
|
`Built LiveSync CLI was not found at ${liveSyncCli}. Run 'npm run build -w self-hosted-livesync-cli' first, or set LIVESYNC_CLI_COMMAND.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const binary = requireObsidianBinary();
|
||||||
|
const obsidianCli = discoverObsidianCli();
|
||||||
|
if (!obsidianCli.binary) {
|
||||||
|
throw new Error(`Could not find obsidian-cli. Checked paths: ${obsidianCli.checked.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const couchDb = await loadCouchDbConfig();
|
||||||
|
const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "cli-to-obsidian");
|
||||||
|
const cliState = await mkdtemp(join(tmpdir(), "livesync-cli-to-obsidian-e2e-"));
|
||||||
|
const cliDatabasePath = join(cliState, "database");
|
||||||
|
const cliSettingsPath = join(cliState, "settings.json");
|
||||||
|
const cliSourcePath = join(cliState, "source", "cli-to-obsidian.md");
|
||||||
|
const vault = await createTemporaryVault();
|
||||||
|
let session: ObsidianLiveSyncSession | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assertCouchDbReachable(couchDb);
|
||||||
|
await createCouchDbDatabase(couchDb, dbName);
|
||||||
|
await mkdir(cliDatabasePath, { recursive: true });
|
||||||
|
await configureLiveSyncCli(liveSyncCliCommand, cliSettingsPath, couchDb, dbName);
|
||||||
|
|
||||||
|
if (process.env.LIVESYNC_CLI_COMMAND === undefined) {
|
||||||
|
console.log(`Using locally built LiveSync CLI: ${liveSyncCli}`);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Using LiveSync CLI command override: ${JSON.stringify(liveSyncCliCommand.executable)} ` +
|
||||||
|
`with ${liveSyncCliCommand.prefixArgs.length} prefix argument(s)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`Using Obsidian executable: ${binary}`);
|
||||||
|
console.log(`Temporary Obsidian vault: ${vault.path}`);
|
||||||
|
console.log(`Temporary CouchDB database: ${dbName}`);
|
||||||
|
|
||||||
|
const cliFileInfo = await writeCliNote(liveSyncCliCommand, cliDatabasePath, cliSettingsPath, cliSourcePath);
|
||||||
|
await waitForCouchDbDocs(couchDb, dbName, (docs) => {
|
||||||
|
const ids = new Set(docs.map((doc) => doc._id));
|
||||||
|
return ids.has(cliFileInfo.id) && cliFileInfo.children.every((childId) => ids.has(childId));
|
||||||
|
});
|
||||||
|
|
||||||
|
session = await startObsidianLiveSyncSession({
|
||||||
|
binary,
|
||||||
|
cliBinary: obsidianCli.binary,
|
||||||
|
vault,
|
||||||
|
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||||
|
});
|
||||||
|
await waitForLiveSyncCoreReady(obsidianCli.binary, session.cliEnv);
|
||||||
|
await configureCouchDb(
|
||||||
|
obsidianCli.binary,
|
||||||
|
session.cliEnv,
|
||||||
|
{
|
||||||
|
uri: couchDb.uri,
|
||||||
|
username: couchDb.username,
|
||||||
|
password: couchDb.password,
|
||||||
|
dbName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
encrypt: true,
|
||||||
|
passphrase: e2eePassphrase,
|
||||||
|
usePathObfuscation: true,
|
||||||
|
E2EEAlgorithm: "v2",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await waitForLiveSyncCoreReady(obsidianCli.binary, session.cliEnv);
|
||||||
|
await prepareRemote(obsidianCli.binary, session.cliEnv);
|
||||||
|
await pushLocalChanges(obsidianCli.binary, session.cliEnv);
|
||||||
|
|
||||||
|
const received = await waitForVaultContent(vault.path, notePath);
|
||||||
|
assertEqual(received, noteContent, "The real Obsidian plug-in did not materialise the CLI-created note.");
|
||||||
|
console.log("CLI-created encrypted note was retrieved by the real Obsidian plug-in with identical content.");
|
||||||
|
} finally {
|
||||||
|
if (session) {
|
||||||
|
await session.app.stop();
|
||||||
|
}
|
||||||
|
await vault.dispose();
|
||||||
|
await rm(cliState, { recursive: true, force: true });
|
||||||
|
if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") {
|
||||||
|
await deleteCouchDbDatabase(couchDb, dbName).catch((error: unknown) => {
|
||||||
|
console.warn(error instanceof Error ? error.message : error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error: unknown) => {
|
||||||
|
console.error(error instanceof Error ? error.stack : error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -8,10 +8,17 @@ type Step = {
|
|||||||
|
|
||||||
const testSteps: Step[] = [
|
const testSteps: Step[] = [
|
||||||
{ name: "build", args: ["run", "build"] },
|
{ name: "build", args: ["run", "build"] },
|
||||||
|
...(process.env.LIVESYNC_CLI_COMMAND === undefined
|
||||||
|
? [{ name: "CLI build", args: ["run", "build", "-w", "self-hosted-livesync-cli"] }]
|
||||||
|
: []),
|
||||||
{ name: "discover", args: ["run", "test:e2e:obsidian:discover"] },
|
{ name: "discover", args: ["run", "test:e2e:obsidian:discover"] },
|
||||||
{ name: "smoke", args: ["run", "test:e2e:obsidian:smoke"] },
|
{ name: "smoke", args: ["run", "test:e2e:obsidian:smoke"] },
|
||||||
{ name: "vault reflection", args: ["run", "test:e2e:obsidian:vault-reflection"] },
|
{ name: "vault reflection", args: ["run", "test:e2e:obsidian:vault-reflection"] },
|
||||||
{ name: "CouchDB upload", args: ["run", "test:e2e:obsidian:couchdb-upload"] },
|
{ name: "CouchDB upload", args: ["run", "test:e2e:obsidian:couchdb-upload"] },
|
||||||
|
{
|
||||||
|
name: "CLI to real Obsidian synchronisation",
|
||||||
|
args: ["run", "test:e2e:obsidian:cli-to-obsidian-sync"],
|
||||||
|
},
|
||||||
{ name: "Object Storage upload", args: ["run", "test:e2e:obsidian:minio-upload"] },
|
{ name: "Object Storage upload", args: ["run", "test:e2e:obsidian:minio-upload"] },
|
||||||
{ name: "startup scan", args: ["run", "test:e2e:obsidian:startup-scan"] },
|
{ name: "startup scan", args: ["run", "test:e2e:obsidian:startup-scan"] },
|
||||||
{ name: "two-vault synchronisation", args: ["run", "test:e2e:obsidian:two-vault-sync"] },
|
{ name: "two-vault synchronisation", args: ["run", "test:e2e:obsidian:two-vault-sync"] },
|
||||||
|
|||||||
@@ -105,6 +105,13 @@ for (const dir of workspaceDirs) {
|
|||||||
console.log(` Dependency '${dep}': ${oldVer} -> ${newVer}`);
|
console.log(` Dependency '${dep}': ${oldVer} -> ${newVer}`);
|
||||||
pkg.dependencies[dep] = newVer;
|
pkg.dependencies[dep] = newVer;
|
||||||
}
|
}
|
||||||
|
if (lockWorkspace) {
|
||||||
|
lockWorkspace.dependencies ??= {};
|
||||||
|
if (lockWorkspace.dependencies[dep] !== newVer) {
|
||||||
|
lockWorkspace.dependencies[dep] = newVer;
|
||||||
|
packageLockChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +126,13 @@ for (const dir of workspaceDirs) {
|
|||||||
console.log(` DevDependency '${dep}': ${oldVer} -> ${newVer}`);
|
console.log(` DevDependency '${dep}': ${oldVer} -> ${newVer}`);
|
||||||
pkg.devDependencies[dep] = newVer;
|
pkg.devDependencies[dep] = newVer;
|
||||||
}
|
}
|
||||||
|
if (lockWorkspace) {
|
||||||
|
lockWorkspace.devDependencies ??= {};
|
||||||
|
if (lockWorkspace.devDependencies[dep] !== newVer) {
|
||||||
|
lockWorkspace.devDependencies[dep] = newVer;
|
||||||
|
packageLockChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -5,9 +5,15 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## 0.25.82
|
||||||
|
|
||||||
|
15th July, 2026
|
||||||
|
|
||||||
|
Recently, I created a repository called Fancy Kit and have been trying to build some proper infrastructure around it. Does any of this look like cumbersome bureaucracy? No need to worry: you can carry on as usual. Codex is simply tidying up my usual rambling prose, terrible pull requests, and disjointed remarks.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Refresh the remote Security Seed before each replication, preventing a client that remained open during a remote database rebuild from uploading data encrypted with the previous seed (#1018).
|
- Refreshed the remote Security Seed before each replication, preventing a client that remained open during a remote database rebuild from uploading data encrypted with the previous seed (#1018).
|
||||||
- The P2P **Start Sync & Close** action now waits for synchronisation to settle before closing the dialogue, avoiding premature release of screen-awake protection while work remains in flight.
|
- The P2P **Start Sync & Close** action now waits for synchronisation to settle before closing the dialogue, avoiding premature release of screen-awake protection while work remains in flight.
|
||||||
|
|
||||||
### Improved
|
### Improved
|
||||||
@@ -15,6 +21,14 @@ The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsid
|
|||||||
- On supported mobile and desktop devices, one-shot replication, P2P peer discovery and selection, database rebuild and fetch operations, and remote chunk fetching now keep the screen awake for the duration of these finite remote operations. LiveSync also postpones its normal visibility suspension until the operation finishes, although platform restrictions may still pause or terminate background work.
|
- On supported mobile and desktop devices, one-shot replication, P2P peer discovery and selection, database rebuild and fetch operations, and remote chunk fetching now keep the screen awake for the duration of these finite remote operations. LiveSync also postpones its normal visibility suspension until the operation finishes, although platform restrictions may still pause or terminate background work.
|
||||||
- The 📲 status-bar indicator now covers finite remote work as well as HTTP requests. It reports broad remote activity, such as P2P peer selection and remote chunk fetching, rather than an exact physical connection count.
|
- The 📲 status-bar indicator now covers finite remote work as well as HTTP requests. It reports broad remote activity, such as P2P peer selection and remote chunk fetching, rather than an exact physical connection count.
|
||||||
|
|
||||||
|
### Improved (CLI)
|
||||||
|
|
||||||
|
- CLI container images are now published for both AMD64 and ARM64 platforms.
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- Added a local real-Obsidian compatibility test that writes an encrypted, path-obfuscated note through the LiveSync CLI and verifies that the plug-in materialises the same content.
|
||||||
|
|
||||||
## 0.25.81
|
## 0.25.81
|
||||||
|
|
||||||
14th July, 2026
|
14th July, 2026
|
||||||
|
|||||||
@@ -113,6 +113,15 @@ describe("release workflow", () => {
|
|||||||
expect(workflow).toMatch(/git add[^\n]*_types/);
|
expect(workflow).toMatch(/git add[^\n]*_types/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("installs Deno before post-processing fallback type definitions", () => {
|
||||||
|
const workflow = readFileSync(prepareReleaseWorkflow, "utf8");
|
||||||
|
const setupDeno = workflow.indexOf("denoland/setup-deno@v2");
|
||||||
|
const buildTypes = workflow.indexOf("npm run build:lib:types");
|
||||||
|
|
||||||
|
expect(setupDeno).toBeGreaterThan(-1);
|
||||||
|
expect(setupDeno).toBeLessThan(buildTypes);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the release PR in draft until BRAT validation", () => {
|
it("keeps the release PR in draft until BRAT validation", () => {
|
||||||
const workflow = readFileSync(prepareReleaseWorkflow, "utf8");
|
const workflow = readFileSync(prepareReleaseWorkflow, "utf8");
|
||||||
|
|
||||||
@@ -158,9 +167,18 @@ describe("workspace version update", () => {
|
|||||||
it("keeps workspace package and lockfile versions together", () => {
|
it("keeps workspace package and lockfile versions together", () => {
|
||||||
const directory = makeTemporaryDirectory();
|
const directory = makeTemporaryDirectory();
|
||||||
const workspaces = ["src/apps/cli", "src/apps/webpeer", "src/apps/webapp"];
|
const workspaces = ["src/apps/cli", "src/apps/webpeer", "src/apps/webapp"];
|
||||||
writeJson(directory, "package.json", { version: "0.25.81", workspaces });
|
writeJson(directory, "package.json", {
|
||||||
|
version: "0.25.81",
|
||||||
|
workspaces,
|
||||||
|
dependencies: { "octagonal-wheels": "^0.1.51" },
|
||||||
|
devDependencies: { typescript: "^5.9.3" },
|
||||||
|
});
|
||||||
for (const workspace of ["cli", "webpeer", "webapp"]) {
|
for (const workspace of ["cli", "webpeer", "webapp"]) {
|
||||||
writeJson(directory, `src/apps/${workspace}/package.json`, { version: `0.25.80-${workspace}` });
|
writeJson(directory, `src/apps/${workspace}/package.json`, {
|
||||||
|
version: `0.25.80-${workspace}`,
|
||||||
|
dependencies: { "octagonal-wheels": "^0.1.50" },
|
||||||
|
devDependencies: { typescript: "^5.8.0" },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
writeJson(directory, "package-lock.json", {
|
writeJson(directory, "package-lock.json", {
|
||||||
name: "obsidian-livesync",
|
name: "obsidian-livesync",
|
||||||
@@ -168,9 +186,21 @@ describe("workspace version update", () => {
|
|||||||
lockfileVersion: 3,
|
lockfileVersion: 3,
|
||||||
packages: {
|
packages: {
|
||||||
"": { version: "0.25.80", workspaces },
|
"": { version: "0.25.80", workspaces },
|
||||||
"src/apps/cli": { version: "0.25.80-cli" },
|
"src/apps/cli": {
|
||||||
"src/apps/webpeer": { version: "0.25.80-webpeer" },
|
version: "0.25.80-cli",
|
||||||
"src/apps/webapp": { version: "0.25.80-webapp" },
|
dependencies: { "octagonal-wheels": "^0.1.50" },
|
||||||
|
devDependencies: { typescript: "^5.8.0" },
|
||||||
|
},
|
||||||
|
"src/apps/webpeer": {
|
||||||
|
version: "0.25.80-webpeer",
|
||||||
|
dependencies: { "octagonal-wheels": "^0.1.50" },
|
||||||
|
devDependencies: { typescript: "^5.8.0" },
|
||||||
|
},
|
||||||
|
"src/apps/webapp": {
|
||||||
|
version: "0.25.80-webapp",
|
||||||
|
dependencies: { "octagonal-wheels": "^0.1.50" },
|
||||||
|
devDependencies: { typescript: "^5.8.0" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -180,6 +210,8 @@ describe("workspace version update", () => {
|
|||||||
for (const workspace of ["cli", "webpeer", "webapp"]) {
|
for (const workspace of ["cli", "webpeer", "webapp"]) {
|
||||||
const packageJson = JSON.parse(readFileSync(join(directory, `src/apps/${workspace}/package.json`), "utf8"));
|
const packageJson = JSON.parse(readFileSync(join(directory, `src/apps/${workspace}/package.json`), "utf8"));
|
||||||
expect(packageJson.version).toBe(`0.25.81-${workspace}`);
|
expect(packageJson.version).toBe(`0.25.81-${workspace}`);
|
||||||
|
expect(packageJson.dependencies["octagonal-wheels"]).toBe("^0.1.51");
|
||||||
|
expect(packageJson.devDependencies.typescript).toBe("^5.9.3");
|
||||||
}
|
}
|
||||||
const packageLock = JSON.parse(readFileSync(join(directory, "package-lock.json"), "utf8"));
|
const packageLock = JSON.parse(readFileSync(join(directory, "package-lock.json"), "utf8"));
|
||||||
expect(packageLock.version).toBe("0.25.81");
|
expect(packageLock.version).toBe("0.25.81");
|
||||||
@@ -187,5 +219,9 @@ describe("workspace version update", () => {
|
|||||||
expect(packageLock.packages["src/apps/cli"].version).toBe("0.25.81-cli");
|
expect(packageLock.packages["src/apps/cli"].version).toBe("0.25.81-cli");
|
||||||
expect(packageLock.packages["src/apps/webpeer"].version).toBe("0.25.81-webpeer");
|
expect(packageLock.packages["src/apps/webpeer"].version).toBe("0.25.81-webpeer");
|
||||||
expect(packageLock.packages["src/apps/webapp"].version).toBe("0.25.81-webapp");
|
expect(packageLock.packages["src/apps/webapp"].version).toBe("0.25.81-webapp");
|
||||||
|
for (const workspace of workspaces) {
|
||||||
|
expect(packageLock.packages[workspace].dependencies["octagonal-wheels"]).toBe("^0.1.51");
|
||||||
|
expect(packageLock.packages[workspace].devDependencies.typescript).toBe("^5.9.3");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-1
@@ -3,5 +3,6 @@
|
|||||||
"0.25.60": "1.7.2",
|
"0.25.60": "1.7.2",
|
||||||
"1.0.1": "0.9.12",
|
"1.0.1": "0.9.12",
|
||||||
"1.0.0": "0.9.7",
|
"1.0.0": "0.9.7",
|
||||||
"0.25.81": "1.7.2"
|
"0.25.81": "1.7.2",
|
||||||
|
"0.25.82": "1.7.2"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user