Files
OmniRoute/.github/workflows
Diego Rodrigues de Sa e Souza 7eca04fd12 feat(ci): gate the publish on clean-install AND upgrade-over-previous (#8953)
* feat(ci): gate the publish on clean-install AND upgrade-over-previous

`check:pack-boot` proves a fresh install boots. It does not prove the path that actually
broke us: installing over an existing version, where ~110 SQLite migrations run against a
populated database. v3.8.48 shipped as a hotfix because the published 3.8.47 crashed on
boot, and the v3.8.49 upgrade path was only ever exercised end-to-end by hand — on VPS .16,
against a real 3.8.48 install with a 165 MB database, AFTER publishing. That is backwards.

New gate (`scripts/check/check-install-upgrade.mjs`), wired into npm-publish.yml as step 12,
BEFORE `npm stage publish` — so a broken upgrade never reaches the registry and a staged
package that is never approved simply expires, with no `npm deprecate` needed:

- Phase A: fresh prefix + fresh DATA_DIR, install the packed tarball, boot, health.
- Phase B: fresh prefix + fresh DATA_DIR, install the PREVIOUS published version, boot it
  (creates + migrates the DB), stop, install the tarball over the SAME prefix, boot against
  the SAME DATA_DIR. Asserts no table present before the upgrade was dropped.
- Schema convergence, and its DIRECTION is the whole point:
    fresh − upgraded ≠ ∅  → FAIL. Structure a clean install creates but an upgrade does not
                            means every existing user is missing it. Not allowlistable.
    upgraded − fresh ≠ ∅  → residue; fails only when NEW (allowlist carries the known ones).

A naive symmetric check would either block every release on harmless residue or, if relaxed,
let the dangerous direction through. Measured on VPS .16 (2026-07-30): a real 3.8.48 install
upgraded to 3.8.49 ended with 117 tables against 116 for a clean 3.8.49 install — the extra
being `cache_metrics`, recorded in config/quality/install-upgrade-allowlist.json with the
measurement. Both installs healthy, zero `no such table` in 150 log lines.

`evaluateConvergence` is exported and pure so the asymmetry is testable without packing,
installing or booting anything (same reason check-test-masking exports its helpers):
tests/unit/check-install-upgrade-convergence.test.ts, 8 cases, ~6ms.

A previous version that fails to boot degrades to a warning — a historically bad publish
must not block the current one. Uses node:sqlite (Node 24, already the publish job's
runtime): no new dependency.

* fix(ci): require the reused next-build artifact to come from this repository

CodeQL raised actions/artifact-poisoning/critical on the `next-build` fast path
this PR builds on (#8941). The finding is real and it sits on the path that
produces the published npm tarball.

The step picks a CI run by querying the runs API for `head_sha` and filtering on
`name == "CI" and conclusion == "success"`. That query also returns
`pull_request` runs from FORKS: they execute in this repository's context and
upload their own `next-build`, built from fork-controlled source. Measured
today, 57 runs in this repo have a `head_repository` other than the repo itself.
So the selection trusted bytes by coincidence of commit SHA — anything that made
a fork's head commit coincide with the publish commit could put attacker-built
bytes on npm.

Adds `and .head_repository.full_name == env.REPO` to the selection. Provenance
is now explicit; `head_sha` still carries tree-equality. Verified against the
live API using the expression extracted from the workflow itself — the same
single run (30518663668) is selected either way for the current tip, so the fast
path keeps working while every fork run is excluded.

Not a dismissal (hard rule #14) — the clause removes the flagged trust.

    node --import tsx/esm --test tests/unit/npm-publish-artifact-provenance.test.ts
    # 3 pass, 0 fail        (base: 2 pass, 1 fail)

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-07-30 09:07:57 -03:00
..
2026-07-29 15:18:55 -03:00
2026-06-20 07:09:43 -03:00
2026-07-29 15:18:55 -03:00
2026-07-29 15:18:55 -03:00
2026-06-06 19:13:11 -03:00
2026-06-29 08:40:06 -03:00
2026-07-29 15:18:55 -03:00
2026-07-29 15:18:55 -03:00
2026-07-29 15:18:55 -03:00
2026-07-29 15:18:55 -03:00
2026-07-29 15:18:55 -03:00
2026-07-29 15:18:55 -03:00
2026-07-29 15:18:55 -03:00
2026-07-29 15:18:55 -03:00
2026-07-29 15:18:55 -03:00