ci: build nightly versions twice a day (#2136)

Adds a nightly channel. Twice a day nightly.yml builds the newest main commit that passed CI and publishes it as a prerelease (OpenIDEA Nightly, ai.l0ng.openidea.nightly, own data and update manifest), or stops when nothing is newer than the last nightly. Platform builds move from release.yml into the reusable release-build.yml so both channels build the same way.

Versions are one patch above main with a timestamp suffix; each build gets its own v…-nightly.… tag (remote development downloads the server by version) and the last 14 are kept; a rolling nightly release holds latest.json. No MSI or RPM for nightlies. Nightly builds skip the CNB mirror manifest, and their automatic update prompts (startup and periodic) appear at most once a day; a manual check always answers.

Workflow permissions default to contents: read with write granted per job, Apple signing secrets reach only macOS builds, stable release.yml rejects nightly tags, tag pruning only matches -nightly. tags, and nightly runs share one concurrency group.
This commit is contained in:
l0ng-ai
2026-11-10 12:30:17 +08:00
committed by GitHub
parent f50d3099ce
commit bfe99c06e2
12 changed files with 596 additions and 240 deletions
+34
View File
@@ -0,0 +1,34 @@
// @vitest-environment node
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { NIGHTLY_ENDPOINT, nightlyVersion, setNightlyIdentity } from "./nightly-prepare.ts";
describe("nightlyVersion", () => {
it("is one patch above the current version, numbered by its stamp", () => {
expect(nightlyVersion("0.1.39", "202611081417")).toBe("0.1.40-nightly.202611081417");
});
it.each([undefined, "", "2026", "012611081417", "20261108141a"])("rejects the stamp %s", (stamp) => {
expect(() => nightlyVersion("0.1.39", stamp)).toThrow("stamp");
});
it("rejects a current version that is not a release version", () => {
expect(() => nightlyVersion("0.1.40-nightly.202611081417", "202611081417")).toThrow("expected a version");
});
});
describe("setNightlyIdentity", () => {
const conf = JSON.parse(setNightlyIdentity(readFileSync("src-tauri/tauri.conf.json", "utf8")));
it("is an app of its own that updates from the nightly manifest", () => {
expect(conf.productName).toBe("OpenIDEA Nightly");
expect(conf.identifier).toBe("ai.l0ng.openidea.nightly");
expect(conf.plugins.updater.endpoints).toEqual([NIGHTLY_ENDPOINT]);
});
it("leaves out the bundles that reject prerelease versions", () => {
expect(conf.bundle.targets).not.toContain("msi");
expect(conf.bundle.targets).not.toContain("rpm");
expect(conf.bundle.createUpdaterArtifacts).toBe(true);
});
});
+54
View File
@@ -0,0 +1,54 @@
/**
* Prepares a nightly build's commit: `node scripts/nightly-prepare.ts 202611081417` sets the
* version one patch above the current one, with that stamp as its prerelease number, in the
* files `release:prepare` covers, and gives tauri.conf.json the nightly app's identity, so it
* installs next to the stable app and updates from its own manifest. Prints the version.
*/
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { cargoVersion, edit, parseVersion, setCargoVersion, setJsonVersion } from "./release-prepare.ts";
export const NIGHTLY_ENDPOINT = "https://github.com/l0ng-ai/openidea/releases/download/nightly/latest.json";
/** `0.1.40-nightly.202611081417` from `0.1.39`: always above the last stable version and the last nightly. */
export function nightlyVersion(current: string, stamp: string | undefined): string {
if (!stamp || !/^[1-9]\d{11}$/.test(stamp)) throw new Error(`expected a YYYYMMDDHHMM stamp, got ${stamp}`);
const [major, minor, patch] = parseVersion(current).split(".").map(Number);
return `${major}.${minor}.${patch! + 1}-nightly.${stamp}`;
}
/** MSI and RPM reject prerelease versions, so a nightly build leaves them out of its bundles. */
export function setNightlyIdentity(json: string): string {
const conf = JSON.parse(json);
conf.productName = "OpenIDEA Nightly";
conf.identifier = "ai.l0ng.openidea.nightly";
conf.bundle.targets = ["app", "dmg", "appimage", "deb", "nsis"];
conf.plugins.updater.endpoints = [NIGHTLY_ENDPOINT];
return `${JSON.stringify(conf, null, 2)}\n`;
}
function main() {
const root = fileURLToPath(new URL("..", import.meta.url));
const version = nightlyVersion(cargoVersion(readFileSync(join(root, "Cargo.toml"), "utf8")), process.argv[2]);
edit(join(root, "Cargo.toml"), (text) => setCargoVersion(text, version));
edit(join(root, "package.json"), (text) => setJsonVersion(text, version));
edit(join(root, "src-tauri/tauri.conf.json"), (text) => setNightlyIdentity(setJsonVersion(text, version)));
const cargo = spawnSync("cargo", ["update", "--workspace"], {
cwd: root,
stdio: ["ignore", 2, 2],
});
if (cargo.status !== 0) throw new Error("cargo update --workspace failed");
console.log(version);
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
try {
main();
} catch (error) {
console.error(`nightly-prepare: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
+1 -1
View File
@@ -75,7 +75,7 @@ export function syncApiSnapshot(root: string): { from: string; to: string; copie
return { from, to, copied: true };
}
function edit(path: string, change: (text: string) => string) {
export function edit(path: string, change: (text: string) => string) {
const before = readFileSync(path, "utf8");
const after = change(before);
if (after === before) throw new Error(`${path}: nothing changed`);