Tags

Tags give the ability to mark specific points in history as being important
  • v5.14.0

    protected Release: pipeline v5.14.0
    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

    protected Release: pipeline v5.13.0
    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

    protected Release: pipeline v5.12.0
    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

    protected Release: pipeline v5.11.0
    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

    protected Release: pipeline v5.10.0
    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

    protected Release: pipeline v5.9.1
    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

    protected Release: pipeline v5.9.0
    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

    protected Release: pipeline v5.8.0
    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

    protected Release: pipeline v5.7.0
    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

    protected Release: pipeline v5.6.0
    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

    protected Release: pipeline v5.5.0
    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

    protected Release: pipeline v5.4.2
    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

    protected Release: pipeline v5.4.1
    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

    protected Release: pipeline v5.4.0
    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

    protected Release: pipeline v5.3.2
    v5.3.2 -- rust-cli back-compat alias + latex build-pdfs fix
  • v5.3.1

    protected Release: pipeline v5.3.1
    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

    protected Release: pipeline v5.3.0
    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

    protected Release: pipeline v5.2.2
    pipeline v5.2.2 -- image-pin lockstep (maturity): nomograph image refs pinned to release version + coherence gate + sync script
  • v5.2.1

    protected Release: pipeline v5.2.1
    pipeline v5.2.1 -- registry-coherence semver-correct pre-release handling (#2)
  • v5.2.0

    protected Release: pipeline v5.2.0
    v5.2.0 — latex: just in image; justfile-first build-pdfs