@@ -77,9 +77,32 @@ do not reach up the tree.
7777
7878SemVer, ** not** CalVer — this repo is the documented exception (alongside ` srtla-send-rs ` ).
7979ONE unified tag ` vX.Y.Z ` releases ** both** artifacts: ` @ceralive/modem-control@X.Y.Z ` on
80- npm and the ` .deb ` set. ` .deb ` versions encode the tag as ` <upstream>-<rev>~ceralive<X.Y.Z> `
81- (upstream-ordered, apt-safe; injected with ` dch --force-bad-version ` ). Non-tag CI builds use
82- ` ~ceralive0.0.0~dev ` . Full contract: ` docs/VERSIONING.md ` .
80+ npm and the ` .deb ` set. Non-tag CI builds use ` ~ceralive0.0.0~dev ` . Full contract:
81+ ` docs/VERSIONING.md ` .
82+
83+ ** ` .deb ` versions no longer encode the tag.** Releases are DIFFERENTIAL, so each upstream
84+ source carries its own rebuild counter: ` <upstream>-<rev>~ceralive.N ` (upstream-ordered,
85+ apt-safe; injected with ` dch --force-bad-version ` ). A REBUILT source takes its previous
86+ counter + 1, derived from the previous release manifest's rows for that source; an
87+ UNTOUCHED source is carried forward byte-identically and keeps the counter it already had,
88+ never re-stamped with the new tag. Two sources at different counters in one release is the
89+ normal shape, not drift — coherence is a PER-SOURCE property, and a source disagreeing with
90+ ITSELF fails closed naming that source. Derivation reads every row of a source (both arches,
91+ runtime and aux) and refuses on a counter disagreement, a counter/legacy mixture, or a
92+ malformed suffix; entirely-legacy rows and an absent previous manifest bootstrap at ` .1 ` .
93+
94+ The pre-` v1.0.0 ` releases WERE built with one uniform ` ~ceralive<X.Y.Z> ` suffix shared by all
95+ four sources; those published artifacts are unchanged. No release mixes the two schemes — the
96+ first differential release force-rebuilds every source at ` .1 ` , because this effort's own
97+ ` packaging/ci/** ` changes are a shared build input and force-all on their own. The
98+ migration-continuity chain `~ ceralive0.2.0 < ~ ceralive1.0.0 < ~ ceralive1.1.0 < ~ ceralive.1 <
99+ ~ ceralive.2 < ~ ceralive.10 < <upstream >-<rev >` is proven with real ` dpkg --compare-versions`
100+ by the ONE sourced library ` packaging/ci/suffix-contract.sh ` , from both
101+ ` test-suffix-coherence-manifest.sh ` (host) and ` test-package-contract.sh ` CHECK 5/6
102+ (container). The release manifest states ` suffix_scheme: per-source-counter ` and carries ** no
103+ ` deb_version_suffix: ` ** — under per-source counters no single suffix value is truthful. The
104+ companion ` ceralive-modem-support ` stays outside this entirely: bare SemVer tag version, no
105+ ` ~ceralive ` suffix, and always rebuilt.
83106
84107## FROZEN V1.1 DOMAIN CONTRACTS
85108
@@ -249,6 +272,17 @@ arch-dependent stanzas + enumerated `-dbgsym`) for exact per-source set **equali
249272` packaging/ci/check-package-sets.sh ` (add/remove/rename fails closed). Full detail:
250273` packaging/README.md ` .
251274
275+ ** The pins are watched, never auto-bumped.** ` .github/workflows/upstream-watch.yml ` runs
276+ weekly (plus ` workflow_dispatch ` ) and calls ` packaging/ci/check-upstream-freshness.sh ` , which
277+ enumerates each source's upstream release tags and salsa ` debian/* ` packaging tags via
278+ ` git ls-remote --tags ` , filters the development series out, and compares the survivors to the
279+ four pins above. On ` behind ` it opens ** or updates** ONE issue labelled ` upstream-freshness ` ,
280+ and closes it when everything is current again. It is ** issue-only** : it never edits
281+ ` upstream-pins.yaml ` and never dispatches a build, which is why it is the only workflow here
282+ holding ` issues: write ` and no dispatch token. A newer upstream release with no matching
283+ Debian packaging tag reports the distinct ` upstream-ahead-no-packaging ` — there is no
284+ ` <upstream>-<rev> ` pair to pin, so there is no bump to recommend.
285+
252286## MUTATION ADMISSION + EXCLUSIVE OWNERSHIP
253287
254288` control/src/ports/mutation-admission.ts ` defines ` MutationAdmissionPort ` . It is an injected
@@ -1350,13 +1384,44 @@ major action versions, per-manager caches, weekly grouped Dependabot, test-befor
13501384 before any other job). Exports ` version ` + ` sha ` .
13511385 2 . ** test** (needs tag-guard) — full bun lane + packaging contract lane
13521386 (test-before-publish).
1353- 3 . ** build-deb** (needs [ tag-guard, test] ) — injects ` <upstream>-<rev>~ceralive<X.Y.Z> `
1354- (non-tag runs ` ~ceralive0.0.0~dev ` ) via ` packaging/ci/inject-deb-version.sh ` , builds both
1355- arches, runs the package contract suite + daemon smoke, builds the ` Architecture: all `
1356- companion ONCE (` packaging/ci/build-companion.sh ` ) and runs its clean-chroot contract
1357- (` packaging/ci/test-companion-chroot.sh ` ), generates the manifest-complete
1358- release manifest (` packaging/ci/generate-release-manifest.sh ` ), and uploads the ` .deb `
1359- artifacts + manifest.
1387+ 3 . ** build-deb** (needs [ tag-guard, test] ) — the ** DIFFERENTIAL** ` .deb ` job. Steps, in file
1388+ order: ** Checkout the resolved commit** (` fetch-depth: 0 ` — load-bearing here and nowhere
1389+ else, since the detector diffs ` <prev-tag>..HEAD ` ; a shallow checkout would silently
1390+ force-all forever) → ** Assert checkout is pinned to the resolved SHA** → ** Set up QEMU
1391+ (arm64 emulation)** → ** Resolve previous release + fetch its manifest (once)**
1392+ (` id: prev-release ` ; ` gh release list ` , never ` git describe ` — the previous release is the
1393+ latest PUBLISHED one; no release or no manifest asset leaves both outputs empty, which is
1394+ the bootstrap case, not an error) → ** Detect changed sources (per-source verdicts)**
1395+ (` packaging/ci/detect-changed-sources.sh --out verdicts.txt ` ) → ** Stage carry-forward debs
1396+ (unchanged sources, sha256-verified)** (` packaging/ci/stage-carryforward-debs.sh ` ) →
1397+ ** Build the MM 1.24 stack (.deb) — amd64 + arm64** (` build-bookworm.sh amd64 ` /` arm64 ` ,
1398+ with ` VERDICTS_FILE ` + the already-resolved ` PREV_MANIFEST_FILE ` ) → ** Package contract
1399+ suite** (amd64 full, arm64 metadata) → ** Daemon smoke (amd64)** → ** Build the first-party
1400+ companion .deb (Architecture: all)** (UNCONDITIONAL — the companion is never detected and
1401+ never carried) → ** Companion package contract (clean Debian chroot)** → ** Generate release
1402+ manifest** (` packaging/ci/generate-release-manifest.sh ` ) → ** Upload .deb artifacts +
1403+ release manifest** .
1404+
1405+ Three things about that order are load-bearing. The previous release is resolved and its
1406+ manifest downloaded ** exactly once** , and that one path feeds all three consumers
1407+ (detection, carry-forward staging, per-source counter derivation). Carry-forward staging
1408+ runs ** strictly before any ` build-bookworm.sh ` call** , because carried debs are a build
1409+ INPUT — ` build-bookworm.sh ` seeds its Pin-Priority-1001 local apt repo from
1410+ ` packaging/build/<arch>/ ` , so a changed source resolves its build-deps and gir typelibs
1411+ against the carried ` -dev ` /` gir1.2-* ` packages rather than stock bookworm; staging late
1412+ still goes green and silently reintroduces stock dependencies, which is why
1413+ ` packaging/ci/test-release-workflow-wiring.sh ` pins the ordering statically. And a
1414+ zero-build run starts no container at all yet still asserts the merged runtime closure
1415+ over the carried set.
1416+
1417+ Detection is ** fail-SAFE toward rebuilding** : an absent previous release, a manifest with
1418+ no ` closure_version: ` header (an absent header IS closure version 1), a shared-input
1419+ change under ` packaging/ci/** ` or ` packaging/BOOKWORM-ADAPTATIONS.md ` , or the operator's
1420+ escape hatch all yield ` mode=force-all ` . That escape hatch is the ` force_rebuild `
1421+ ` workflow_dispatch ` boolean input (default ` false ` ), mapped to the script's
1422+ ` FORCE_REBUILD=all ` env via
1423+ ` ${{ github.event.inputs.force_rebuild == 'true' && 'all' || '' }} ` — defense in depth, since
1424+ a shared-input change force-alls on its own.
13601425 4 . ** publish-npm** (needs [ tag-guard, build-deb] ) — OIDC trusted publishing
13611426 (` id-token: write ` ), verifies ` control/package.json ` version === tag, then an
13621427 ** integrity-idempotent** publish: ` npm pack ` → classify registry state (404 → publish;
@@ -1379,6 +1444,22 @@ major action versions, per-manager caches, weekly grouped Dependabot, test-befor
13791444 the release exists. An operator's own ` gh api ` call would test the operator's CLI token and
13801445 prove nothing about the repository secret — which is exactly why this lives here.
13811446 ` cancel-in-progress: false ` (never cancel a release/publish mid-run).
1447+ - ** ` .github/workflows/upstream-watch.yml ` ** — the weekly ** upstream freshness watch**
1448+ (schedule + ` workflow_dispatch ` , ` cancel-in-progress: false ` ). Runs
1449+ ` packaging/ci/check-upstream-freshness.sh ` , which enumerates each source's upstream release
1450+ tags and salsa ` debian/* ` packaging tags via ` git ls-remote --tags ` , filters out the
1451+ development series, and compares the survivors to the pins. On ` behind ` it opens ** or
1452+ updates** ONE issue labelled ` upstream-freshness ` ; when everything is current again it closes
1453+ it. ** Issue-only** — it never edits ` packaging/upstream-pins.yaml ` and never dispatches a
1454+ build, which is why it is the ONLY workflow here that escalates ` issues: write ` and why it
1455+ holds no dispatch token. The stable filter is the substance: all four projects publish their
1456+ unstable train on the same tag namespace (ModemManager ` 1.25.95 ` → Debian * experimental* ), so
1457+ ` -rc ` /` -dev ` , non-` X.Y.Z ` , ** odd-minor** and ` .9x ` -micro tags are rejected, as are ` ~ ` -bearing
1458+ Debian revisions. A newer upstream release with no Debian packaging tag reports the distinct
1459+ ` upstream-ahead-no-packaging ` — NOT ` behind ` , because with no ` <upstream>-<rev> ` pair there is
1460+ no bump to recommend. ` packaging/ci/test-check-upstream-freshness.sh ` pins all of it offline
1461+ through a fixture seam. NOTE: GitHub disables scheduled workflows after 60 days of repository
1462+ inactivity — a silent watch reads exactly like an up-to-date one.
13821463
13831464Action pins track the latest stable ** major** (resolved via the ` gh api ` releases/latest
13841465endpoint); Dependabot keeps them current. JS/TS CI runs on ** Node 26** — the CeraLive CI
0 commit comments