diff --git a/.github/workflows/build-and-make.yaml b/.github/workflows/build-and-make.yaml index 632978e3c..4b2e724db 100644 --- a/.github/workflows/build-and-make.yaml +++ b/.github/workflows/build-and-make.yaml @@ -493,7 +493,9 @@ jobs: # offer it, and it must be in package.json before the frontend # and backend builds and electron-builder read it. The value # comes from the nightly-version job so every job of this run - # builds the same version. + # builds the same version. The script also sets the electron- + # builder publish channel to "nightly", which names the updater + # metadata nightly-mac.yml / nightly.yml / nightly-linux.yml. if: needs.nightly-version.outputs.version != '' # Windows runners default to PowerShell, where "${NIGHTLY_VERSION}" # expands to nothing and the script rejects the empty version. diff --git a/CLAUDE.md b/CLAUDE.md index 7d1012edf..9ba37dbf2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1715,7 +1715,7 @@ The factory pattern ensures a single codebase works in both environments without CI injects the git commit into `apps/web/src/environments/build-commit.ts` via `tools/build/inject-build-commit.mjs` (same placeholder pattern as the TMDB key inject); `Settings > About` then shows `" ()"`. The semver version itself stays untouched on PR and tag builds — a `-sha` suffix would flip electron-updater into prerelease mode and leak into installer/artifact version fields. Local/dev builds keep the placeholder empty and show the plain version. **Nightly Builds And Update Channel**: -Master pushes are the nightly channel. The leading `nightly-version` job computes one `-nightly..` version per run (`tools/release/nightly-version.mjs`; the patch is bumped only when `v` already exists on origin, so the release-cut window stays below the imminent release), every build job writes it into `package.json` (`--apply --version`), and the `create-release` job publishes the artifacts as a prerelease of `4gray/iptvnator-nightly` (secret `NIGHTLY_RELEASE_TOKEN`; missing token only warns) instead of the rolling `test-master` draft, keeping the newest 20. electron-builder names the updater metadata after the prerelease tag (`nightly-mac.yml` / `nightly.yml` / `nightly-linux.yml`), so upload globs and the macOS merge accept both names. `Settings.updateChannel` (Settings → About, Electron only, default `stable`) is mirrored into the main-process config (`APP_UPDATE_CHANNEL`, `app-update-channel.ts`) for the startup check; `AppUpdateService` applies the channel to electron-updater before every check (`app-update-feed.ts`: feed repository, `allowPrerelease`, channel name, and `allowDowngrade` forced back to `false` — assigning a channel silently enables downgrades) and reads release notes from the repository the requested version belongs to (`app-update-release-catalog.ts`). Switching back to stable is forward-only: the nightly stays until a newer stable release exists, because a downgrade could hit a database schema a nightly migration already applied. Contract: `docs/architecture/release-pipeline.md` ("Nightly channel"). +Master pushes are the nightly channel. The leading `nightly-version` job computes one `-nightly..` version per run (`tools/release/nightly-version.mjs`; the patch is bumped only when `v` already exists on origin, so the release-cut window stays below the imminent release), every build job writes it into `package.json` and sets `publish[0].channel: nightly` in `electron-builder.json` (`--apply --version`; electron-builder does not derive the channel from the prerelease tag for the GitHub provider), and the `create-release` job publishes the artifacts as a prerelease of `4gray/iptvnator-nightly` (secret `NIGHTLY_RELEASE_TOKEN`; missing token only warns) instead of the rolling `test-master` draft, keeping the newest 20. the explicit publish channel names the updater metadata `nightly-mac.yml` / `nightly.yml` / `nightly-linux.yml`, and upload globs plus the macOS merge accept both names. `Settings.updateChannel` (Settings → About, Electron only, default `stable`) is mirrored into the main-process config (`APP_UPDATE_CHANNEL`, `app-update-channel.ts`) for the startup check; `AppUpdateService` applies the channel to electron-updater before every check (`app-update-feed.ts`: feed repository, `allowPrerelease`, channel name, and `allowDowngrade` forced back to `false` — assigning a channel silently enables downgrades) and reads release notes from the repository the requested version belongs to (`app-update-release-catalog.ts`). Switching back to stable is forward-only: the nightly stays until a newer stable release exists, because a downgrade could hit a database schema a nightly migration already applied. Contract: `docs/architecture/release-pipeline.md` ("Nightly channel"). ### Testing Strategy diff --git a/docs/architecture/release-pipeline.md b/docs/architecture/release-pipeline.md index f422d7427..5d8764db0 100644 --- a/docs/architecture/release-pipeline.md +++ b/docs/architecture/release-pipeline.md @@ -299,9 +299,13 @@ frontend and backend builds and before electron-builder reads it is offered it; smaller than `0.23.1` and `0.24.0`, so the next stable release is offered to nightly users on either channel. - The run number only grows, so nightlies order correctly within a day. -- electron-builder derives the updater channel files from the prerelease - tag: `nightly-mac.yml`, `nightly.yml`, `nightly-linux.yml`. The artifact - upload globs and the macOS metadata merge accept both names. +- The same `--apply` sets `publish[0].channel: nightly` in + `electron-builder.json`, which names the updater metadata + `nightly-mac.yml`, `nightly.yml`, `nightly-linux.yml`. electron-builder does + not derive that name from the prerelease tag for the GitHub provider (the + first nightly run produced `latest-*.yml` and the publish step refused + it). The artifact upload globs and the macOS metadata merge accept both + names. - The root `package.json` is an Nx `sharedGlobals` input, so the rewritten version reaches the `web` and `electron-backend` bundles (which embed it) instead of a cache hit built from the released version. diff --git a/libs/shared/interfaces/src/lib/app-update-channel.util.ts b/libs/shared/interfaces/src/lib/app-update-channel.util.ts index fad76a03c..6c2ca2d83 100644 --- a/libs/shared/interfaces/src/lib/app-update-channel.util.ts +++ b/libs/shared/interfaces/src/lib/app-update-channel.util.ts @@ -24,9 +24,10 @@ export const DEFAULT_APP_UPDATE_CHANNEL: AppUpdateChannel = 'stable'; /** * Prerelease identifier every nightly version carries - * (`0.23.1-nightly.20260915.1234`). electron-builder derives the updater - * channel file (`nightly-mac.yml`, `nightly.yml`, `nightly-linux.yml`) from - * it, and electron-updater matches release tags on the same identifier. + * (`0.23.1-nightly.20260915.1234`). The nightly build sets the same name as + * electron-builder's publish channel, which names the updater metadata + * (`nightly-mac.yml`, `nightly.yml`, `nightly-linux.yml`), and + * electron-updater matches release tags on the same identifier. */ export const NIGHTLY_PRERELEASE_TAG = 'nightly'; diff --git a/tools/release/nightly-version.mjs b/tools/release/nightly-version.mjs index 86c457200..3931c8c73 100644 --- a/tools/release/nightly-version.mjs +++ b/tools/release/nightly-version.mjs @@ -16,8 +16,12 @@ * - The date is the commit's UTC date and the last identifier is the * workflow run number, which only ever grows, so nightlies order * correctly across days and within one day. - * - electron-builder derives the updater channel file names from the - * prerelease tag (`nightly-mac.yml`, `nightly.yml`, `nightly-linux.yml`). + * - `--apply` also sets `publish[0].channel` to `nightly` in + * `electron-builder.json`, which is what names the updater metadata + * `nightly-mac.yml` / `nightly.yml` / `nightly-linux.yml`. electron-builder + * does NOT derive that name from the prerelease tag for the GitHub + * provider: the first nightly run shipped `latest-*.yml` files and the + * publish step refused them. * * A release cut commits the new version to master before (or together * with) its tag. While `v` does not exist yet, that @@ -36,6 +40,7 @@ * Usage: * node tools/release/nightly-version.mjs # print * node tools/release/nightly-version.mjs --apply --version X # write X + * into package.json + electron-builder.json * * Inputs default to the repository `package.json`, the HEAD commit date, * `GITHUB_RUN_NUMBER`, and a `git ls-remote` probe for the base tag; @@ -108,6 +113,26 @@ export function parseBooleanFlag(value, flag) { throw new Error(`${flag} must be "true" or "false", got "${value}".`); } +/** + * Points electron-builder's updater metadata at the nightly channel. Written + * as 4-space JSON like `configure-linux-frame-copy-build.mjs`, which has + * already rewritten the file the same way on Linux by the time this runs. + */ +export function applyNightlyPublishChannel(electronBuilderJsonText) { + const config = JSON.parse(electronBuilderJsonText); + const publish = Array.isArray(config.publish) ? config.publish : null; + + if (!publish || publish.length === 0 || publish[0]?.provider !== 'github') { + throw new Error( + 'electron-builder.json must declare a github publish provider first.' + ); + } + + publish[0] = { ...publish[0], channel: NIGHTLY_TAG }; + + return `${JSON.stringify(config, null, 4)}\n`; +} + /** Replaces the `version` line in `package.json` text, formatting intact. */ export function applyNightlyVersion(packageJsonText, version) { if (!PACKAGE_VERSION_LINE.test(packageJsonText)) { @@ -239,11 +264,32 @@ function main(argv) { } if (options.apply) { - writeFileSync( - packageJsonPath, - applyNightlyVersion(packageJsonText, version) + // Resolve both replacements before touching either file, so a bad + // electron-builder.json cannot leave package.json half-applied. + const electronBuilderJsonPath = new URL( + '../../electron-builder.json', + import.meta.url + ); + let nextPackageJson; + let nextElectronBuilderJson; + + try { + nextPackageJson = applyNightlyVersion(packageJsonText, version); + nextElectronBuilderJson = applyNightlyPublishChannel( + readFileSync(electronBuilderJsonPath, 'utf8') + ); + } catch (error) { + console.error( + error instanceof Error ? error.message : String(error) + ); + return 1; + } + + writeFileSync(packageJsonPath, nextPackageJson); + writeFileSync(electronBuilderJsonPath, nextElectronBuilderJson); + console.error( + `Applied nightly version ${version} to package.json and the nightly publish channel to electron-builder.json.` ); - console.error(`Applied nightly version ${version} to package.json.`); } console.log(version); diff --git a/tools/release/nightly-version.test.mjs b/tools/release/nightly-version.test.mjs index e9abf02c5..a3d9356ce 100644 --- a/tools/release/nightly-version.test.mjs +++ b/tools/release/nightly-version.test.mjs @@ -1,6 +1,8 @@ import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; import { describe, it } from 'node:test'; import { + applyNightlyPublishChannel, applyNightlyVersion, buildNightlyVersion, isNightlyVersion, @@ -164,6 +166,60 @@ describe('applyNightlyVersion', () => { }); }); +describe('applyNightlyPublishChannel', () => { + it('sets the github publish channel and keeps the rest of the config', () => { + const source = JSON.stringify( + { + appId: 'x', + publish: [{ provider: 'github', owner: '4gray', repo: 'iptvnator' }], + mac: { target: ['dmg'] }, + }, + null, + 4 + ); + + const result = applyNightlyPublishChannel(source); + + assert.deepEqual(JSON.parse(result), { + appId: 'x', + publish: [ + { + provider: 'github', + owner: '4gray', + repo: 'iptvnator', + channel: 'nightly', + }, + ], + mac: { target: ['dmg'] }, + }); + assert.ok(result.endsWith('}\n')); + assert.match(result, /^\{\n {4}"appId"/); + }); + + it('applies to the repository config as committed', () => { + const committed = readFileSync( + new URL('../../electron-builder.json', import.meta.url), + 'utf8' + ); + + assert.equal( + JSON.parse(applyNightlyPublishChannel(committed)).publish[0].channel, + 'nightly' + ); + }); + + it('refuses a config without a github publish provider', () => { + assert.throws( + () => applyNightlyPublishChannel('{"publish":[{"provider":"generic"}]}'), + /github publish provider/ + ); + assert.throws( + () => applyNightlyPublishChannel('{}'), + /github publish provider/ + ); + }); +}); + describe('explicit version input', () => { it('accepts only the shape the script itself produces', () => { assert.equal(isNightlyVersion('0.23.1-nightly.20260915.1234'), true);