Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ jobs:
cargo metadata --format-version 1 --no-deps > "$RUNNER_TEMP/cargo-metadata.json"
node --input-type=module <<'NODE'
import fs from "node:fs";
import { toMsiVersion } from "./scripts/generate-windows-release-config.mjs";

const expected = process.env.RELEASE_TAG.slice(1);
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
Expand All @@ -69,6 +70,7 @@ jobs:
}
process.exit(1);
}
toMsiVersion(expected);
fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${expected}\n`);
NODE

Expand Down Expand Up @@ -173,10 +175,14 @@ jobs:
file target/release/bundle/appimage/*.AppImage
- name: Build Windows bundles
if: matrix.platform == 'windows'
shell: pwsh
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ""
run: make windows CONFIG=src-tauri/tauri.updater.conf.json
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
run: |
node scripts/generate-windows-release-config.mjs src-tauri/tauri.updater.conf.json "$env:RELEASE_VERSION" target/tauri.windows.release.conf.json
make windows CONFIG=target/tauri.windows.release.conf.json
- name: Smoke-test Windows application
if: matrix.platform == 'windows'
shell: pwsh
Expand Down
1 change: 1 addition & 0 deletions docs/releases/v0.1.0-alpha.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ This checklist is the evidence record for the first CleanerX alpha. Do not publi
- [ ] Both macOS architecture artifacts launch on a clean supported system; Finder **Open** or System Settings approval is sufficient without disabling Gatekeeper.
- [ ] The Linux AppImage and `.deb` launch on a clean supported environment; the AppImage updater path and `.deb` manual-update explanation are correct.
- [ ] The Windows MSI and NSIS installers launch on Windows 10/11; SmartScreen disclosure and the passive NSIS updater path are correct.
- [ ] The MSI metadata uses numeric surrogate version `0.0.65535.10001`, while the application, NSIS installer, updater manifest, and release filenames retain `0.1.0-alpha.1`.
- [ ] Read-only scan, bounded detail loading, review dialog, backup listing, settings persistence, and explicit read-only degradation are exercised on packaged builds.
- [ ] Disposable Agent homes complete the applicable cleanup, optional backup, restore, interruption, and post-operation verification cycles without changing protected fixtures or source trees.
- [ ] Keyboard navigation, visible focus, Chinese and English, light and dark system themes, and reduced-motion behavior receive an alpha acceptance pass.
Expand Down
2 changes: 2 additions & 0 deletions docs/update-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,6 @@ The fixed stable endpoint intentionally excludes GitHub prereleases. A future be

Normal developer bundle commands remain unsigned and do not require the updater private key. Tagged releases add `src-tauri/tauri.updater.conf.json`, set `TAURI_SIGNING_PRIVATE_KEY` from GitHub Actions secrets, and ask Tauri to create update artifacts and `.sig` files. The release-draft job gathers every architecture, runs `scripts/generate-update-manifest.mjs`, and fails closed on a missing/empty signature before staging `latest.json` and checksums in a draft GitHub Release. A maintainer verifies the complete draft before publishing it.

WiX/MSI does not accept alphanumeric Semantic Versioning prerelease identifiers. The Windows release job therefore uses `scripts/generate-windows-release-config.mjs` to derive a numeric MSI-only version that sorts below the eventual stable version (`0.1.0-alpha.1` becomes `0.0.65535.10001`). The application, tag, updater manifest, NSIS installer, and release asset names retain the public Semantic Versioning value. Unsupported prerelease shapes fail during tag validation before platform builds begin.

Tauri's signature establishes continuity with the public key embedded in an installed CleanerX build. It does not establish operating-system publisher identity. Artifact names and release warnings therefore continue to say that current binaries are unsigned and not notarized.
84 changes: 84 additions & 0 deletions scripts/generate-windows-release-config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";

const prereleaseOffsets = {
alpha: 10_000,
beta: 20_000,
rc: 30_000,
};

export function toMsiVersion(version) {
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(alpha|beta|rc)\.(0|[1-9]\d*))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
if (!match) {
throw new Error(`Unsupported release version for MSI: ${version}`);
}

let major = Number(match[1]);
let minor = Number(match[2]);
let patch = Number(match[3]);
const prereleaseKind = match[4];
const prereleaseNumber = match[5] === undefined ? undefined : Number(match[5]);

if (major > 255 || minor > 255 || patch > 65_535) {
throw new Error(`Release version exceeds MSI numeric limits: ${version}`);
}
if (!prereleaseKind) return `${major}.${minor}.${patch}`;
if (prereleaseNumber > 9_999) {
throw new Error(`Prerelease sequence exceeds the MSI allocation: ${version}`);
}

if (patch > 0) {
patch -= 1;
} else if (minor > 0) {
minor -= 1;
patch = 65_535;
} else if (major > 0) {
major -= 1;
minor = 255;
patch = 65_535;
} else {
throw new Error("An MSI prerelease cannot sort below version 0.0.0");
}

const build = prereleaseOffsets[prereleaseKind] + prereleaseNumber;
return `${major}.${minor}.${patch}.${build}`;
}

export function buildWindowsReleaseConfig(baseConfig, version) {
return {
...baseConfig,
bundle: {
...baseConfig.bundle,
windows: {
...baseConfig.bundle?.windows,
wix: {
...baseConfig.bundle?.windows?.wix,
version: toMsiVersion(version),
},
},
},
};
}

export function writeWindowsReleaseConfig({ baseConfigPath, version, destination }) {
const baseConfig = JSON.parse(fs.readFileSync(baseConfigPath, "utf8"));
const config = buildWindowsReleaseConfig(baseConfig, version);
fs.mkdirSync(path.dirname(destination), { recursive: true });
fs.writeFileSync(destination, `${JSON.stringify(config, null, 2)}\n`, {
encoding: "utf8",
flag: "wx",
});
return destination;
}

const invokedFile = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : "";
if (import.meta.url === invokedFile) {
const [baseConfigPath, version, destination] = process.argv.slice(2);
if (!baseConfigPath || !version || !destination) {
throw new Error(
"Usage: node scripts/generate-windows-release-config.mjs <base-config> <version> <destination>",
);
}
writeWindowsReleaseConfig({ baseConfigPath, version, destination });
}
33 changes: 33 additions & 0 deletions scripts/generate-windows-release-config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import {
buildWindowsReleaseConfig,
toMsiVersion,
} from "./generate-windows-release-config.mjs";

describe("Windows release configuration", () => {
it("maps ordered prereleases below their eventual stable MSI version", () => {
expect(toMsiVersion("0.1.0-alpha.1")).toBe("0.0.65535.10001");
expect(toMsiVersion("1.2.3-beta.4")).toBe("1.2.2.20004");
expect(toMsiVersion("1.0.0-rc.7")).toBe("0.255.65535.30007");
expect(toMsiVersion("1.2.3")).toBe("1.2.3");
});

it("preserves updater settings while adding the WiX override", () => {
expect(buildWindowsReleaseConfig({
bundle: { createUpdaterArtifacts: true },
}, "0.1.0-alpha.1")).toEqual({
bundle: {
createUpdaterArtifacts: true,
windows: {
wix: { version: "0.0.65535.10001" },
},
},
});
});

it("fails closed for unsupported or non-representable prereleases", () => {
expect(() => toMsiVersion("0.1.0-preview.1")).toThrow(/Unsupported release version/);
expect(() => toMsiVersion("0.0.0-alpha.1")).toThrow(/cannot sort below/);
expect(() => toMsiVersion("0.1.0-alpha.10000")).toThrow(/exceeds the MSI allocation/);
});
});