Compare commits

..

15 Commits

Author SHA1 Message Date
vorotamoroz 3efc72bab9 limit permission 2026-07-08 03:24:33 +00:00
vorotamoroz b8a4c824e2 improve release flow 2026-07-08 03:21:40 +00:00
vorotamoroz 6a9918677f Merge pull request #996 from vrtmrz/0_25_80
releasing 0.25.80
2026-07-07 21:21:34 +09:00
vorotamoroz 2d42b92a89 bump 2026-07-07 12:15:58 +00:00
vorotamoroz a2b794c520 Merge pull request #995 from vrtmrz/fix_conflict_issues
Fix conflict issues
2026-07-07 21:13:00 +09:00
vorotamoroz 6f9446f447 Merge remote-tracking branch 'origin/main' into fix_conflict_issues 2026-07-07 12:11:38 +00:00
vorotamoroz 3f9cd67b1c Merge pull request #992 from vrtmrz/fix_989
fix: enable hidden file sync before overwrite setup
2026-07-07 21:10:09 +09:00
vorotamoroz dbcbf2c5ca fix: document safer conflict preservation 2026-07-07 12:05:40 +00:00
vorotamoroz eed0fca8d3 fix: preserve ambiguous conflict candidates 2026-07-07 12:00:24 +00:00
vorotamoroz 05e031b90b update submodule pointer 2026-07-07 11:31:29 +00:00
vorotamoroz bec767c13f add notes 2026-07-07 11:30:36 +00:00
vorotamoroz 8046a777af fix: refine conflict merge policy 2026-07-07 11:29:51 +00:00
vorotamoroz 0c58b0c513 test: reproduce conflict issue reports 2026-07-07 11:07:09 +00:00
vorotamoroz b66e227a02 update doc
update submodule pointer
2026-07-07 10:54:49 +00:00
vorotamoroz af6df84b5d fix: enable hidden file sync before overwrite setup 2026-07-03 05:09:55 +00:00
18 changed files with 620 additions and 87 deletions
+82
View File
@@ -0,0 +1,82 @@
name: Finalise Release Tags
on:
workflow_dispatch:
inputs:
version:
description: Release version, for example 0.25.81
required: true
type: string
release_branch:
description: Release PR branch. Defaults to the version with dots replaced by underscores.
required: false
type: string
jobs:
finalise:
runs-on: ubuntu-latest
environment: release
permissions:
contents: write
steps:
- name: Resolve release branch
id: branch
env:
VERSION: ${{ inputs.version }}
RELEASE_BRANCH_INPUT: ${{ inputs.release_branch }}
run: |
set -euo pipefail
BRANCH="${RELEASE_BRANCH_INPUT}"
if [[ -z "${BRANCH}" ]]; then
BRANCH="${VERSION//./_}"
fi
echo "name=${BRANCH}" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
with:
ref: ${{ steps.branch.outputs.name }}
fetch-depth: 0
submodules: recursive
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
- name: Validate release head
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
node utils/release-notes.mjs validate "${VERSION}"
git fetch --tags --force
if git rev-parse --verify --quiet "refs/tags/${VERSION}" >/dev/null; then
echo "Tag already exists: ${VERSION}" >&2
exit 1
fi
if git rev-parse --verify --quiet "refs/tags/${VERSION}-cli" >/dev/null; then
echo "Tag already exists: ${VERSION}-cli" >&2
exit 1
fi
- name: Create and push release tags
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
git tag "${VERSION}"
git tag "${VERSION}-cli"
git push origin "${VERSION}" "${VERSION}-cli"
- name: Summarise next steps
env:
VERSION: ${{ inputs.version }}
run: |
{
echo "Created tags \`${VERSION}\` and \`${VERSION}-cli\`."
echo ""
echo "The plug-in release workflow is triggered by the \`${VERSION}\` tag and creates a draft release by default."
echo "The CLI Docker workflow is triggered by the \`${VERSION}-cli\` tag and publishes the version, major-minor, latest, and SHA-qualified image tags."
echo ""
echo "To create a pre-release instead of the default draft flow, run \`Release Obsidian Plugin\` manually with \`tag=${VERSION}\`, \`draft=false\`, and \`prerelease=true\`."
} >> "$GITHUB_STEP_SUMMARY"
+102
View File
@@ -0,0 +1,102 @@
name: Prepare Release PR
on:
workflow_dispatch:
inputs:
version:
description: Release version, for example 0.25.81
required: true
type: string
base_branch:
description: Base branch for the release PR
required: false
type: string
default: main
release_branch:
description: Release branch name. Defaults to the version with dots replaced by underscores.
required: false
type: string
allow_empty_updates:
description: Allow an empty Unreleased section
required: false
type: boolean
default: false
jobs:
prepare:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.base_branch }}
fetch-depth: 0
submodules: recursive
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "24.x"
cache: npm
- name: Install dependencies
run: npm ci
- name: Prepare release changes
id: prepare
env:
VERSION: ${{ inputs.version }}
RELEASE_BRANCH_INPUT: ${{ inputs.release_branch }}
ALLOW_EMPTY_UPDATES: ${{ inputs.allow_empty_updates }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
BRANCH="${RELEASE_BRANCH_INPUT}"
if [[ -z "${BRANCH}" ]]; then
BRANCH="${VERSION//./_}"
fi
if git ls-remote --exit-code --heads origin "${BRANCH}" >/dev/null 2>&1; then
echo "Release branch already exists: ${BRANCH}" >&2
exit 1
fi
git switch -c "${BRANCH}"
npm version "${VERSION}" --no-git-tag-version
node utils/release-notes.mjs prepare "${VERSION}"
npm run pretty:json
git add package.json package-lock.json manifest.json versions.json updates.md src/apps/cli/package.json src/apps/webpeer/package.json src/apps/webapp/package.json
git diff --cached --check
git commit -m "Releasing ${VERSION}"
git push --set-upstream origin "${BRANCH}"
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
- name: Create draft release PR
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
BASE_BRANCH: ${{ inputs.base_branch }}
RELEASE_BRANCH: ${{ steps.prepare.outputs.branch }}
run: |
cat > /tmp/release-pr-body.md <<EOF
## Release checklist
- [ ] Review and polish \`updates.md\`
- [ ] Confirm \`manifest.json\`, \`versions.json\`, and workspace package versions
- [ ] Confirm CI has passed
- [ ] Run the finalise release workflow after the PR head is fixed
- [ ] Merge this PR after the draft or pre-release has been created
EOF
gh pr create \
--base "${BASE_BRANCH}" \
--head "${RELEASE_BRANCH}" \
--draft \
--title "Releasing ${VERSION}" \
--body-file /tmp/release-pr-body.md
+31 -2
View File
@@ -6,10 +6,26 @@ on:
- '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10 - '*' # Push events to matching any tag format, i.e. 1.0, 20.15.10
- '!*-cli' # Exclude command-line interface tags - '!*-cli' # Exclude command-line interface tags
workflow_dispatch: workflow_dispatch:
inputs:
tag:
description: Release tag to build
required: true
type: string
draft:
description: Create the GitHub Release as a draft
required: false
type: boolean
default: true
prerelease:
description: Mark the GitHub Release as a pre-release
required: false
type: boolean
default: false
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: release
permissions: permissions:
contents: write contents: write
id-token: write id-token: write
@@ -19,6 +35,7 @@ jobs:
with: with:
fetch-depth: 0 # otherwise, you will failed to push refs to dest repo fetch-depth: 0 # otherwise, you will failed to push refs to dest repo
submodules: recursive submodules: recursive
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
@@ -27,7 +44,18 @@ jobs:
- name: Get Version - name: Get Version
id: version id: version
run: | run: |
echo "tag=$(git describe --abbrev=0 --tags)" >> $GITHUB_OUTPUT if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
TAG="${{ inputs.tag }}"
DRAFT="${{ inputs.draft }}"
PRERELEASE="${{ inputs.prerelease }}"
else
TAG="${GITHUB_REF_NAME}"
DRAFT="true"
PRERELEASE="false"
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "draft=${DRAFT}" >> $GITHUB_OUTPUT
echo "prerelease=${PRERELEASE}" >> $GITHUB_OUTPUT
# Build the plugin # Build the plugin
- name: Build - name: Build
id: build id: build
@@ -58,4 +86,5 @@ jobs:
styles.css styles.css
name: ${{ steps.version.outputs.tag }} name: ${{ steps.version.outputs.tag }}
tag_name: ${{ steps.version.outputs.tag }} tag_name: ${{ steps.version.outputs.tag }}
draft: true draft: ${{ steps.version.outputs.draft }}
prerelease: ${{ steps.version.outputs.prerelease }}
+55
View File
@@ -148,6 +148,20 @@ Hence, the new feature should be implemented as follows:
- **Service Hub** (`src/modules/services/`): Central service registry using dependency injection - **Service Hub** (`src/modules/services/`): Central service registry using dependency injection
- **Common Library** (`src/lib/`): Platform-independent sync logic, shared with other tools - **Common Library** (`src/lib/`): Platform-independent sync logic, shared with other tools
### Conflict Merge Policy
Markdown conflict auto-merge should behave like a conservative three-way merge. The guiding rule is to merge changes when they touch non-overlapping regions, and to keep a manual conflict when the edits overlap semantically.
When in doubt, prefer the safer outcome: preserve data, keep the conflict visible, and ask the user rather than silently discarding content or choosing one side.
- If one side deletes a line and the other side leaves that same line unchanged, treat it as a safe deletion. The deleted line must not be reintroduced into the merged result.
- If one side inserts new content in a different region while the other side deletes an unchanged old region, preserve the insertion and the deletion.
- If one side deletes a line and the other side modifies that same line, keep the conflict for user resolution.
- If both sides insert different content at the same position, keep both insertions in a deterministic order unless the surrounding deletion context indicates that they are competing replacements.
- Avoid resolving conflicts by simply choosing the newest revision unless the user has explicitly selected that behaviour.
This policy is intentionally aligned with the conflict checkboxes and compatibility settings: automatic merge should remove avoidable prompts, but it must not silently choose between overlapping user intentions.
### File Structure Conventions ### File Structure Conventions
- **Platform-specific code**: Use `.platform.ts` suffix (replaced with `.obsidian.ts` in production builds via esbuild) - **Platform-specific code**: Use `.platform.ts` suffix (replaced with `.obsidian.ts` in production builds via esbuild)
@@ -238,6 +252,47 @@ export class ModuleExample extends AbstractObsidianModule {
In short, the situation remains unchanged for me, but it means you all become a little safer. Thank you for your understanding! In short, the situation remains unchanged for me, but it means you all become a little safer. Thank you for your understanding!
## Release Notes
- Keep the top section of `updates.md` as `## Unreleased` during normal development.
- When opening a feature or fix PR, update `## Unreleased` in the same PR if the change is user-facing.
- Add only user-facing changes that help users understand what they gain, what has changed, or what they may need to do after updating.
- Avoid listing purely internal refactors, maintenance chores, generated-file changes, and dependency updates unless they affect users; group and label them when they are included.
- When preparing a release, replace `## Unreleased` with the target version heading (for example, `## 0.25.81`) and add a fresh empty `## Unreleased` section above it for the next cycle.
- Review and polish the released section in the release PR before tagging, because the content is embedded into the plug-in and may be reused as the GitHub Release notes.
## Release Workflow
This workflow is for maintainers. Contributors should update `## Unreleased` for user-facing feature or fix PRs, but do not need to run the release workflows.
The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `release` GitHub Environment. Configure Environment protection in the repository settings so tag creation and release publication require maintainer approval.
- Run the `Prepare Release PR` workflow with the target version. It creates the release branch, updates versions, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR.
- Do not tag the release branch when the PR is first created. Polish the release PR first, especially `updates.md`.
- Once the release PR head is fixed, run the `Finalise Release Tags` workflow. It validates the release branch and pushes both the plug-in tag (for example, `0.25.81`) and the CLI tag (for example, `0.25.81-cli`) to the same commit.
- The plug-in tag triggers the release workflow and creates a draft GitHub Release by default. The CLI tag triggers the Docker workflow and publishes the fixed version tag, the major-minor moving tag (for example, `0.25-cli`), `latest`, and the SHA-qualified tag.
- If a pre-release is needed, run the `Release Obsidian Plugin` workflow manually with the target tag, `draft=false`, and `prerelease=true`.
### Release Cheat Sheet
1. Before starting, add user-facing notes under `## Unreleased` in `updates.md`.
2. Run `Prepare Release PR` from GitHub Actions.
- `version`: the target version, for example `0.25.81`.
- `base_branch`: normally `main`.
- `release_branch`: leave blank to use the default branch name, for example `0_25_81`.
- `allow_empty_updates`: leave disabled unless the release intentionally has no user-facing notes.
3. Review the generated draft PR.
- Polish `updates.md`.
- Confirm `package.json`, `manifest.json`, `versions.json`, and workspace package versions.
- Confirm that `manifest.json` has the intended `minAppVersion`.
- Wait for the necessary CI checks.
4. When the PR head is fixed, run `Finalise Release Tags`.
- `version`: the same target version.
- `release_branch`: leave blank unless the release branch used a custom name.
5. Check the generated draft GitHub Release for the plug-in tag.
6. Check the CLI Docker workflow started from the `*-cli` tag.
7. Publish the draft GitHub Release when ready, then merge the release PR into `main`.
8. If the release should be a pre-release instead of a draft release, run `Release Obsidian Plugin` manually with the target `tag`, `draft=false`, and `prerelease=true`.
## Contribution Guidelines ## Contribution Guidelines
- Follow existing code style and conventions - Follow existing code style and conventions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"id": "obsidian-livesync", "id": "obsidian-livesync",
"name": "Self-hosted LiveSync", "name": "Self-hosted LiveSync",
"version": "0.25.79", "version": "0.25.80",
"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",
+5 -5
View File
@@ -1,12 +1,12 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.25.79", "version": "0.25.80",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.25.79", "version": "0.25.80",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"src/apps/cli", "src/apps/cli",
@@ -16210,7 +16210,7 @@
}, },
"src/apps/cli": { "src/apps/cli": {
"name": "self-hosted-livesync-cli", "name": "self-hosted-livesync-cli",
"version": "0.25.79-cli", "version": "0.25.80-cli",
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"minimatch": "^10.2.5", "minimatch": "^10.2.5",
@@ -16236,7 +16236,7 @@
}, },
"src/apps/webapp": { "src/apps/webapp": {
"name": "livesync-webapp", "name": "livesync-webapp",
"version": "0.25.79-webapp", "version": "0.25.80-webapp",
"dependencies": { "dependencies": {
"octagonal-wheels": "^0.1.47" "octagonal-wheels": "^0.1.47"
}, },
@@ -16251,7 +16251,7 @@
} }
}, },
"src/apps/webpeer": { "src/apps/webpeer": {
"version": "0.25.79-webpeer", "version": "0.25.80-webpeer",
"dependencies": { "dependencies": {
"octagonal-wheels": "^0.1.47" "octagonal-wheels": "^0.1.47"
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "obsidian-livesync", "name": "obsidian-livesync",
"version": "0.25.79", "version": "0.25.80",
"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",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "self-hosted-livesync-cli", "name": "self-hosted-livesync-cli",
"private": true, "private": true,
"version": "0.25.79-cli", "version": "0.25.80-cli",
"main": "dist/index.cjs", "main": "dist/index.cjs",
"type": "module", "type": "module",
"scripts": { "scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "livesync-webapp", "name": "livesync-webapp",
"private": true, "private": true,
"version": "0.25.79-webapp", "version": "0.25.80-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": {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "webpeer", "name": "webpeer",
"private": true, "private": true,
"version": "0.25.79-webpeer", "version": "0.25.80-webpeer",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@@ -51,6 +51,7 @@ import { EVENT_SETTING_SAVED, eventHub } from "@/common/events.ts";
import { Semaphore } from "octagonal-wheels/concurrency/semaphore"; import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
import type { LiveSyncCore } from "@/main.ts"; import type { LiveSyncCore } from "@/main.ts";
import { tryGetFilePath } from "@lib/common/utils.doc.ts"; import { tryGetFilePath } from "@lib/common/utils.doc.ts";
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce"; type SyncDirection = "push" | "pull" | "safe" | "pullForce" | "pushForce";
declare global { declare global {
@@ -1832,43 +1833,35 @@ ${messageFetch}${messageOverwrite}${messageMerge}
} }
async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) { async configureHiddenFileSync(mode: keyof OPTIONAL_SYNC_FEATURES) {
if ( const result = await configureHiddenFileSyncMode(mode, {
mode != "FETCH" && disable: async () => {
mode != "OVERWRITE" && // await this.core.$allSuspendExtraSync();
mode != "MERGE" && await this.core.services.setting.applyPartial(
mode != "DISABLE" && {
mode != "DISABLE_HIDDEN" syncInternalFiles: false,
) { },
return; true
} );
// this.core.settings.syncInternalFiles = false;
if (mode == "DISABLE" || mode == "DISABLE_HIDDEN") { // await this.core.saveSettings();
// await this.core.$allSuspendExtraSync();
await this.core.services.setting.applyPartial(
{
syncInternalFiles: false,
},
true
);
// this.core.settings.syncInternalFiles = false;
// await this.core.saveSettings();
return;
}
this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
if (mode == "FETCH") {
await this.initialiseInternalFileSync("pullForce", true);
} else if (mode == "OVERWRITE") {
await this.initialiseInternalFileSync("pushForce", true);
} else if (mode == "MERGE") {
await this.initialiseInternalFileSync("safe", true);
}
await this.core.services.setting.applyPartial(
{
useAdvancedMode: true,
syncInternalFiles: true,
}, },
true enable: async () => {
); this._log("Gathering files for enabling Hidden File Sync", LOG_LEVEL_NOTICE);
await this.core.services.setting.applyPartial(
{
useAdvancedMode: true,
syncInternalFiles: true,
},
true
);
},
initialise: async (direction) => {
await this.initialiseInternalFileSync(direction, true);
},
});
if (result == "ignored" || result == "disabled") {
return;
}
// this.plugin.settings.useAdvancedMode = true; // this.plugin.settings.useAdvancedMode = true;
// this.plugin.settings.syncInternalFiles = true; // this.plugin.settings.syncInternalFiles = true;
@@ -0,0 +1,45 @@
type HiddenFileSyncMode = "FETCH" | "OVERWRITE" | "MERGE" | "DISABLE" | "DISABLE_HIDDEN";
type HiddenFileSyncDirection = "pullForce" | "pushForce" | "safe";
type ConfigureHiddenFileSyncHandlers = {
disable: () => Promise<void>;
enable: () => Promise<void>;
initialise: (direction: HiddenFileSyncDirection) => Promise<void>;
};
export type ConfigureHiddenFileSyncResult = "ignored" | "disabled" | "enabled";
function getInitialiseDirection(mode: keyof OPTIONAL_SYNC_FEATURES): HiddenFileSyncDirection | false {
if (mode == "FETCH") return "pullForce";
if (mode == "OVERWRITE") return "pushForce";
if (mode == "MERGE") return "safe";
return false;
}
function isDisableMode(mode: keyof OPTIONAL_SYNC_FEATURES): boolean {
return mode == "DISABLE" || mode == "DISABLE_HIDDEN";
}
function isHiddenFileSyncMode(mode: keyof OPTIONAL_SYNC_FEATURES): mode is HiddenFileSyncMode {
return mode == "FETCH" || mode == "OVERWRITE" || mode == "MERGE" || isDisableMode(mode);
}
export async function configureHiddenFileSyncMode(
mode: keyof OPTIONAL_SYNC_FEATURES,
handlers: ConfigureHiddenFileSyncHandlers
): Promise<ConfigureHiddenFileSyncResult> {
if (!isHiddenFileSyncMode(mode)) {
return "ignored";
}
if (isDisableMode(mode)) {
await handlers.disable();
return "disabled";
}
const direction = getInitialiseDirection(mode);
if (direction === false) {
return "ignored";
}
await handlers.enable();
await handlers.initialise(direction);
return "enabled";
}
@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from "vitest";
import { configureHiddenFileSyncMode } from "./configureHiddenFileSyncMode.ts";
describe("configureHiddenFileSyncMode", () => {
it.each([
["FETCH", "pullForce"],
["OVERWRITE", "pushForce"],
["MERGE", "safe"],
] as const)("enables hidden file sync before initialising %s", async (mode, direction) => {
const calls: string[] = [];
const result = await configureHiddenFileSyncMode(mode, {
disable: vi.fn(async () => {
calls.push("disable");
}),
enable: vi.fn(async () => {
calls.push("enable");
}),
initialise: vi.fn(async (actualDirection) => {
calls.push(`init:${actualDirection}`);
}),
});
expect(result).toBe("enabled");
expect(calls).toEqual(["enable", `init:${direction}`]);
});
it.each(["DISABLE", "DISABLE_HIDDEN"] as const)("disables hidden file sync immediately for %s", async (mode) => {
const calls: string[] = [];
const result = await configureHiddenFileSyncMode(mode, {
disable: vi.fn(async () => {
calls.push("disable");
}),
enable: vi.fn(async () => {
calls.push("enable");
}),
initialise: vi.fn(async (direction) => {
calls.push(`init:${direction}`);
}),
});
expect(result).toBe("disabled");
expect(calls).toEqual(["disable"]);
});
});
+1 -1
Submodule src/lib updated: cf9810dec5...a0efb7274e
+18 -36
View File
@@ -3,6 +3,24 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## Unreleased
## 0.25.80
7th July, 2026
### Fixed
- Improved Markdown conflict auto-merge so that non-overlapping edits are merged while overlapping delete-and-edit cases remain visible for manual resolution (#993).
- Behaviour change:
- When one side deletes an unchanged line and the other side edits a different region, the deleted line is no longer reintroduced into the merged result.
- When one side deletes a line and the other side modifies that same line, the conflict is preserved instead of silently choosing one side.
- Fixed an issue where applying a newer database entry to storage could incorrectly preserve an older local file as a conflict (#994).
- Behaviour change:
- Local storage is preserved as a conflict when it may contain unsynchronised changes that are not represented in the revision history. A newer incoming text entry is applied without creating a conflict only when it clearly extends the existing local text.
- Fixed an issue where choosing Disable and then Overwrite in Hidden File Sync could silently skip hidden files, because the overwrite setup ran while hidden file synchronisation was still disabled (#989, PR #992).
- Hidden File Sync is now re-enabled before the Fetch, Overwrite, or Merge initialisation runs, instead of after it completes. If that initialisation fails, the setting may remain enabled.
## 0.25.79 ## 0.25.79
29th June, 2026 29th June, 2026
@@ -96,41 +114,5 @@ Also, this update is a very large one, even if we had a lot of time, and we had
- Some dependencies have been updated. - Some dependencies have been updated.
- Now we check the compatibility with iOS 15 in the CI tests to ensure the plugin continues to work on older iOS versions even after we upgrade some dependencies. - Now we check the compatibility with iOS 15 in the CI tests to ensure the plugin continues to work on older iOS versions even after we upgrade some dependencies.
## 0.25.74
8th June, 2026
### Fixed
- Fixed an issue where disabling hidden file synchronisation did not take effect, allowing non-target hidden files to continue to be processed and synchronised by replication or boot-sequence scan (#941).
- Prevented the automatic merging of conflicted revisions when one of the revisions has been deleted, which was causing deleted files to reappear (#911).
- The startup sequence now saves the state more effectively (Thank you so much for @bmcyver)!
## Only CLI
8th June, 2026
I should also consider the version numbering for the CLI...
### Improved
- Added new remote database management commands: `remote-status`, `unlock-remote`, `lock-remote`, and `mark-resolved`.
- --vault option is now available for daemon and mirror commands! (Thank you so much for @starskyzheng)!
- Decoupled the database directory path from the actual vault directory path using the `--vault` (or `-V`) option.
### Fixed (preventive)
- Validated that the specified vault path exists and is indeed a directory before starting the CLI.
- Integrated path resolution and validations for one-off commands (such as `'push'`, `'pull'`, `'cat'`, `'rm'`, `'info'`, and `'resolve'`) against the decoupled vault path instead of the database path.
## 0.25.73
4th June, 2026
### Fixed
- Adjust CouchDB's database name checking to its specification (#926).
- `Reset Syncronisation on This Device` for minio and P2P is now working properly.
Full notes are in Full notes are in
[updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md).
+58
View File
@@ -4,6 +4,64 @@ Since 19th July, 2025 (beta1 in 0.25.0-beta1, 13th July, 2025)
The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope. The head note of 0.25 is now in [updates_old.md](https://github.com/vrtmrz/obsidian-livesync/blob/main/updates_old.md). Because 0.25 got a lot of updates, thankfully, compatibility is kept and we do not need breaking changes! In other words, when get enough stabled. The next version will be v1.0.0. Even though it my hope.
## 0.25.80
7th July, 2026
### Fixed
- Improved Markdown conflict auto-merge so that non-overlapping edits are merged while overlapping delete-and-edit cases remain visible for manual resolution (#993).
- Behaviour change:
- When one side deletes an unchanged line and the other side edits a different region, the deleted line is no longer reintroduced into the merged result.
- When one side deletes a line and the other side modifies that same line, the conflict is preserved instead of silently choosing one side.
- Fixed an issue where applying a newer database entry to storage could incorrectly preserve an older local file as a conflict (#994).
- Behaviour change:
- Local storage is preserved as a conflict when it may contain unsynchronised changes that are not represented in the revision history. A newer incoming text entry is applied without creating a conflict only when it clearly extends the existing local text.
- Fixed an issue where choosing Disable and then Overwrite in Hidden File Sync could silently skip hidden files, because the overwrite setup ran while hidden file synchronisation was still disabled (#989, PR #992).
- Hidden File Sync is now re-enabled before the Fetch, Overwrite, or Merge initialisation runs, instead of after it completes. If that initialisation fails, the setting may remain enabled.
## 0.25.79
29th June, 2026
### Fixed
- Fast Fetch now retries transient stream interruptions and resumes from the latest persisted checkpoint, instead of starting over after ordinary network or platform interruptions (#977, PR #978; commonlib PR #59). Thank you so much for @apple-ouyang for the fix!
- Simple Fetch now remembers the selected setup choices while an interrupted Fetch All operation is still pending, so users are not asked the same questions again on retry (#977, PR #978). Thank you so much for @apple-ouyang for the fix!
- No longer hidden storage events, such as `.git` paths, reach the normal target-file filter when internal file synchronisation is disabled. This avoids noisy non-target logs before those files are skipped (commonlib PR #60). Thank you so much for @apple-ouyang for the fix!
- Fixed an issue where a file deleted from storage could be resurrected by the offline scanner because the database tombstone was not written when the storage file was already gone (commonlib PR #56). Thank you so much for @cosmic-fire-eng for the fix!
### Improved
- Local database maintenance commands now ask before applying the required chunk settings, and can apply those prerequisites before continuing (#980, PR #981). Thank you so much for @apple-ouyang for the improvement!
- Improved CouchDB replication event handling by using the new `StreamInbox` helper from `octagonal-wheels` (commonlib PR #62).
### Documentation
- Added `nginx` to the setup documentation table of contents (PR #976). Thank you so much for @kiraventom for the improvement!
### Miscellaneous
- Updated `octagonal-wheels` to `0.1.47` across the plug-in and workspace packages to use the newly published helper modules.
## 0.25.78
23rd June, 2026
### Fixed
- No longer fast synchronisation (a.k.a. Fast Fetch) causes a rewind and re-fetch of the entire database when some errors occur during the process (#972, PR #973). Thank you so much for @apple-ouyang for the fix!
### Improved
- Overhauled the Object Storage (e.g., MinIO and S3) replication engine ('Journal Replicator 2nd Edition').
- It now leverages the standard Web Streams API for a resilient, backpressure-aware architecture, reducing memory footprints/temporary storage usage on large vaults.
- Decoupled the physical storage logic to make it easier to add new storage backends in the future.
- Stricter compliance with CouchDB's replication protocol (proper `_revisions` transfers with `new_edits: false`) when using Object Storage.
### Testing
- Added comprehensive unit tests for the new `JournalSyncCore` engine, covering streams, backpressure, and `new_edits: false` validation.
- Improved integration test workflows in the CI pipeline to run MinIO tests automatically using standard environment variables.
## 0.25.77 ## 0.25.77
19th June, 2026 19th June, 2026
+140
View File
@@ -0,0 +1,140 @@
import { readFileSync, writeFileSync } from "fs";
const updatesPath = "updates.md";
// Utility used by the release workflows to rotate and validate `updates.md`.
// It intentionally keeps the Markdown format simple: top-level `##` headings
// are treated as release boundaries, and only one `## Unreleased` section is
// allowed.
function fail(message) {
console.error(message);
process.exit(1);
}
function readJson(path) {
return JSON.parse(readFileSync(path, "utf8"));
}
function assertVersion(version) {
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) {
fail(`Invalid release version: ${version}`);
}
}
function formatReleaseDate(date = new Date()) {
const day = date.getUTCDate();
const suffix = day % 10 === 1 && day !== 11 ? "st" : day % 10 === 2 && day !== 12 ? "nd" : day % 10 === 3 && day !== 13 ? "rd" : "th";
const month = new Intl.DateTimeFormat("en-GB", { month: "long", timeZone: "UTC" }).format(date);
const year = date.getUTCFullYear();
return `${day}${suffix} ${month}, ${year}`;
}
function headingPattern(heading) {
return new RegExp(`^## ${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
}
// Return a `##` section body without interpreting lower-level headings.
function findSection(markdown, heading) {
const pattern = headingPattern(heading);
const match = pattern.exec(markdown);
if (!match) return undefined;
const headingStart = match.index;
const bodyStart = match.index + match[0].length;
const rest = markdown.slice(bodyStart);
const next = /^## .+$/m.exec(rest);
const end = next ? bodyStart + next.index : markdown.length;
return {
headingStart,
bodyStart,
end,
body: markdown.slice(bodyStart, end),
};
}
function assertSingleUnreleased(markdown) {
const matches = markdown.match(/^## Unreleased\s*$/gm) || [];
if (matches.length !== 1) {
fail(`Expected exactly one '## Unreleased' section in ${updatesPath}, found ${matches.length}.`);
}
}
function prepare(version) {
assertVersion(version);
const markdown = readFileSync(updatesPath, "utf8");
assertSingleUnreleased(markdown);
if (headingPattern(version).test(markdown)) {
fail(`Release notes for ${version} already exist in ${updatesPath}.`);
}
const unreleased = findSection(markdown, "Unreleased");
if (!unreleased) fail(`Could not find '## Unreleased' in ${updatesPath}.`);
const allowEmpty = process.env.ALLOW_EMPTY_UPDATES === "true";
if (!allowEmpty && unreleased.body.trim() === "") {
fail(`The '## Unreleased' section is empty. Set ALLOW_EMPTY_UPDATES=true if this is intentional.`);
}
// Keep a fresh empty Unreleased section above the newly dated release notes.
const releasedBody = `${unreleased.body.trim()}\n\n`;
const releaseDate = process.env.RELEASE_DATE || formatReleaseDate();
const replacement = `## Unreleased\n\n## ${version}\n\n${releaseDate}\n\n${releasedBody}`;
const nextMarkdown = markdown.slice(0, unreleased.headingStart) + replacement + markdown.slice(unreleased.end);
writeFileSync(updatesPath, nextMarkdown, "utf8");
}
function validate(version) {
assertVersion(version);
const rootPackage = readJson("package.json");
const manifest = readJson("manifest.json");
if (rootPackage.version !== version) {
fail(`package.json version is ${rootPackage.version}, expected ${version}.`);
}
if (manifest.version !== version) {
fail(`manifest.json version is ${manifest.version}, expected ${version}.`);
}
for (const workspace of ["cli", "webpeer", "webapp"]) {
const workspacePackage = readJson(`src/apps/${workspace}/package.json`);
const expected = `${version}-${workspace}`;
if (workspacePackage.version !== expected) {
fail(`src/apps/${workspace}/package.json version is ${workspacePackage.version}, expected ${expected}.`);
}
}
const versions = readJson("versions.json");
if (versions[version] !== manifest.minAppVersion) {
fail(`versions.json does not map ${version} to manifest minAppVersion ${manifest.minAppVersion}.`);
}
const markdown = readFileSync(updatesPath, "utf8");
assertSingleUnreleased(markdown);
const releaseSection = findSection(markdown, version);
if (!releaseSection) {
fail(`Could not find '## ${version}' in ${updatesPath}.`);
}
if (releaseSection.body.trim() === "") {
fail(`The release notes for ${version} are empty.`);
}
if (/\b(?:TODO|WIP)\b/i.test(releaseSection.body)) {
fail(`The release notes for ${version} still contain TODO or WIP markers.`);
}
}
const [command, version] = process.argv.slice(2);
if (!command || !version) {
fail("Usage: node utils/release-notes.mjs <prepare|validate> <version>");
}
if (command === "prepare") {
prepare(version);
} else if (command === "validate") {
validate(version);
} else {
fail(`Unknown command: ${command}`);
}
+2 -2
View File
@@ -11,7 +11,7 @@ writeFileSync("manifest.json", JSON.stringify(manifest, null, 4));
// update versions.json with target version and minAppVersion from manifest.json // update versions.json with target version and minAppVersion from manifest.json
// but only if the target version is not already in versions.json // but only if the target version is not already in versions.json
const versions = JSON.parse(readFileSync('versions.json', 'utf8')); const versions = JSON.parse(readFileSync('versions.json', 'utf8'));
if (!Object.values(versions).includes(minAppVersion)) { if (!(targetVersion in versions)) {
versions[targetVersion] = minAppVersion; versions[targetVersion] = minAppVersion;
writeFileSync('versions.json', JSON.stringify(versions, null, 4)); writeFileSync('versions.json', JSON.stringify(versions, null, 4));
} }