fix(vale): Bake vale and pinned styles into ci-tools

  • vale had stopped linting anything, and the job's own red square hid it. On 2026-08-04 the job failed three pipelines in a row on reference with HTTP 429, exhausting its retries while asking gitlab.com to archive the gitlab-org/gitlab monorepo at master for doc/.vale. It examined zero files each time. Because the component is normally included with allow_failure: true so prose findings advise rather than block, "the styles download was rate-limited so nothing ran" was indistinguishable from "the prose is clean" and from "there are 313 findings" -- one ignored red job standing for three different states. That is why finding counts on this estate kept moving between sessions.

    vale and a pinned snapshot of the GitLab docs styles now ship inside ci-tools, at /opt/vale-styles with VALE_STYLES_PATH set. The default job path makes no network call at all: the binary is no longer pulled from a GitHub release per run, and the styles are no longer archived out of a 3 GB monorepo per run per consumer. A styles failure is now a loud ci-tools build failure that blocks, instead of a job nobody reads. This also makes the component offline-capable, which the airgapped consumers this catalog exists for actually need.

  • The vale ruleset drifted under consumers with no diff in their repository. styles_archive defaulted to master, and master is not a version. Diffing the pinned tag against master on the same day showed a new rule (BoldEmphasis), two new style packages, and four changed rule files. A consumer's green pipeline could go red because someone else merged documentation upstream. VALE_STYLES_REF pins it (v18.11.8-ee) and carries a Renovate annotation, so the ruleset now moves when a maintainer chooses and arrives as a reviewable MR.

    vale_version and styles_archive keep working as runtime overrides, and both now default to empty (use the image). A failed override fetch, or an image with no styles, exits 2 -- check could not run -- and never falls through to a vale invocation that would report zero findings against zero rules and read as a pass. Verified against all three branches: baked styles present, baked styles missing, and an override URL that 404s.

    Not fixed here, and it needs a major. allow_failure: true still swallows exit 2 along with exit 1, so a consumer who wants prose findings advisory cannot yet make could-not-run blocking. The mechanism is allow_failure: { exit_codes: [1] }, which means changing the allow_failure input's shape: a breaking contract change for the six current consumers, and one whose empty-array semantics should be proven on a lab before shipping. The measured defect above is fixed regardless, because the runtime failure mode is now gone rather than relabelled.

  • The shared Renovate preset stripped the leading v from every version it wrote. extractVersionTemplate: "^v?(?<version>.+)$" on the Containerfile ARG manager rewrote v2.6.3 as 2.6.4, but the ARG is used as the release TAG in the download URL (the filename gets a separately-stripped copy). Six merge requests across ci-tools, ci-buildah and ci-buildkit proposed URLs that 404: verified against the live API, gitlab-org/cli release 1.109.0 returns 404 and v1.109.0 returns 200. Replaced with versioningTemplate: "semver-coerced". The same line sat on the catalog component-pin manager, where it is worse: that manager's matchStrings requires the v, so the stripped value fails confirmIfDepUpdated, the file reverts, and Renovate throws update-failure. That is the errored branch updates, and why no consumer was on the current catalog version. Same root cause, two failure shapes: one writes silently wrong, the other cannot write at all.

  • validate:containers built nothing and passed. Its while read loop had no input redirect, so it read stdin, which is empty in a CI job. It printed a correct build plan and then built 0 image(s); Job succeeded. No pipeline had ever run it, because its rules require a merge request that changes a Containerfile. It now redirects from the plan and asserts built == planned.

  • osv-verdict was fail-open in merge-request diff mode. Consumers may see MRs newly go red, correctly. The head scan wrote its findings inside the working tree and git stash --include-untracked deleted them; the stash is never popped. sort on the missing file exited 2 to stderr, comm read an empty stream and exited 0, and the verdict was pass no matter what the head introduced. There is no set -e in that script, so nothing surfaced. Scan output now lives outside the working tree, the declared osv-head.json artifact is restored through an EXIT trap covering all five exit paths, and a guard fail-closes when either side of the diff is absent.

  • osv-verdict treated an unparseable scan as a clean one. The jq parse ended in || : > "$ids_file", so any schema change, truncated write or missing file became an empty findings list and a pass. Since osv_scanner_version is Renovate-managed, a major bump reshaping the JSON would have silently disarmed the check estate-wide. The parse now fails closed, and a scanner exit of 1 (findings present) that parses to zero findings is treated as schema drift rather than as clean.

  • audit set LANG, the POSIX locale variable, to the language name. On the vanilla bases every current consumer overrides to, LANG is already exported, so the assignment propagated LANG=go to every child process (locale: Cannot set LC_CTYPE to default locale, collation flipping to C ordering). Renamed to AUDIT_LANG.

Changed

  • audit's image input is now optional, derived from language. It had no default, which made it required: the component could not be included without naming an image, and its own error text coached a vanilla base ("add 'image: rust:1.88.0'"). Across all 57 group projects every one of audit's six consumers passes image: and five pass a vanilla base, four of them naming a language ci-tools already bakes. Conformity was zero by construction. Existing overrides keep working; migration is deleting the line. rust resolves to ci-rust, go to ci-go, node and python to ci-tools. Removing the input entirely remains the eventual major.

  • audit no longer installs audit tooling at job runtime. The rust arm ran cargo install --locked cargo-deny --version 0.19.2 2>/dev/null || true, shadowing the image's Renovate-tracked pin with an unpinned second source of truth and swallowing its own failure. The go arm ran go install golang.org/x/vuln/cmd/govulncheck@v1.1.4, which was OLDER than the baked version, so the audit ran an older scanner than the image shipped. The python arm ran an unpinned pip install pip-audit, which on this image's pip backtracks to an older release rather than failing. All three now check the baked tool is present and fail pointing at a stale image.

  • ci-tools bakes pip-audit at a Renovate-tracked PIP_AUDIT_VERSION.

  • .image-build runs on the catalog's own ci-buildah and no longer curl-installs cosign at runtime, which happened on every image build, against this repo's own no-runtime-curl-installs rule, without checksum verification, in the job that signs every catalog image.

  • Digest pinning is off. pinDigests: false for the docker datasource, and invariant 1 in ADOPTION.md rewritten to match: the semver tag is the pin. Digests rot faster than a review-gated MR can land.

  • Containerfile changes are built on the merge request. No build:ci-* job ran on merge_request_event, so a Containerfile change was never exercised until after merge, which is why six MRs proposing 404 URLs were all green. validate:containers builds the changed images, and their dependents, without pushing or signing.

  • BREAKING: automerge-gate no longer fails the pipeline for a customer-shipping class whose checks are green. It reports GATE_DECISION=human-review and exits zero. Consumers on the default (customer_shipping: true) will see this job go from permanently red to green. That default previously exited nonzero unconditionally, which put "not automerge-eligible" on the same pipeline red/green channel only_allow_merge_if_pipeline_succeeds reads to gate human merge. The documented adoption shape therefore blocked the human lane rather than routing to it, and the catalog's own sim had to wrap that shape in allow_failure: true to stay green. A failing or absent check still exits nonzero on the shipping path. The automerge block itself was never carried by this exit code; it lives in renovate-config.json (platformAutomerge: false plus automerge: false on every rule).

  • automerge-gate emits a GATE_DECISION dotenv (automerge-eligible, human-review, blocked, tampered) with when: always, so the decision is auditable and assertable rather than only a log line.

  • automerge-gate refuses to decide against a tampered floor. CUSTOMER_SHIPPING is a job-level variable and GitLab ranks project and group CI/CD variables above job-level ones, so a Maintainer on a consuming project could set it in Settings and silently send the job down the non-shipping path with no diff in any file. The gate now compares the runtime value against the literal it was compiled with and emits GATE_DECISION=tampered with a nonzero exit if they differ. Documentation claiming the floor was "enforced at two layers" has been corrected in renovate-config.json, README.md, templates/automerge-gate/README.md and ADOPTION.md; it was one layer plus a report.

[4.2.0] - 2026-07-27

Added

  • site-check component. Runs a project's own check command -- type checking, framework diagnostics -- on merge requests. It exists because a build is not a check: astro build and its equivalents transform sources without type-checking them, so a TypeScript or framework major goes green in CI while breaking types. Four projects in this group each defined "check": "astro check" in package.json and no pipeline ran it; a TypeScript v7 merge request passed CI in one of them purely because the build never invoked the checker, and running that checker by hand against the current pinned TypeScript surfaced two pre-existing type errors nobody had seen. allow_failure defaults to false, because a check that cannot fail the pipeline is not a gate. The component stays deliberately thin -- install, then run the project's own command -- so the check keeps living in package.json where maintainers can run it locally.

  • ADOPTION.md — the adoption profile every consumer of this catalog conforms to: three invariants (everything Renovatable, the gate runs on the path that merges, CI references nothing a refactor can break), the standard consumer shape, adoption tiers, and enforcement. It exists so that "this project is deliberately exempt" and "nobody got round to this project yet" stop being indistinguishable — which is how dependency traffic went unmanaged here for months without anything looking wrong.

  • Inherited enrollment for the Renovate runner. The runner now injects this catalog's own preset as base config for every autodiscovered project (RENOVATE_CONFIG + RENOVATE_REQUIRE_CONFIG: optional). Under the previous required model, 37 of 57 projects in the group carried no renovate.json and were therefore silently unmanaged. A project now opts out explicitly with {"enabled": false}, and a project needing overrides still ships its own renovate.json, which merges on top of the injected base. Enrollment is opt-out instead of opt-in, which is the only version that stays complete as the group grows.

  • RENOVATE_FORCE cadence normalizer in the runner preset — {"schedule":null,"automergeSchedule":null,"prHourlyLimit":0}. The CI cron becomes the single source of cadence, so a config schedule inherited from anywhere can no longer silently park updates (see Fixed, below). prConcurrentLimit is deliberately left alone: it stays the useful throttle, a per-repo ceiling on open MRs, where prHourlyLimit interacts badly with an infrequent cron — at 4/hour on a weekly run a project drains four updates per week.

  • validate:renovate-config job. renovate.json, renovate-config.json and presets/renovate.json are now schema-validated on every change, pinned to the Renovate major the runner executes. Every project in the group resolves the shared preset at scan time, so a schema error here is a group-wide outage rather than a local one. The pin matters: an unpinned npx renovate-config-validator resolves an older major that reports false errors on current keys such as managerFilePatterns.

  • needs: [] on the renovate job. The job sits in the last stage, so an earlier-stage failure skipped it — and the likely failure on this catalog is a container build on a rotated base-image digest, which is precisely what Renovate would have bumped. That is a deadlock. Scheduled runs already exclude the build jobs, but a web-triggered run executes the full pipeline, so this is load-bearing there.

Changed

  • Evidence is frozen, by path: reproduction material stops being renovated unattended. A file under bench/, archive/, evidence/, experiments/, rig/, receipts/ or results/ is not a dependency choice tracking the moving world — it is the experimental condition some captured result describes. Bumping it does not maintain the experiment, it voids the receipt. Measured: three of the six benchmark subjects in lab/rootless were bumped before this rule existed, and because a receipt names its subject only as "dockerfile": "go-scratch" and records no versions, nothing in the receipt could reveal the drift.

    Keyed on path, not on repository. Only 7 of 18 lab projects hold receipts at all — the rest are tools, docs and landing pages that must stay current — so a repository-wide freeze both over-freezes those and would rot the receipt-bearing labs' own site builds and CI. Per-path also generalises: an evidence directory appearing in a non-lab project is covered with no config change, and no list of projects has to be maintained as labs are added.

    Two supporting choices: dependencyDashboardApproval rather than enabled: false, so the dashboard still lists every available update and a thaw is one checkbox (re-run, re-capture, tick) instead of a config change; and a vulnerabilityAlerts override, so a genuine CVE still opens an MR against frozen evidence. Globs verified against minimatch at both root and nested depth, including that benchmarks/ and site/src/rigging.ts do not match.

    Separately, the 13 unmaintained lab/project-* hackathon repositories now carry {"enabled": false} of their own: they are kept as reference for new work rather than maintained software, so updates there generate review nobody will read. Left writable rather than archived so code can still be lifted out.

  • TypeScript ceilinged below v7 in the shared preset, by upstream limitation rather than preference. v7 is the native-compiler port and its package no longer exports the in-process compiler API, so every tool that drives TypeScript programmatically breaks. @astrojs/check (latest 0.9.9) peers ^5.0.0 || ^6.0.0, install fails ERESOLVE, and forcing it through crashes @astrojs/language-server at runtime on the removed API. Upstream tracks this at withastro/astro#17268, labelled triage: unable to fix; Microsoft's own 7.0 announcement tells Astro/Vue/Svelte/MDX projects to stay on 6.x. The ceiling still modernizes -- 6.0.3 is stable and compatible, so Renovate now proposes the v6 major instead of a v7 that cannot merge, and four projects stop regenerating a red MR every day. Removable in one line when @astrojs/check ships v7 support. Two related traps worth knowing: typescript-eslint carries a hard peer ceiling (<6.1.0) and fails loudly at install, while vue-tsc and svelte-check declare loose >=5.0.0 peers, so they install cleanly against v7 and fail at RUNTIME instead of in CI.

  • Renovate scan timeout raised to 3h, sized for a cold run. The first scan after enrolling a whole group must open every branch it finds and regenerate a lockfile per JS project. Measured on this estate: 41 of 56 projects in 72 minutes, so the previous 1h 30m could not complete a single cold pass. Steady-state daily runs are a fraction of that, since most projects have nothing to do. A timeout is deliberately still not retried -- job_execution_timeout is a distinct failure reason from the infrastructure classes, and retrying a scan that legitimately needs more wall-clock than it was given cannot succeed, it only spends the budget twice.

  • Renovate autodiscovery skips purge-pending projects. The filter now excludes *-deletion_scheduled-*. Those projects linger for the retention window, autodiscovery still finds them, and the bot's Dependency Dashboard write fails — a guaranteed warning every scan for a project that is already gone.

  • renovate job robustness. Explicit timeout: 1h 30m so a growing autodiscover scan is not killed mid-run into a partial green, and retry broadened to the three infrastructure classes (runner_system_failure, stuck_or_timeout_failure, scheduler_failure) rather than retrying a real Renovate error into a different failure.

  • Runner preset documents the cadence and enrollment doctrines in its header, including the schedule-window failure mode in full, so an operator standing one up from this asset does not rediscover it. The shipped default stays RENOVATE_REQUIRE_CONFIG: required — correct for a fresh operator with no shared preset — with the inversion documented for operators who have one. The recommended cron is now daily rather than weekly.

Fixed

  • ci-tools: glab is looked up on the right forge. The GLAB_VERSION pin carried datasource=github-releases for gitlab-org/cli, a GitLab-hosted project. The lookup returned no-result silently every scan, so the pin sat at v1.92.1 while v1.108.0 shipped — sixteen minor versions of drift, reported only as a warning on a dashboard. Now gitlab-releases.

  • Renovate preset: scheduled runs actually open merge requests. The preset scheduled updates for before 6am on Monday (Mon 00:00-05:59 UTC) while the runner's cron fires at 06:00 and the job starts 06:07-06:09 — every run landed outside its own window. Renovate's updateNotScheduled default still refreshed existing branches, so the runner looked healthy (56 repos autodiscovered, job green, dashboards current) while never creating a new branch. Every update parked in Awaiting Schedule indefinitely: 12 items on this project, 11 on tach, 24 on reference. Measured effect estate-wide — Renovate MRs created fell from 22 in May to 12 in June to 2 in July. The window is now before 9am on Monday, giving the 06:00 cron roughly three hours of slack for queue delay and the job's own ~12 minute runtime.

  • UBI grouping covers UBI10. The group rule matched registry.access.redhat.com/ubi9{/,}** only, so consumers on ubi10 received ungrouped erratum bumps. Now matches ubi{9,10}{/,}**.

  • Catalog component pins no longer emit a spurious lookup-failure warning. Consumers write component: $CI_SERVER_FQDN/gitlab-com/public-sector/pipeline/<name>@vX.Y.Z. Renovate's native gitlabci manager treats the unresolved CI variable as the registry host, so its gitlab-tags lookup always returned no-result and eight consumer dashboards carried a Failed to look up gitlab-tags package gitlab-com/public-sector/pipeline warning. The pins themselves were never actually unmanaged — the customManagers regex entry tracks them correctly with a hardcoded packageName and the gitlab-releases datasource — so the native extraction was pure duplicate noise. It is now disabled for this package. Closes #2 (closed).

[4.1.2] - 2026-07-22

Fixed

  • pages template: default-branch deploys actually fire. The 4.1.1 fix repaired explicit literal branch inputs but not the default: nested variables inside a variable value do not expand during rules:if evaluation, so the job variable holding $CI_DEFAULT_BRANCH still compared as literal text and the pages job stayed absent under default inputs -- verified against a live consumer pipeline. The branch input default is now the empty string and the rule splits in two: empty input deploys when $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH (direct variable-to-variable, no nesting), an explicit input deploys from exactly that branch. Both paths verified on the consumer before release. (!82 (merged))

[4.1.1] - 2026-07-22

Fixed

  • pages template: the deploy rule now fires with the default branch input. The rule interpolated the input inside quotes (== "$[[ inputs.branch ]]"), and GitLab rules:if treats a quoted right side as a literal string -- with the default ($CI_DEFAULT_BRANCH) the rendered rule compared the branch name against that literal text, never matched, and the pages job silently dropped from every consumer pipeline using defaults. The input now lands in a job variable (PAGES_DEPLOY_BRANCH) and the rule compares variable to variable, which works for both the variable default and an explicit literal branch. Consumers that worked around the bug by pinning a literal (branch: "main") can drop the workaround. Swept the other component templates for the same quoted-input-in-rules shape: pages was the only one. (!81 (merged))

[4.1.0] - 2026-07-19

Added

  • automerge-gate template -- the terminal, fail-closed decision job for Renovate automerge on Public Sector projects, with a hard customer-shipping human-signoff floor. It consumes per-check dotenv verdicts (e.g. osv-verdict) over needs: with optional: true, and exits nonzero unless every check named in required-checks is both present and == pass -- an absent or non-pass signal, or zero checks evaluated (empty/typo'd required-checks), blocks automerge, so a weakly-instrumented project cannot clear its own gate.
  • osv-verdict template -- emits an OSV-scan verdict for automerge-gate (a signal of the L0 CI gate). Installs osv-scanner as a checksum-verified binary (the same idiom ci-tools uses for cosign/syft/glab/yq); on a merge-request pipeline it scans the merge-base and head and fails only on findings the head newly introduces (pre-existing findings do not block an unrelated bump), fail-closing if the merge-base is unreachable. Writes CHECK_OSV=pass|fail for the gate.
  • ci-tools now ships diffutils (diff + cmp). ubi9-minimal ships neither. A consumer's shell no-op/idempotency check built on diff/cmp failed command-not-found and, under a 2>/dev/null redirect, degraded silently to the "changed" path -- a real bug this hid downstream. The Containerfile carries a keep-this note.

Changed

  • Dependency bumps: UBI9 base image, golang 1.26.4, gitlab-org/cli (glab) 1.102.0, and convergence of the shared renovate preset onto the root config.

Fixed

  • release reflows the annotated tag body before publishing. Tag bodies are hard-wrapped at ~72 columns (git convention), but GitLab renders single newlines as line breaks, so the release page showed mid-sentence breaks. The notes generation now joins wrapped prose into single-line paragraphs while preserving blank-line paragraph breaks and list items (-, *, 1.). The PGP signature is still stripped and stays verifiable via git verify-tag. Consumers pin @v4.1.0 to pick this up; existing release pages can be re-rendered by re-running the tag's create-release job.

[4.0.2] - 2026-06-05

Fixed

  • verify runs after the separate container-sign job. v4.0.0 split signing into its own job, but verify only needs: the build job, so on a DAG it raced ahead of signing -> "no signatures found". verify is now stage-gated (no needs:) -- it runs after the build+sign stage(s) and inherits the digests from the container job's dotenv. Keep the verify stage after build/sign.
  • verify guards :latest. :latest is pushed only on the default branch / tags (the v4 :latest gate), so verify checks IMAGE_DIGEST_LATEST only when present -- no spurious failure on MR pipelines. Consumers on v4.0.x should pin @v4.0.2.

[4.0.1] - 2026-06-05

Fixed

  • Catalog images now resolve for consumers that don't override them. v4.0.0 gave container, container-sign, vale, and registry-cleanup an image input defaulting to ci-*:$[[ component.version ]]. GitLab does not interpolate $[[ component.version ]] inside a resolved input default, so any consumer using the default pulled the literal ci-*:$[[ component.version ]] and failed with "invalid reference format". Reverted to a body-hardcoded image: .../ci-*:$[[ component.version ]] (which interpolates) and dropped the image input. The in-repo container sim (which used the input to override at a SHA include) is removed; sim-go-binary remains. Consumers on v4.0.0 should pin @v4.0.1.

[4.0.0] - 2026-06-04

This is a breaking release (catalog v4.0.0): container build and container signing are now separate components, and :latest is gated to the default branch.

Added

  • container-sign component. Cosign-signs a container image's pushed digests (keyless via GitLab OIDC), reading container.env from the container component (IMAGE_DIGEST_SHA / _LATEST / _TAG). This is the signing half of the build/sign split; it mirrors the post-build shape of container-sbom and container-attest.
  • image input on container and container-sign. Defaults to the catalog image at your catalog tag (ci-buildah / ci-tools at $[[ component.version ]]); override for a private fork or a :latest image when including at a SHA (e.g. the in-repo sim harness, where component.version is empty).
  • registry-cleanup component. The container registry cleanup policy (container_expiration_policy) is disabled estate-wide, so SHA build tags and cosign .sig signature tags accumulate without bound. This component applies a verified baseline (enabled: true, cadence: 7d, keep_n: 10, older_than: 30d, name_regex_delete: .*) via an idempotent PUT projects/:id on the default branch; it needs a token with api scope (CI_JOB_TOKEN cannot edit project settings). The name_regex_keep carve-out protects cosign signatures -- ^(latest.*|v\d+\.\d+\.\d+(-.*)?|sha256-[a-f0-9]{64}\.sig)$ -- because GitLab cleanup treats sha256-<digest>.sig tags as ordinary tags; without the .sig clause it deletes signatures of images you keep and breaks cosign verify. The clause also protects latest and suffix variants (latest-fips) and all semver tags. The component is the single source of truth; its README documents a manual one-shot fallback for projects not yet wired to CI.

Changed

  • BREAKING: container no longer pushes or signs :latest on merge request pipelines. :latest now tracks the default branch only (default branch + tag pipelines); :SHA is still built on every pipeline (downstream smoke/scan need it) and :TAG on tag pipelines. Previously every MR pipeline moved :latest to unreviewed MR code and signed it. Fixes the transient-clobber + spurious-signature gap.

  • BREAKING: container no longer signs. The container component now only builds, pushes, and writes container.env. Cosign signing moved to the new container-sign component (the id_tokens / COSIGN_YES wiring moved with it). Consumers that relied on container to sign must add a container-sign job per build job to preserve signatures. Rationale: signing is a distinct concern with distinct identity/perms; decoupling lets a consumer build without signing or re-sign without rebuilding.

  • container-sbom no longer declares artifacts:reports:cyclonedx. GitLab-side SBOM ingestion (Dependency List, Security, Licensing) is owned by Ultimate Container Scanning, which emits a GitLab-conformant gl-sbom-report.cdx.json with the required gitlab:* CycloneDX properties. Submitting this component's syft SBOM in parallel produced the "Required GitLab CycloneDX properties are missing" parse error on the Security and Licensing tabs. The component keeps its real purpose -- the cosign attestation bound to the image digest (external / Iron Bank channel, verifiable via cosign verify-attestation). Consumers that relied on this component to populate the Dependency List should ensure Jobs/Container-Scanning.gitlab-ci.yml is included (Ultimate).

  • vale gained an image input. Defaults to the catalog ci-tools at your catalog tag, overridable for a fork or a :latest at SHA includes. Aligns with container / container-sign / registry-cleanup and fixes the empty-tag (ci-tools:) "invalid reference format" failure when included at a SHA, where component.version is empty.

Removed

  • Phantom *-version rows in component READMEs. The cosign-version / syft-version rows in container-attest, container-sbom, and container-manifest READMEs documented inputs that are not declared in those components' spec.inputs (the tool versions are pinned in-component, not consumer-overridable). Removed the misleading rows.

  • templates/reference relocated to examples/reference. The reference Go/Rust verify + lockfile implementations are examples, not a CI component (no template.yml); moving them out of templates/ keeps that tree component-only. Links in the top-level README and templates/attest/README.md updated.

  • build-container-buildah component. A near-duplicate of container (buildah build + push + the now-extracted sign) with no external consumers. container + container-sign supersede it; the image input added to both gives the sim harness the override it previously relied on build-container-buildah for. sim-buildah now exercises container + container-sign end-to-end.

Fixed

  • release component preserves backtick-delimited content in release descriptions. The previous implementation passed CI_COMMIT_TAG_MESSAGE through the release: keyword's description: field, where release-cli / glab shell-eval'd the value. Backtick-delimited code spans, fenced code blocks, version regexes, refs, and any other backticked prose were interpreted as command substitution and silently dropped from the rendered release page (/bin/sh: eval: line 198: <token>: not found shows up in the job log; everything inside backticks vanishes from the page). The component now writes the annotated tag body to a file via git tag -l --format='%(contents)' (with PGP signature stripping for signed tags) and publishes it through glab release create --notes-file. The body reaches the release page byte-for-byte, with no shell parsing.

  • release component re-runs are idempotent. glab release create updates an existing release by default and the component does not pass --no-update. Re-runs on the same tag (manual replay, Renovate retry, transient CI flake) now refresh the release page with the current tag annotation body instead of failing with release for tag "<tag>" already exists and --no-update flag was specified. The upstream release: keyword wrapper, which always invoked glab with --no-update, no longer mediates the call.

[3.4.0]

ci-tools grows to cover reference-style projects too -- yq + node

  • npm + python3 + make are now baked in alongside cosign / syft / glab / jq. Downstream projects that were patching alpine packages in inline before_script blocks can drop the patches and pull this image.

Note: the reference-check component originally drafted for this release ships from the gitlab-com/public-sector/reference catalog instead, not from this pipeline catalog. The ci-tools expansion below stays here because reference-check (and other reference-style projects) pull this image at runtime.

Added

  • ci-tools image expanded. Adds make, nodejs + npm (Fedora's node 22 / UBI9's available), python3 + python3-pip, findutils, and yq (mikefarah v4.52.5, checksum-verified upstream binary). The reference's own CI was patching these in via alpine apk add chains; consumers pulling ci-tools:v3.4.0 get the full set with one image reference and zero patching.

  • Renovate customManagers rule for catalog component pins in consumer .gitlab-ci.yml files. Picks up pipeline/<component>@vX.Y.Z and proposes bumps against the catalog's GitLab releases.

Changed

  • vale component gains a disable_rules input. Space-separated rule names get appended to the generated .vale.ini as <rule> = NO lines. Intended for voice-specific opt-outs -- projects that legitimately choose against a particular gitlab-docs convention (e.g. em-dash in long prose, "via" in technical writing) without needing to fork the styles archive. Defaults to empty (no rules disabled).

[3.3.0]

Two new catalog primitives — the paper component and the ci-latex image — plus a clutch of consumer-friendly runner_tag and OCI metadata improvements, Renovate hardening, and the dependency freshness pass.

Added

  • New ci-latex catalog image (Fedora 44 base): TeX Live full + latexmk + tectonic + typst + poppler-utils + Node.js + npm + Chromium runtime closure (Playwright-ready). Built and cosign- signed by the catalog's own pipeline like every other ci-* image. Pinned by FROM + dnf install, no runtime curl-fetches. Replaces the upstream texlive/texlive default in the paper component. Node + Chromium added in the same release so consumers that pair PDF rendering with HTML preview capture (the reference's capture-template-previews job) don't have to layer them in.

  • New paper component: build an academic or technical paper PDF from LaTeX or Typst source. Engine-agnostic (latexmk, tectonic, or typst selectable). Default image is the new catalog-managed ci-latex, which carries all three engines. Optional style_url / style_name inputs fetch a shared style package (e.g., gitlab.sty from the public-sector reference project) before build. Emits the PDF as a 30-day artifact at a configurable path; suitable for download, mirror to Pages, or attachment to a release.

  • compliance, container-attest, container-sbom, and vale components now accept a runner_tag input (default saas-linux-large-amd64). Previously these jobs ran on GitLab's default saas-linux-small-amd64, which is undersized for cosign attest, syft SBOM generation, and concurrent vale runs across larger docs trees. The container and verify components already exposed runner_tag; this brings the rest of the catalog in line. Consumers that don't set the input get the new default; consumers that need a different tier pass it explicitly.

  • container component now injects three OCI image-metadata build-args automatically: OCI_VERSION ($CI_COMMIT_TAG falling back to $CI_COMMIT_SHORT_SHA), OCI_REVISION ($CI_COMMIT_SHA), and OCI_CREATED ($CI_JOB_STARTED_AT, ISO 8601). Consumer Dockerfiles that declare matching ARG lines and reference them in LABEL org.opencontainers.image.version|.revision|.created will have the values populated. Consumers that don't declare the ARGs see no effect (Docker logs an unused-build-arg warning, no failure).

    Use case: chain of custody from a running image back to a specific git SHA, release tag, and build moment without any consumer-side --build-arg plumbing. Pairs with the forthcoming container-image standard in reference/ which codifies the full OCI label set Public Sector projects ship.

Changed

  • Fedora base for ci-latex bumped from 42 to 44 (current stable). Two release cycles of TeX Live + toolchain freshness. Renovate is pinned to <=44 so future MRs stay on shipped releases (skips Rawhide).
  • Renovate fileMatch made explicit for both gitlabci and dockerfile managers — presets/*.yml and containers/*.Containerfile are now unambiguously in scope. Defensive; no behavior change for already-tracked files.
  • Dependency freshness pass via the now-self-hosted runner:
    • cargo-audit 0.21.2 → 0.22.0 (ci-rust image)
    • golang 1.25.7 → 1.26.3 (ci-go image; cascade rebuilds ci-go-fips + posture-verify)
    • govulncheck v1.1.4 → v1.3.0 (ci-go image)
    • gitlab-org/cli (glab) v1.92.1 → v1.98.1 (ci-tools image)
    • quay.io/buildah/stable v1.43.0 → v1.43.1 (bootstrap image in catalog .gitlab-ci.yml)
    • UBI9 base images patched to current minor

[3.0.0]

Namespace migration. The group gitlab-com/public-sector-tools is now gitlab-com/public-sector. Every artifact under the catalog's identity moves with it: registry image paths, cosign OIDC identity in signatures, SLSA provenance subjects, catalog component include URLs. The -tools suffix dropped to reflect the seriousness of the work and to keep URLs and provenance simpler.

This is a hard break. v2.x catalog components reference a registry path that no longer exists. Consumers must update their .gitlab-ci.yml include lines from gitlab-com/public-sector-tools/pipeline/... to gitlab-com/public-sector/pipeline/... and bump the version pin to @v3.0.0. There is no compatibility shim.

Removed

  • posture-verify component. The audit/dashboard layer (consumer- declared claims with arbitrary verify shell, fleet-audit ledger refresh) is being divested. GitLab's compliance product is the right home for that work. Reference's reference-check component (lands in v3.1.0) is the standards-conformance replacement at a different layer.
  • ci-kaniko image is not published in v3.0.0. The image depends on gitlab-com/public-sector/kaniko:latest which is not yet republished at the new path. The build job remains in .gitlab-ci.yml with allow_failure: true. Returns whenever the kaniko fork ships at the new path.

Changed

  • Every image path under the catalog moves to registry.gitlab.com/gitlab-com/public-sector/pipeline/.... v-stripped semver tag scheme unchanged.
  • All include: component: examples updated to the new path.
  • Renovate preset and runner config target gitlab-com/public-sector/**.
  • templates/reference/go/verify.go example uses <NAMESPACE> / <TOOL> placeholders instead of the previous tach hardcoded reference.

[2.7.0]

Validation cleanup. The catalog's self-validation surface was scattered across job names that didn't read as validation (syntax, lint-templates, sim-*). v2.7.0 renames the template-validation jobs into a validate: namespace and adds a single roll-up artifact that says "is this MR safe to merge?" in one place.

Added

  • pipeline-self-validates job — runs at the end of the sim stage. Inspects every validate:*, sim-*, build:*, secret_detection, and container_scanning job in the same pipeline via the GitLab Jobs API; emits pipeline-self-validation.md with:
    • Pipeline URL, ref, SHA
    • Diff class detection (containerfile / template / script / ci-shape / sim / preset / renovate-config / misc) from the files changed since the MR base
    • Per-job status + duration table
    • Single-line verdict: SAFE iff no required validation job failed
  • The artifact is paste-friendly for both human review and LLM triage — the "is this Renovate MR safe to merge" answer in one place rather than a separate confidence job per concern.

Changed

  • Validation job names — the legibility move:
    • syntaxvalidate:templates:syntax
    • lint-templatesvalidate:templates:shape
    • sim jobs (sim-go-binary, sim-buildah-build) retain their names (component job_name input regex disallows colons); the verdict job reports them under the same table.

Why

Renovate is actuating bumps weekly. Each bump's "is this safe?" question deserves a structured answer, not lore-based pipeline reading. The verdict artifact compresses six prior validation layers into one paste-into-review unit.

[2.6.1]

Patch release. Scheduled pipelines on the catalog were firing every job (image builds, sims, lint) because rules:changes evaluates as "all changed" on schedule, defeating the change-based gates. v2.6.0's catalog-as-runner intent was: scheduled pipelines run Renovate only.

Fixed

  • if: $CI_PIPELINE_SOURCE == "schedule" when: never prepended to every non-renovate job's rules:
    • .gitlab-ci.yml: 8 image build jobs + create-release + syntax + lint-templates.
    • .gitlab/ci/sim/sim-go-binary.yml + sim-buildah.yml: the job_rules passed to the binary / build-container-buildah components.
  • Result: scheduled pipelines now produce a single renovate job; commit/tag pipelines unchanged.

[2.6.0]

presets/renovate-runner.gitlab-ci.yml — a Renovate runner shape shipped as a catalog asset (companion to the existing presets/renovate.json consumer config). The catalog project itself becomes the Renovate runner for the PST estate by self-including the preset; no sister renovate-runner project is required.

Added

  • presets/renovate-runner.gitlab-ci.yml — self-contained scheduled-pipeline definition for a Renovate bot. Ships the renovate (schedule + web) and renovate-dry-run (MR + feature- branch) jobs, plus defaults for endpoint, platform, ignore- onboarding, require-config, and cache. Operator overrides RENOVATE_AUTODISCOVER_FILTER for the group glob and RENOVATE_REPOSITORIES for the dry-run target.

Changed

  • .gitlab-ci.yml — self-includes presets/renovate-runner.gitlab-ci.yml and overrides the two PST-specific variables. New renovate stage gated by source=schedule/web, so commit/tag pipelines never trigger it. The catalog repo is now the operational center for Renovate as well as the source-of-truth for the components Renovate keeps fresh.

Migration (one-time)

The legacy gitlab-com/public-sector/renovate-runner project is superseded by this self-included setup. Operationally:

  1. Disable the schedule on the legacy runner project.
  2. Move the RENOVATE_TOKEN masked CI variable from the legacy runner project to this catalog project.
  3. Create the pipeline schedule on this catalog project (0 6 * * 1 UTC, ref main).
  4. Archive or delete the legacy runner project.

Why catalog-as-runner

Conceptual sprawl reduction: the catalog is the source of truth for the components Renovate keeps fresh; it's also the right operational center for the bot that does the keeping. One project, one set of practices, one place to look.

[2.5.0]

Ultimate-features alignment release. CycloneDX SBOMs that the catalog already produces (and cosign-attests) now also feed GitLab's Dependency List via artifacts.reports.cyclonedx. The cosign attestation channel was the external-auditor surface; this release adds the GitLab-side ingestion that populates the Dependency Management UI and unlocks Vulnerability Report correlation against SBOM data.

Changed

  • binary template -- declare artifacts.reports.cyclonedx: [dist/*.sbom.cdx.json, dist/*-sbom.cdx.json] so any CycloneDX SBOM the consumer's build_script emits is ingested by GitLab Ultimate. Both naming patterns are matched so cargo-cyclonedx default (<name>.sbom.cdx.json) and consumer variations work without changes.
  • container-sbom template -- declare artifacts.reports.cyclonedx: [dist/sbom/sbom.cdx.json]. The syft- generated CycloneDX SBOM now feeds Dependency List in addition to being cosign-attested against the image digest. Dual channels for dual audiences (external auditor + GitLab maintainer).

[2.4.0]

Catalog dogfooding. The catalog's own image-build pipeline now uses its own ci-buildah image as the build host, replacing the quay.io/buildah/stable bootstrap. Pinned one release behind (ci-buildah:v2.3.0 for v2.4.0 builds) so the chain is non-circular.

Changed

  • Catalog build host image -- .image-build: template under .gitlab-ci.yml now uses registry.gitlab.com/gitlab-com/public-sector/pipeline/ci-buildah:v2.3.0 instead of quay.io/buildah/stable:v1.43.0. Cosign is now baked in, so the runtime cosign install step is removed (saves ~5 sec per image build + drops the GitHub CDN dependency).
  • Bootstrap note in .gitlab-ci.yml updated to reflect dogfooding has landed.

[2.3.0]

Maintenance-signal release. Two enhancements aimed at one job: a single maintainer should be able to grow the estate without surprises -- finding signal should surface automatically in a shape a triage LLM can act on immediately.

Added

  • container-scan-summary component. Renders an LLM-pasteable markdown summary of GitLab Container Scanning findings. Reads gl-container-scanning-report.json, tallies by severity, lists the top Critical + High findings in a table (CVE, package, version, fix-in, severity). Image, pipeline URL, ref are surfaced at the top of the summary. Pair with Jobs/Container-Scanning.gitlab-ci.yml -- container_scanning stays advisory on default-branch and MR (so pipelines don't go red on every CVE), while this component produces a single-page artifact + log block that a maintainer can paste straight into a triage conversation. Default rules: MR + default-branch + tag. The summary is empty-state safe -- if no report is found (rule-excluded upstream), the artifact says so honestly rather than failing the job.

Changed

  • vale template -- emit "vale: no findings on " when vale produces zero output. Eliminates the 0-byte artifact ambiguity (no-op vs clean pass). Artifact + log now record the positive result.

[2.2.0]

Minor release. vale component grows a mode input that splits the ruleset into two presets: project (default) for READMEs and project-level markdown, and docs for content shipping to docs.gitlab.com. Existing consumers that want the v2.1.x behavior pin mode: docs.

Added

  • vale component: new mode input with options [project, docs], default project.
    • mode: project generates a .vale.ini that enables gitlab_base + gitlab_docs packages but disables the rules that only apply to docs.gitlab.com content: gitlab_docs.FrontMatter, gitlab_docs.InternalLinkExtension, gitlab_docs.InternalLinksCode, gitlab_docs.RelativeLinks, and gitlab_base.Spelling. gitlab_base.Spelling is disabled because the upstream rule ships with vocab: false (Spelling.yml:11), which silently ignores any project-side Vocab/<name>/accept.txt. Term-substitution and style rules (Substitutions, SubstitutionWarning, Offerings, British, Uppercase, etc.) still fire and carry the high-value discipline (e.g. airgappedoffline, self-managed capitalization, GitLab branded-term enforcement).
    • mode: docs preserves the v2.1.x behavior: full gitlab_base + gitlab_docs ruleset.

Changed

  • vale component default behavior shifts to mode: project. This is the new sensible default for project-level READMEs. Consumers on v2.2.0+ that ship content to docs.gitlab.com must opt in to mode: docs explicitly. No code changes needed for projects that pin @v2.1.x and don't bump.

Fixed

  • posture-verify honors $-references in template_vars. The v2.0.0 implementation used shlex.quote on every template_var value, which wraps strings in single quotes and prevents shell expansion. Consumers like kaniko declaring variants_all: ["${KANIKO_VERSION}-fips", ...] saw the literal ${KANIKO_VERSION} reach cosign verify, which then failed with could not parse reference. v2.2.0 uses bash double-quote semantics so $VAR / ${VAR} expand against the verify subprocess's environment (inherited from the job). Literal $ must be written \$ in YAML when needed.

[2.1.1]

Patch release. Restores the binary component's runtime cosign install for consumers that override build_image to a non-catalog image (rust:1.88.0, golang:1.25, etc). Surfaced during the assay/posture/tach migrations -- their build_image overrides don't include cosign, and v2.0.0's image-cutover MR removed the unconditional install.

Fixed

  • binary -- conditional cosign install. Skips the install if cosign is already on PATH (catalog ci-go/ci-rust images), curl- installs v2.6.3 otherwise. Restores compatibility with the wide range of consumer-overridden build_image values that worked under v1.x.

[2.1.0]

Additive release. Goal: stable footing for consumer migration. One new component, in-repo sim harness expanded, and the buildkit/kaniko engine components are explicitly deferred with documented gaps so the v2.1.0 contract is honest about what ships.

Added

  • build-container-buildah component. Functionally equivalent to the legacy container component, named for symmetry with the buildkit and kaniko siblings that will follow in later v2.x. Exposes an image input so consumers (and the sim harness) can override the catalog-managed ci-buildah reference. See templates/build-container-buildah/README.md.
  • Sim harness expansion. .gitlab/ci/sim/sim-buildah.yml builds a tiny UBI-micro fixture image via the new component end-to-end and validates cosign sign-against-digest on the just-built ci-buildah image. The harness is the catalog's own integration test surface.
  • lint-templates enforces spec.description ≤ 256 chars. GitLab's parser rejects longer descriptions; the local lint now catches this before MR pipelines.

Changed

  • binary component is CWD-resilient. A consumer build_script that cd's elsewhere no longer breaks the subsequent dist/ manipulation -- every step that touches dist/ cd's back to $CI_PROJECT_DIR first; shopt -s nullglob so empty globs no longer expand to literal dist/*. Surfaced by the sim-go-binary test pre-v2.0.0; carried forward as a quality fix.

Known gaps (deferred to a later v2.x release)

  • build-container-buildkit: POC sim (.gitlab/ci/sim/sim-buildkit-poc.yml, manual trigger via REBUILD_BUILDKIT_POC=true) established that rootless buildkitd via rootlesskit on the gitlab.com SaaS Linux Docker executor errors with No subuid ranges found for user 0 ("root") because rootlesskit refuses to run as root and the runner provides a root user. Real fix needs a non-root user baked into ci-buildkit plus /etc/subuid + /etc/subgid plus GitLab runner accepting the non-root USER directive. The image rework lands in a later v2.x.
  • build-container-kaniko: blocked on factory/kaniko publishing the chainguard-fork kaniko image at :latest. The ci-kaniko Containerfile + build job exist (with allow_failure: true) and unblock automatically once the upstream ships.

[2.0.2]

Patch release. Drops a redundant catalog publish step that 422-failed the create-release job on v2.0.1 even though the release itself was created (and auto-published to the catalog) successfully.

Fixed

  • create-release no longer 422s on the catalog-publish step. glab release create on a project that is already a CI/CD Catalog resource auto-publishes the version. The explicit glab repo publish catalog that followed was redundant and returned 422 {message: Release has already been published}, failing the job. Verified via GraphQL ciCatalogResource query immediately after v2.0.1's release-create: the version appeared in catalog within seconds.

[2.0.1]

Patch release: the v2.0.0 create-release CI job failed to authenticate because glab sent CI_JOB_TOKEN as a Bearer token, which the /api/v4/user endpoint rejects. v2.0.0 was manually published to the catalog as a one-time fix; v2.0.1 makes the CI path work end-to-end.

Fixed

  • create-release CI auth. Added GLAB_ENABLE_CI_AUTOLOGIN: "true" to the job variables. With GITLAB_CI=true (always set in CI), this routes CI_JOB_TOKEN through gitlab.JobTokenAuthSource (not Bearer), which the release and catalog-publish endpoints accept. Same mechanism auto-discovers host/protocol/ca-cert from CI_SERVER_* predefined variables.

    Source: gitlab-org/cli internal/config/config_mapping.go:47-50.

[2.0.0]

Breaking release. Consumers must update their includes to the new shape. Pin to @v1.6.1 to defer the migration.

Highlights

  • Catalog-managed images. Every tool a component needs ships in an image the catalog builds, signs, and publishes at every tag. No more curl-installs of cosign, syft, glab, or skopeo at job runtime. Image set: ci-tools, ci-go, ci-go-fips, ci-rust, ci-buildah, ci-buildkit, ci-kaniko, posture-verify.
  • $[[ component.version ]] interpolation. Every template declares spec:component: [name, version]. Image references resolve to pipeline/<image>:<your-catalog-tag> automatically.
  • Typed, validated inputs. Every input declares type, description, default (or marked required by absence of default), and where appropriate options enums and regex validators. Lint runs on every MR.
  • Modern publish path. Catalog publishes via glab repo publish catalog (glab >= 1.55.0), replacing the release-cli path.

Added

  • containers/ci-tools.Containerfile -- shared UBI9-minimal base with cosign, syft, glab, skopeo, jq, curl, git.
  • containers/ci-go.Containerfile -- Go toolchain layered on ci-tools.
  • containers/ci-go-fips.Containerfile -- Go with GOFIPS140=v1.0.0.
  • containers/ci-rust.Containerfile -- Rust toolchain + clippy + rustfmt + cargo-audit + cargo-deny + cargo-zigbuild + zig.
  • containers/ci-buildah.Containerfile -- rootless OCI build engine.
  • containers/ci-buildkit.Containerfile -- rootless OCI build engine (modern default; replaces archived kaniko upstream).
  • containers/ci-kaniko.Containerfile -- Kaniko (Chainguard fork) for permanent onramp/offramp customers.
  • containers/posture-verify.Containerfile -- tooling for the posture-verify component.
  • CONTRIBUTING.md -- canonical template shape and tag protocol.
  • scripts/lint-templates.py -- enforces the v2 canonical shape on every MR. Run locally with python3 scripts/lint-templates.py.
  • Renovate custom-manager regex for containers/*.Containerfile ARG VERSION pin-comments. Tool bumps auto-merge on minor/patch.

Changed

  • All 14 templates moved to canonical v2 shape. Each declares spec:component: [name, version]; every input is typed; image references use the catalog's managed images via $[[ component.version ]]. The full input map:

    Old (v1, kebab-case) New (v2, snake_case)
    job-name job_name
    image-name image_name
    cosign-version cosign_version (removed where hardcoded image carries the version)
    syft-version syft_version (removed where hardcoded image carries the version)
    tag-suffix tag_suffix
    runner-tag runner_tag
    min-alert-level min_alert_level
    allow-failure allow_failure
    job-rules job_rules
    build-image build_image
    build-script build_script
    package-name package_name
    binary-job binary_job
    attest-job attest_job
    container-image container_image
    container-job container_job
    needs-job needs_job
    clone-path clone_path
    upstream-url upstream_url
    upstream-ref upstream_ref
    security-file security_file
    vale-version vale_version
    styles-archive styles_archive
    arch-suffixes arch_suffixes
    arch-jobs arch_jobs
    final-suffix final_suffix
    skip-if-tag-exists skip_if_tag_exists
    install-command install_command
    build-command build_command
    output-dir output_dir
    fail-if-empty fail_if_empty (and is now type: boolean, was string-typed "true")
  • audit component: language input is now constrained by options: [rust, go, node, python]. Unknown values fail at compose time instead of at script time.

  • extra-assets component: fail_if_empty is now a proper type: boolean (was string-typed "true" in v1).

  • vale component: keeps a runtime install of vale (vale is docs-only and not bundled in ci-tools). All other curl-installs removed.

  • Endorsement language removed repo-wide. "DoD-grade", "FedRAMP-aligned", and "Iron Bank compatible" framings replaced with descriptive equivalents. Standards references (NIST 800-53, CMMC 2.0 L2, SSDF v1.1, FIPS-140-3) retained as factual citations.

  • Catalog publish flow now uses glab repo publish catalog $CI_COMMIT_TAG after glab release create, replacing the release-cli path (release-cli is on a deprecation track for GitLab 20.0).

  • Catalog tag protocol is vX.Y.Z for finals and vX.Y.Z-rc.N for release candidates. ~latest excludes pre-releases (source-confirmed); RC tags publish images but the RC publish to catalog only happens for finals.

Removed

  • cosign_version input from attest, container, container-attest, container-manifest, container-sbom, verify. The version is pinned via the catalog image at the consumer's catalog tag.
  • syft_version input from container-sbom.

Migration guide

For each catalog component you consume:

  1. Bump the include: change @v1.6.1 to @v2.0.0 (or pin to @v1.6.1 to stay on v1).
  2. Convert input keys to snake_case (see the table above).
  3. Drop cosign_version / syft_version where you set them on attest/container/container-attest/container-manifest/ container-sbom/verify.
  4. Audit: if you set language, confirm the value is one of rust|go|node|python (other values now error at compose time).
  5. Binary: the default build_image is now ci-go. If you build Rust, set build_image: registry.gitlab.com/gitlab-com/public-sector/pipeline/ci-rust:$[[ component.version ]].

Internal (catalog repo only)

  • Project Container Registry was enabled (was disabled, blocking CI_REGISTRY_IMAGE resolution).
  • Commit-message push rule regex fixed (double-backslash bug that rejected scoped Conventional Commits; now accepts feat(scope)!: for breaking-change markers too).

Verification

Verified before pushing:

  • The archive URL shape at a pinned ref: ?path=doc/.vale nests under <project>-<ref>-doc-.vale/doc/.vale/, so --strip-components=3 leaves gitlab_base/ and gitlab_docs/ at the StylesPath root, which is what Packages = gitlab_base, gitlab_docs resolves against. 52 base rules, 17 docs rules, 24K archive.
  • The drift claim, by diffing the pinned tag against master the same day.
  • All three styles-resolution branches: baked present (passes, names the pinned ref in the log), baked missing (exits 2), override URL that 404s (exits 2 after retries).
  • Template structure: two YAML documents, every input carries type + description, one job key, no global keywords.

The image build itself runs in this pipeline; that is where the baked layers first execute.

Merge request reports

Loading