mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-27 19:11:35 +03:00
Compare commits
3 Commits
1.0.0-rc.1
...
1.0.0-cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1e382c6ed | ||
|
|
2b95766d4f | ||
|
|
c3f2163204 |
3
.github/workflows/finalise-release.yml
vendored
3
.github/workflows/finalise-release.yml
vendored
@@ -137,7 +137,8 @@ jobs:
|
||||
echo "Keep the release pull request in draft and unmerged after BRAT validation; close it only through a separate maintainer action."
|
||||
elif [[ "${PRERELEASE}" == "true" ]]; then
|
||||
echo "Publish the draft initially as a pre-release without replacing the latest stable release."
|
||||
echo "After BRAT validation, remove the pre-release designation and make this exact release the latest stable release before merging the release pull request."
|
||||
echo "After BRAT validation, merge the release pull request into its reviewed base branch and integrate the exact release commit into the default branch."
|
||||
echo "Only after the default branch contains the exact release metadata, remove the pre-release designation and make this exact release the latest stable release."
|
||||
echo "Create the stable CLI tag and publish its latest and major-minor image tags through a separate maintainer gate."
|
||||
else
|
||||
echo "Publish the draft as the latest stable release, keep the release pull request in draft, and merge only after BRAT validation succeeds."
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { writeFileSync } from "fs";
|
||||
import { allMessages } from "../src/common/messages/combinedMessages.dev.ts";
|
||||
import path from "path";
|
||||
|
||||
const { writeFileSync } = process.getBuiltinModule("node:fs");
|
||||
const path = process.getBuiltinModule("node:path");
|
||||
const __dirname = import.meta.dirname;
|
||||
const currentPath = __dirname;
|
||||
const outDir = path.resolve(currentPath, "../src/common/messages/combinedMessages.prod.ts");
|
||||
|
||||
console.log(`Writing to ${outDir}`);
|
||||
process.stdout.write(`Writing to ${outDir}\n`);
|
||||
writeFileSync(
|
||||
outDir,
|
||||
`export const allMessages: Readonly<Record<string, Readonly<Record<string, string>>>> = ${JSON.stringify(allMessages, null, 4)};`
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { readFile } from "fs/promises";
|
||||
import { join, resolve } from "path";
|
||||
import { glob } from "tinyglobby";
|
||||
import { parse } from "yaml";
|
||||
import { objectToDotted } from "./messagelib.ts";
|
||||
|
||||
const fsPromises = process.getBuiltinModule("node:fs/promises");
|
||||
const path = process.getBuiltinModule("node:path");
|
||||
const __dirname = import.meta.dirname;
|
||||
const targetDir = resolve(join(__dirname, "../src/common/messagesYAML/"));
|
||||
const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesYAML/"));
|
||||
const files = (await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir })).sort();
|
||||
|
||||
function flattenMessages(src: Record<string, unknown>) {
|
||||
function flattenMessages(src: unknown) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(objectToDotted(src))
|
||||
.map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value] as const)
|
||||
@@ -20,9 +20,14 @@ function flattenMessages(src: Record<string, unknown>) {
|
||||
const localeData = new Map<string, Record<string, string>>();
|
||||
for (const file of files) {
|
||||
const segments = file.split(/[/\\]/);
|
||||
const locale = segments[segments.length - 1]!.replace(/\.yaml$/, "");
|
||||
const content = await readFile(file, "utf-8");
|
||||
localeData.set(locale, flattenMessages(parse(content) ?? {}));
|
||||
const localeFilename = segments[segments.length - 1];
|
||||
if (localeFilename === undefined) {
|
||||
throw new Error(`Could not determine the locale name for ${file}`);
|
||||
}
|
||||
const locale = localeFilename.replace(/\.yaml$/, "");
|
||||
const content = await fsPromises.readFile(file, "utf-8");
|
||||
const parsed: unknown = parse(content);
|
||||
localeData.set(locale, flattenMessages(parsed ?? {}));
|
||||
}
|
||||
|
||||
const baseLocale = "en";
|
||||
@@ -55,4 +60,4 @@ const report = Object.fromEntries(
|
||||
})
|
||||
);
|
||||
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
import { SUPPORTED_I18N_LANGS, type I18N_LANGS } from "../src/common/rosetta";
|
||||
import { allMessages } from "../src/common/messages/combinedMessages.dev.ts";
|
||||
|
||||
import path from "path";
|
||||
const { writeFileSync } = process.getBuiltinModule("node:fs");
|
||||
const path = process.getBuiltinModule("node:path");
|
||||
const thisFileDir = __dirname;
|
||||
const outDir = path.join(thisFileDir, "i18n");
|
||||
|
||||
|
||||
@@ -1,26 +1,17 @@
|
||||
import { writeFileSync } from "fs";
|
||||
|
||||
import { allMessages } from "../src/common/messages/combinedMessages.prod.ts";
|
||||
const __dirname = import.meta.dirname;
|
||||
import path from "path";
|
||||
|
||||
const { writeFileSync } = process.getBuiltinModule("node:fs");
|
||||
const path = process.getBuiltinModule("node:path");
|
||||
const thisFileDir = __dirname;
|
||||
const outDir = path.resolve(thisFileDir, "../src/common/messagesJson");
|
||||
|
||||
const out = {} as Record<string, { [key: string]: string | undefined }>;
|
||||
|
||||
for (const [key, value] of Object.entries(allMessages)) {
|
||||
//@ts-ignore
|
||||
for (const [lang, langValue] of Object.entries(allMessages[key])) {
|
||||
for (const [lang, langValue] of Object.entries(value)) {
|
||||
if (!out[lang]) out[lang] = {};
|
||||
if (lang in value) {
|
||||
out[lang][key] = langValue as string;
|
||||
} else {
|
||||
if (lang === "def") {
|
||||
out[lang][key] = key;
|
||||
} else {
|
||||
out[lang][key] = undefined;
|
||||
}
|
||||
}
|
||||
out[lang][key] = langValue;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { access, readFile } from "node:fs/promises";
|
||||
import { dirname, relative, resolve } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
const fsPromises = process.getBuiltinModule("node:fs/promises");
|
||||
const path = process.getBuiltinModule("node:path");
|
||||
const url = process.getBuiltinModule("node:url");
|
||||
|
||||
type InspectionError = {
|
||||
check: "current-label" | "local-reference" | "retired-label";
|
||||
@@ -20,7 +20,7 @@ const messageCataloguePath = "src/common/messagesJson/en.json";
|
||||
const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^)]*["'])?\)/gu;
|
||||
|
||||
function repositoryRootFromThisFile(): string {
|
||||
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
return path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), "..");
|
||||
}
|
||||
|
||||
function normaliseReferenceTarget(rawTarget: string): string {
|
||||
@@ -48,14 +48,14 @@ async function inspectLocalReferences(
|
||||
const [pathPart] = target.split("#", 1);
|
||||
if (!pathPart) continue;
|
||||
checked++;
|
||||
const referencedPath = resolve(repositoryRoot, dirname(documentPath), pathPart);
|
||||
const referencedPath = path.resolve(repositoryRoot, path.dirname(documentPath), pathPart);
|
||||
try {
|
||||
await access(referencedPath);
|
||||
await fsPromises.access(referencedPath);
|
||||
} catch {
|
||||
errors.push({
|
||||
check: "local-reference",
|
||||
file: documentPath,
|
||||
detail: `Missing local reference: ${relative(repositoryRoot, referencedPath)}`,
|
||||
detail: `Missing local reference: ${path.relative(repositoryRoot, referencedPath)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -68,14 +68,13 @@ export async function inspectTroubleshootingDocs(
|
||||
const errors: InspectionError[] = [];
|
||||
const documents = new Map<string, string>();
|
||||
for (const guidePath of guidePaths) {
|
||||
documents.set(guidePath, await readFile(resolve(repositoryRoot, guidePath), "utf8"));
|
||||
documents.set(guidePath, await fsPromises.readFile(path.resolve(repositoryRoot, guidePath), "utf8"));
|
||||
}
|
||||
|
||||
const troubleshooting = documents.get("docs/troubleshooting.md")!;
|
||||
const catalogue = JSON.parse(await readFile(resolve(repositoryRoot, messageCataloguePath), "utf8")) as Record<
|
||||
string,
|
||||
string
|
||||
>;
|
||||
const catalogue = JSON.parse(
|
||||
await fsPromises.readFile(path.resolve(repositoryRoot, messageCataloguePath), "utf8")
|
||||
) as Record<string, string>;
|
||||
const requiredMessageKeys = [
|
||||
"TweakMismatchResolve.Action.UseConfigured",
|
||||
"TweakMismatchResolve.Action.UseMine",
|
||||
@@ -132,7 +131,7 @@ async function runCli(): Promise<void> {
|
||||
if (!result.ok) process.exitCode = 1;
|
||||
}
|
||||
|
||||
const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : undefined;
|
||||
const invokedPath = process.argv[1] ? url.pathToFileURL(path.resolve(process.argv[1])).href : undefined;
|
||||
if (invokedPath === import.meta.url) {
|
||||
await runCli();
|
||||
}
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
// Convert Application convenient Message Resources (JSON) to Human-Editable format (YAML)
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { join, resolve } from "path";
|
||||
import { stringify } from "yaml";
|
||||
import { glob } from "tinyglobby";
|
||||
import { dottedToObject } from "./messagelib";
|
||||
|
||||
const fsPromises = process.getBuiltinModule("node:fs/promises");
|
||||
const path = process.getBuiltinModule("node:path");
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
const targetDir = resolve(join(__dirname, "../src/common/messagesJson/"));
|
||||
console.log(`Target directory: ${targetDir}`);
|
||||
const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesJson/"));
|
||||
process.stdout.write(`Target directory: ${targetDir}\n`);
|
||||
const files = await glob(`*.json`, { expandDirectories: false, absolute: true, cwd: targetDir });
|
||||
for (const file of files) {
|
||||
const filePath = resolve(file);
|
||||
console.log(`Processing file: ${filePath}`);
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const jsonDataSrc = JSON.parse(content);
|
||||
const filePath = path.resolve(file);
|
||||
process.stdout.write(`Processing file: ${filePath}\n`);
|
||||
const content = await fsPromises.readFile(filePath, "utf-8");
|
||||
const jsonDataSrc: unknown = JSON.parse(content);
|
||||
if (typeof jsonDataSrc !== "object" || jsonDataSrc === null || Array.isArray(jsonDataSrc)) {
|
||||
throw new TypeError(`Expected ${filePath} to contain a JSON object`);
|
||||
}
|
||||
const jsonDataD2 = Object.fromEntries(
|
||||
Object.entries(jsonDataSrc).sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
||||
);
|
||||
const jsonData = dottedToObject(jsonDataD2);
|
||||
const yamlData = stringify(jsonData, { indent: 2 });
|
||||
const yamlFilePath = filePath.replace(/\.json$/, ".yaml").replace("Json", "YAML");
|
||||
await writeFile(yamlFilePath, yamlData, "utf-8");
|
||||
console.log(`Converted ${filePath} to ${yamlFilePath}`);
|
||||
await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8");
|
||||
process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`);
|
||||
}
|
||||
|
||||
// console.dir(files, { depth: 0 });
|
||||
|
||||
@@ -1,38 +1,49 @@
|
||||
export function objectToDotted(obj: any, prefix = ""): Record<string, any> {
|
||||
return Object.entries(obj).reduce(
|
||||
(acc, [key, value]) => {
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||
Object.assign(acc, objectToDotted(value, newKey));
|
||||
} else {
|
||||
acc[newKey] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
);
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
export function dottedToObject(obj: Record<string, any>): Record<string, any> {
|
||||
return Object.entries(obj).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (key.includes(" ")) {
|
||||
// Return as is.
|
||||
return { ...acc, [key]: value }; // Skip keys with spaces
|
||||
}
|
||||
const keys = key.split(".");
|
||||
keys.reduce((nestedAcc, currKey, index) => {
|
||||
if (currKey in nestedAcc && typeof nestedAcc[currKey] !== "object") {
|
||||
nestedAcc[currKey] = { _value: nestedAcc[currKey] }; // Convert to object if not already
|
||||
}
|
||||
if (index === keys.length - 1) {
|
||||
nestedAcc[currKey] = value;
|
||||
} else {
|
||||
nestedAcc[currKey] = nestedAcc[currKey] || {};
|
||||
}
|
||||
return nestedAcc[currKey];
|
||||
}, acc);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>
|
||||
);
|
||||
|
||||
export function objectToDotted(obj: unknown, prefix = ""): Record<string, unknown> {
|
||||
if (!isRecord(obj)) {
|
||||
throw new TypeError("Expected a message catalogue object");
|
||||
}
|
||||
const flattened: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const newKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (isRecord(value)) {
|
||||
Object.assign(flattened, objectToDotted(value, newKey));
|
||||
} else {
|
||||
flattened[newKey] = value;
|
||||
}
|
||||
}
|
||||
return flattened;
|
||||
}
|
||||
|
||||
export function dottedToObject(obj: unknown): Record<string, unknown> {
|
||||
if (!isRecord(obj)) {
|
||||
throw new TypeError("Expected a dotted message catalogue object");
|
||||
}
|
||||
const nestedResult: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
if (key.includes(" ")) {
|
||||
nestedResult[key] = value;
|
||||
continue;
|
||||
}
|
||||
const keys = key.split(".");
|
||||
let nested = nestedResult;
|
||||
for (const [index, currentKey] of keys.entries()) {
|
||||
if (index === keys.length - 1) {
|
||||
nested[currentKey] = value;
|
||||
continue;
|
||||
}
|
||||
const currentValue = nested[currentKey];
|
||||
if (isRecord(currentValue)) {
|
||||
nested = currentValue;
|
||||
continue;
|
||||
}
|
||||
const replacement = currentValue === undefined || currentValue === null ? {} : { _value: currentValue };
|
||||
nested[currentKey] = replacement;
|
||||
nested = replacement;
|
||||
}
|
||||
}
|
||||
return nestedResult;
|
||||
}
|
||||
|
||||
41
_tools/messagelib.unit.spec.ts
Normal file
41
_tools/messagelib.unit.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { dottedToObject, objectToDotted } from "./messagelib";
|
||||
|
||||
describe("message catalogue conversion", () => {
|
||||
it("flattens nested objects while preserving leaf values", () => {
|
||||
expect(
|
||||
objectToDotted({
|
||||
dialogue: {
|
||||
title: "Title",
|
||||
options: ["first", "second"],
|
||||
},
|
||||
"literal key": "Literal",
|
||||
})
|
||||
).toEqual({
|
||||
"dialogue.title": "Title",
|
||||
"dialogue.options": ["first", "second"],
|
||||
"literal key": "Literal",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves an existing leaf under _value when a dotted child follows it", () => {
|
||||
expect(
|
||||
dottedToObject({
|
||||
section: "Base value",
|
||||
"section.child": "Child value",
|
||||
"literal key": "Literal",
|
||||
})
|
||||
).toEqual({
|
||||
section: {
|
||||
_value: "Base value",
|
||||
child: "Child value",
|
||||
},
|
||||
"literal key": "Literal",
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects non-object catalogue roots", () => {
|
||||
expect(() => objectToDotted("not an object")).toThrow("Expected a message catalogue object");
|
||||
expect(() => dottedToObject(["not", "an", "object"])).toThrow("Expected a dotted message catalogue object");
|
||||
});
|
||||
});
|
||||
@@ -1,29 +1,30 @@
|
||||
// Convert Human-Editable format (YAML) to Application convenient Message Resources (JSON)
|
||||
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { join, resolve } from "path";
|
||||
import { parse } from "yaml";
|
||||
import { glob } from "tinyglobby";
|
||||
import { objectToDotted } from "./messagelib";
|
||||
|
||||
const fsPromises = process.getBuiltinModule("node:fs/promises");
|
||||
const path = process.getBuiltinModule("node:path");
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
const targetDir = resolve(join(__dirname, "../src/common/messagesYAML/"));
|
||||
console.log(`Target directory: ${targetDir}`);
|
||||
const targetDir = path.resolve(path.join(__dirname, "../src/common/messagesYAML/"));
|
||||
process.stdout.write(`Target directory: ${targetDir}\n`);
|
||||
const files = await glob(`*.yaml`, { expandDirectories: false, absolute: true, cwd: targetDir });
|
||||
for (const file of files) {
|
||||
const filePath = resolve(file);
|
||||
const content = await readFile(filePath, "utf-8");
|
||||
const jsonDataSrc = parse(content);
|
||||
const filePath = path.resolve(file);
|
||||
const content = await fsPromises.readFile(filePath, "utf-8");
|
||||
const jsonDataSrc: unknown = parse(content);
|
||||
const jsonDataD2 = objectToDotted(jsonDataSrc);
|
||||
const jsonData = Object.fromEntries(
|
||||
Object.entries(jsonDataD2)
|
||||
.map(([key, value]) => [key.endsWith("._value") ? key.slice(0, -7) : key, value])
|
||||
.map(([key, value]): [string, unknown] => [key.endsWith("._value") ? key.slice(0, -7) : key, value])
|
||||
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
|
||||
);
|
||||
const yamlData = JSON.stringify(jsonData, null, 4) + "\n";
|
||||
const yamlFilePath = filePath.replace(/\.yaml$/, ".json").replace("YAML", "Json");
|
||||
await writeFile(yamlFilePath, yamlData, "utf-8");
|
||||
console.log(`Converted ${filePath} to ${yamlFilePath}`);
|
||||
await fsPromises.writeFile(yamlFilePath, yamlData, "utf-8");
|
||||
process.stdout.write(`Converted ${filePath} to ${yamlFilePath}\n`);
|
||||
}
|
||||
|
||||
// console.dir(files, { depth: 0 });
|
||||
|
||||
5
devs.md
5
devs.md
@@ -246,6 +246,7 @@ export class ModuleExample extends AbstractObsidianModule {
|
||||
- A plug-in review release may omit the CLI image when the CLI artefact is not part of the required validation. When a pre-release CLI image is published, it receives immutable version and SHA-qualified tags only; it must not advance `latest` or a stable major-minor tag.
|
||||
- Keep a hyphenated pre-release's release pull request in draft and unmerged after BRAT validation. Reconcile the published version's metadata into its base branch through a reviewed metadata-only commit, then close the release pull request only through a separate maintainer action.
|
||||
- Stage a stable version for BRAT by publishing its exact `x.y.z` tag initially as a GitHub pre-release with `prerelease=true` and `publish_cli=false`. The stable manifest version would otherwise make the CLI workflow advance `latest` and the major-minor image tag before validation.
|
||||
- After a staged stable version passes BRAT validation, merge its exact release commit into the reviewed base branch and integrate it through the reviewed branch chain into the repository's default branch. Promote the GitHub Release only after the default branch contains the exact stable metadata; publish stable CLI tags through a separate maintainer gate.
|
||||
- If validation fails, leave every published tag unchanged and prepare the next pre-release or patch version.
|
||||
|
||||
## Release Notes
|
||||
@@ -271,7 +272,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel
|
||||
- Publish the draft as a GitHub pre-release without replacing the latest stable release. Keep its release pull request in draft and leave its base branch unchanged throughout BRAT validation. Record that state in the pull request.
|
||||
- Validate the published release through BRAT. Confirm start-up, ordinary bidirectional synchronisation, and any regression scenario relevant to the release.
|
||||
- After a hyphenated pre-release passes, keep its release pull request unmerged. Add a reviewed metadata-only commit to the selected base branch which records the published version in `versions.json` and moves its exact tagged release notes out of `## Unreleased`, then close the release pull request only through a separate maintainer action.
|
||||
- After a stable version passes, remove its GitHub pre-release designation and make that exact release the latest stable release. Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate. Only then mark the stable release pull request ready and merge it into the selected base branch with a merge commit.
|
||||
- After a stable version passes, mark its release pull request ready and merge the exact release commit into the selected base branch with a merge commit. Integrate that exact commit through the reviewed branch chain into the repository's default branch. Only after the default branch contains the matching stable metadata, remove the GitHub pre-release designation and make that exact release the latest stable release. Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate.
|
||||
- If BRAT validation fails, keep the release PR in draft and do not move published tags. Before preparing the next version, add a reviewed metadata-only commit to the selected base branch which records the published version in `versions.json` and moves its exact tagged release notes out of `## Unreleased`. Keep only changes made after that tag under `## Unreleased`. Compare the historical section with `git show <tag>:updates.md`; do not merge the failed release PR or describe it as validated. The next release PR can then rotate only the correction notes while preserving the immutable release history.
|
||||
- Prepare and publish the next patch or pre-release version from that reconciled base. Leave the failed release PR draft until it is deliberately closed as superseded under a separate maintainer action.
|
||||
- A hyphenated version is rejected unless `prerelease=true`. A stable version staged with `prerelease=true` is rejected unless `publish_cli=false`.
|
||||
@@ -302,7 +303,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel
|
||||
8. Update the PR state message to describe the published pre-release and state that merging remains on hold.
|
||||
9. Validate the published release through BRAT, including start-up, ordinary bidirectional synchronisation, and any release-specific regression scenario.
|
||||
10. After a hyphenated pre-release passes, keep its release PR unmerged, reconcile its `versions.json` entry and exact release-note section into the selected base branch as metadata only, then close the PR through a separate maintainer action.
|
||||
11. After a stable version passes, remove its pre-release designation, make the exact release the latest stable release, publish the stable CLI tags through a separate maintainer gate if selected, then mark the release PR ready and merge it into the selected base branch.
|
||||
11. After a stable version passes, mark the release PR ready and merge the exact release commit into the selected base branch. Integrate that commit through the reviewed branch chain into the repository's default branch. Once the default branch contains the matching stable metadata, remove the pre-release designation, make the exact release the latest stable release, and publish stable CLI tags through a separate maintainer gate if selected.
|
||||
12. If validation fails, leave the PR in draft and do not move the published tags. Reconcile the published version's `updates.md` section and `versions.json` entry into the base branch as metadata only, then prepare the next patch or pre-release version from the remaining `## Unreleased` entries.
|
||||
|
||||
## Contribution Guidelines
|
||||
|
||||
161
docs/releases/1.0-previews.md
Normal file
161
docs/releases/1.0-previews.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 1.0 preview release history
|
||||
|
||||
This document records the opt-in beta and release-candidate builds published before 1.0.0. Most users upgrading from 0.25.83 only need the consolidated [1.0.0 release notes](../../updates.md).
|
||||
|
||||
The prepared `1.0.0-rc.0` tag was not published as a plug-in release and is therefore omitted.
|
||||
|
||||
## 1.0.0-rc.1
|
||||
|
||||
27th July, 2026
|
||||
|
||||
The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here).
|
||||
|
||||
### Important
|
||||
|
||||
- This candidate retains the plug-in behaviour prepared for rc.0. The version was advanced because release tags are immutable; rc.0 was stopped during CLI validation before a plug-in release was published.
|
||||
- This remains an opt-in pre-release for BRAT validation and does not replace the latest stable release. The exact rc.1 plug-in and CLI artefacts will be validated separately after publication.
|
||||
|
||||
### CLI and release validation
|
||||
|
||||
- CLI release validation now generates Setup URIs through the supported ESM package interface, allowing the Docker test to reach the CLI container instead of stopping during test preparation.
|
||||
- The CLI Docker image now assigns its entrypoint permissions explicitly, so non-root execution does not depend on permissions inherited from the source checkout.
|
||||
- Release finalisation now explicitly dispatches the CLI container workflow when CLI publication is selected, rather than relying on a workflow-created tag to start another workflow.
|
||||
- Focused regression tests guard the ESM execution mode and deterministic container entrypoint permissions, while the existing release-workflow tests now require explicit CLI dispatch with non-dry-run, immutable-tag inputs.
|
||||
|
||||
### Testing
|
||||
|
||||
- The native CLI setup, put, cat, list, information, deletion, conflict-resolution, and revision-retrieval scenario completed with the packaged Commonlib dependency.
|
||||
- The same scenario completed through the rebuilt non-root Docker image.
|
||||
- The focused CLI and release-workflow unit tests passed after first demonstrating all three regressions against the unmodified implementation.
|
||||
|
||||
## 1.0.0-beta.5
|
||||
|
||||
26th July, 2026
|
||||
|
||||
### Improved
|
||||
|
||||
- **Inspect conflicts and file/database differences** now compares the current Vault file with the database winner and every live conflict revision. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflict branches remain.
|
||||
- Each reported file and live revision now has a compact wrench menu. Its available actions can compare readable text, apply the selected revision to the Vault, record an exact byte match, store the Vault content as a child of the selected branch, retry retrieving missing chunks without changing the revision tree, or discard only the selected live branch after confirmation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- A winning logical deletion is no longer reported as a missing Vault file when the file is already absent.
|
||||
|
||||
### Testing
|
||||
|
||||
- Strengthened Real Obsidian coverage for start-up with an existing configuration, Security Seed readiness, failure diagnostics, and P2P status pane placement in separate desktop and mobile sessions.
|
||||
|
||||
## 1.0.0-beta.4
|
||||
|
||||
25th July, 2026
|
||||
|
||||
### Improved
|
||||
|
||||
- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation.
|
||||
- Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**.
|
||||
- Text in setup and review dialogues can now be selected for copying or translation.
|
||||
- When LiveSync adopts an available interface translation on first start-up, it now continues initialisation and leaves a persistent Notice from which the translation details can be opened, instead of waiting for an unsolicited dialogue.
|
||||
|
||||
### Fixed
|
||||
|
||||
- An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device.
|
||||
- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. Collection now stops when device progress cannot be verified, and a compaction timeout is no longer followed by a contradictory success message.
|
||||
|
||||
### Testing
|
||||
|
||||
- Added regressions for revision repair, command availability, selectable dialogues, conflict-aware chunk reachability, device-progress safeguards, and compaction timeouts.
|
||||
- Added a real CouchDB integration test for logical chunk deletion, shared and conflict chunk retention, compaction completion, downstream replication, and content-addressed chunk recreation.
|
||||
- Added a real Obsidian encrypted reconnect scenario which replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation refreshes it without restoring the old value, and proves a bidirectional encrypted round-trip.
|
||||
|
||||
## 1.0.0-beta.3
|
||||
|
||||
24th July, 2026
|
||||
|
||||
### Improved
|
||||
|
||||
- Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices.
|
||||
- P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits.
|
||||
- First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`.
|
||||
- Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation.
|
||||
- Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings.
|
||||
- Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart.
|
||||
|
||||
### Testing
|
||||
|
||||
- Added regressions for P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues.
|
||||
|
||||
## 1.0.0-beta.2
|
||||
|
||||
23rd July, 2026
|
||||
|
||||
### Improved
|
||||
|
||||
- Choosing **Not now** on a merge conflict now postpones repeated dialogues for that conflict while the active file retains an unresolved-conflict warning. Three or more live versions show their current count and are reviewed one deterministic pair at a time; completed pairs remain resolved across restart. The existing conflict commands can reopen a postponed conflict explicitly, and a later conflict prompts again after the current one has been resolved.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Answering or externally closing a merge dialogue immediately no longer leaves conflict processing waiting for a response which has already occurred.
|
||||
|
||||
### Testing
|
||||
|
||||
- Added revision-tree regressions and focused real-Obsidian scenarios for multiple-version review and restart between resolution stages.
|
||||
|
||||
## 1.0.0-beta.1
|
||||
|
||||
22nd July, 2026
|
||||
|
||||
### Important
|
||||
|
||||
- This corrected opt-in integration preview follows `1.0.0-beta.0` and does not replace the latest stable release. Update every participating device before resuming synchronisation, and continue to use a current backup while testing with an existing Vault.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Conflict resolutions made on another device no longer recreate the same conflict when the receiving Vault still contains the exact content of the deleted losing revision. Automatic text and structured-data merge now uses the nearest revision actually shared by both branches instead of inferring ancestry from revision generation numbers.
|
||||
- Edits, deletions, and renames made while a file is conflicted now extend the exact revision displayed on that device. If LiveSync cannot prove the displayed branch, it preserves the affected branches for review instead of silently applying the operation to the database winner.
|
||||
|
||||
### Testing
|
||||
|
||||
- Added revision-tree regressions and focused real-Obsidian scenarios for propagated resolutions and file operations performed while a conflict remains active.
|
||||
|
||||
## 1.0.0-beta.0
|
||||
|
||||
22nd July, 2026
|
||||
|
||||
### Important
|
||||
|
||||
- This is an opt-in 1.0 integration preview for BRAT and testing with existing Vaults. It does not replace the latest stable release. Use it with a current backup, and update every participating device before resuming synchronisation.
|
||||
- An upgraded, copied, or restored Vault may pause replication for an explicit compatibility review. The review preserves the existing automatic synchronisation choices and resumes them only after the decision has been saved successfully.
|
||||
|
||||
### Improved
|
||||
|
||||
- An unconfigured installation now waits for you to start setup. A long-lived Notice offers the setup action, and **Open onboarding wizard** remains available from the command palette instead of the dialogue opening automatically.
|
||||
- The setup wizard now creates named remote profiles for CouchDB, Object Storage, and P2P. Current Setup URIs preserve their profile names and selections, and the wizard reserves Rebuild or Fetch before the ordinary start-up scan begins.
|
||||
- Peer-to-Peer Synchronisation (P2P) and Hidden File Sync are supported opt-in features. JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 remain previews. Customisation Sync remains a supported advanced workflow.
|
||||
- Data Compression remains available after measurement showed a modest, workload-dependent reduction in stored and transferred chunk data. Its benefits, costs, and reason for remaining disabled by default in 1.0 are described in the Data Compression specification.
|
||||
- Compatibility review now runs before Config Doctor without overlapping it. Existing Vaults retain their automatic synchronisation choices and explicit file-name case setting. For installations created by earlier releases, LiveSync preserves whether setup had been completed and saves a missing legacy case setting as case-insensitive.
|
||||
- P2P connections now restart reliably after settings are reapplied or the local database is reset. Setup on an additional device asks you to select the source device once. Disconnecting leaves the LiveSync room and closes its signalling relay connections so that reconnecting can establish a new room.
|
||||
- Action buttons are stacked vertically, long setup dialogues keep their controls reachable on mobile screens, and persistent Notices no longer cover close controls. Hidden File Sync reload and restart requests are grouped into one message, including the case reported in issue #555.
|
||||
- Warnings about estimated remote storage size now appear as long-lived clickable Notices instead of timed dialogues. Initial uploads and Rebuild operations no longer prompt to send every chunk in advance; ordinary replication completes the transfer.
|
||||
- Removed the obsolete **Use the trash bin** control and the setting for fixed chunk revisions. Remote deletion still follows Obsidian's preference, and chunk revisions remain content-derived. The Change Log remains available but no longer opens automatically or tracks unread versions.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The optional Custom HTTP Handler used by Object Storage now sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request.
|
||||
- When selectors, ignore files, size limits, modification-time limits, or file-name case settings are broadened, LiveSync now rechecks previously received files without requiring another remote update.
|
||||
- P2P setup on the first device no longer displays reset or upload steps for a central database, and Config Doctor now offers its chunk size recommendation for CouchDB only when a CouchDB remote profile is selected.
|
||||
|
||||
### Security
|
||||
|
||||
- Fly.io setup now generates CouchDB and Vault encryption secrets with cryptographically secure randomness. Dependency updates prevent excessive CPU use from specially crafted path patterns and `mailto:` links. The CLI rejects path traversal and symbolic-link components detected before Vault operations.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Self-hosted LiveSync now owns its translation catalogue. Commonlib provides English messages to other applications, and translation contributions can be made directly to the Self-hosted LiveSync repository.
|
||||
|
||||
### Testing
|
||||
|
||||
- Expanded automated testing in Obsidian for upgrades, synchronisation between two devices, CouchDB, Object Storage, P2P, Hidden File Sync, mobile dialogues, and clean-up after failures.
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-livesync",
|
||||
"name": "Self-hosted LiveSync",
|
||||
"version": "1.0.0-rc.1",
|
||||
"version": "1.0.0",
|
||||
"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.",
|
||||
"author": "vorotamoroz",
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.0-rc.1",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.0-rc.1",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"src/apps/cli",
|
||||
@@ -15913,7 +15913,7 @@
|
||||
},
|
||||
"src/apps/cli": {
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"version": "1.0.0-rc.1-cli",
|
||||
"version": "1.0.0-cli",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"minimatch": "^10.2.5",
|
||||
@@ -15938,7 +15938,7 @@
|
||||
},
|
||||
"src/apps/webapp": {
|
||||
"name": "livesync-webapp",
|
||||
"version": "1.0.0-rc.1-webapp",
|
||||
"version": "1.0.0-webapp",
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.51"
|
||||
},
|
||||
@@ -15950,7 +15950,7 @@
|
||||
}
|
||||
},
|
||||
"src/apps/webpeer": {
|
||||
"version": "1.0.0-rc.1-webpeer",
|
||||
"version": "1.0.0-webpeer",
|
||||
"dependencies": {
|
||||
"octagonal-wheels": "^0.1.51"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsidian-livesync",
|
||||
"version": "1.0.0-rc.1",
|
||||
"version": "1.0.0",
|
||||
"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",
|
||||
"type": "module",
|
||||
@@ -12,6 +12,7 @@
|
||||
"buildDev": "node esbuild.config.mjs dev",
|
||||
"lint": "eslint --cache --cache-strategy content --concurrency off src",
|
||||
"lint:community": "eslint --config eslint.community.config.mjs --concurrency off src",
|
||||
"lint:community:tools": "eslint --config eslint.community.config.mjs --concurrency off --max-warnings 0 _tools",
|
||||
"svelte-check": "svelte-check --tsconfig ./tsconfig.json --fail-on-warnings",
|
||||
"tsc-check": "tsc --noEmit",
|
||||
"tsc-check:apps": "tsc --noEmit -p src/apps/browser/tsconfig.json && tsc --noEmit -p src/apps/cli/tsconfig.json && tsc --noEmit -p src/apps/webapp/tsconfig.json && tsc --noEmit -p src/apps/webpeer/tsconfig.app.json && tsc --noEmit -p src/apps/webpeer/tsconfig.node.json",
|
||||
@@ -22,7 +23,7 @@
|
||||
"prettyNoWrite": "prettier --config ./.prettierrc.mjs \"**/*.js\" \"**/*.ts\" \"**/*.json\" ",
|
||||
"precheck:compatibility": "npm run build",
|
||||
"check:compatibility": "node utils/check-compatibility.js --file main.js --ios 15",
|
||||
"check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run svelte-check && npm run check:compatibility",
|
||||
"check": "npm run tsc-check && npm run tsc-check:apps && npm run lint && npm run lint:community -- --quiet && npm run lint:community:tools && npm run svelte-check && npm run check:compatibility",
|
||||
"i18n:bake": "npm run i18n:yaml2json && npm run i18n:bakejson && npm run i18n:format",
|
||||
"i18n:bakejson": "tsx _tools/bakei18n.ts",
|
||||
"i18n:format": "prettier --config .prettierrc.mjs --write --log-level error 'src/common/messagesJson/*.json' 'src/common/messages/*.ts'",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "self-hosted-livesync-cli",
|
||||
"private": true,
|
||||
"version": "1.0.0-rc.1-cli",
|
||||
"version": "1.0.0-cli",
|
||||
"main": "dist/index.cjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "livesync-webapp",
|
||||
"private": true,
|
||||
"version": "1.0.0-rc.1-webapp",
|
||||
"version": "1.0.0-webapp",
|
||||
"type": "module",
|
||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "webpeer",
|
||||
"private": true,
|
||||
"version": "1.0.0-rc.1-webpeer",
|
||||
"version": "1.0.0-webpeer",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
245
updates.md
245
updates.md
@@ -12,224 +12,99 @@ Earlier releases remain available in the 0.25 release history and the legacy rel
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 1.0.0-rc.1
|
||||
## 1.0.0
|
||||
|
||||
27th July, 2026
|
||||
|
||||
The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here).
|
||||
|
||||
### Important
|
||||
### Setup and compatibility
|
||||
|
||||
- This candidate retains the plug-in behaviour prepared for rc.0. The version was advanced because release tags are immutable; rc.0 was stopped during CLI validation before a plug-in release was published.
|
||||
- This remains an opt-in pre-release for BRAT validation and does not replace the latest stable release. The exact rc.1 plug-in and CLI artefacts will be validated separately after publication.
|
||||
|
||||
### CLI and release validation
|
||||
|
||||
- CLI release validation now generates Setup URIs through the supported ESM package interface, allowing the Docker test to reach the CLI container instead of stopping during test preparation.
|
||||
- The CLI Docker image now assigns its entrypoint permissions explicitly, so non-root execution does not depend on permissions inherited from the source checkout.
|
||||
- Release finalisation now explicitly dispatches the CLI container workflow when CLI publication is selected, rather than relying on a workflow-created tag to start another workflow.
|
||||
- Focused regression tests guard the ESM execution mode and deterministic container entrypoint permissions, while the existing release-workflow tests now require explicit CLI dispatch with non-dry-run, immutable-tag inputs.
|
||||
|
||||
### Testing
|
||||
|
||||
- The native CLI setup, put, cat, list, information, deletion, conflict-resolution, and revision-retrieval scenario completed with the packaged Commonlib dependency.
|
||||
- The same scenario completed through the rebuilt non-root Docker image.
|
||||
- The focused CLI and release-workflow unit tests passed after first demonstrating all three regressions against the unmodified implementation.
|
||||
|
||||
## 1.0.0-rc.0
|
||||
|
||||
27th July, 2026
|
||||
|
||||
The work towards 1.0 has become so substantial that I have written [an article about it](https://fancy-syncing.vrtmrz.net/blog/0036-livesync-1_0_0-en.html) (linked again here).
|
||||
|
||||
### Important
|
||||
|
||||
- This is the first 1.0 release candidate. It remains an opt-in pre-release for BRAT validation and does not replace the latest stable release. Update every participating device before resuming synchronisation, and continue to use a current backup while testing with an existing Vault.
|
||||
- An upgraded, copied, or restored Vault may pause replication for an explicit compatibility review. Existing automatic synchronisation choices are preserved and resume only after the decision has been saved.
|
||||
|
||||
### Changes consolidated from beta.0 through beta.5
|
||||
|
||||
#### Setup and compatibility
|
||||
#### Improved
|
||||
|
||||
- An unconfigured Vault now waits for the user to start setup. Onboarding is offered through a persistent Notice and remains available from **Self-hosted LiveSync settings** → **Setup**.
|
||||
- Setup now creates named CouchDB, Object Storage, and P2P connections. Setup URIs preserve their connection names and selections, and reserve Fetch or Rebuild before the ordinary start-up scan begins.
|
||||
- Existing Vaults retain their effective legacy settings, including the case-insensitive file-name fallback used when an older release had no explicit case setting.
|
||||
- Manual CouchDB setup distinguishes creating the first database from connecting another device. Onboarding requires a successful connection, while Settings can explicitly save an unverified connection and offers each server-setting correction separately.
|
||||
- Compatible differences limited to the chunk hash algorithm, chunk size, or splitter version are aligned automatically by default. Existing chunks remain readable, an explicit opt-out remains available, and differences involving incompatible settings still require review.
|
||||
|
||||
#### Conflict handling and recovery
|
||||
#### Fixed
|
||||
|
||||
- Existing Vaults retain their effective legacy settings, including the case-insensitive file-name fallback used when an older release had no explicit case setting.
|
||||
|
||||
#### Security
|
||||
|
||||
- Fly.io setup generates CouchDB and Vault encryption secrets with cryptographically secure randomness.
|
||||
- Dependency updates address excessive CPU use from crafted path patterns and `mailto:` links.
|
||||
|
||||
### Conflict handling and recovery
|
||||
|
||||
#### Improved
|
||||
|
||||
- Automatic text and structured-data merge now uses the nearest revision actually shared by both branches. A resolution received from another device no longer recreates the same conflict merely because the Vault still contains the exact content of the removed branch.
|
||||
- Edits, logical deletions, and renames made while a file remains conflicted extend the revision displayed on that device. When the relationship cannot be proved, LiveSync preserves the branches for review.
|
||||
- **Not now** postpones repeated automatic merge dialogues while retaining the unresolved-conflict warning. Three or more live revisions are reviewed one reproducible pair at a time, completed pairs remain resolved across restart, and explicit commands can reopen a postponed conflict.
|
||||
- **Inspect conflicts and file/database differences** compares the Vault with the database winner and every live conflict revision. Compact indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflicts remain.
|
||||
- Each reported file and live revision has a compact wrench menu for comparison, applying an exact readable revision, recording an exact byte match, storing the Vault content as a child of a selected branch, retrying missing chunks without changing the tree, or explicitly discarding one selected live branch.
|
||||
|
||||
#### Fixed
|
||||
|
||||
- Automatic text and structured-data merge now uses the nearest revision actually shared by both branches. A resolution received from another device no longer recreates the same conflict merely because the Vault still contains the exact content of the removed branch.
|
||||
- Edits, logical deletions, and renames made while a file remains conflicted extend the revision displayed on that device. When the relationship cannot be proved, LiveSync preserves the branches for review.
|
||||
- Unreadable live revisions are preserved during automatic handling. An absent Vault file and a winning logical deletion are treated as agreement unless another live branch still requires attention.
|
||||
- Garbage Collection V3 is limited to CouchDB and now protects every live conflict branch, required shared ancestry, and shared chunks. It stops when device progress cannot be verified and reports compaction failure without a contradictory success message.
|
||||
|
||||
#### P2P and optional synchronisation features
|
||||
### P2P and optional synchronisation features
|
||||
|
||||
#### Improved
|
||||
|
||||
- P2P and Hidden File Sync remain supported opt-in features. Customisation Sync remains a supported Advanced workflow, while Data Compression remains available but disabled by default.
|
||||
- P2P controls remain outside the ordinary CouchDB experience until P2P is configured. The current status pane distinguishes announcing changes, following a peer, and persistent per-device actions.
|
||||
- First-device P2P setup can complete its signalling test without another peer online. Fetch on an additional device still requires an available source peer and a completed P2P Rebuild.
|
||||
- P2P setup and guidance now distinguish the required signalling relay from optional TURN, describe the replaceable public relay's privacy and availability limits, and reliably close and recreate relay connections across settings changes and database resets.
|
||||
- P2P setup and guidance now distinguish the required signalling relay from optional TURN and describe the replaceable public relay's privacy and availability limits.
|
||||
- Enabling Hidden File Sync opens one progress Notice before saving the setting and reuses it until the initial scan has finished instead of stacking phase, reload, and restart messages.
|
||||
- Broadening selectors, ignore rules, size or modification-time limits, or file-name case handling now rechecks previously received files without requiring another remote update.
|
||||
|
||||
#### Interface and operations
|
||||
#### Fixed
|
||||
|
||||
- First-device P2P setup can complete its signalling test without another peer online. Fetch on an additional device still requires an available source peer and a completed P2P Rebuild.
|
||||
- P2P relay connections now close and are recreated reliably after settings changes and database resets.
|
||||
|
||||
### Interface, translation, and operations
|
||||
|
||||
#### Improved
|
||||
|
||||
- Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands retain their identifiers so that existing hotkeys continue to work.
|
||||
- Setup and review dialogue text can be selected for copying or translation. Applying an available interface translation no longer holds start-up behind an unsolicited dialogue; a persistent Notice opens the existing details on demand.
|
||||
- Action buttons are arranged for narrow mobile screens, long dialogues keep their controls reachable, and persistent Notices no longer cover close controls.
|
||||
- Setup and review dialogue text can be selected for copying or translation.
|
||||
- Remote-size warnings use persistent clickable Notices. Initial uploads and Rebuild no longer ask to send every chunk in advance; ordinary replication completes the transfer.
|
||||
- Obsolete controls for the plug-in trash setting and fixed chunk revisions were removed. The Change Log remains available but no longer opens automatically or tracks an unread count.
|
||||
|
||||
#### Other fixes and security
|
||||
|
||||
- The optional Custom HTTP Handler used by Object Storage sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request.
|
||||
- Fly.io setup generates CouchDB and Vault encryption secrets with cryptographically secure randomness. Dependency updates address excessive CPU use from crafted path patterns and `mailto:` links, and the CLI rejects detected path traversal and symbolic-link components before Vault operations.
|
||||
- Self-hosted LiveSync now owns its translation catalogue. Commonlib supplies canonical English to other consumers, while translation contributions can be made in the main Self-hosted LiveSync repository.
|
||||
|
||||
### Changes since beta.5
|
||||
#### Fixed
|
||||
|
||||
- The Hatch action for **Inspect conflicts and file/database differences** is now labelled **Begin inspection** so that its purpose is clear without repeating the setting name.
|
||||
- Start-up and full-inspection scans now omit built-in legacy LiveSync log files and recovery flag files before comparing Vault and local-database state. Existing ignored database records remain untouched, and user-configured ignore behaviour is unchanged.
|
||||
- Applying an available interface translation no longer holds start-up behind an unsolicited dialogue; a persistent Notice opens the existing details on demand.
|
||||
- Action buttons are arranged for narrow mobile screens, long dialogues keep their controls reachable, and persistent Notices no longer cover close controls.
|
||||
|
||||
### Testing
|
||||
### Storage and file selection
|
||||
|
||||
#### Fixed
|
||||
|
||||
- The optional Custom HTTP Handler used by Object Storage sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request.
|
||||
- Broadening selectors, ignore rules, size or modification-time limits, or file-name case handling now rechecks previously received files without requiring another remote update.
|
||||
- Start-up and full-inspection scans omit built-in legacy LiveSync log files and recovery flag files before comparing Vault and local-database state. Existing ignored database records remain untouched, and user-configured ignore behaviour is unchanged.
|
||||
|
||||
### Command-line tool
|
||||
|
||||
#### Fixed
|
||||
|
||||
- CLI Setup URI validation now uses the supported Commonlib ESM package interface.
|
||||
- The non-root Docker image no longer depends on permissions inherited from the source checkout.
|
||||
|
||||
#### Security
|
||||
|
||||
- The CLI rejects detected path traversal and symbolic-link components before Vault operations.
|
||||
|
||||
### Validation
|
||||
|
||||
#### Testing
|
||||
|
||||
- Expanded automated Real Obsidian coverage for upgrades, two-device synchronisation, CouchDB, Object Storage, P2P, Hidden File Sync, mobile dialogues, conflict and revision recovery, failure diagnostics, and strict clean-up.
|
||||
- Real CouchDB integration coverage verifies logical deletion, shared and conflict chunk retention, compaction, downstream replication, and recreation of content-addressed chunks.
|
||||
- An encrypted Real Obsidian reconnect scenario replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation adopts the replacement without restoring the old value, and proves a bidirectional encrypted round-trip.
|
||||
- The beta series was exercised through BRAT on macOS, iOS, and Android, including upgrade from 0.25.83, bidirectional synchronisation, P2P setup, conflict handling, recovery controls, mobile layouts, and start-up with existing configurations. The exact RC artefact will be validated separately after publication.
|
||||
|
||||
## 1.0.0-beta.5
|
||||
|
||||
26th July, 2026
|
||||
|
||||
### Improved
|
||||
|
||||
- **Inspect conflicts and file/database differences** now compares the current Vault file with the database winner and every live conflict revision. Compact, mobile-friendly indicators show missing chunks, `Δsize`, `Δtime`, whether the Vault matches the winner, and whether conflict branches remain.
|
||||
- Each reported file and live revision now has a compact wrench menu. Its available actions can compare readable text, apply the selected revision to the Vault, record an exact byte match, store the Vault content as a child of the selected branch, retry retrieving missing chunks without changing the revision tree, or discard only the selected live branch after confirmation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- A winning logical deletion is no longer reported as a missing Vault file when the file is already absent.
|
||||
|
||||
### Testing
|
||||
|
||||
- Strengthened Real Obsidian coverage for start-up with an existing configuration, Security Seed readiness, failure diagnostics, and P2P status pane placement in separate desktop and mobile sessions.
|
||||
|
||||
## 1.0.0-beta.4
|
||||
|
||||
25th July, 2026
|
||||
|
||||
### Improved
|
||||
|
||||
- **Verify and repair all files** now reports the database winner, every conflict revision, missing chunks, and unavailable shared ancestors separately. It can retry an exact revision without changing the tree, while discarding an unreadable live revision requires explicit confirmation.
|
||||
- Command-palette actions now use clearer names and appear only when their feature and current context make them usable. Renamed commands keep their identifiers, so hotkeys already assigned to them continue to work. The onboarding wizard can be reopened from **Self-hosted LiveSync settings** → **Setup**.
|
||||
- Text in setup and review dialogues can now be selected for copying or translation.
|
||||
- When LiveSync adopts an available interface translation on first start-up, it now continues initialisation and leaves a persistent Notice from which the translation details can be opened, instead of waiting for an unsolicited dialogue.
|
||||
|
||||
### Fixed
|
||||
|
||||
- An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device.
|
||||
- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. Collection now stops when device progress cannot be verified, and a compaction timeout is no longer followed by a contradictory success message.
|
||||
|
||||
### Testing
|
||||
|
||||
- Added regressions for revision repair, command availability, selectable dialogues, conflict-aware chunk reachability, device-progress safeguards, and compaction timeouts.
|
||||
- Added a real CouchDB integration test for logical chunk deletion, shared and conflict chunk retention, compaction completion, downstream replication, and content-addressed chunk recreation.
|
||||
- Added a real Obsidian encrypted reconnect scenario which replaces the remote Security Seed while one client retains the previous value, verifies that synchronisation refreshes it without restoring the old value, and proves a bidirectional encrypted round-trip.
|
||||
|
||||
## 1.0.0-beta.3
|
||||
|
||||
24th July, 2026
|
||||
|
||||
### Improved
|
||||
|
||||
- Enabling Hidden File Sync now opens one progress Notice before its setting is saved and reuses that Notice throughout the initial file scan, instead of stacking separate phase and restart Notices.
|
||||
- P2P is now presented only after it has been configured: its status pane no longer opens at start-up, its ribbon icon remains hidden for CouchDB-only Vaults, and the retired P2P pane command has been removed. The current pane distinguishes announcing changes, following a peer, and persistent per-device actions. Setup and guidance now distinguish the required signalling relay from optional TURN, and describe the public signalling relay's privacy and availability limits.
|
||||
- First-device P2P setup now accepts a successfully opened signalling room without requiring another peer to be online. Additional-device Fetch still requires selecting a source peer and completing `P2P Rebuild`.
|
||||
- Manual CouchDB setup now distinguishes creating a first database from connecting an additional device to an existing one. Settings mode can save an unverified profile explicitly, while onboarding requires a successful connection, and each proposed server-configuration fix requires separate confirmation.
|
||||
- Differences limited to the chunk hash algorithm, chunk size, or splitter version are now aligned automatically by default. Existing content remains readable, while an explicit opt-out and any difference which also involves an incompatible setting retain manual review.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings.
|
||||
- Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart.
|
||||
|
||||
### Testing
|
||||
|
||||
- Added regressions for P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, and mobile dialogues.
|
||||
|
||||
## 1.0.0-beta.2
|
||||
|
||||
23rd July, 2026
|
||||
|
||||
### Improved
|
||||
|
||||
- Choosing **Not now** on a merge conflict now postpones repeated dialogues for that conflict while the active file retains an unresolved-conflict warning. Three or more live versions show their current count and are reviewed one deterministic pair at a time; completed pairs remain resolved across restart. The existing conflict commands can reopen a postponed conflict explicitly, and a later conflict prompts again after the current one has been resolved.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Answering or externally closing a merge dialogue immediately no longer leaves conflict processing waiting for a response which has already occurred.
|
||||
|
||||
### Testing
|
||||
|
||||
- Added revision-tree regressions and focused real-Obsidian scenarios for multiple-version review and restart between resolution stages.
|
||||
|
||||
## 1.0.0-beta.1
|
||||
|
||||
22nd July, 2026
|
||||
|
||||
### Important
|
||||
|
||||
- This corrected opt-in integration preview follows `1.0.0-beta.0` and does not replace the latest stable release. Update every participating device before resuming synchronisation, and continue to use a current backup while testing with an existing Vault.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Conflict resolutions made on another device no longer recreate the same conflict when the receiving Vault still contains the exact content of the deleted losing revision. Automatic text and structured-data merge now uses the nearest revision actually shared by both branches instead of inferring ancestry from revision generation numbers.
|
||||
- Edits, deletions, and renames made while a file is conflicted now extend the exact revision displayed on that device. If LiveSync cannot prove the displayed branch, it preserves the affected branches for review instead of silently applying the operation to the database winner.
|
||||
|
||||
### Testing
|
||||
|
||||
- Added revision-tree regressions and focused real-Obsidian scenarios for propagated resolutions and file operations performed while a conflict remains active.
|
||||
|
||||
## 1.0.0-beta.0
|
||||
|
||||
22nd July, 2026
|
||||
|
||||
### Important
|
||||
|
||||
- This is an opt-in 1.0 integration preview for BRAT and testing with existing Vaults. It does not replace the latest stable release. Use it with a current backup, and update every participating device before resuming synchronisation.
|
||||
- An upgraded, copied, or restored Vault may pause replication for an explicit compatibility review. The review preserves the existing automatic synchronisation choices and resumes them only after the decision has been saved successfully.
|
||||
|
||||
### Improved
|
||||
|
||||
- An unconfigured installation now waits for you to start setup. A long-lived Notice offers the setup action, and **Open onboarding wizard** remains available from the command palette instead of the dialogue opening automatically.
|
||||
- The setup wizard now creates named remote profiles for CouchDB, Object Storage, and P2P. Current Setup URIs preserve their profile names and selections, and the wizard reserves Rebuild or Fetch before the ordinary start-up scan begins.
|
||||
- Peer-to-Peer Synchronisation (P2P) and Hidden File Sync are supported opt-in features. JWT authentication, ignore files, automatic newer-file conflict resolution, and Garbage Collection V3 remain previews. Customisation Sync remains a supported advanced workflow.
|
||||
- Data Compression remains available after measurement showed a modest, workload-dependent reduction in stored and transferred chunk data. Its benefits, costs, and reason for remaining disabled by default in 1.0 are described in the Data Compression specification.
|
||||
- Compatibility review now runs before Config Doctor without overlapping it. Existing Vaults retain their automatic synchronisation choices and explicit file-name case setting. For installations created by earlier releases, LiveSync preserves whether setup had been completed and saves a missing legacy case setting as case-insensitive.
|
||||
- P2P connections now restart reliably after settings are reapplied or the local database is reset. Setup on an additional device asks you to select the source device once. Disconnecting leaves the LiveSync room and closes its signalling relay connections so that reconnecting can establish a new room.
|
||||
- Action buttons are stacked vertically, long setup dialogues keep their controls reachable on mobile screens, and persistent Notices no longer cover close controls. Hidden File Sync reload and restart requests are grouped into one message, including the case reported in issue #555.
|
||||
- Warnings about estimated remote storage size now appear as long-lived clickable Notices instead of timed dialogues. Initial uploads and Rebuild operations no longer prompt to send every chunk in advance; ordinary replication completes the transfer.
|
||||
- Removed the obsolete **Use the trash bin** control and the setting for fixed chunk revisions. Remote deletion still follows Obsidian's preference, and chunk revisions remain content-derived. The Change Log remains available but no longer opens automatically or tracks unread versions.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The optional Custom HTTP Handler used by Object Storage now sends the correct byte range from binary request bodies and reports unsupported body types instead of silently sending an empty request.
|
||||
- When selectors, ignore files, size limits, modification-time limits, or file-name case settings are broadened, LiveSync now rechecks previously received files without requiring another remote update.
|
||||
- P2P setup on the first device no longer displays reset or upload steps for a central database, and Config Doctor now offers its chunk size recommendation for CouchDB only when a CouchDB remote profile is selected.
|
||||
|
||||
### Security
|
||||
|
||||
- Fly.io setup now generates CouchDB and Vault encryption secrets with cryptographically secure randomness. Dependency updates prevent excessive CPU use from specially crafted path patterns and `mailto:` links. The CLI rejects path traversal and symbolic-link components detected before Vault operations.
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- Self-hosted LiveSync now owns its translation catalogue. Commonlib provides English messages to other applications, and translation contributions can be made directly to the Self-hosted LiveSync repository.
|
||||
|
||||
### Testing
|
||||
|
||||
- Expanded automated testing in Obsidian for upgrades, synchronisation between two devices, CouchDB, Object Storage, P2P, Hidden File Sync, mobile dialogues, and clean-up after failures.
|
||||
- The plug-in code in this release was installed through BRAT and validated on macOS, iOS, and Android, including upgrade from 0.25.83, bidirectional synchronisation, P2P setup, conflict handling, recovery controls, mobile layouts, and start-up with existing configurations.
|
||||
- Native and non-root Docker CLI scenarios cover setup, write, read, list, information, deletion, conflict resolution, and revision retrieval with the packaged Commonlib dependency.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
The release history is now kept as one chronological sequence across smaller files:
|
||||
|
||||
- [Current 1.x releases](updates.md)
|
||||
- [1.0 beta and release-candidate history](docs/releases/1.0-previews.md)
|
||||
- [0.25 releases](docs/releases/0.25.md)
|
||||
- [Releases before 0.25](docs/releases/legacy.md)
|
||||
|
||||
|
||||
@@ -52,15 +52,16 @@ export function renderReleasePrBody(version, baseBranch) {
|
||||
: "Confirm the draft GitHub Release assets; keep stable CLI publication deferred until BRAT validation passes";
|
||||
const holdInstruction = isPrerelease
|
||||
? `Publishing and validating this pre-release does not unblock this pull request. Keep it in draft and unmerged, and leave ${baseBranchCode} unchanged.`
|
||||
: `Publishing the GitHub pre-release does not unblock this pull request. Keep it in draft, and leave ${baseBranchCode} unchanged, until the exact published build has passed BRAT validation and has been promoted to the latest stable release.`;
|
||||
: `Publishing the GitHub pre-release does not unblock this pull request. Keep it in draft, and leave ${baseBranchCode} unchanged, until the exact published build has passed BRAT validation. Promotion remains on hold until the exact release commit has been integrated into the repository's default branch.`;
|
||||
const completionInstructions = isPrerelease
|
||||
? [
|
||||
"- [ ] Keep this pre-release pull request unmerged; close it only through a separate maintainer action",
|
||||
]
|
||||
: [
|
||||
"- [ ] Remove the pre-release designation and make this exact release the latest stable release",
|
||||
`- [ ] After BRAT validation passes, mark this pull request ready and merge it into ${baseBranchCode} with a merge commit`,
|
||||
"- [ ] Integrate the exact release commit through the reviewed branch chain into the repository's default branch",
|
||||
"- [ ] Confirm the default branch contains the exact release metadata, then remove the pre-release designation and make this exact release the latest stable release",
|
||||
"- [ ] Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate",
|
||||
`- [ ] Mark this pull request ready and merge it into ${baseBranchCode} with a merge commit`,
|
||||
];
|
||||
|
||||
return [
|
||||
|
||||
@@ -207,12 +207,26 @@ describe("release workflow", () => {
|
||||
"Publish the GitHub Release initially as a pre-release without replacing the latest stable release"
|
||||
);
|
||||
expect(stable).toContain(
|
||||
"Remove the pre-release designation and make this exact release the latest stable release"
|
||||
"After BRAT validation passes, mark this pull request ready and merge it into `main` with a merge commit"
|
||||
);
|
||||
expect(stable).toContain(
|
||||
"Integrate the exact release commit through the reviewed branch chain into the repository's default branch"
|
||||
);
|
||||
expect(stable).toContain(
|
||||
"Confirm the default branch contains the exact release metadata, then remove the pre-release designation and make this exact release the latest stable release"
|
||||
);
|
||||
expect(stable).toContain(
|
||||
"Create the stable CLI tag and publish its `latest` and major-minor image tags, if selected, through a separate maintainer gate"
|
||||
);
|
||||
expect(stable).toContain("Mark this pull request ready and merge it into `main` with a merge commit");
|
||||
expect(stable.indexOf("After BRAT validation passes")).toBeLessThan(
|
||||
stable.indexOf("Integrate the exact release commit")
|
||||
);
|
||||
expect(stable.indexOf("Integrate the exact release commit")).toBeLessThan(
|
||||
stable.indexOf("Confirm the default branch contains the exact release metadata")
|
||||
);
|
||||
expect(stable.indexOf("Confirm the default branch contains the exact release metadata")).toBeLessThan(
|
||||
stable.indexOf("Create the stable CLI tag")
|
||||
);
|
||||
expect(stable).not.toContain("prerelease=false");
|
||||
});
|
||||
|
||||
@@ -224,7 +238,10 @@ describe("release workflow", () => {
|
||||
"Keep the release pull request in draft and unmerged after BRAT validation; close it only through a separate maintainer action."
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
"After BRAT validation, remove the pre-release designation and make this exact release the latest stable release before merging the release pull request."
|
||||
"After BRAT validation, merge the release pull request into its reviewed base branch and integrate the exact release commit into the default branch."
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
"Only after the default branch contains the exact release metadata, remove the pre-release designation and make this exact release the latest stable release."
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
'if [[ "${VERSION}" != *-* && "${PRERELEASE}" == "true" && "${PUBLISH_CLI}" == "true" ]]; then'
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"0.25.61": "1.7.2",
|
||||
"0.25.60": "1.7.2",
|
||||
"1.0.1": "0.9.12",
|
||||
"1.0.0": "0.9.7",
|
||||
"0.25.81": "1.7.2",
|
||||
"0.25.82": "1.7.2",
|
||||
"0.25.83": "1.7.2",
|
||||
@@ -13,5 +11,6 @@
|
||||
"1.0.0-beta.4": "1.7.2",
|
||||
"1.0.0-beta.5": "1.7.2",
|
||||
"1.0.0-rc.0": "1.7.2",
|
||||
"1.0.0-rc.1": "1.7.2"
|
||||
"1.0.0-rc.1": "1.7.2",
|
||||
"1.0.0": "1.7.2"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user