mirror of
https://github.com/software-mansion/argent.git
synced 2026-09-14 19:27:14 +08:00
c4a9f0bd8a
**Update after team discussion:** the open question from the original
version of this PR ("should we fire `next` on every push to main?") got
a yes, so this now does exactly that — continuous canary publishing from
`main`. Original tag-sync wiring is still here too. Branch is merged up
to current `main`.
### what it does
every push to main publishes a canary prerelease under the `next`
dist-tag, so `next` always tracks the tip of main.
- **"Publish prerelease to npm (next)"** now also triggers on `push:
branches: [main]`. on that path it derives a version, stamps it into
`packages/argent/package.json`, and publishes `--tag next`. the manual
`workflow_dispatch` path is untouched (gated behind `if:
github.event_name == 'workflow_dispatch'`).
- **version scheme** = `<patch-bump of the highest published
version>-next.<counter>`. a prerelease of the next patch still outranks
the current `latest` (so `next` lands above it) but stays under the
eventual stable. the **counter resets per base**: next index after the
highest `<base>-next.<k>` already on npm, or 0 if the base has no
canaries yet — so each patch line starts at `.0`:
```
0.13.1-next.0, 0.13.1-next.1, ... ← latest is 0.13.0
... 0.13.1 ships stable ...
0.13.2-next.0, 0.13.2-next.1, ... ← counter restarts
```
lives in `scripts/next-canary-version.mjs` (+ tests) as a pure function
of the published-versions list, reusing the semver helpers from the sync
script.
- **`argent -v` reports the right thing.** the CLI reads its version
from `package.json` at runtime, so stamping is enough — and there's an
in-pipeline step that runs the built `argent --version` and fails the
publish if it doesn't match the version about to ship. verified
end-to-end against the real registry: computed `0.13.1-next.0`, stamped,
built, `argent --version` → `0.13.1-next.0`.
- **collisions self-heal.** the counter is registry-derived, so canary
publishes are serialized (a `concurrency` group) to kill the concurrent
race; the publish step additionally retries a version-exists collision
(npm read-after-write lag on back-to-back runs) by recomputing. any
other publish failure still fails fast.
- canary builds pull the floating `argent-main` / `radon-main` signed
binaries. sync workflow still fires after every publish (self-heals
`next`, and still does the real work after a *stable* release).
### stuff to sanity-check before merge
- this publishes to npm on **every** merge to main — a lot of versions +
a full build/publish run each time. fine if that's the intent (it's the
whole point), just calling it out.
- **native binaries on the canary path are the floating `argent-main`
tag** — if radon's binary build lags a native-source change on main, a
canary can ship fresh JS against slightly-older signed
dylibs/ax-service/APK with no signal (the *missing* case fails loud +
safe; the *stale* case is silent). there's no per-commit signed-binary
build to pin against, so this is inherent to canary-from-main rather
than something this PR can fix — flagging it as the one real "preview
channel can be skewed" caveat.
- during a rapid burst of merges, GitHub's concurrency supersedes
intermediate queued runs — published `-next.N` stay contiguous from 0,
but not literally every commit in a burst gets its own canary.
- like any push trigger, it only goes live once this is on main.
101 lines
4.2 KiB
JavaScript
101 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Compute the version for a canary prerelease published on every push to main:
|
|
*
|
|
* <patch-bump of the highest STABLE published version>-next.<counter>
|
|
*
|
|
* e.g. with latest=0.13.0 and no canaries yet → `0.13.1-next.0`.
|
|
*
|
|
* Why patch-bump-of-stable: each main build is a preview of the next patch
|
|
* release. A prerelease of 0.13.1 still outranks the 0.13.0 release by SemVer
|
|
* precedence, so `next` always lands strictly above `latest`, while staying
|
|
* below the eventual 0.13.1 stable.
|
|
*
|
|
* The counter resets per base: it's the next index after the highest
|
|
* `<base>-next.<k>` already on npm, or 0 if this base has no canaries yet. So
|
|
* each new patch line starts at `.0` and increments per publish:
|
|
* 0.13.1-next.0, 0.13.1-next.1, ... then 0.13.1 ships stable ...
|
|
* 0.13.2-next.0, 0.13.2-next.1, ...
|
|
*
|
|
* Because the counter is read from the registry, the `concurrency` group in
|
|
* publish-next.yml serializes canary publishes so two runs can't read the same
|
|
* index at once. That removes the concurrent race but not npm's read-after-write
|
|
* lag (a freshly published version may not be visible to the next run's read for
|
|
* a few seconds), so the publish step ALSO retries on a version-exists collision
|
|
* by recomputing — see "Publish canary" in publish-next.yml. Either way a
|
|
* collision fails safe (npm 409, no bad artifact, no tag move).
|
|
*
|
|
* Zero npm dependencies (runs in a lightweight CI job without `npm ci`); the
|
|
* SemVer helpers are shared with sync-next-dist-tag.mjs.
|
|
*
|
|
* Usage:
|
|
* node scripts/next-canary-version.mjs print the canary version
|
|
* node scripts/next-canary-version.mjs --write also stamp it into
|
|
* packages/argent/package.json
|
|
*/
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { argv as processArgv } from "node:process";
|
|
import { pathToFileURL } from "node:url";
|
|
import { compareSemver, parseSemver } from "./sync-next-dist-tag.mjs";
|
|
|
|
const PACKAGE = "@swmansion/argent";
|
|
const PKG_JSON = "packages/argent/package.json";
|
|
|
|
/**
|
|
* Pure version math, separated from npm I/O so it can be unit-tested.
|
|
* @param {string[]} versions published versions (unparseable entries ignored)
|
|
* @returns {string} the canary version, e.g. "0.13.1-next.0"
|
|
*/
|
|
export function computeCanaryVersion(versions) {
|
|
const valid = versions.filter((v) => parseSemver(v) !== null);
|
|
if (valid.length === 0) {
|
|
throw new Error("no parseable published versions found");
|
|
}
|
|
|
|
const stable = valid.filter((v) => parseSemver(v).pre.length === 0);
|
|
// Prefer the highest stable release and bump its patch. With no stable
|
|
// release yet, preview the highest prerelease's own target version (no bump).
|
|
const pool = stable.length > 0 ? stable : valid;
|
|
const { main } = parseSemver(pool.slice().sort((a, b) => compareSemver(b, a))[0]);
|
|
const [major, minor, patch] = main;
|
|
const base = stable.length > 0 ? [major, minor, patch + 1] : [major, minor, patch];
|
|
|
|
// Counter resets per base: next index after the highest existing
|
|
// `<base>-next.<k>`, or 0 if this base has no canaries yet.
|
|
let counter = 0;
|
|
for (const v of valid) {
|
|
const { main: m, pre } = parseSemver(v);
|
|
const sameBase = m[0] === base[0] && m[1] === base[1] && m[2] === base[2];
|
|
if (sameBase && pre.length === 2 && pre[0] === "next" && /^\d+$/.test(pre[1])) {
|
|
counter = Math.max(counter, Number(pre[1]) + 1);
|
|
}
|
|
}
|
|
|
|
return `${base[0]}.${base[1]}.${base[2]}-next.${counter}`;
|
|
}
|
|
|
|
function main() {
|
|
const write = processArgv.includes("--write");
|
|
const raw = JSON.parse(
|
|
execFileSync("npm", ["view", PACKAGE, "versions", "--json"], { encoding: "utf8" })
|
|
);
|
|
// npm returns a bare string when a package has exactly one published version.
|
|
const versions = Array.isArray(raw) ? raw : [raw];
|
|
const version = computeCanaryVersion(versions);
|
|
|
|
if (write) {
|
|
const pkg = JSON.parse(readFileSync(PKG_JSON, "utf8"));
|
|
pkg.version = version;
|
|
writeFileSync(PKG_JSON, JSON.stringify(pkg, null, 2) + "\n");
|
|
}
|
|
|
|
process.stdout.write(version + "\n");
|
|
}
|
|
|
|
// Run only when invoked directly, not when imported by the test.
|
|
if (processArgv[1] && import.meta.url === pathToFileURL(processArgv[1]).href) {
|
|
main();
|
|
}
|