mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(updater): compute the nightly version once and keep re-runs safe
Review follow-ups: the nightly version is resolved by a leading job and handed to every build job, and the patch is bumped only when the base tag already exists so the release-cut window stays below the imminent release. A re-run never deletes a published nightly; only a draft left by a failed run is replaced. Typed update-status literals in the remaining specs carry the new channel fields. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
c9272f5635
commit
c9a5a9d73e
8 files changed
+260
-47
No files matched your search
@@ -14,13 +14,13 @@ name: Build and Make Electron App
|
||||
# window is visible and bounded; refreshing drafts on skipped runs is not
|
||||
# worth a separate workflow.
|
||||
#
|
||||
# Master pushes are the nightly channel: every build job rewrites the
|
||||
# package.json version to <next patch>-nightly.<date>.<run number>
|
||||
# (tools/release/nightly-version.mjs) so electron-updater treats the build
|
||||
# as newer than the released version, and the release job publishes the
|
||||
# artifacts as a prerelease of 4gray/iptvnator-nightly instead of the
|
||||
# rolling test-master draft. Contract: docs/architecture/release-pipeline.md
|
||||
# ("Nightly channel").
|
||||
# Master pushes are the nightly channel: the nightly-version job computes
|
||||
# one <patch>-nightly.<date>.<run number> version for the whole run
|
||||
# (tools/release/nightly-version.mjs), every build job writes it into
|
||||
# package.json so electron-updater treats the build as newer than the
|
||||
# released version, and the release job publishes the artifacts as a
|
||||
# prerelease of 4gray/iptvnator-nightly instead of the rolling test-master
|
||||
# draft. Contract: docs/architecture/release-pipeline.md ("Nightly channel").
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -50,6 +50,39 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# One version for the whole run. Computed here rather than in each build
|
||||
# job because the rule depends on whether the base tag exists on origin:
|
||||
# a tag pushed while the matrix runs would otherwise give one run two
|
||||
# different versions. Empty output means "not a nightly build".
|
||||
nightly-version:
|
||||
name: Resolve nightly version
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
version: ${{ steps.resolve.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == '4gray/iptvnator'
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Resolve nightly version
|
||||
id: resolve
|
||||
shell: bash
|
||||
env:
|
||||
NIGHTLY: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == '4gray/iptvnator' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${NIGHTLY}" != "true" ]; then
|
||||
echo "version=" >> "${GITHUB_OUTPUT}"
|
||||
echo "Not a master push; no nightly version."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
VERSION="$(node tools/release/nightly-version.mjs)"
|
||||
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
echo "Nightly version: ${VERSION}"
|
||||
|
||||
linux-embedded-mpv-runtime:
|
||||
name: Build pinned Linux Embedded MPV runtime
|
||||
runs-on: ubuntu-22.04
|
||||
@@ -348,6 +381,7 @@ jobs:
|
||||
|
||||
build-cross-platform:
|
||||
name: Build on ${{ matrix.os }} ${{ matrix.arch }}
|
||||
needs: nightly-version
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 120
|
||||
concurrency:
|
||||
@@ -457,11 +491,13 @@ jobs:
|
||||
# Master merges feed the nightly update channel. The version
|
||||
# must be greater than the released one for electron-updater to
|
||||
# offer it, and it must be in package.json before the frontend
|
||||
# and backend builds and electron-builder read it. Every job of
|
||||
# this run derives the same value from the commit date and the
|
||||
# run number, so nothing has to be handed between jobs.
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == '4gray/iptvnator'
|
||||
run: node tools/release/nightly-version.mjs --apply
|
||||
# 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.
|
||||
if: needs.nightly-version.outputs.version != ''
|
||||
env:
|
||||
NIGHTLY_VERSION: ${{ needs.nightly-version.outputs.version }}
|
||||
run: node tools/release/nightly-version.mjs --apply --version "${NIGHTLY_VERSION}"
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm nx build web --skip-nx-cache
|
||||
@@ -1260,7 +1296,9 @@ jobs:
|
||||
|
||||
build-linux:
|
||||
name: Build on ${{ matrix.os }} ${{ matrix.arch }} (${{ matrix.linux_profile }})
|
||||
needs: linux-embedded-mpv-runtime
|
||||
needs:
|
||||
- linux-embedded-mpv-runtime
|
||||
- nightly-version
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 120
|
||||
concurrency:
|
||||
@@ -1297,6 +1335,7 @@ jobs:
|
||||
create-release:
|
||||
name: Create Draft Release
|
||||
needs:
|
||||
- nightly-version
|
||||
- build-cross-platform
|
||||
- build-linux
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
@@ -1665,10 +1704,14 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
NIGHTLY_RELEASE_TOKEN: ${{ secrets.NIGHTLY_RELEASE_TOKEN }}
|
||||
VERSION: ${{ needs.nightly-version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="$(node tools/release/nightly-version.mjs)"
|
||||
if [ -z "${VERSION}" ]; then
|
||||
echo "::error::The nightly-version job produced no version for this master push."
|
||||
exit 1
|
||||
fi
|
||||
{
|
||||
echo "version=${VERSION}"
|
||||
echo "tag=v${VERSION}"
|
||||
@@ -1738,10 +1781,12 @@ jobs:
|
||||
|
||||
# Created as a draft, assets uploaded, then published in one edit,
|
||||
# so electron-updater never sees a release whose channel file is
|
||||
# still missing. The release job is serialized per ref but two
|
||||
# master runs can still finish out of order, so a nightly that is
|
||||
# older than the newest published one is dropped instead of
|
||||
# becoming the feed's newest entry.
|
||||
# still missing. A published release is never deleted here: a
|
||||
# rerun after a successful publish is a no-op, and only a draft
|
||||
# left behind by a failed run is replaced. The release job is
|
||||
# serialized per ref but two master runs can still finish out of
|
||||
# order, so a nightly that is older than the newest published one
|
||||
# is dropped instead of becoming the feed's newest entry.
|
||||
- name: Publish nightly release
|
||||
if: steps.nightly-meta.outputs.publish == 'true'
|
||||
shell: bash
|
||||
@@ -1799,9 +1844,16 @@ jobs:
|
||||
artifacts/windows-artifacts/*.blockmap
|
||||
)
|
||||
|
||||
if gh release view "${TAG}" --repo "${NIGHTLY_REPOSITORY}" > /dev/null 2>&1; then
|
||||
echo "Release ${TAG} already exists (re-run); replacing it."
|
||||
gh release delete "${TAG}" --repo "${NIGHTLY_REPOSITORY}" --cleanup-tag --yes
|
||||
EXISTING="$(gh api "repos/${NIGHTLY_REPOSITORY}/releases?per_page=100" --paginate |
|
||||
jq -c --arg tag "${TAG}" 'map(select(.tag_name == $tag)) | first // empty')"
|
||||
if [ -n "${EXISTING}" ]; then
|
||||
if [ "$(jq -r '.draft' <<< "${EXISTING}")" != "true" ]; then
|
||||
echo "::notice::${TAG} is already published in ${NIGHTLY_REPOSITORY}; nothing to do for this re-run."
|
||||
exit 0
|
||||
fi
|
||||
DRAFT_ID="$(jq -r '.id' <<< "${EXISTING}")"
|
||||
echo "Removing the draft ${TAG} (id ${DRAFT_ID}) a failed run left behind."
|
||||
gh api -X DELETE "repos/${NIGHTLY_REPOSITORY}/releases/${DRAFT_ID}"
|
||||
fi
|
||||
|
||||
gh release create "${TAG}" "${assets[@]}" \
|
||||
|
||||
@@ -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 `"<version> (<short-sha>)"`. 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. Each build job rewrites the `package.json` version to `<next patch>-nightly.<commit date>.<run number>` (`tools/release/nightly-version.mjs --apply`, deterministic per run), 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 `<patch>-nightly.<commit date>.<run number>` version per run (`tools/release/nightly-version.mjs`; the patch is bumped only when `v<package.json version>` 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").
|
||||
|
||||
### Testing Strategy
|
||||
|
||||
|
||||
@@ -14,8 +14,9 @@ jest.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: jest.fn(
|
||||
(channel: string, handler: (...args: unknown[]) => unknown) => {
|
||||
mockHandlers.set(channel, handler);
|
||||
}),
|
||||
mockHandlers.set(channel, handler);
|
||||
}
|
||||
),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -32,6 +33,8 @@ describe('AppUpdateEvents', () => {
|
||||
'https://github.com/4gray/iptvnator/releases/latest',
|
||||
status: ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Idle,
|
||||
supportedSelfUpdate: true,
|
||||
channel: 'stable',
|
||||
installedChannel: 'stable',
|
||||
};
|
||||
const service = {
|
||||
checkForUpdates: jest.fn().mockResolvedValue({
|
||||
@@ -59,7 +62,8 @@ describe('AppUpdateEvents', () => {
|
||||
status: ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Downloaded,
|
||||
})),
|
||||
};
|
||||
const { default: AppUpdateEvents } = await import('./app-update.events');
|
||||
const { default: AppUpdateEvents } =
|
||||
await import('./app-update.events');
|
||||
|
||||
AppUpdateEvents.bootstrapAppUpdateEvents(service);
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ const availableStatus: ElectronBridgeAppUpdateStatus = {
|
||||
manualDownloadUrl: 'https://github.com/4gray/iptvnator/releases/latest',
|
||||
status: ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Available,
|
||||
supportedSelfUpdate: true,
|
||||
channel: 'stable',
|
||||
installedChannel: 'stable',
|
||||
};
|
||||
|
||||
describe('AppUpdateNotificationPanelComponent', () => {
|
||||
|
||||
@@ -12,6 +12,9 @@ const BASE_STATUS: ElectronBridgeAppUpdateStatus = {
|
||||
currentVersion: '0.23.0',
|
||||
status: ELECTRON_BRIDGE_APP_UPDATE_STATUSES.Idle,
|
||||
supportedSelfUpdate: true,
|
||||
manualDownloadUrl: 'https://github.com/4gray/iptvnator/releases/latest',
|
||||
channel: 'stable',
|
||||
installedChannel: 'stable',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -302,15 +302,20 @@ frontend and backend builds and before electron-builder reads it
|
||||
- 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.
|
||||
- Every job derives the same value from the same inputs (commit date + run
|
||||
number), so nothing is handed between jobs. The release job recomputes it
|
||||
to name the tag `v<version>`.
|
||||
- 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.
|
||||
- The base is the released version in `package.json`. In the short window
|
||||
between a version bump commit and its tag, a nightly is numbered above the
|
||||
upcoming release; the next nightly after the tag corrects that.
|
||||
- The base is the version in `package.json`. The patch is bumped only when
|
||||
`v<base>` already exists on origin. A release cut commits the bump before
|
||||
(or together with) its tag, and while that tag is missing the base is the
|
||||
UPCOMING release, so the nightly keeps its patch
|
||||
(`0.23.1` untagged → `0.23.1-nightly.<date>.<run>`): still above every
|
||||
earlier nightly, still below the imminent `0.23.1`, so nightly users are
|
||||
offered that release instead of skipping it.
|
||||
- The version is computed once, in the leading `nightly-version` job, and
|
||||
handed to every build job as `--version` — a tag pushed while the matrix
|
||||
runs cannot give one run two different versions. The release job reads
|
||||
the same output to name the tag `v<version>`.
|
||||
|
||||
**Publication** (steps at the end of the `create-release` job):
|
||||
|
||||
@@ -327,7 +332,10 @@ frontend and backend builds and before electron-builder reads it
|
||||
3. The release is created as a draft, assets are uploaded, then it is
|
||||
published in one edit, so electron-updater never sees a release whose
|
||||
channel file is still missing. Missing `nightly-mac.yml`,
|
||||
`nightly.yml` or `nightly-linux.yml` fails the step instead.
|
||||
`nightly.yml` or `nightly-linux.yml` fails the step instead. A published
|
||||
release is never deleted by a re-run: re-running after a successful
|
||||
publish is a no-op, and only a draft left behind by a failed run is
|
||||
replaced.
|
||||
4. The release job is serialized per ref, but two master runs can finish out
|
||||
of order. electron-updater takes the newest feed entry, so a nightly older
|
||||
than the newest published one is dropped rather than published.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* released version in `package.json` is what stable users run, so a nightly
|
||||
* bumps the patch and adds a prerelease tag:
|
||||
*
|
||||
* 0.23.0 → 0.23.1-nightly.20260915.1234
|
||||
* 0.23.0 (tag v0.23.0 exists) → 0.23.1-nightly.20260915.1234
|
||||
*
|
||||
* - `0.23.1-nightly.*` is greater than the stable `0.23.0`, so a stable user
|
||||
* who switches to nightly is offered it.
|
||||
@@ -19,15 +19,27 @@
|
||||
* - electron-builder derives the updater channel file names from the
|
||||
* prerelease tag (`nightly-mac.yml`, `nightly.yml`, `nightly-linux.yml`).
|
||||
*
|
||||
* Every build job of one workflow run computes the same value from the same
|
||||
* inputs, so no job needs to hand the version to another.
|
||||
* A release cut commits the new version to master before (or together
|
||||
* with) its tag. While `v<package.json version>` does not exist yet, that
|
||||
* version is the UPCOMING release, so the patch is not bumped:
|
||||
*
|
||||
* 0.23.1 (no tag v0.23.1 yet) → 0.23.1-nightly.20260915.1240
|
||||
*
|
||||
* That still orders above every earlier `0.23.1-nightly.*` (the run number
|
||||
* grew) and below the imminent `0.23.1`, so nightly users are offered the
|
||||
* release instead of skipping it.
|
||||
*
|
||||
* The workflow computes the version once, in a leading job, and hands it to
|
||||
* every build job with `--version`, so a tag pushed while the matrix runs
|
||||
* cannot give one run two different versions.
|
||||
*
|
||||
* Usage:
|
||||
* node tools/release/nightly-version.mjs # print the version
|
||||
* node tools/release/nightly-version.mjs --apply # also write package.json
|
||||
* node tools/release/nightly-version.mjs # print
|
||||
* node tools/release/nightly-version.mjs --apply --version X # write X
|
||||
*
|
||||
* Inputs default to the repository `package.json`, the HEAD commit date, and
|
||||
* `GITHUB_RUN_NUMBER`; `--base`, `--date` and `--run-number` override them.
|
||||
* Inputs default to the repository `package.json`, the HEAD commit date,
|
||||
* `GITHUB_RUN_NUMBER`, and a `git ls-remote` probe for the base tag;
|
||||
* `--base`, `--date`, `--run-number` and `--base-released` override them.
|
||||
*/
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
@@ -38,13 +50,21 @@ export const NIGHTLY_TAG = 'nightly';
|
||||
const STABLE_VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)$/;
|
||||
const DATE_PATTERN = /^\d{8}$/;
|
||||
const PACKAGE_VERSION_LINE = /^(\s*"version":\s*")([^"]+)(")/m;
|
||||
const NIGHTLY_VERSION_PATTERN = /^\d+\.\d+\.\d+-nightly\.\d{8}\.\d+$/;
|
||||
const VALUE_FLAGS = {
|
||||
'--base': 'base',
|
||||
'--base-released': 'baseReleased',
|
||||
'--date': 'date',
|
||||
'--run-number': 'runNumber',
|
||||
'--version': 'version',
|
||||
};
|
||||
|
||||
export function buildNightlyVersion({ baseVersion, date, runNumber }) {
|
||||
export function buildNightlyVersion({
|
||||
baseVersion,
|
||||
date,
|
||||
runNumber,
|
||||
baseReleased = true,
|
||||
}) {
|
||||
const match = STABLE_VERSION_PATTERN.exec(String(baseVersion ?? '').trim());
|
||||
|
||||
if (!match) {
|
||||
@@ -66,8 +86,26 @@ export function buildNightlyVersion({ baseVersion, date, runNumber }) {
|
||||
}
|
||||
|
||||
const [, major, minor, patch] = match;
|
||||
const nightlyPatch = baseReleased ? Number(patch) + 1 : Number(patch);
|
||||
|
||||
return `${major}.${minor}.${Number(patch) + 1}-${NIGHTLY_TAG}.${date}.${run}`;
|
||||
return `${major}.${minor}.${nightlyPatch}-${NIGHTLY_TAG}.${date}.${run}`;
|
||||
}
|
||||
|
||||
/** Accepts only the shape this script produces, so `--version` cannot smuggle junk in. */
|
||||
export function isNightlyVersion(value) {
|
||||
return NIGHTLY_VERSION_PATTERN.test(String(value ?? ''));
|
||||
}
|
||||
|
||||
export function parseBooleanFlag(value, flag) {
|
||||
if (value === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value === 'false') {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(`${flag} must be "true" or "false", got "${value}".`);
|
||||
}
|
||||
|
||||
/** Replaces the `version` line in `package.json` text, formatting intact. */
|
||||
@@ -127,23 +165,78 @@ function readCommitDate() {
|
||||
return commitDate.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
}
|
||||
|
||||
/** Whether `v<base>` already exists on the origin remote. */
|
||||
function isBaseVersionReleased(baseVersion) {
|
||||
try {
|
||||
execFileSync(
|
||||
'git',
|
||||
[
|
||||
'ls-remote',
|
||||
'--exit-code',
|
||||
'--tags',
|
||||
'origin',
|
||||
`refs/tags/v${baseVersion}`,
|
||||
],
|
||||
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] }
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Exit status 2 is git's "no matching refs"; anything else (no
|
||||
// remote, network) must not silently pick a version.
|
||||
if (error && error.status === 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Cannot probe origin for tag v${baseVersion}: ${error?.message ?? error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersion(options, packageJsonText) {
|
||||
if (options.version !== undefined) {
|
||||
if (!isNightlyVersion(options.version)) {
|
||||
throw new Error(
|
||||
`--version must look like X.Y.Z-nightly.YYYYMMDD.N, got "${options.version}".`
|
||||
);
|
||||
}
|
||||
|
||||
return options.version;
|
||||
}
|
||||
|
||||
const baseVersion = options.base ?? JSON.parse(packageJsonText).version;
|
||||
|
||||
return buildNightlyVersion({
|
||||
baseVersion,
|
||||
date: options.date ?? readCommitDate(),
|
||||
runNumber: options.runNumber ?? process.env.GITHUB_RUN_NUMBER,
|
||||
baseReleased:
|
||||
options.baseReleased === undefined
|
||||
? isBaseVersionReleased(baseVersion)
|
||||
: parseBooleanFlag(options.baseReleased, '--base-released'),
|
||||
});
|
||||
}
|
||||
|
||||
function main(argv) {
|
||||
const options = parseArguments(argv);
|
||||
|
||||
if (!options) {
|
||||
console.error(
|
||||
'Usage: node tools/release/nightly-version.mjs [--apply] [--base X.Y.Z] [--date YYYYMMDD] [--run-number N]'
|
||||
'Usage: node tools/release/nightly-version.mjs [--apply] [--version X.Y.Z-nightly.YYYYMMDD.N | --base X.Y.Z --base-released true|false --date YYYYMMDD --run-number N]'
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const packageJsonPath = new URL('../../package.json', import.meta.url);
|
||||
const packageJsonText = readFileSync(packageJsonPath, 'utf8');
|
||||
const version = buildNightlyVersion({
|
||||
baseVersion: options.base ?? JSON.parse(packageJsonText).version,
|
||||
date: options.date ?? readCommitDate(),
|
||||
runNumber: options.runNumber ?? process.env.GITHUB_RUN_NUMBER,
|
||||
});
|
||||
let version;
|
||||
|
||||
try {
|
||||
version = resolveVersion(options, packageJsonText);
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (options.apply) {
|
||||
writeFileSync(
|
||||
|
||||
@@ -3,7 +3,9 @@ import { describe, it } from 'node:test';
|
||||
import {
|
||||
applyNightlyVersion,
|
||||
buildNightlyVersion,
|
||||
isNightlyVersion,
|
||||
parseArguments,
|
||||
parseBooleanFlag,
|
||||
} from './nightly-version.mjs';
|
||||
|
||||
describe('buildNightlyVersion', () => {
|
||||
@@ -76,6 +78,30 @@ describe('buildNightlyVersion', () => {
|
||||
assert.ok(semverOrder(nextDay, '0.24.0') < 0);
|
||||
});
|
||||
|
||||
it('keeps the base patch while the base version is not tagged yet', () => {
|
||||
// The release-cut commit bumps package.json to 0.23.1 before the
|
||||
// v0.23.1 tag exists: the nightly must sit BELOW 0.23.1 so the
|
||||
// imminent stable release is still offered.
|
||||
assert.equal(
|
||||
buildNightlyVersion({
|
||||
baseVersion: '0.23.1',
|
||||
date: '20260915',
|
||||
runNumber: 1240,
|
||||
baseReleased: false,
|
||||
}),
|
||||
'0.23.1-nightly.20260915.1240'
|
||||
);
|
||||
assert.equal(
|
||||
buildNightlyVersion({
|
||||
baseVersion: '0.23.1',
|
||||
date: '20260915',
|
||||
runNumber: 1241,
|
||||
baseReleased: true,
|
||||
}),
|
||||
'0.23.2-nightly.20260915.1241'
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a base version that is already a prerelease', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
@@ -138,6 +164,25 @@ describe('applyNightlyVersion', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('explicit version input', () => {
|
||||
it('accepts only the shape the script itself produces', () => {
|
||||
assert.equal(isNightlyVersion('0.23.1-nightly.20260915.1234'), true);
|
||||
assert.equal(isNightlyVersion('0.23.1'), false);
|
||||
assert.equal(isNightlyVersion('0.23.1-beta.1'), false);
|
||||
assert.equal(isNightlyVersion('0.23.1-nightly.2026915.1'), false);
|
||||
assert.equal(isNightlyVersion(undefined), false);
|
||||
});
|
||||
|
||||
it('parses the base-released flag strictly', () => {
|
||||
assert.equal(parseBooleanFlag('true', '--base-released'), true);
|
||||
assert.equal(parseBooleanFlag('false', '--base-released'), false);
|
||||
assert.throws(
|
||||
() => parseBooleanFlag('yes', '--base-released'),
|
||||
/--base-released must be "true" or "false"/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseArguments', () => {
|
||||
it('reads the flags and ignores a bare separator', () => {
|
||||
assert.deepEqual(
|
||||
@@ -150,12 +195,18 @@ describe('parseArguments', () => {
|
||||
'20260915',
|
||||
'--run-number',
|
||||
'12',
|
||||
'--base-released',
|
||||
'false',
|
||||
'--version',
|
||||
'0.23.0-nightly.20260915.12',
|
||||
]),
|
||||
{
|
||||
apply: true,
|
||||
base: '0.23.0',
|
||||
date: '20260915',
|
||||
runNumber: '12',
|
||||
baseReleased: 'false',
|
||||
version: '0.23.0-nightly.20260915.12',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user