Prepare the 1.0 beta release lifecycle

This commit is contained in:
vorotamoroz
2026-07-21 07:15:55 +00:00
parent 8bf1ae81d7
commit e005f9eb13
12 changed files with 210 additions and 54 deletions
+2 -19
View File
@@ -70,7 +70,7 @@ jobs:
fi
git switch -c "${BRANCH}"
npm version "${VERSION}" --no-git-tag-version
npm version "${VERSION}" --no-git-tag-version --allow-same-version
node utils/release-notes.mjs prepare "${VERSION}"
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
@@ -87,24 +87,7 @@ jobs:
BASE_BRANCH: ${{ inputs.base_branch }}
RELEASE_BRANCH: ${{ steps.prepare.outputs.branch }}
run: |
cat > /tmp/release-pr-body.md <<EOF
> [!IMPORTANT]
> **Merge intentionally on hold**
>
> Publishing the GitHub Release does not unblock this pull request. Keep this pull request in draft, and leave \`main\` on the previous release, until the published build has passed BRAT validation.
## Release checklist
- [ ] Review and polish \`updates.md\`
- [ ] Confirm the release date
- [ ] Confirm \`manifest.json\`, \`versions.json\`, workspace package versions, and the locked Commonlib package version
- [ ] Confirm CI has passed
- [ ] Run the finalise release workflow with this PR's fixed head SHA
- [ ] Confirm the draft GitHub Release assets and the published CLI image
- [ ] Publish the GitHub Release as the latest stable release while keeping this pull request in draft
- [ ] Validate the published release with BRAT
- [ ] Mark this pull request ready and merge it with a merge commit
EOF
node utils/release-pr-body.mjs "${VERSION}" "${BASE_BRANCH}" > /tmp/release-pr-body.md
gh pr create \
--base "${BASE_BRANCH}" \
+5 -5
View File
@@ -234,7 +234,7 @@ export class ModuleExample extends AbstractObsidianModule {
## Pre-release Policy
- New pre-releases use SemVer identifiers such as `1.0.0-rc.0`. Historical `-patchedN` releases remain unchanged in the release history.
- Use SemVer beta identifiers such as `1.0.0-beta.0` for immutable integration previews. Increment the beta number when a published preview needs a correction. Reserve `1.0.0-rc.0` for the first feature- and contract-frozen release candidate. Historical `-patchedN` releases remain unchanged in the release history.
- Publish a pre-release from an immutable reviewed tag, mark its GitHub Release as a pre-release, and do not replace the latest stable release.
- 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 the release pull request in draft until the exact published plug-in has passed BRAT validation. If validation fails, prepare the next pre-release version rather than moving the existing tag.
@@ -253,14 +253,14 @@ export class ModuleExample extends AbstractObsidianModule {
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, confirms that Commonlib is locked to an immutable package version, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR.
- Run the `Prepare Release PR` workflow with the target version and selected base branch. It creates the release branch, updates versions, confirms that Commonlib is locked to an immutable package version, moves the `## Unreleased` notes to the target version, commits the release preparation, pushes the branch, and opens a draft release PR. The base branch may already select the target development version; the workflow still runs the version lifecycle so that release-only metadata such as `versions.json` is recorded in the release commit.
- 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 with its full head commit SHA. It validates the release branch, ensures that the plug-in tag points to that commit, optionally creates the corresponding CLI tag, and dispatches the plug-in release workflow. A CLI tag starts its own container workflow. The finalisation workflow can be retried when existing tags already point to the reviewed commit, but stops if a selected tag points elsewhere.
- The plug-in publishing workflow is intentionally dispatch-only. Pushing a plug-in tag directly does not publish a GitHub Release; use `Finalise Release Tags`, or dispatch `Release Obsidian Plugin` explicitly for recovery or a pre-release. The CLI Docker workflow retains its documented branch, tag, and manual triggers.
- Approve the `Release Obsidian Plugin` workflow for the `release` environment, then inspect the generated draft GitHub Release. For a selected CLI publication, confirm the image tags appropriate to a stable or pre-release version.
- Publish a stable draft as the latest release, or publish a pre-release draft without replacing the latest stable release. In either case, keep the release PR in draft and leave its base branch unchanged until BRAT validation succeeds. Record that state in the PR.
- Validate the published release through BRAT. Confirm start-up, ordinary bidirectional synchronisation, and any regression scenario relevant to the release.
- After BRAT validation succeeds, mark the release PR ready and merge it with a merge commit. This keeps the tagged release commit in the `main` history.
- After BRAT validation succeeds, mark the release PR ready and merge it into the selected base branch with a merge commit. This keeps the tagged release commit in that branch's history.
- If BRAT validation fails, keep the release PR in draft. Do not move published tags; prepare and publish a new patch release instead.
- For a pre-release, set `prerelease=true` in `Finalise Release Tags`. A hyphenated version is rejected unless that input is enabled.
@@ -269,7 +269,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel
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`.
- `base_branch`: normally `main`, or the reviewed integration branch for an integration preview.
- `release_branch`: leave blank to use the default branch name, for example `0_25_81`.
- `release_date`: use an ordinal date such as `14th July, 2026`, or leave blank to use the current UTC date.
- `allow_empty_updates`: leave disabled unless the release intentionally has no user-facing notes.
@@ -289,7 +289,7 @@ The `Finalise Release Tags` and `Release Obsidian Plugin` workflows use the `rel
7. Publish the draft as a stable release or pre-release as selected, but keep the release PR in draft and leave its base branch unchanged.
8. Update the PR state message to describe the published release and state that merging remains on hold until BRAT validation is complete.
9. Validate the published release through BRAT, including start-up, ordinary bidirectional synchronisation, and any release-specific regression scenario.
10. After BRAT validation succeeds, mark the release PR ready and merge it into `main` with a merge commit.
10. After BRAT validation succeeds, mark the release PR ready and merge it into the selected base branch with a merge commit.
11. If validation fails, leave the PR in draft and prepare a new patch release without moving the published tags.
## Contribution Guidelines
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-livesync",
"name": "Self-hosted LiveSync",
"version": "1.0.0-rc.0",
"version": "1.0.0-beta.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",
+5 -5
View File
@@ -1,12 +1,12 @@
{
"name": "obsidian-livesync",
"version": "1.0.0-rc.0",
"version": "1.0.0-beta.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-livesync",
"version": "1.0.0-rc.0",
"version": "1.0.0-beta.0",
"license": "MIT",
"workspaces": [
"src/apps/cli",
@@ -16211,7 +16211,7 @@
},
"src/apps/cli": {
"name": "self-hosted-livesync-cli",
"version": "1.0.0-rc.0-cli",
"version": "1.0.0-beta.0-cli",
"dependencies": {
"chokidar": "^4.0.0",
"minimatch": "^10.2.5",
@@ -16236,7 +16236,7 @@
},
"src/apps/webapp": {
"name": "livesync-webapp",
"version": "1.0.0-rc.0-webapp",
"version": "1.0.0-beta.0-webapp",
"dependencies": {
"octagonal-wheels": "^0.1.51"
},
@@ -16251,7 +16251,7 @@
}
},
"src/apps/webpeer": {
"version": "1.0.0-rc.0-webpeer",
"version": "1.0.0-beta.0-webpeer",
"dependencies": {
"octagonal-wheels": "^0.1.51"
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsidian-livesync",
"version": "1.0.0-rc.0",
"version": "1.0.0-beta.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",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "self-hosted-livesync-cli",
"private": true,
"version": "1.0.0-rc.0-cli",
"version": "1.0.0-beta.0-cli",
"main": "dist/index.cjs",
"type": "module",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "livesync-webapp",
"private": true,
"version": "1.0.0-rc.0-webapp",
"version": "1.0.0-beta.0-webapp",
"type": "module",
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "webpeer",
"private": true,
"version": "1.0.0-rc.0-webpeer",
"version": "1.0.0-beta.0-webpeer",
"type": "module",
"scripts": {
"dev": "vite",
+5 -12
View File
@@ -22,18 +22,6 @@ Earlier releases remain available in the [0.25 release history](docs/releases/0.
- Manual onboarding now creates and selects CouchDB, Object Storage, and P2P remote profiles directly while preserving existing profiles. Current Setup URIs retain profile names and selections, while older flat settings remain supported through compatibility migration.
- P2P panes and explicit rebuild actions now use the current transport after settings or database replacement. Disconnecting leaves the LiveSync room and closes signalling relay sockets without destroying Trystero-owned shared peers.
- Self-hosted CouchDB provisioning and Setup URI generation now consume the immutable Commonlib registry package. New database provisioning records the package-owned database version, and generated CouchDB, Object Storage, and random-room P2P URIs use current new-Vault defaults and selected remote profiles instead of legacy embedded settings.
### Security
- Fly.io setup now generates CouchDB and Vault encryption secrets from cryptographically secure randomness instead of short word combinations.
- Updated the glob expansion used by Commonlib path matching and CLI ignore rules to prevent pathological brace patterns from consuming excessive CPU.
## 1.0.0-rc.0
19th July, 2026
### Improved
- Removed the ineffective 'Use the trash bin' toggle from the settings interface. Remote deletions continue to follow Obsidian's deletion preference, while the legacy setting key remains accepted for compatibility.
- Kept content-derived chunk revisions permanently enabled, as they have been since 0.25.6, and removed the obsolete stored key from recommendations, database-maintenance prerequisites, and review tooling.
- Aligned new-Vault, full-reset, and CLI-generated settings with the 1.0 recommendations. New Vaults use cross-platform case-insensitive file-name handling, while an existing Vault with no saved case policy remains paused until case-sensitive legacy behaviour is explicitly retained or a case-insensitive database rebuild is planned.
@@ -45,6 +33,11 @@ Earlier releases remain available in the [0.25 release history](docs/releases/0.
- Replaced remote-size decision prompts shown during startup with long-lived, clickable notices. The detailed choices now open only when requested and no longer select an answer automatically after a timeout.
- Removed the obsolete prompt and automatic bulk chunk pre-send from initial and rebuild uploads. These operations retain the standard two-pass replication used to converge follow-up writes and conflict resolution.
### Security
- Fly.io setup now generates CouchDB and Vault encryption secrets from cryptographically secure randomness instead of short word combinations.
- Updated the glob expansion used by Commonlib path matching and CLI ignore rules to prevent pathological brace patterns from consuming excessive CPU.
### Miscellaneous
- Replaced the embedded Commonlib source and generated fallback declarations with a locked compiled package, reducing duplicated release and repository-scanner inputs without changing synchronisation behaviour.
+82
View File
@@ -0,0 +1,82 @@
import { pathToFileURL } from "node:url";
/**
* Escape a value for use as inline Markdown code.
*
* Use a fence longer than any backtick run in the value so that branch names
* supplied by a caller cannot break the surrounding Markdown.
*
* @param {string} value
* @returns {string}
*/
function inlineCode(value) {
const backtickRuns = value.match(/`+/g) ?? [];
const fenceLength = Math.max(1, ...backtickRuns.map((run) => run.length + 1));
const fence = "`".repeat(fenceLength);
const padding = value.startsWith("`") || value.endsWith("`") ? " " : "";
return `${fence}${padding}${value}${padding}${fence}`;
}
/**
* Render the reader-facing checklist for a draft release pull request.
*
* The version decides whether publication is stable or pre-release. The base
* branch is included explicitly because integration previews can target a
* reviewed integration branch rather than `main`.
*
* @param {string} version
* @param {string} baseBranch
* @returns {string}
*/
export function renderReleasePrBody(version, baseBranch) {
const selectedVersion = version.trim();
const selectedBaseBranch = baseBranch.trim();
if (selectedVersion.length === 0) throw new Error("A release version is required.");
if (selectedBaseBranch.length === 0) throw new Error("A base branch is required.");
const versionCode = inlineCode(selectedVersion);
const baseBranchCode = inlineCode(selectedBaseBranch);
const isPrerelease = selectedVersion.includes("-");
const purpose = isPrerelease
? `an immutable pre-release for BRAT validation without replacing the latest stable release`
: `the next stable release`;
const finaliseInstruction = isPrerelease
? "Run the finalise release workflow with this PR's fixed head SHA and `prerelease=true`"
: "Run the finalise release workflow with this PR's fixed head SHA and `prerelease=false`";
const publicationInstruction = isPrerelease
? "Publish the GitHub Release as a pre-release without replacing the latest stable release, while keeping this pull request in draft"
: "Publish the GitHub Release as the latest stable release while keeping this pull request in draft";
return [
`This release pull request prepares Self-hosted LiveSync ${versionCode} from ${baseBranchCode} as ${purpose}.`,
"",
"> [!IMPORTANT]",
"> **Merge intentionally on hold**",
">",
`> Publishing the GitHub Release does not unblock this pull request. Keep this pull request in draft, and leave ${baseBranchCode} unchanged, until the exact published build has passed BRAT validation.`,
"",
"## Release checklist",
"",
"- [ ] Review and polish `updates.md`",
"- [ ] Confirm the release date",
"- [ ] Confirm `manifest.json`, `versions.json`, workspace package versions, and the locked Commonlib package version",
"- [ ] Confirm CI has passed",
`- [ ] ${finaliseInstruction}`,
"- [ ] Confirm the draft GitHub Release assets and the published CLI image, if selected",
`- [ ] ${publicationInstruction}`,
"- [ ] Validate the exact published release with BRAT",
`- [ ] Mark this pull request ready and merge it into ${baseBranchCode} with a merge commit`,
"",
].join("\n");
}
const invokedPath = process.argv[1];
if (invokedPath !== undefined && pathToFileURL(invokedPath).href === import.meta.url) {
const [, , version, baseBranch] = process.argv;
try {
process.stdout.write(renderReleasePrBody(version ?? "", baseBranch ?? ""));
} catch (error) {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
}
}
+105 -6
View File
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
import { ensureTags } from "./release-tags.mjs";
const releaseNotesScript = fileURLToPath(new URL("./release-notes.mjs", import.meta.url));
const releasePrBodyScript = fileURLToPath(new URL("./release-pr-body.mjs", import.meta.url));
const versionBumpScript =
process.env.VERSION_BUMP_SCRIPT || fileURLToPath(new URL("../version-bump.mjs", import.meta.url));
const workspaceUpdateScript = fileURLToPath(new URL("../update-workspaces.mjs", import.meta.url));
@@ -42,6 +43,14 @@ function runNode(script: string, args: string[], cwd: string, env: Record<string
});
}
function runNpm(args: string[], cwd: string) {
return spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", args, {
cwd,
encoding: "utf8",
env: process.env,
});
}
function createTagGit(expectedRevision: string, initialTags: Record<string, string> = {}) {
const tags = new Map(Object.entries(initialTags));
const git = (args: string[], allowMissing = false): string | undefined => {
@@ -133,22 +142,59 @@ describe("release notes", () => {
describe("release workflow", () => {
it("uses the locked Commonlib package instead of generated fallback declarations", () => {
const workflow = readFileSync(prepareReleaseWorkflow, "utf8");
const body = runNode(releasePrBodyScript, ["1.0.0-beta.0", "integration"], makeTemporaryDirectory());
expect(workflow).not.toContain("npm run build:lib:types");
expect(workflow).not.toMatch(/git add[^\n]*_types/);
expect(workflow).toMatch(/git add[^\n]*package-lock\.json/);
expect(workflow).toContain("locked Commonlib package version");
expect(body.status, body.stderr).toBe(0);
expect(body.stdout).toContain("locked Commonlib package version");
});
it("reruns the version lifecycle when the integration branch already selects the release version", () => {
const workflow = readFileSync(prepareReleaseWorkflow, "utf8");
expect(workflow).toContain('npm version "${VERSION}" --no-git-tag-version --allow-same-version');
});
it("generates the release PR body from the selected version and base branch", () => {
const workflow = readFileSync(prepareReleaseWorkflow, "utf8");
expect(workflow).toContain('node utils/release-pr-body.mjs "${VERSION}" "${BASE_BRANCH}"');
expect(workflow).not.toContain("leave \\`main\\`");
expect(workflow).not.toContain("latest stable release");
});
it("keeps the release PR in draft until BRAT validation", () => {
const workflow = readFileSync(prepareReleaseWorkflow, "utf8");
const prerelease = runNode(
releasePrBodyScript,
["1.0.0-beta.0", "common-library-package-boundary"],
makeTemporaryDirectory()
);
expect(workflow).toContain("Merge intentionally on hold");
expect(workflow).toContain(
expect(prerelease.status, prerelease.stderr).toBe(0);
expect(prerelease.stdout).toContain("Merge intentionally on hold");
expect(prerelease.stdout).toContain("Self-hosted LiveSync `1.0.0-beta.0`");
expect(prerelease.stdout).toContain("leave `common-library-package-boundary` unchanged");
expect(prerelease.stdout).toContain("prerelease=true");
expect(prerelease.stdout).toContain(
"Publish the GitHub Release as a pre-release without replacing the latest stable release"
);
expect(prerelease.stdout).toContain("Validate the exact published release with BRAT");
expect(prerelease.stdout).toContain(
"Mark this pull request ready and merge it into `common-library-package-boundary` with a merge commit"
);
});
it("keeps stable release instructions distinct from pre-release instructions", () => {
const stable = runNode(releasePrBodyScript, ["1.0.0", "main"], makeTemporaryDirectory());
expect(stable.status, stable.stderr).toBe(0);
expect(stable.stdout).toContain("prerelease=false");
expect(stable.stdout).toContain(
"Publish the GitHub Release as the latest stable release while keeping this pull request in draft"
);
expect(workflow).toContain("Validate the published release with BRAT");
expect(workflow).toContain("Mark this pull request ready and merge it with a merge commit");
expect(stable.stdout).not.toContain("as a pre-release without replacing");
});
it("dispatches the plug-in workflow and lets the CLI tag trigger its own workflow", () => {
@@ -252,6 +298,59 @@ describe("version bump", () => {
"0.25.81": "1.7.2",
});
});
it("runs release metadata scripts when the selected version is already the package version", () => {
const directory = makeTemporaryDirectory();
const workspaces = ["src/apps/cli", "src/apps/webpeer", "src/apps/webapp"];
writeJson(directory, "package.json", {
name: "release-lifecycle-fixture",
version: "1.0.0-beta.0",
private: true,
workspaces,
scripts: {
version: `node ${JSON.stringify(versionBumpScript)} && node ${JSON.stringify(workspaceUpdateScript)}`,
},
});
writeJson(directory, "manifest.json", { version: "1.0.0-alpha.9", minAppVersion: "1.7.2" });
writeJson(directory, "versions.json", { "0.25.83": "1.7.2" });
const lockPackages: Record<string, { version: string; workspaces?: string[] }> = {
"": { version: "1.0.0-beta.0", workspaces },
};
for (const workspace of ["cli", "webpeer", "webapp"]) {
writeJson(directory, `src/apps/${workspace}/package.json`, {
name: `release-lifecycle-${workspace}`,
version: `1.0.0-alpha.9-${workspace}`,
});
lockPackages[`src/apps/${workspace}`] = { version: `1.0.0-alpha.9-${workspace}` };
}
writeJson(directory, "package-lock.json", {
name: "release-lifecycle-fixture",
version: "1.0.0-beta.0",
lockfileVersion: 3,
requires: true,
packages: lockPackages,
});
const result = runNpm(["version", "1.0.0-beta.0", "--no-git-tag-version", "--allow-same-version"], directory);
expect(result.status, result.stderr).toBe(0);
expect(JSON.parse(readFileSync(join(directory, "manifest.json"), "utf8"))).toMatchObject({
version: "1.0.0-beta.0",
minAppVersion: "1.7.2",
});
expect(JSON.parse(readFileSync(join(directory, "versions.json"), "utf8"))).toEqual({
"0.25.83": "1.7.2",
"1.0.0-beta.0": "1.7.2",
});
const packageLock = JSON.parse(readFileSync(join(directory, "package-lock.json"), "utf8"));
expect(packageLock.version).toBe("1.0.0-beta.0");
expect(packageLock.packages[""].version).toBe("1.0.0-beta.0");
for (const workspace of ["cli", "webpeer", "webapp"]) {
const packageJson = JSON.parse(readFileSync(join(directory, `src/apps/${workspace}/package.json`), "utf8"));
expect(packageJson.version).toBe(`1.0.0-beta.0-${workspace}`);
expect(packageLock.packages[`src/apps/${workspace}`].version).toBe(`1.0.0-beta.0-${workspace}`);
}
});
});
describe("workspace version update", () => {
+1 -2
View File
@@ -5,6 +5,5 @@
"1.0.0": "0.9.7",
"0.25.81": "1.7.2",
"0.25.82": "1.7.2",
"0.25.83": "1.7.2",
"1.0.0-rc.0": "1.7.2"
"0.25.83": "1.7.2"
}