Tags give the ability to mark specific points in history as being important
-
v5.14.0
protectedRelease: pipeline v5.14.0c4d7f0bb · ·v5.14.0 — osv-verdict: give the default branch a diff base A newly published advisory against an unchanged tree reddened main on the next unrelated push, and the fail-closed automerge-gate then blocked every subsequent merge. Live instance: nomograph/reference pipeline 2744075217 went red on nanoid GHSA-2v37-7h3g-55p8 (published 2026-08-07T20:50Z, astro->vite->postcss->nanoid) though nothing in that push touched it. The default branch now diffs against CI_COMMIT_BEFORE_SHA, so a pre-existing finding is in both the base and head scans and is not newly-introduced. Ports the symmetric main-push regression gate dunn.dev/pipeline has carried since its 4.1.0 (7092500). When a diff base exists the strict MR rule applies; the WARN/FAIL class split now governs only the true no-base fallback, the case it was written for. Also: the default-branch rule no longer hardcodes "main".
-
v5.13.0
protectedRelease: pipeline v5.13.0d85cd095 · ·pipeline v5.13.0 -- osv-verdict: un-vacuum the MR gate, split the verdict by exploitability Two `osv-verdict` fixes. The first is urgent for every consumer: the component's merge-request OSV signal has been absent, not clean. - **The MR diff was vacuous -- it passed unconditionally.** The diff path runs `git stash --include-untracked` before checking out the base commit. The head scan wrote `osv-head.json`/`osv-head.ids` into `$CI_PROJECT_DIR` as untracked files, so the stash swept them away and nothing restored them. Every MR pipeline logged `sort: cannot read: /builds/<...>/osv-head.ids` and then, one line later, `no newly-introduced findings (pre-existing ignored)` -- `comm -13 base head` against an empty head can only be empty. `CHECK_OSV=pass` on every merge request since the diff path landed, regardless of what the branch introduced. The same stash ate the artifact (`osv-head.json: no matching files. ERROR: No files to upload`), which is why no MR pipeline ever had a scan report attached -- the visible tell nobody chased. Scan outputs now live under `/tmp`, out of the stash's reach, and the head report is copied into the project dir at the end for collection. Probed against the real defect: an MR adding `quick-xml 0.37.5` on top of two pre-existing findings returns `pass` on the old code and `fail`, naming both `RUSTSEC-2026-0194/0195`, on the new. Consumers should assume their MR OSV gate has been reporting nothing since the diff path landed, and re-read anything it waved through. - **Findings are classified by exploitability off an MR.** On main/tag/bare-branch the verdict now fails on EXPLOITABLE findings only; MAINTENANCE-STATUS advisories are reported, counted separately, and do not fail. On 2026-08-05 three nomograph default branches were red at once, and sysml's were `RUSTSEC-2025-0141` (bincode unmaintained) and `RUSTSEC-2024-0436` (paste no longer maintained) -- neither has a fixed version, so no dependency bump could ever clear them: a vuln-DB publication reddened a default branch with no code change and no available remedy. The discriminator is RustSec's own `affected[].database_specific.informational` marker (`unmaintained` / `unsound` / `notice`), required on EVERY `affected[]` entry. "No fixed version available" was rejected as the discriminator because it would have exempted genuine threats: verified that `@ctrl/tinycolor 4.1.2` / `MAL-2025-47141` (the Shai-Hulud npm worm) has zero `fixed` events and still fails. Non-RustSec databases (GHSA, Go, PyPI) carry no `informational` field, so they always classify exploitable -- the fail-closed side. MR mode is deliberately untouched and stays strict. - **Adds an `osv-head.cls` artifact** -- the classified finding table (tuple, `WARN`/`FAIL`, informational value, summary) alongside the raw `osv-head.json`, so a narrowed verdict stays auditable after the fact. Release-prep: every `${NOMOGRAPH_REGISTRY}` image pin and every self-referential catalog component include across `templates/` moves to v5.13.0 in lockstep (94 lines), per `scripts/sync-image-pins.sh`. -
v5.12.0
protectedRelease: pipeline v5.12.0834e0fd5 · ·pipeline v5.12.0 -- python + rust-cli Fedora migration, registry repoint, security-lane pipeline gate Completes the Debian->Fedora 44 base migration across all six catalog images (go-tool/worker-ts landed v5.11.0; python and rust-cli land here), repoints every Fedora base pull off quay.io's fast-GC'ing mirror onto registry.fedoraproject.org, and closes a live security-lane gap where a deliberately-parked, CI-less repo was still auto-merging [SECURITY] MRs. - python: migrated docker.io/library/python:3.12-slim -> Fedora 44, uv-first pip bootstrap (Fedora's venv/ensurepip refuses to bootstrap pip; no matching python3.12-pip package exists). - rust-cli: migrated docker.io/library/rust:1.95-bookworm -> Fedora 44, last on the line since nearly every catalog gate runs inside it. Bakes gh/glab/jq/node into the image, closing four job-time apt-get sites that had no dpkg equivalent on Fedora. - All six Fedora-based images repointed quay.io/fedora/fedora -> registry.fedoraproject.org/fedora: quay.io GC'd three digests in one night (33/33 resolved on the new registry vs 1/33 on quay.io across 34 days of pins). Two follow-on GC-forced re-pins landed and are documented as their own Fixed entries. - Renovate: vulnerabilityAlerts gains platformAutomerge:false. The security fast lane stays enabled (automerge: true unchanged), but a CI-less repo no longer auto-merges on GitLab's native automerge, which registered a branch with zero status checks as mergeable instantly. Merge now runs through Renovate's own checkAutoMerge (resolveBranchStatus === 'green'), which a no-CI branch never reaches. Confirmed live: a parked, CI-less thegredc repo had auto-merged 12 [SECURITY] MRs, including a vite v5->v6 major, all with pipeline: null. Release-prep: scripts/sync-image-pins.sh v5.12.0 moved every ${NOMOGRAPH_REGISTRY}/*:v5.11.0 image pin and component:.../*@v5.11.0 include across templates/ to v5.12.0 in the same commit as this tag. -
v5.11.0
protectedRelease: pipeline v5.11.0eaa2185e · ·pipeline v5.11.0 -- Fedora 44 base migration, retry-reason convergence, blocking secret detection Consumer-visible base-image swap for two wrapper components, the retry-reason cleanup finally reaching every template it should have, the secret gate that actually blocks a leak, and a floating buildah pin repinned to something Renovate can track. MINOR because `go-tool` and `worker-ts` consumers now build and run on a materially different base distro, not because any interface changed. - **`go-tool` and `worker-ts`: migrated from the upstream Debian-based images to Fedora 44, with `just` baked in.** Proves the same Debian->Fedora pattern `latex` and `website` already run, ahead of `python` and `rust-cli` following next. Consumers pinning these two components now receive a glibc-based Fedora userland instead of Debian's -- different package manager, different base libraries, different toolchain packaging conventions -- and `just` ships in the image rather than being installed per-job. `go-tool`: Fedora carries one golang stream per release rather than a per-minor package, so `GO_VERSION` now gates a build-time assertion instead of selecting a package, and `GOPATH=/go` is set explicitly since Fedora's golang package (unlike the upstream golang image) does not export it, which would otherwise silently break the preserved `/go/bin` PATH entry. `worker-ts`: a bare `dnf install nodejs` resolves to Fedora 44's default stream (24), not the pinned 22, so the versioned `nodejsNN`/`nodejsNN-npm` packages are installed explicitly alongside their weak-dep `-bin` counterparts that own the unversioned `/usr/bin/node` and `/usr/bin/npm` (`install_weak_deps=False` otherwise drops them). Verified against the freshly built images before landing, via temporary `verify:go-tool`/`verify:worker-ts` jobs that printed toolchain versions in place; both jobs are gone again once the base was confirmed. - **Retry-reason correction now reaches every template, including `secret-verdict`.** GitLab deprecated the generic `stuck_or_timeout_failure` retry:when value in 19.1 (removal in 20.0); it aliased exactly `stuck_pending_with_matching_runners`, `stuck_pending_no_matching_runners`, `no_updates_running`, and `no_updates_canceling`, so replacing it with that set preserves identical retry coverage. The first pass landed across `templates/*.yml` and `.gitlab-ci.yml`, but `secret-verdict.yml` was added on a parallel branch and slipped past it -- its retry block, and the README's documented default-policy example, both still carried the deprecated value. GitLab still accepted it (deprecated, not yet removed), so this was drift rather than a hard failure, but it meant the one component landing in this same release was already inconsistent with the fix. Caught and closed during this release's prep; `ci/lint` confirms the corrected 10-value list produces zero deprecation warnings everywhere it now appears. - **`secret-verdict`: secret detection that actually blocks.** Replaces the upstream `Jobs/Secret-Detection.gitlab-ci.yml` include. That template's analyzer runs gitleaks with `--exit-code 0`, so it exits 0 even when it finds secrets, and no `allow_failure` setting can turn a leak into a red pipeline. `secret-verdict` runs the same analyzer with the same ruleset -- identical scan surface, so migration is a swap of the `include:`, not a change in what gets flagged -- then parses `gl-secret-detection-report.json` directly and exits nonzero on a non-empty `vulnerabilities` array. A missing, empty, or malformed report fails closed rather than reading as "no findings." - **`container-image`: stopped floating on `quay.io/buildah/stable:latest`.** quay.io rebuilds the `vX`/`vX.Y`/ `vX.Y.Z`/`latest` tags in place (proven 2026-08-03: all four resolved to one digest pushed 31 May), and nothing tracked the pin -- no Renovate customManager covered `templates/*.yml` bodies, and `pin-audit.py` only scanned `.gitlab-ci.yml`/`Containerfile`/ `*.container`. Repinned to `v1.43.1-immutable@sha256:fc649e18...`, a real Renovate-trackable version tag quay does not rebuild in place, matching ADOPTION.md's `<name>:<semver>@sha256:<digest>` convention. Zero live consumers of this component today, so this was latent rather than exploited; closed the same gap on both the Renovate side (new customManager) and the local-gate side (pin-audit.py file match extended to `templates/*.yml`). Release-prep: `scripts/sync-image-pins.sh v5.11.0` moved every `${NOMOGRAPH_REGISTRY}/*:v5.10.0` image pin (8 distinct refs) and every `component:.../*@v5.10.0` include (15 distinct refs) across `templates/` to v5.11.0 in the same commit as this tag, so `image-pin-coherence` and `component-pin-coherence` land green rather than catching what release-prep forgot -- exactly what those two tag-only gates exist to enforce. -
v5.10.0
protectedRelease: pipeline v5.10.008ae40cd · ·pipeline v5.10.0 -- secret-verdict, component-pin convergence, cloudflare group A blocking secret gate, the first release whose wrapper component pins actually track the release, and a Renovate group that stops shipping a lockless MR. `secret-verdict` reads the analyzer's report instead of its exit code, so a leaked credential reddens the pipeline for the first time. `scripts/sync-image-pins.sh` rewrote 53 component pins alongside its 41 image pins across 21 files, absorbing 15+ releases of wrapper drift in one move; `component-pin-coherence` is the tag-pipeline gate that now keeps it converged. And the `cloudflare workers ecosystem` group drops `matchUpdateTypes`, so the coupled wrangler/workers-types pair travels in one resolvable MR at every update type. - **`secret-verdict`: secret detection that actually blocks.** GitLab's `Jobs/Secret-Detection.gitlab-ci.yml` template cannot fail a pipeline on a leaked credential, and flipping its `allow_failure: true` to `false` does not change that: the analyzer (`gitlab-org/security-products/analyzers/secrets`) invokes gitleaks with `--exit-code 0` (`gitleaks/scanner.go:12,52`), so the process exits 0 even when it found secrets -- the findings go into the JSON report and nowhere else. Upstream issues #421529 and #324634 both closed without fixing it, so `allow_failure: false` only makes analyzer OPERATIONAL failures blocking, which is not the thing anyone wanted gated. This component runs the same analyzer with the same ruleset (identical scan surface -- migration is a swap of the `include:`, not a change in what gets flagged), then PARSES `gl-secret-detection-report.json` and exits nonzero when the `vulnerabilities` array is non-empty. Same idiom as `osv-verdict`/`cargo-verdict`: run the scanner, read its artifact, decide, emit `CHECK_SECRET=pass|fail` as a dotenv for `automerge-gate`. Unlike those two it also exits nonzero on `fail` -- a leaked credential must redden the pipeline on its own, not merely withhold an automerge signal. - **Absence fails closed.** A missing report is not "no findings", it is a scan that did not happen. Report absent, zero-length, missing its `vulnerabilities` key, holding a non-array there, or the analyzer exiting nonzero at all: every one emits `fail` and exits 1. A stale report from a retried attempt is deleted before the run, so a crashed scan cannot inherit a previous clean result. - Parsing is POSIX shell against the FIRST occurrence of the top-level `"vulnerabilities"` key (only an immediately-closed array counts as clean). The analyzer image is an alpine minirootfs carrying only the analyzer and gitleaks binaries -- no `jq`, no `python` (verified by reading the published image layers) -- and `apk add jq` at job time was rejected as a job-time network fetch inside a security gate. - Inputs: `stage`, `job_name`, `check_name` (the `CHECK_<NAME>` mapping `cargo-verdict` established), `analyzer_image`, `excluded_paths`, `historic_scan`. Deliberately NO severity threshold and NO `allow_failure` input: secrets have no meaningful severity gradient, and either knob would be a fail-open lever on the one gate whose whole purpose is not failing open. Scope control is `excluded_paths`, which is path-level and visible in a diff. - Carries the bare-branch rule clause standardised in 5.8.0/5.9.1 (`$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS == null`) alongside the `merge_request_event` and `main` clauses, so it does not silently skip on a branch push -- which is exactly when a leaked credential first lands. - **Renovate: group the Cloudflare workers toolchain into one MR, at every update type.** `wrangler` declares `@cloudflare/workers-types` as a `peerOptional` and moves that peer FLOOR almost every release, so a split raises one side against a floor the other side no longer satisfies: npm ERESOLVEs, Renovate's lockfile artifact update aborts, and the MR ships `package.json` WITHOUT its regenerated lock -- `npm ci` then fails EUSAGE. That failure is near-invisible, posting as a `renovate/artifacts` commit status with the ERESOLVE text only in an MR note, never in the MR body or on the Dependency Dashboard, so it reads as CI flake. Hit twice in this group and repaired by hand both times: `beacon!11` (`peerOptional @cloudflare/workers-types@"^4.20260430.1" from wrangler@4.87.0`, against `workers-types@5.20260703.1`) and `behest!21` (same shape at `wrangler@4.107.0`). New `packageRules` entry sets `groupName: "cloudflare workers ecosystem"` over `wrangler`, `@cloudflare/workers-types` and `@cloudflare/vitest-pool-workers`, with NO `matchUpdateTypes` -- verified against the npm registry 2026-08-02, wrangler moved that peer from `^4` to `^5` at 4.108.0, a MINOR (4.107.1 was the last `^4`), and no wrangler 5.x exists (latest 4.118.0), so a major-scoped group can never admit wrangler at all; 29 of wrangler's 111 v4 peer-floor moves landed at PATCH, so any type-scoped grouping still splits the pair. `@cloudflare/vitest-pool-workers` is in the matcher despite having no peer edge of its own because it pins wrangler as an exact hard dependency and so drags that wrangler's types floor in with it (live: `beacon` holds `workers-types 5.20260729.1` while `vitest-pool-workers 0.20.1` pins `wrangler 4.118.0`, whose floor is `^5.20260730.1`). A second, major-scoped rule carries `dependencyDashboardApproval`: the blanket major rule sets only `automerge: false`, so without it a coupled major would still be auto-raised, but left on the group itself it would park every one of these near-daily date-versioned minors behind a manual tick. Renovate holds a whole group when any member needs approval, so a coupled major still lands as one deliberate MR. npm-only, so inert for the Rust majority of nomograph -- the live surface is `beacon`, `behest` and `reference`. Ported from `dunn.dev/pipeline`'s rule of the same name. - **`automerge-gate`: `secret_job` needs-slot and `secret` as a `required-checks` name.** Inert until opted into: a consumer that does not include `secret-verdict` names a job that never exists in its pipeline, which `needs:optional:true` tolerates (the mechanism `cargo_job_2`'s sentinel already proves), and `secret` is deliberately absent from the `required-checks` default so adding the slot does not start blocking existing consumers on a verdict they do not yet emit. - **`release:` job: `Components:`, `Containers:`, and the pre-release name suffix rendered empty on every catalog Release page** (confirmed on v5.9.0, v5.8.0, v5.7.0, v5.6.0). `COMPONENTS`, `CONTAINERS`, and `REL_SUFFIX` were computed as plain shell locals in the job's `script:` block, but `release:description:`/`release:name:` are evaluated after `script:` runs, outside that shell -- and per GitLab's own docs, a job's own `artifacts:reports:dotenv` cannot feed back into that SAME job's `release:` keyword either, only into a downstream job's. Split into a new `release-prepare` job (computes the three values, reports them via a `dotenv` artifact) and `release` (`needs: release-prepare`, unchanged `release:` block) -- the two-job shape GitLab's own release_cicd_examples doc uses for this exact case. No change to what the release computes or its wording, only how the values reach it. - **Wrapper components shipped frozen internal pins: 10 wrappers `include:` other components from this same catalog at a hardcoded `@v4.3.0`, unchanged across 15+ releases.** `tool-rust`, `go-tool`, `grammar-rust`, `latex-template`, `lib-rust`, `python-bench`, `python-lib`, `tree-sitter`, `worker-ts` and `data-only` each compose the universal gates (`hygiene`, `license-policy`, `readme-shape`, `release-discipline`, `deps-scan`, `registry-coherence`) by pinned `component:` include, and those pins were hand-maintained -- so every gate fix since v4.3.0, including the v5.8.0 and v5.9.1 silent-zero branch-push fixes, reached a repo that consumes a gate DIRECTLY but not the 18 of 37 nomograph repos that reach it through a wrapper. A catalog fix landing green and not arriving is the same silent-zero class those two releases closed, one level up: the composition layer looked current and forwarded nothing. `grammar-rust` double-indirects through `tool-rust@v4.3.0`, so it was two frozen hops from a fix; `worker-ts` alone had advanced `deps-scan` to `@v5.4.1`, leaving the catalog in violation of its own ADOPTION invariant 1 (`ONE catalog version across the whole file`). Fixed by extending the mechanism that already keeps image pins in lockstep rather than inventing a second one. `scripts/sync-image-pins.sh` now rewrites `component: gitlab.com/nomograph/pipeline/<name>@vX.Y.Z` pins to the release version alongside the `${NOMOGRAPH_REGISTRY}` image pins it already rewrote, and a new tag-pipeline gate `component-pin-coherence` fails the release on a drifted component pin exactly as `image-pin-coherence` does on a drifted image pin. The two land together on purpose: a rewrite with no check is precisely how this drifted unnoticed for 15 releases. Kept a separate job from `image-pin-coherence` so a red means `a wrapper is withholding catalog fixes`, never confused with a stale base image. Scoped to `templates/*.yml` structurally, not by exclusion rule: the rewrite and the check are each handed that literal glob, so a root-level file is unreachable by construction. This matters because this repo's own `.gitlab-ci.yml` self-include must NOT move at release-prep -- the tag does not exist yet, and pointing the catalog's own pipeline at a nonexistent ref would break the release it is cutting. Renovate bumps that self-include after the fact (precedent: v5.7.0 was cut by `b2ede00`, touching `templates/` only; the self-include went v5.6.0 -> v5.7.0 the next morning in `5c2424d`). Non-semver `@<version>` placeholders in the doc headers match neither the rewrite nor the check, by design. The tooling and the gate landed on their own, moving no pins. This release's prep commit is the first real rewrite, and it absorbs all 15 releases of accumulated drift in one move: 53 component pins (52 from `@v4.3.0`, one from `@v5.4.1`) and 41 image pins across 21 files under `templates/`. `worker-ts`'s comment justifying its lone `deps-scan@v5.4.1` advance "ahead of the rest of the frozen v4.3.0 gate baseline" went with them -- there is no split left for it to describe, and the catalog is back inside its own ADOPTION invariant 1 (`ONE catalog version across the whole file`). -
v5.9.1
protectedRelease: pipeline v5.9.1409f7ef8 · ·pipeline v5.9.1 - **`deps-scan`: gate never instantiates on a bare branch push, same class as the `automerge-gate`/`osv-verdict`/`cargo-verdict` fix in 5.8.0, one template over.** All four language jobs' (`rust`, `node`, `python`, `go`) `rules:` blocks matched only `merge_request_event`, `main`, a tag, or a schedule -- a Renovate bot branch pushed but not yet carrying an open MR pipeline matched none of those, so the scan never ran and the pipeline reported green having evaluated zero `cargo audit` / `cargo deny` / `npm audit` / `pip-audit` / `govulncheck` at all. Confirmed live: `nomograph/extension`'s branch-push pipeline for !16 (real dependency scanning across four manifests) ran 4 jobs and zero `deps-scan:*`, while the MR pipeline for the identical commit ran all four. Added the same `if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS == null` clause 5.8.0 landed for the other three templates, once per language job, each carrying that job's own unchanged `exists:` list -- dropping `exists:` on the new clause would have instantiated a job for a language the repo does not use, trading one silent-zero for a false-positive job on a bare branch push. Verified via `ci/lint` `dry_run` against `nomograph/extension` (rootless, four manifest dirs) and `nomograph/sysml` (root manifest): a no-open-MR branch now instantiates the expected jobs where it previously instantiated none, a branch with an open MR does not duplicate them, a root-manifest repo renders the same job set it did at 5.9.0, and the `exists:` guard still withholds jobs for languages a repo does not carry.
-
v5.9.0
protectedRelease: pipeline v5.9.048450be5 · ·pipeline v5.9.0 -- deps-scan nested-manifest coverage Nested-manifest coverage for `deps-scan`. The catalog's only cargo-deny carrier assumed every manifest sits at the repository root, so a workspace whose crates live in subdirectories got no scan at all while reporting green. Same silent-zero class v5.8.0 closed for the L0 gate, one template over: the gate looked present and proved nothing. Additive and default-inert -- no existing consumer's jobs, names, or rules change. - **`deps-scan`: `manifest_dir` + `job_name` -- scan Rust/Node/Python/Go manifests that do not sit at the repository root.** `deps-scan` is the only template that runs `cargo deny check advisories licenses bans`, and both its `exists:` rules and its scanner working directory were hard-wired to the repo root. A repo whose manifests live in subdirectories with no root manifest (nomograph/extension: `bench-guest/`, `crates/extension-host/`, `host/`, `phase-policy/`) therefore matched no `exists:` clause and got ZERO deps-scan jobs -- a green pipeline in which cargo-deny never ran, the same silent-zero class as the v5.8.0 gate-coverage fix. `manifest_dir` moves BOTH the rule and the working directory together (moving one alone reproduces the silent zero in a new form), and a bad value now hard-fails instead of falling back to the root. `job_name` is a prefix for the four per-language job names so the component can be included once per manifest directory, mirroring cargo-verdict's job_name split for exactly this repo shape. `manifest_dir` defaults to the empty string, NOT `"."` as cargo-verdict's same-named input does: cargo-verdict has no `exists:` rule, whereas GitLab's exists matcher is a literal glob over repo-relative paths that does not normalize a leading `./`, so `exists: ["./Cargo.toml"]` matches nothing in a root-manifest repo -- a `"."` default would have silently disabled this scan estate-wide. Verified both directions by ci/lint `dry_run` against real trees: with defaults, nomograph/sysml instantiates the identical `deps-scan:rust` and nomograph/extension still instantiates nothing; with nested values, extension instantiates one rust job per crate, while a nonexistent directory instantiates none (control proving the rule truly resolves against the tree). Trailing slash optional -- `host` and `host/` both work.
-
v5.8.0
protectedRelease: pipeline v5.8.0fdb1d90a · ·pipeline v5.8.0 -- L0 gate branch-push coverage + catalog Renovate policy Gate-coverage repair and catalog-side Renovate policy. The L0 gate now instantiates on a bare branch push -- roughly a quarter of consumer repos were reporting green having evaluated zero gate logic -- and `automerge-gate` gained a second independent cargo-verdict slot. The estate preset picked up an internal-trust exemption from `minimumReleaseAge` and wasmtime-family grouping. Separately, this catalog's own Renovate bot gained a signal digest, a token-expiry alarm, CI validation of the preset, and stage decoupling (catalog-internal -- nothing a consumer pins or extends moves with those). - **`automerge-gate`: `cargo_job_2` -- gate on a second independent cargo-verdict job.** A repo with 2+ Cargo manifests and no root manifest (nomograph/extension: `host/` + `crates/extension-host/`) could not vouch itself through the L0 gate -- `cargo_job` had exactly one needs: slot, so extension shipped hand-rolled CI instead (see its `.gitlab-ci.yml` header). `cargo-verdict` gained a `check_name` input (default `"cargo"`, unchanged) so a second instance can emit `CHECK_CARGO_HOST`/`CHECK_CARGO_EXTENSION_HOST` instead of colliding on `CHECK_CARGO`; `automerge-gate` gained `cargo_job_2` pointing at that instance's `job_name`, plus a hyphen-to-underscore fold when resolving `required-checks` names to `CHECK_<NAME>` variables (needed for hyphenated multi-cargo check names; a no-op for the existing bare `osv`/`cargo` names). `cargo_job_2` defaults to a sentinel job name no consumer defines, so every existing single-cargo-job consumer is unaffected (verified: `needs: job: <undefined-name> optional: true` lints valid with no such job in the pipeline). A third manifest would follow the identical pattern (`cargo_job_3`); not shipped preemptively. - **Renovate: internal-trust exemption from `minimumReleaseAge`, all update types including major.** `minimumReleaseAge: "7 days"` exists to give a compromised third-party release time to be caught before we consume it -- there is no third party in the loop for artifacts we author, build, and tag ourselves. Two new `packageRules` set `minimumReleaseAge: null` for the nomograph/pipeline catalog itself (both the `component:` includes and the `${NOMOGRAPH_REGISTRY}`-prefixed images collapse to depName/packageName `nomograph/pipeline` on the `gitlab-tags` datasource -- verified against Renovate's `gitlab-ci` manager source), our own crates (`nomograph-claim`, `nomograph-workflow`, confirmed live on crates.io), and the `gitlab-releases`-datasource mise tools (`andunn/dkg`, `gapvec`, `glean`). Majors deliberately keep `automerge: false` via the existing major-updates rule -- a major can still break consumers and the releaser owns consumer green, so the age gate is gone but human review is not. Mirrors the equivalent (narrower) rule in `dunn.dev/pipeline`'s preset. - **Renovate: group the wasmtime crate family into one MR.** `wasmtime` and every `/^wasmtime-/` crate are pinned in lockstep by a shared `wasmtime-environ` exact-pin, so Renovate splitting them into separate MRs produced MRs that were each red in isolation -- including a security pair that could not be landed incrementally (observed on `extension`: !14, a security fix bumping `wasmtime` to 46.0.2, broke Cargo resolution against `wasmtime-wasi` =46.0.1; !13 bumping `wasmtime-wasi` to v47 left two `wasmtime` versions in one graph with cross-version type mismatches; both sat red until landed together as `extension!15`). New `packageRules` entry sets `groupName: "wasmtime family"` only -- majors keep `automerge: false` from the existing major-updates rule, minor/patch keep automerge from the minor+patch rule, and a mixed-type group will not automerge because its updates disagree, which is the correct conservative outcome. - **Renovate bot: signal digest + token-expiry alarm** (catalog-internal; no template or preset behavior moves). Parity port from `dunn.dev/pipeline` so both estates run the same alerting logic. New `scripts/signal-digest.py` and a scoped `justfile` (this repo had none; other jobs call scripts directly) drive a `signal-digest` job that runs after the scheduled scan with `when: always` and folds five cheap health signals -- the scan's own trace and `artifactErrors`, every configured group's Dependency Dashboard "Errored" section, every repo's default-branch pipeline status, runner liveness, and wedged MRs -- into one pinned GitLab issue, plus an optional webhook. `renovate-token-check` reds within 30 days of `RENOVATE_TOKEN_EXPIRES` (a PAT cannot introspect its own expiry, so the date is carried in that variable and updated on rotation). No new secret: `GITLAB_TOKEN` maps from the `RENOVATE_TOKEN` the scan already holds. Adapted rather than copied blind -- both jobs use this repo's digest-pinned alpine and default `storr` tag, and `signal-digest`'s rules carry the sibling `renovate` job's `$RENOVATE == "true"` guard so the weekly container-rebuild schedule (where `renovate` does not run) does not fail pipeline creation on `needs: [renovate]`. - **CI: `validate-renovate` gate on this repo's own preset** (catalog-internal). Edits to `renovate.json` / `renovate-config.json` now run `renovate-config-validator --strict` inside the same digest-pinned `renovate/renovate` image the bot pulls, on MR and on main push -- so a green pipeline means that exact bot build accepts the config, not that it parsed on a laptop. Every preset edit before this landed unchecked. - **`automerge-gate`, `osv-verdict`, `cargo-verdict`: gate never instantiates on a bare branch push.** All three `rules:` blocks matched only `$CI_PIPELINE_SOURCE == "merge_request_event"` or `$CI_COMMIT_BRANCH == "main"` -- a Renovate bot branch pushed but not yet carrying an open MR pipeline is neither, so the gate and its verdict producers simply did not run: `needs: optional: true` swallowed the absent job, and the pipeline reported green having evaluated zero gate logic. Added a third clause, `if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS == null`, scoped so it fires only when the branch carries no open MR yet: once an MR opens, its `merge_request_event` pipeline (clause above) independently covers the same push, so the branch-pipeline copy stays out of the way instead of duplicating a full OSV scan / `cargo build --all-features` + test run on every push thereafter. The first push after MR creation still races GitLab populating `$CI_OPEN_MERGE_REQUESTS`, so one duplicated run per MR remains -- accepted, not engineered away. Verified via `ci/lint` against a synthetic consumer carrying the refined 3-clause shape (`valid: true`, no errors). Measured across the estate: roughly 25 of 97 consumer repos were losing gate coverage in branch context before this fix. - **Renovate bot: `needs: []` decouples the scan from every prior stage** (catalog-internal). The `renovate` job sits in the last stage with no `needs:`, so GitLab's stage-sequential default made it wait on all of `lint` -- confirmed on scheduled pipeline 2724786185, where all seven lint-stage jobs ran to completion before the scan started. A failing or stuck earlier-stage job (a container build on a rotated base-image digest, say) would then block or skip the one job positioned to fix it. `needs: []` lets it run on schedule regardless, matching `dunn.dev/pipeline` (05ce0f7c) and `yotoshelf/pipeline`'s existing convention. -
v5.7.0
protectedRelease: pipeline v5.7.01ad20954 · ·v5.7.0 Two independent hardening tracks: CI-resilience fixes for the retry/timeout gap that let a hung job burn its full ceiling with zero retry, and Renovate config hardening for the estate's self-hosted setup (Fedora rawhide exposure, an undetected image ref, `mise lock` blocked estate-wide, and a `force`-vs-schedule interaction that was double-firing lockFileMaintenance). ### Added - **Renovate: Fedora image-tag ceiling + major-version review gate.** The estate preset had zero Fedora-specific rules, so quay.io's numerically-newer rawhide tags looked like an ordinary major bump -- `nomograph/reference` (pinned `quay.io/fedora/fedora:42`) was queuing a bump straight to v45 = rawhide. Ported three rules verbatim from `dunn.dev/pipeline`'s preset (re-read from source, not memory), scoped to `quay.io/fedora/fedora`, `-bootc`, `-minimal`, `-toolbox` and their `registry.fedoraproject.org` equivalents: an `allowedVersions: "<=44"` cap (bump at each Fedora GA -- this is a maintenance point, source of truth https://docs.fedoraproject.org/en-US/releases/); a `matchUpdateTypes: major` rule keeping major bumps `automerge: false` for human review (split from the cap into its own rule so it doesn't block digest automerge -- Renovate forbids `allowedVersions` + `matchUpdateTypes` together in one rule); and `pinDigests: false` (quay.io GCs Fedora digests within hours, so a captured pin loses the race before the branch can even merge -- confirmed by two prior main breaks on GC'd digests and weeks of Errored dashboard branches on `cairn/base`, `cairn/pipeline`, `immutable/pipeline`, `immutable/basef`). - **Renovate: `${VAR}`-prefixed image custom manager.** No built-in manager sees a `${NOMOGRAPH_REGISTRY}/tree-sitter:v4.1.4`-shaped `image:` line -- `tree-sitter-sysml`'s Dependency Dashboard listed only its `component:` include, never this image. New regex custom manager targets `.gitlab-ci.yml`, `datasource: gitlab-tags`, `packageName` fixed to `nomograph/pipeline` (every image under that registry root is built and re-tagged in lockstep with a `nomograph/pipeline` release, confirmed against this repo's own container-build job), `versioningTemplate: "loose"` (mandatory -- the default strict semver silently drops non-3-part tags like `v4.1.4`). Verified against the real `tree-sitter-sysml` file and also matches `nomograph/extension`'s `${NOMOGRAPH_REGISTRY}/rust-cli:v5.4.0`. ### Changed - **All 18 job-level `retry:` bloc -
v5.6.0
protectedRelease: pipeline v5.6.0774ffb83 · ·v5.6.0 -- Rust toolchain floor raised to rustc 1.95 Unblocks sysinfo 0.39 / rusqlite 0.40 / libsqlite3-sys cfg_select dependency majors that require rustc 1.95. rust-cli image rebuilt on rust:1.95-bookworm; all affected crates compile-verified on 1.95 before release.
-
v5.5.0
protectedRelease: pipeline v5.5.0740db2f3 · ·v5.5.0 -- rust gates compile feature-gated code (--all-features) cargo-verdict, tool-rust, lib-rust gain a check_features input (default --all-features), applied to build/clippy/test. Closes a blind spot: the rust gates compiled default features only, so feature-gated code (mcp/gpu/ gitlab features) was never compiled -- a dep bump could break it while the pipeline stayed green (sysml's rmcp+schemars bumps rode green a month while the mcp feature did not compile). Adversarially proven: a reintroduced break turns the gate RED; --all-features is literal in the trace. Escape hatch: check_features="" or "--features x". rust-cli (frozen alias) unchanged. Estate scan: no consumer breaks under --all-features.
-
v5.4.2
protectedRelease: pipeline v5.4.2365eeaa4 · ·v5.4.2 worker-ts bundle advances its deps-scan gate to v5.4.1 (npm audit --omit=dev), so bundle consumers get the node prod-tree-only audit fix. Targeted advance -- the other four bundled gates stay at v4.3.0. Unblocks beacon (all-dev tree, hard-failed on devDependency-only advisories).
-
v5.4.1
protectedRelease: pipeline v5.4.1094e1c5f · ·v5.4.1 deps-scan (node): npm audit --omit=dev -- audit the production tree only. devDependency advisories (wrangler/miniflare/vitest transitives) reach no deployed artifact and hard-failed consumers with all-dev trees (beacon: 6 high, all dev transitives, no in-range fix). Production-dep advisories still hard-fail at >= fail_on.
-
v5.4.0
protectedRelease: pipeline v5.4.059a58699 · ·nomograph/pipeline v5.4.0 Changed: - rust-cli image: Rust toolchain floor 1.93 -> 1.94. Unblocks crates whose MSRV moved to 1.94 (immediate driver: wasmtime 46 in extension). The check job runs `cargo clippy -- -D warnings`; 1.93->1.94 clippy delta canaried clean on muxr + synthesist (0 new lints). Fail-closed gate: a stray lint can only stall a bump MR, never redden main. - hygiene gate no longer forbids AGENTS.md -- the portable, committed agent-onboarding file (dunn.dev tracks it in cycle/, othala/). nomograph now matches that convention. .claude/ stays forbidden. Reaches direct-include consumers on a v5.4.0 bump (un-reds runes); bundle intra-catalog hygiene pins remain at v4.3.0 (deliberate gate baseline).
-
v5.3.2
protectedRelease: pipeline v5.3.2389c464e · ·v5.3.2 -- rust-cli back-compat alias + latex build-pdfs fix
-
v5.3.1
protectedRelease: pipeline v5.3.13978c047 · ·v5.3.1: automerge-gate fail-open guard + osv-verdict merge-base reachability - automerge-gate now blocks (exit 1) when zero required checks were evaluated (empty/typo'd required-checks no longer fail-OPENs to GREEN). - Bundles the osv-verdict merge-base reachability fixes that were on main since v5.3.0 but never tagged (v5.3.0 was 6 commits behind main). - All template image pins bumped to v5.3.1.
-
v5.3.0
protectedRelease: pipeline v5.3.0e469ecae · ·pipeline v5.3.0 Fail-closed Renovate automerge gate (L1 verdict contract): osv-verdict, cargo-verdict, automerge-gate. cargo-verdict added on estate evidence (0.x caret-breaking crate bumps dominate the backlog; OSV cannot catch a build/test break). Image pins synced to v5.3.0.
-
v5.2.2
protectedRelease: pipeline v5.2.2858e876f · ·pipeline v5.2.2 -- image-pin lockstep (maturity): nomograph image refs pinned to release version + coherence gate + sync script
-
v5.2.1
protectedRelease: pipeline v5.2.1db26f331 · ·pipeline v5.2.1 -- registry-coherence semver-correct pre-release handling (#2)
-
v5.2.0
protectedRelease: pipeline v5.2.07ababa3d · ·v5.2.0 — latex: just in image; justfile-first build-pdfs