Decision log¶
Every design decision behind these components is recorded as a dated spec
under docs/development/specs/, per spec-first development.
This page is the exhaustive index — every spec, in date order. The major
design decisions are also distilled into readable
Explanation pages; this table is the complete,
unfiltered record those pages draw from.
| Date | Spec | Status | Decides | Component(s) |
|---|---|---|---|---|
| 2026-05-15 | cicd v0.1 | approved | Four initial components shipped as v0.1.0: single-file form, the infra-tools image, the core input rules. |
all |
| 2026-05-16 | tofu-plan-apply v0.2 | approved | OIDC-authenticated tofu plan/apply, the GitLab HTTP state backend, manual-gated apply. |
tofu-plan, tofu-apply |
| 2026-05-16 | tofu-apply-plan-sources v0.3 | approved | plan_source (job/ref); the hidden-job extends: pattern; mode-coupled rules:; tag_pattern. |
tofu-plan, tofu-apply |
| 2026-05-18 | gate-component-rules v0.4 | approved | Gate jobs carry an explicit rules: so they run in merge-request pipelines. |
tofu-lint, tofu-security, tofu-validate, zensical-pages |
| 2026-05-19 | token-inputs v0.5 | approved | Token-requiring inputs default to $CI_JOB_TOKEN; the consumer overrides. |
tofu-plan, tofu-apply |
| 2026-05-24 | provider-cache v0.6 | approved | OpenTofu provider plugin cache via TF_PLUGIN_CACHE_DIR, reduces re-download flakes. |
tofu-plan, tofu-apply, tofu-validate |
| 2026-05-27 | module-publish v0.7 | approved | tofu-module-publish publishes the tagged tree to the GitLab Terraform Module Registry. |
tofu-module-publish |
| 2026-08-01 | 0061-releaser-pleaser-ff-tag-commit | approved | On a project combining merge_method: ff with GitLab 19.2's automatic rebase before merge, releaser-pleaser cuts the tag at the Release MR's recorded head. Automatic rebase is what lets a Release MR that is behind its target be merged at all — GitLab rebases it during the merge and never writes that back to the MR; without the setting GitLab blocks the merge until an explicit rebase, which does update the head, so such a project is not exposed. The fleet keeps it on deliberately, for Renovate volume. A fast-forward merge produces neither a merge commit nor a squash commit, so upstream (v0.9.0, latest) falls through to pr.SHA — and GitLab never writes the rebase it performs DURING the merge back to the MR, so that head is permanently pre-rebase. The tag then names a commit that is not on the default branch and the release silently omits whatever landed while the MR was open. Measured: 9 of 231 recent tags across phpboyscout/go, 8 repos. The MR only goes stale when rp declines to force-push, which it does whenever the landed commits do not change the changelog — so the loss set is always the trailing run of docs/chore/ci/style/test commits and released code cannot be dropped this way. Upstream added that fallback deliberately in PR #210 ("support Fast-forward merge", v0.6.1), replacing a loud "pull request is missing the merge commit" failure with a silent wrong release; it merged unreviewed with 0% patch coverage, and no upstream issue tracks it. Adds releaser-pleaser:verify: sets squash: true on the open Release MR so the merge records a squash_commit_sha (a commit GitLab creates on top of the current target head, hence always on the branch — no project-wide squash setting, so the never-squash house rule is untouched) and asserts the tag is on the release branch after each release, exit 2. A separate job because the upstream image has no HTTP client at all. Rebasing the stale MR, and setting squash_option=require fleet-wide, both considered and rejected; forking rp deferred. |
releaser-pleaser |
| 2026-05-30 | module-cache v0.7.2 | approved | .terraform/modules/ cache reduces flakes from transient upstream-registry outages. |
tofu-validate |
| 2026-06-02 | renovate-self v0.8 | approved | renovate-self component + the shared Renovate preset with a cicd-component-pin custom manager. |
renovate-self |
| 2026-06-03 | go-track v0.9 | approved | Four Go components extracted from go-tool-base. | go-lint, go-test, go-security, goreleaser |
| 2026-06-03 | rust-track v0.10 | approved | Five Rust components; disk-pressure tuning; cargo-binstall bootstrap. | rust-lint, rust-test, rust-security, rust-docs, release-plz |
| 2026-06-08 | gitleaks-scan-scoping v0.10.3 | approved | Gitleaks scans scoped to the MR's commit range, avoiding cross-branch false positives on shared runners. | go-security, rust-security, tofu-security |
| 2026-06-12 | release-plz-split-jobs v0.10.3 | approved | release-plz split into separate pr and release jobs to avoid working-tree mutation blocking publish. |
release-plz |
| 2026-06-16 | goreleaser-retry v0.10.5 | approved | goreleaser gains retry_max (default 2) auto-retry on transient failures. |
goreleaser |
| 2026-06-16 | release-plz-order-pr-after-release v0.10.4 | approved | release-plz:pr ordered after release-plz:release to avoid a spurious same-version MR. |
release-plz |
| 2026-06-17 | release-plz-checkout-pinned-sha v0.10.6 | approved | Checkout forced to the pipeline's commit SHA, avoiding a stale branch ref on reused runners. | release-plz |
| 2026-06-19 | renovate-self-token-self-ref v0.10.7 | approved | Token aliased to a non-colliding runtime variable name, avoiding a self-referencing job/group-variable collision. | renovate-self |
| 2026-06-21 | schedule-pipeline-scoping v0.10.8 | approved | Every component except renovate-self gains a leading schedule → never guard. |
all |
| 2026-06-21 | gate-components-tag-scoping v0.11.1 | approved | Gate jobs also skip tag pipelines ($CI_COMMIT_TAG → never) — tags are for publish jobs, not gates. |
tofu-lint, tofu-security, tofu-validate, zensical-pages |
| 2026-06-21 | go-image-default-1.26.4 v0.11.2 | approved | Image defaults bumped to match go.mod's toolchain requirement. |
go-test, go-security |
| 2026-06-21 | goreleaser-gotoolchain-auto v0.11.3 | approved | GOTOOLCHAIN default changed from local to auto, so goreleaser resolves go.mod's toolchain directive. |
goreleaser |
| 2026-06-21 | releaser-pleaser-component v0.11 | approved | releaser-pleaser wrapper component: schedule-never guard + token convention over apricote's image. |
releaser-pleaser |
| 2026-06-21 | self-test-churn-scoping | approved | Self-test triggers scoped to changed components, not run unconditionally. | all |
| 2026-06-22 | components-use-dev-tools v0.14 | approved | go-*/rust-* components default to the consolidated dev-tools image; runtime tool installs deleted. |
go-lint, go-test, go-security, goreleaser, rust-lint, rust-test, rust-security, rust-docs |
| 2026-06-22 | hugo-pages-component v0.12 | approved | hugo-pages component: build+deploy split, an MR build gate hand-rolled Hugo sites lacked. |
hugo-pages |
| 2026-06-22 | hugo-pages-mr-gate v0.13 | approved | mr_gate boolean input — a loose, direct-to-main mode for content sites. |
hugo-pages |
| 2026-06-22 | pipeline-churn-interruptible-cache v0.11.5 | approved | interruptible: true on gate jobs; terminal cache readers go pull-only; stable per-project cache keys. |
all |
| 2026-06-22 | renovate-image-default-43 v0.11.4 | approved | Image default bumped to match the preset's managerFilePatterns usage (≥39). |
renovate-self |
| 2026-06-23 | change-detection | implemented | changes array input on gate components; security category exempt (always-on). |
go-*, rust-*, tofu-*, zensical-pages, hugo-pages, svelte-* |
| 2026-06-23 | skill-security-component v0.19 | approved | skill-security: hidden-char, injection-heuristic, plugin-schema, and gitleaks scans for AI instruction-file repos. |
skill-security |
| 2026-06-23 | svelte-frontend-track | implemented | svelte-build/svelte-lint/svelte-test/svelte-security; the Go↔Svelte embed coupling. |
svelte-build, svelte-lint, svelte-test, svelte-security |
| 2026-06-30 | docs-diataxis-restructure | approved | This site's restructure into Diátaxis — the spec behind the page you're reading. | docs (no component) |
| 2026-07-06 | goreleaser-pro-toggle-v0.20.0 | draft | A pro: true input runs goreleaser-pro (bundled in dev-tools) for Pro-only config — app_bundles/dmg/native-notarize. Driven by krites' signed macOS .dmg. |
goreleaser |
| 2026-07-12 | image-pins-track-latest v0.20.2 | approved | Renovate custom managers (gitlab-tags) auto-track the dev-tools / infra-tools image tags pinned in component defaults + reference docs; stale pins bumped to latest in one pass. |
go-*, rust-*, svelte-*, tofu-*, zensical-pages |
| 2026-07-12 | track-scanner-image-pins v0.21.1 | approved | Renovate custom manager (docker) auto-tracks the scanner tool images (trivy, gitleaks, osv-scanner, semgrep) in the *-security components + reference docs; skewed/stale pins reconciled to latest. |
go-security, rust-security, svelte-security, skill-security |
| 2026-07-12 | preset-default-json v0.21.2 | approved | Renovate preset renamed default.json5 → default.json: Renovate resolves the unnamed gitlab>phpboyscout/cicd preset from default.json only, so consumers were silently missing the component-pin manager. |
renovate-self (preset) |
| 2026-07-12 | osv-scanner-toolchain-and-waiver | approved | osv-scanner: disable call-analysis by default (osv_scanner_call_analysis) to end the GOTOOLCHAIN=local exit-127; always apply the repo .osv-scanner.toml via --config; ship a component-wide GO-2026-5932 (x/crypto) waiver; bump image to v2.4.0. |
go-security |
| 2026-07-13 | bake-zensical-into-image | approved | Bake the Zensical docs toolchain into the infra-tools image (pipx, like checkov); zensical-pages runs the baked CLI instead of pip install --require-hashes -r requirements-lock.txt. Removes the un-rehashable hand-rolled lockfile (and 18 fleet-wide stuck Renovate MRs), drops the python_lock input, pins the toolchain in one place. |
zensical-pages |
| 2026-07-13 | tag-pipeline-workflow-guard | approved | The fleet-wide workflow: dedup rule keyed on $CI_OPEN_MERGE_REQUESTS && push also matched tag pushes, intermittently suppressing the whole tag pipeline so release publish jobs never ran (issue #2). Swap to the tag-safe $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS guard; fix the root pipeline + document it; remediate all 16 consumer repos. No release. |
root pipeline, docs |
| 2026-07-20 | releaser-pleaser-project-scoping | approved | releaser-pleaser v0.8.0's PendingReleases called the instance-wide GET /merge_requests, so one merged-but-untagged Release MR jammed the release job in every project the group token could see (issue #4). Bump the pinned image to v0.9.0, where it is project-scoped; the --owner/--repo fix proposed in the issue is inert under GitLab CI. Add the missing Renovate manager for the apricote + release-plz image pins that let this bump go unnoticed. |
releaser-pleaser, release-plz (pin), renovate.json |
| 2026-07-20 | tflint-authenticated-ruleset-lookup | approved | tflint --init resolves a non-baked ruleset version from api.github.com unauthenticated — rate-limited per source IP, so it 403s on shared runners. Adds a github_token input (default $GITHUB_COM_TOKEN) exported as GITHUB_TOKEN on the tflint job only. The consumer-side half of infra-tools' plugin-dir fix: that one authenticates the bake, this the fallback. |
tofu-lint |
| 2026-07-20 | renovate-terraform-docs-postupgrade | approved | Renovate bumps version = in .tf but never regenerates the terraform-docs README tables, so every Terraform module bump failed terraform-docs-drift by construction. Add a postUpgradeTasks hook via an opt-in named preset, plus terraform_docs_version (the binary is absent from the Renovate image and not Containerbase-installable) and allowed_commands (global-only, so it cannot ship in the preset) inputs on renovate-self. Allowlist anchored to the exact terraform-docs shape — no shell. |
renovate-self, preset, tofu-lint (backstop) |
| 2026-07-20 | renovate-cadence-soak-not-window | approved | Seven repos gated Renovate behind schedule: ["before 6am on monday"] while running it daily, so six of seven runs were no-ops and pin lag was 0–7 days — why the infra-tools v0.4.1 hotfix could not reach consumers. Drop the window for minimumReleaseAge: "3 days" (the Go track's value), and exempt first-party phpboyscout/** releases from the soak via the shared preset. | preset (default.json), renovate.json ×7 |
| 2026-07-21 | rust-resource-group-input | approved | The compile-heavy Rust jobs (clippy, test-linux, test-integration, coverage, cargo-doc) ran concurrently and filled every slot of the shared self-hosted runner with parallel full-workspace compiles, starving other projects. Add a resource_group input to rust-lint/rust-test/rust-docs, defaulting to $CI_PROJECT_PATH_SLUG-rust-compile so a consumer's compile jobs serialise (project-scoped — never affects other projects). Fleet-wide form of rust-tool-base's P0. | rust-lint, rust-test, rust-docs |
| 2026-07-22 | go-test-dind-testcontainers | approved | New config-* projects draft testcontainers-go integration tests that need a Docker daemon. Add an opt-in go-test-integration job (docker:dind service + DOCKER_HOST, Ryuk disabled) mirroring rust-test's test-integration. No runner change (runner1 already privileged) and no dev-tools change (testcontainers-go uses the Docker API). Self-tested by cicd's first Go module fixture — stdlib-only, drives the dind Engine API to run a container, so it validates the privileged runner with zero dependency surface. | go-test, tests/go-test/fixture |
| 2026-07-23 | centralized-renovate-presets | approved | An audit found 51 byte-identical renovate.json files and 69/70 missing osvVulnerabilityAlerts (the gap that hid a HIGH grpc CVE). Move policy into COMPOSABLE presets: a base every repo extends (now carrying OSV alerts + patch+minor automerge + majors-drafted + house posture — live fleet-wide on merge, no per-repo change), plus orthogonal leaves — ecosystem (:go :rust :javascript :tofu :docker :python), static-site supersets (:hugo :zensical), role (:library :application). A project extends its bag of leaves; matchManagers-scoping makes them compose. Adds a renovate-config-validator self-test over every preset. Migration to thin extends is a canary-then-rest follow-on. Pairs with the group-wide ff+pipeline-must-pass merge settings (D9). | default.json (base) + leaf presets, root pipeline |
| 2026-07-22 | renovate-group-run-headroom | approved | The first live renovate-group run processed all 73 phpboyscout/** repos but took 53.7 of the project's 60-min job timeout. The scan is sequential and Renovate's repo order is deterministic, so a run that ever exceeds the timeout drops the SAME tail repos every night, silently. Add a timeout input (default "2 hours") lifting the job ceiling above the 60-min project default — viable because runner1 sets no maximum_timeout. Timeout is the safety fix; persisting Renovate's repositoryCache (to keep the run fast, not just un-capped) is a deferred follow-on. | renovate-group |
| 2026-07-22 | automerge-first-party-cicd-pins | approved | With renovate-group opening phpboyscout/cicd pin bumps across the whole group at once, hand-merging our own CI-verified component pins is the bottleneck. Add one narrow packageRule to the shared preset (default.json) automerging phpboyscout/cicd pins via platformAutomerge (GitLab merge-when-pipeline-succeeds) — the consumer's own green pipeline is the gate, a breaking change leaves the MR open+red. Scoped to phpboyscout/cicd exactly (not image pins or third-party); consumer stricter rules still override. Pairs with the existing first-party soak-skip carve-out. | preset (default.json) |
| 2026-07-22 | renovate-group-autodiscover-component | approved | renovate-self is single-repo and needs a per-repo schedule nobody added — 37/53 go repos silently drifted behind cicd v0.26.0 with the wiring in place but dormant. Add a new renovate-group component running Renovate in autodiscover mode over a required autodiscover_filter (e.g. phpboyscout/**), config-gated (onboarding=false + requireConfig=required) so unconfigured repos are skipped silently. One schedule (dogfooded in cicd, whose filter includes cicd itself) keeps the whole group current and picks up new repos automatically. Reuses renovate-self's token-alias, image-≥39 floor, and terraform-docs postUpgrade mechanics verbatim. Follow-up: dogfood switch after the tag, then a fleet sweep decommissioning the dormant renovate-self usages. | renovate-group (new), renovate-self (sibling), root pipeline |
| 2026-07-23 | go-test-exclude-generated-coverage | implemented | go-test drives its coverage badge off the full cover.out, so mockery/protoc/stringer generated packages (~0% coverage) dilute it well below real hand-written coverage — measured 98.2% → 73.0% from one generated package in go/observability, forcing fragile per-repo paths scoping. Add an exclude_generated input (default true) filtering files that carry Go's // Code generated … DO NOT EDIT. marker out of the profile before the badge is computed, in shell (no image change). paths stays ./...; the raw artifact is untouched; false reproduces today's figure. Issue #5 Part A. | go-test, tests/go-test/fixture |
| 2026-07-23 | releaser-pleaser-stage-ordering | implemented | releaser-pleaser emits needs: [] (from a needs input defaulting to the empty array), which in GitLab means "start immediately, ignore all stages" — so the release job runs concurrently with the lint/test/security gate stages instead of after them, in cicd and fleet-wide (proven: releaser-pleaser started 21s before go-test finished on a main pipeline). Remove the needs: line and the input so the job falls back to stage ordering, matching the already-correct release-plz:release. Consumers wanting DAG order can still override the job. fix, folded into v0.29.0. tofu-apply's own needs: [] (ref-mode tag apply) flagged out of scope. | releaser-pleaser |
| 2026-07-23 | go-changelog-changes-release-ci | implemented | A bootstrap v0.1.0 Release MR changes only CHANGELOG.md, which matches nothing in go-test/go-lint's default changes array, so the first release of a new module is tagged never having passed test or lint and ships an unknown coverage badge (security is exempt — it has no changes filter). Add CHANGELOG.md + **/CHANGELOG.md to both defaults so every Release MR (not just v0.1.0) runs a real test+lint pass before the tag is cut; ordinary code MRs already match **/*.go and are unaffected. Defaults-only patch. Issue #5 Part B. | go-test, go-lint |
| 2026-07-24 | trivy-db-shared-runner-cache | implemented | The v0.29.0 release burst (125 MRs) showed the security jobs are the worst network offenders: every trivy fs re-downloads the 1.2 GB Aqua DB, and every osv-scanner calls api.osv.dev live. Add a shared runner cache (/opt/ci-cache) refreshed once nightly (22:40 UTC, 20 min before the midnight Renovate run): trivy skips its DB update when a DB is present, and osv-scanner runs OFFLINE against a cached ~11 MB Go / ~200 MB npm DB — making the scan hermetic under burst. Guarded on DB presence (osv offline-without-DB hard-errors 127, never a false pass), so with no shared cache both fall back to today's online behaviour. New scanner-db-refresh jobs + scanner-cache-refresh schedule; runner1 volume applied out-of-band. Verified all download/offline commands locally at CI tool versions. | go-security, rust-security, svelte-security, root pipeline |
| 2026-07-25 | tofu-deploy-generate-component | approved | Extract the release-tag promotion generator proven e2e in phpboyscout/infra (selective-env spec D8/D15/D16, prove-then-extract) into a reusable component. A consumer-owned JSON catalog (schema v1 — the versioned public interface) drives a fail-closed stdlib engine emitting one deploy child pipeline per target; children plan from the tag checkout and apply same-pipeline artifacts, killing the stale-plan/rebase race class. Engine bundled in this repo, fetched at exactly the component version (template+engine = one immutable bundle), generator_path overrides with a consumer-local variant. Tier guards: production/other targets forced manual+confirmation; the strict-semver ref gate is hardcoded, never an input. Two modes (generate on tags / verify-fixtures golden-diff on MRs); trigger jobs stay consumer-side (templates cannot loop). | tofu-deploy-generate |
| 2026-07-25 | renovate-gomod-import-paths | approved | Every Go major bump the fleet's Renovate bot opened landed as a broken draft: it wrote the new major into go.mod alongside the old one but never rewrote the /vN import paths, so the code kept compiling against the OLD major and CI went green on a no-op (proven across go-github v88→v89 ×2, azappconfig v1→v2, consul/api v1→v2 — the last caught only by its own TestDependencyFootprint). Cause: gomodUpdateImportPaths was off, so Renovate did the manifest half of a major and left the source half undone. Add postUpdateOptions: ["gomodTidy", "gomodUpdateImportPaths"] to the :go leaf (go.json) so Renovate rewrites import paths itself and lands a go mod tidy-clean manifest — a real, verifiable diff instead of a false-green draft. Go-specific → leaf not base; majors stay drafted (not auto-merged); the full renovate/renovate:43 image already supplies the Go toolchain + mod helper, so no image change. | preset (go.json) |
| 2026-07-26 | renovate-merge-sweep | approved | Renovate cannot complete an automerge on any GitLab project using the fast-forward (ff) merge method: GitLab's merge API requires a sha for an FF merge and Renovate never sends one, so every automerge returns 400 "SHA must be provided when merging" and the MR stays open (confirmed 2026-07-26 by a DEBUG renovate-group run; upstream renovate #26972). The fleet keeps ff (only it gives merge-commit-free linear history), so the automation is fixed around it. Add renovate-merge, a scheduled sibling of renovate-group: one job enumerates the group's open, non-draft, mergeable renovate/* MRs and PUT …/merges each with its head sha (the call that works). Safety-first selection (source-branch prefix guard never touches human MRs; majors excluded as drafts); never rebases; per-MR failures skip, only auth/enumeration fails the job. stdlib-python on infra-tools, own RENOVATE_TASK=merge schedule, dry_run + exclude inputs. Dogfood include + hourly schedule are a post-tag follow-up (as renovate-group did). | renovate-merge (new) |
| 2026-07-25 | stoppable-environments | approved | Non-production environments cost money overnight for nothing. Stop is an overlay apply (scale-to-zero tfvars), never a destroy, so state and data survive; catalog schema v2 adds stoppable + stop_tfvars, tier-guarded so production can never be stopped; tofu-deploy-generate emits a stop lane into each deploy child pipeline; tofu-stop ships as the trunk-pipeline stop lane; tofu-apply gains optional environment recording. | tofu-stop (new), tofu-apply, tofu-deploy-generate |
| 2026-07-27 | renovate-gomod-tool-directives | approved | gomodUpdateImportPaths closed the source-import half of a Go major bump, but Go 1.24 tool directives in go.mod name an import path too and nothing rewrites it to /vN. The old path's dependency tree no longer resolves, so go mod tidy fails and the draft is unbuildable — presenting as a broken upstream dependency rather than an incomplete rewrite (keryx !164, golangci-lint v1→v2). Renovate cannot know which modules a repo lists as tools, so add a prBodyNotes warning to every gomod major on the :go leaf rather than a hand-maintained list. | renovate presets (go.json) |
| 2026-08-01 | 0060-module-publish-stage-default | approved | tofu-module-publish defaulted to stage: .post. Every tofu gate carries if: $CI_COMMIT_TAG → when: never, so on a release tag the publish job was the pipeline's only job — and GitLab does not create a pipeline whose jobs are all in .pre/.post. No tag pipeline was created at all, so the module silently never published and nothing went red (green Release MR, tag present, Release page present, registry empty); three of four iac/* module repos lost their latest tag for two months (issue #8). Default becomes release — what every consumer already declares, and what the one working repo already passed. Guarded twice: a repo-level lint forbidding any component defaulting stage to a built-in stage, and the self-test now takes the default instead of overriding it. Post-publish registry verification considered and deferred. | tofu-module-publish |
| 2026-08-01 | 0062-renovate-enable-pre-commit-manager | approved | Renovate's pre-commit manager is opt-in — documented as beta and off unless a config enables it — so a repo carrying a .pre-commit-config.yaml gets no hook updates and no signal it is missing them: the failure mode is an ABSENCE of MRs, indistinguishable from "nothing to update". Measured 2026-08-01: 13 of 98 active projects carry one, 38 remote hook repos in total, and only 4 enable the manager locally (sigillum, scoutdm, krites, go-tool-base) — the other 9, including all four iac/terraform-aws-* module repos and infra, have been frozen since creation. None restricts enabledManagers, so nothing local blocks a preset-level enable. Enabled once in the BASE preset rather than an ecosystem leaf, because .pre-commit-config.yaml is language-agnostic and the affected repos span Go, Tofu and mixed tracks; it is a no-op where no such file exists. The four local enables are left in place (identical value, no conflict). Expect a one-off burst that the existing automerge policy + hourly merge sweep drain. | renovate presets (default.json) |
| 2026-08-02 | 0063-goreleaser-retry-reasons | approved | goreleaser is the only component setting retry, and it listed stuck_or_timeout_failure — deprecated in GitLab 19.1, so every consumer including the component got a CI lint warning on every validation. The migration is NOT a value swap: 19.1 also moved some failures OUT of runner_system_failure, a value being kept, into runner_external_dependency_failure (registry unreachable — a Docker Hub rate limit lands here) and runner_interrupted (restart, shutdown, spot reclamation). Replacing only the deprecated value would have silently narrowed coverage on runners already running 19.1.0. The deprecated reason expands to its four documented successors, and the two that split away are added alongside the one they came from. Deliberately excluded: runner_configuration_error (an invalid image is deterministic — retrying reaches the same failure slower) and the job_execution_timeout successors (a genuine timeout is not transient and was never covered). The valid value set was established by submitting each candidate to the CI lint API rather than inferred from prose, which also revealed job_execution_timeout is deprecated too. | goreleaser |
| 2026-08-09 | 0064-discord-release-announcements | approved | The Discord announcements channel had nothing publishing to it, and GitLab's own Discord integration cannot fix that without making it worse: its only filtering parameter is branches_to_be_notified (all/default/protected/default_and_protected), a BRANCH filter, so a tag_push_events subscription fires on every tag with no semver awareness — every patch bump from every project, in a group where Renovate churn produces a steady drip of them. It also posts GitLab's generic tag-push message rather than the release notes releaser-pleaser/release-plz already generate. New discord-release component instead: parses the semver tag and announces only when the patch component is 0 (which maps onto Conventional Commits as used here — feat: is a minor, fix: a patch, chore(deps): nothing — so it approximates "something was added"), with announce_patches for headline projects and prereleases quiet by default. Nothing may break a release: allow_failure defaults true and every recoverable condition exits 0, including a MISSING webhook — which is what lets the component roll out across the estate before the Discord webhook exists and start working the day it does. Release notes are best-effort against a real race (the tag pipeline can start before the Release object lands). Deliberate deviation from authoring rule 6: webhook_url names $DISCORD_RELEASE_WEBHOOK as its default, traded for a two-line consumer include across ~20 repos. Filter verified by running the extracted script body locally over an 11-case matrix before it reached CI, which caught two set -e aborts on the degraded no-notes path. | discord-release |
| 2026-08-14 | 0065-renovate-untracked-pin-families | implemented | Two families of version pin were annotated as if Renovate tracked them and were tracked by NOTHING — the failure mode being an ABSENCE of MRs, indistinguishable from "already current". (1) Renovate's native dockerfile manager reads FROM lines, NOT # renovate: comments on ARG/ENV lines; that convention needs a customManager and none existed. The 2026-08-13 group scan extracted "dockerfile": {"fileCount": 1, "depCount": 2} from a dev-tools Dockerfile carrying TEN annotations — the two seen were the FROM and # syntax= lines. Six of the eight missed had rotted, GOVULNCHECK_VERSION by six minors (v1.1.4 vs v1.7.0) — the gate every Go repo's security job runs. infra-tools had the identical hole, all three of its ARG pins stale. Both repos' own comments assert the mechanism exists; it was never written. (2) An image passed as a component inputs: image: is not a job-level image:, so gitlabci never extracts it — go-tool-base sat on dev-tools v0.2.0 for five releases while the SAME image was bumped normally wherever it appears job-level. Dockerfile manager goes in the :docker leaf (all three Dockerfile-carrying repos already extend it; base would misfile container policy across 100+ repos with no Dockerfile); component-input manager goes in the BASE preset, scoped to our own registry so any overlap with gitlabci is a duplicate rather than a conflict. Both match the ANNOTATION/shape rather than a list of names, so a new pin is tracked the day it is written. D4 records that customManagers is REPLACED not merged by an extending repo, so go-tool-base — the repo D3 was written for — must mirror it. D6 records that turning a dormant manager on is not a no-op: TFLINT_AWS_VERSION is interpolated bare into tflint HCL while upstream tags are v-prefixed, so it needs extractVersion or the first bump breaks the image build. | renovate presets (docker.json, default.json) |
| 2026-08-14 | 0066-renovate-group-log-cap | implemented | The nightly group scan failed 3 of the last 10 nights, ALWAYS after logging Repository finished for all 110 repos — so not the mid-run cut-off 0049 addressed — and the reason is unreadable: the trace hit GitLab's 4 MB collection ceiling (4,194,426 bytes, over by 122) and stopped collecting BEFORE the failure. Budget goes on 32 getChangeLogJSON error records, each serialising a full HTTP response plus a 20-frame stack, all GitHub 502 (11) / 504 (21) fetching release notes for aws/aws-sdk-go-v2. NOT auth or rate limiting: GITHUB_COM_TOKEN exists as a group var, no x-ratelimit header appears, and 502/504 are gateway errors not the 403 a limit gives. Fix is to stop losing the tail, not to log less: LOG_FILE/LOG_FILE_LEVEL/LOG_FILE_FORMAT (UNDOCUMENTED on the self-hosted-configuration page — confirmed by reading the shipped code in renovate/renovate:43, whose file stream writes SYNCHRONOUSLY specifically to survive process.exit()) plus an artifact with when: always, since without that clause it would upload only on the runs nobody needs. File level defaults to info, not Renovate's debug, because it uploads every run. D4 explicitly REJECTS suppressing the changelog errors — release notes are the most useful part of a dep MR and the errors are honest. The underlying non-zero exit remains unexplained by design; this makes the next occurrence readable rather than guessing. | renovate-group |
| 2026-08-15 | 0067-release-train-orchestrator | approved | The Go estate is a 76-module DAG EIGHT TIERS deep; a change to go/errors reaches 68 of 76 repos. Renovate is per-repository and stateless ACROSS repositories, so it cannot sequence a cascade — it opens hop N+1 only after a human has released hop N, and it emits incompatible intermediate sets because direct vs indirect is the boundary it uses while core vs adapter is the one that matters (keryx!269, phpbotscout!44 both broke exactly this way). Measured: upstream-first costs 69 releases in 7 ordered rounds, unordered up to 178. Collapsing adapter families into monorepos was measured and REJECTED — it removes width (76 nodes -> 36) but not depth (8 tiers -> 7), so sequencing is still required, and the separation is intentional anyway. New release-train component derives the DAG from go.mod EVERY run (a checked-in tier list would sequence confidently and wrongly), longest-path tiers, cycles are a hard error. plan is side-effect free; run walks the tiers driving a targeted renovate-group pass per tier, and REFUSES to execute on a scheduled pipeline because cutting releases is a per-train decision, never standing. First whole-estate rehearsal found go/artifacts — created the same day as a hand-enumerated sweep and therefore invisible to it, shipping on a Go with two known advisories. | release-train |
| 2026-08-17 | 0068-docs-verify-component | draft | Every quality component is change-detected against code paths, correctly, which leaves DOCUMENTATION with no gate at all: on krites!162 (the MR fixing three stale pages) the only jobs that ran were the security set and zensical-build. Gating the check on docs paths is the obvious design and is WRONG — krites' documented commands went stale because the CODE gained subcommands, so a docs-path filter runs the check exactly when the docs are already right and skips the MRs that break them. So docs-verify is always-on and offers NO changes input, with the self-test asserting its absence structurally. Specced and built CONCURRENTLY by two sessions (this and 0070) which reached the no-changes-input decision independently; the superset implementation shipped as cicd!217. | docs-verify |
| 2026-08-17 | 0069-npm-install-script-execution | implemented | Six of eight npm installs across the svelte-* components ran lifecycle scripts, so a hostile postinstall in any transitive dependency executed on the shared runner during ordinary build/test/lint jobs — and the unprotected installs run earliest and most often. Protection was per-invocation, which is why it drifted. Moved to the npm_config_ignore_scripts JOB VARIABLE so a ninth install site inherits it. Blanket application is NOT sufficient: measured on node 24, the variable SILENTLY SKIPS prebuild/postbuild while npm run build still succeeds, so a consumer generating types in prebuild would lose it with no error. Boundary drawn as 'dependency code must not execute at install', not 'no scripts' — the install is protected job-wide and first-party build commands re-enable explicitly. Both the component header and reference page had claimed the vector was already neutralised; true of two jobs, false of the pipeline. | svelte-build, svelte-test, svelte-lint, svelte-security |
| 2026-08-17 | 0071-retire-db-authenticated-fetch | implemented | retire fetches jsrepository-v5.json from raw.githubusercontent.com ANONYMOUSLY every run and fails the job on HTTP 429; three retries over ~40 minutes all 429'd, and it blocked an unrelated MR plus three in keryx. Applying the 0054 shared-cache pattern does NOT fix it alone — the nightly refresh still has to download, and anonymously hits the same limit. Measured: anonymous 429, authenticated GitHub contents API 200 (571,672 bytes), so the AUTHENTICATED FETCH is load-bearing and the cache is an optimisation. Three tiers, each logged, degrading rather than failing. --jsrepo not --cachedir (cachedir relocates retire's own cache but leaves it deciding whether to re-fetch). The fetcher is DETECTED not assumed — a first version hardcoded wget on the strength of checking node:24-alpine, but npm_image defaults to dev-tools which has curl and NO wget; it shipped red. Verified the gate still DETECTS with a local DB (exit 13 against a jquery 1.4.2 fixture) rather than passing quietly. | svelte-security |
| 2026-08-17 | 0072-opentofu-required-version | implemented | Renovate's terraform manager HARDCODES hashicorp/terraform as the depName for every required_version — not a default, unconfigurable in the extractor — so an OpenTofu estate has its core constraint bumped against a different product on a different version line (Terraform 1.15.8 vs OpenTofu 1.12.5). The resulting ~> 1.15.0 satisfies no OpenTofu release and broke tofu init/validate/plan on every stack in phpboyscout/infra. The ticket proposed DISABLING the bump; re-pointed instead via overrideDepName/overridePackageName, since disabling leaves a hand-maintained pin nobody tracks — the failure 0065 exists to prevent. Proven with a controlled --platform=local pair: without the rule ~> 1.10.0 resolves to ~> 1.15.0, with it to ~> 1.12.0. Also tracks .opentofu-version, which no built-in manager reads. Renumbered from 0068 after a concurrent session claimed that number. Corrected a FALSE belief held in 0065 and go-tool-base's config: customManagers/packageRules are mergeable:true and CONCATENATE across presets; labels/extends do not. | renovate presets (tofu.json) |
| 2026-08-17 | 0073-release-stamp | implemented | org 0001 D3 moved issue closure to MERGE — the right boundary, since a merged MR means no engineering action remains, but it drops which release actually SHIPPED the work. On cicd that gap is 0–2 days; on keryx it has run to a fortnight, during which a ticket reads done and the thing is not yet usable from a tag. NEITHER release tool can close it: releaser-pleaser's Forge interface has no comment method at all and release-plz's forge client has no /comments endpoint — feature requests upstream, not configuration. The chain IS derivable from Free-tier endpoints (compare -> commits/:sha/merge_requests -> merge_requests/:iid/closes_issues), verified live against v0.36.0..v0.37.0: 18 commits, 13 MRs, exactly the 4 issues that release closed. A COMMENT not a label (org 0001 D10 — a label needs a value per version, needs creating per release, and goes stale). IDEMPOTENT on a hidden marker, because a retried tag pipeline would otherwise re-stamp every issue. allow_failure defaults TRUE like discord-release: the tag and artefacts already exist by then, so a red release pipeline over a missing annotation is worse signal than a yellow one. Refuses rather than guessing when the tag is absent from the version-ordered tag list, and filters cross-project issues out by numeric project_id. Two real defects the live dry run caught that unit tests had not: closes_issues returns NO references field (the partition keyed on an invented shape and classified every real issue foreign), and an iid-only dedup key collides across projects. | release-stamp |
| 2026-08-17 | 0074-hardened-base-images | implemented | Measuring before deciding INVERTED the ticket's premise. dev-tools carries 2582 CVEs (2472 os-pkgs) and infra-tools 665 (399 os-pkgs), but the images' own gate is --severity HIGH,CRITICAL --ignore-unfixed and EVERY gate-visible finding in both — 66 and 158 — is lang-pkgs: Go stdlib and golang.org/x/* compiled into third-party release binaries (goreleaser-pro, golangci-lint, syft; terraform-docs, gitleaks, tflint-ruleset-aws, trivy, tofu). Not one OS package reaches the gate, because Debian rates its base CVEs below HIGH or ships them unfixed. So the base swap removes NONE of the churn #9 was raised to fix — that needs its own remedy (D8). It is still worth doing: a controlled pair built and scanned with the same package set took the OS layer from 2468 findings (20 CRITICAL, 235 HIGH) to ZERO. But it does NOT shrink the image — measured 257 MB on Wolfi against 176 MB on Debian, so a size claim is false. Wolfi over Docker Hardened Images: DHI went free (Apache 2.0, Dec 2025) so cost is not the discriminator; DHI's value is concentrated in DISTROLESS runtime images and a CI image cannot be distroless (it needs a shell, a package manager and compilers by definition), and DHI's glibc line is Debian, which keeps the archive being escaped. glibc is the hard constraint that rules out every musl option — cargo-binstall runs --disable-strategies compile (prebuilt-only, no fallback), and aws-cli v2 publishes no musl build. Change the base but NOT the acquisition mechanism: Renovate has no apk manager, so moving ~12 pins to apk add pkg=1.2.3 would blind them all — the exact failure 0065 exists to prevent. wolfi-base has ONE real tag (latest; the rest are cosign artefacts), so it must be digest-pinned with pinDigests or it regresses the just-landed floating-image fix. AMENDED 2026-08-18 to fold in DECOMPOSITION, whose case turns out to be stronger than the rebase's: attributing every megabyte to the components that use it shows all 66 of dev-tools' gate-visible findings sit in Go-track and Node tooling (goreleaser-pro 19, golangci-lint 17, syft 14, goreleaser 9, Node 7) and NOT ONE in the Rust toolchain — which is the largest thing in the image at 920 MB. So a Rust repo pulls 2.7 GB and inherits 66 HIGH/CRITICAL findings from tools it never invokes. Split by track into go-tools / rust-tools / node-tools / tofu-tools / docs-tools / ci-base: a Go repo drops 60%, Svelte 83%, Tofu 87%, an automation-only job 94% (discord-release pulls 2.36 GB TO RUN CURL AND JQ; release-stamp/release-train/renovate-merge pull 2.72 GB for stdlib-only python3). ONE repo, one release stream, several build targets — splitting the repository would mean six release streams and is the wrong half to split. Spec 0024's consolidation win SURVIVES because it was about BAKING (deleting runtime cargo/go installs), not about one-image-ness, and 0024 D3 already establishes the pattern by running each security scanner from its own upstream image. Three deletions needing no split: 141 MB of Go build cache is published inside dev-tools, aws-cli is 257 MB and NO component invokes aws (OIDC goes through the Tofu provider), and tofu-security is the last security component still scanning from the monolith (438 MB). D16 (2026-08-18): keryx and krites embed a Svelte SPA via //go:embed built by goreleaser's go generate before-hook, so their goreleaser job needs Go AND Node in one image. This does NOT block go-tools, because of HOW it fails: build-web.sh exits 0 when npm is missing and leaves the committed placeholder.html, and spaFromFS serves that placeholder — so a Node-less image would build, SIGN, NOTARIZE and PUBLISH a release with a placeholder UI, entirely green. That is a latent hazard TODAY; it only works because dev-tools happens to carry Node. Fix: make the generator CI-aware — outside CI unchanged (build if npm present, placeholder if not, the local-dev affordance), inside CI never build and FAIL LOUDLY if the prebuilt bundle is absent. No copy step needed: svelte-build's output_dir already defaults to embed, its artifact is **/embed, and GitLab restores to the same relative path, so the bundle lands in place — the CI branch is a verification. Verified: svelte-build's if default already includes $CI_COMMIT_TAG so it already runs on tag pipelines; pkg/studio/web/embed/* is gitignored except placeholder.html so the restored artifact is IGNORED not untracked and goreleaser's dirty-tree check still passes (krites lost v0.5.0 to exactly that check); and krites is unaffected by the split anyway since its goreleaser runs tags:[macos] on a shell runner. Side benefit: a broken SPA build is currently found only at tag time as a failed release — svelte-build on MRs moves it pre-merge. IMPLEMENTED 2026-08-19 as cicd v0.38.0: six images published, all twenty components moved, verified against the tag. Measured — ci-base 48 MB/0 findings, node-tools 112 MB (-89%), docs-tools 67 MB, tofu-tools 131 MB (-79%), go-tools 423 MB (-57%, 66->46), rust-tools 589 MB with ZERO findings of any severity, which is the empirical form of D9's argument that none of dev-tools' 66 came from Rust. Four asserted claims became measurements: the Wolfi base is clean; glibc is load-bearing (cargo-binstall prebuilt-only ran on it); the Rust toolchain contributes none of the findings; and RENOVATE OPENED THE ci-base BASE BUMP UNPROMPTED OVERNIGHT — the load-bearing claim under one-repo-per-image. Five defects the spec could not have predicted: Wolfi creates neither /usr/local/bin, /usr/local/sbin nor /usr/local/include though the first two are on PATH; SHELL must follow the apk add that installs bash; Go's .sha256 sidecar returns HTTP 200 and an HTML page; mktemp -t name-XXXXXX.py fails on busybox; and CHANGING THE USER A JOB RUNS AS INVALIDATES ANY CACHE THE PREVIOUS USER WROTE (npm's root-owned cache broke non-root node-tools with EACCES — fixed by changing the cache key, not by reaching for root). The near-miss worth keeping: release-stamp and release-train self-tests tolerate exit 1 to assert a documented refusal, and the mktemp breakage also exits 1, so both reported SUCCESS while the engine had never run — a failure-path test must assert the failure it EXPECTS, not merely that a failure occurred. | dev-tools, infra-tools → go-tools, rust-tools, node-tools, tofu-tools, docs-tools, ci-base; component image defaults; keryx + krites build-web.sh |
| 2026-08-20 | 0075-go-release-orchestrator | draft | Replace releaser-pleaser with a Go release orchestrator we own, rather than forking it. Three measured findings drive it. UPSTREAM IS NOT MAINTAINED: 2 human commits in the last 60, the most recent 2026-07-05 and it was the v0.9.0 release itself; everything since is renovate[bot]; our issue 463 and its accompanying PR 462 (+398/-1, mergeable) have sat unacknowledged for three weeks. THE BUG IT WILL NOT MERGE IS A CORRECTNESS BUG: GitLab 19.2 rebases before a fast-forward merge and does not write the result back to the MR record, so the recorded head stays at the pre-rebase commit; upstream picks MergeCommitSHA, else SquashCommitSHA, else that head, so FF-without-squash tags a commit not on the target branch — measured 9 of 231 tags across 62 projects, omitting docs/chore/ci/style/test commits. WE ARE NOT EXPOSED ONLY BECAUSE spec 0061 forces squash:true, which makes the squash commit on-branch by construction — verified 5/5 open release MRs carry it and 48/48 recent tags are reachable from main, so ensure_release_mr_squash: false re-opens the bug and the input's description should say so. AND WE ALREADY OWN THE PARTS: go/forge is 8,308 LOC + 7,499 test with four adapters, auth and release reads — roughly 4x the whole tool it would replace — plus go-tool-base's cmd/changelog and gtb sign. D1 build not fork (a fork's only gain is dropping the squash mitigation, which currently holds, and it buys nothing toward the goreleaser seam). D2 BEHAVIOURAL PARITY IS NOT A REQUIREMENT — we own all 96 consumers and define the interface; what replaces parity as the constraint is migration sequencing, since 96 projects release automatically and a new tool's bugs land as permanent tags, so rollout is cicd first, then the forge family, then fan out, never a flag day. D3 GitLab and Go ONLY, with Rust/release-plz (12 projects, and a materially healthier upstream at 1,453 stars) and the other three forges as NON-GOALS not deferrals — the unification argument is acknowledged and may become compelling, but scoping for it now is what would prevent it later. D4 the goreleaser seam is IN SCOPE: a tag cut by one tool and binaries attached by another leaves a window where a release object exists with no assets (a live broken krites update path, seen 2026-08-18), and owning both ends removes the seam rather than managing it forever — the strongest argument for greenfield over fork. D5 never trust a forge's MR record for what landed; resolve from the target branch. D6 merging a release MR stays a human action. BLOCKED ON go/forge#12 (merge-request lifecycle, GitLab first) — the only capability the orchestrator needs that the forge modules lack, and the core of what it does. Open: where it lives (gtb subcommand vs new module), how MR state is carried, the changelog contract, migration mechanics (a dry-run that agrees with the old tool before cutover), and releaser-pleaser.yml's deprecation. | releaser-pleaser, goreleaser; future new component |
| 2026-08-21 | 0076-playwright-tools-image | approved | svelte-test's playwright job has not run since the 0074 image split, and allow_failure reported the 12-second failure as a GREEN pipeline for three runs (krites: green through 08-18 17:56, failed 08-20 x3, two of them on main) — both e2e consumers, krites and keryx, shipped without e2e coverage and without a signal. THE CAUSE IS NOT THE ONE IT LOOKS LIKE: the visible error is su: must be suid, which reads as a non-root problem, but running as ROOT only moves the failure to sh: apt-get: not found — --with-deps CAN NEVER WORK ON WOLFI, because Playwright has no apk path, detects an unsupported OS, falls back to ubuntu24.04 and shells to apt-get. So 'run the job as root' is the obvious-looking fix that would have traded away a deliberate 0074 decision to buy nothing. MEASURED (du -sm /usr on node-tools:v0.1.0): baseline 321 MB; +26 apk packages for Chromium's runtime = 711 MB (+390); playwright chromium-headless-shell+ffmpeg 267 MB; full chromium+shell+ffmpeg 655 MB; Wolfi's own chromium package +1,080 MB — the last because it is a DESKTOP browser: only ~380 MB is chrome, the rest is Qt6/GTK4/systemd/x265/librsvg/two Pythons plus 192 MB of libLLVM so Mesa's llvmpipe can JIT shaders. D1 separate playwright-tools image, NOT a fatter node-tools: baking would take 117 MB -> ~780 MB, a 6.5x inflation of the image serving svelte-build/svelte-lint/vitest estate-wide to serve one opt-in job two projects enable, spending the split's headline -89% win; 0024 D3 already establishes per-tool images. D2 provide the libs via apk as root then drop to ci, and DROP --with-deps; the working 26-package set was found by walking the missing-library chain to a successful LAUNCH, not copied from docs — verified by chrome-headless-shell --dump-dom reaching Chromium startup (the dbus socket errors are benign in a container). Wolfi naming traps that each look like 'no such package': nss/nspr are libnss/libnspr (a plain nss exists and is NOT the one with libnspr4.so), atk is libatk-1.0 + libatk-bridge-2.0 (versioned in the package name), font-dejavu is ttf-dejavu. D3 KEEP FULL MESA: libgbm.so.1 is genuinely required, and mesa-gbm is a false economy — it still pulls libLLVM via mesa-libgallium, saves only 40 MB of 390, and adds a libudev.so.1 dependency the full package satisfies. D4 RESOLVED — bake chromium-headless-shell (267 MB) not full chromium (655 MB): pure CI, never headed. Needed checking rather than asserting because 'headless CI' != 'the headless-shell binary' — Playwright's chromium project historically resolved to the FULL browser and both consumers use the default channel with no channel: set (projects: [{name:'chromium', use:{...devices['Desktop Chrome']}}]). Tested directly with ONLY the shell installed against that exact config: 1 passed. Modern Playwright auto-selects the shell when headless on the default channel, so NEITHER CONSUMER NEEDS A CONFIG CHANGE. This holds because the versions match exactly, not approximately: krites and keryx both pin @playwright/test 1.62.1 and the test ran 1.62.1 — a version-dependent behaviour, which is what makes D5's pin load-bearing rather than tidy. D5 PLAYWRIGHT_BROWSERS_PATH + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD so a consumer's npm ci cannot silently fetch a second copy; the image's Playwright version is the pin. D6 RESOLVED — SPLIT THE JOB IN TWO rather than dropping or parameterising allow_failure: playwright:preflight is hard-gated and launches Chromium through the CONSUMER'S OWN @playwright/test (the pairing that must hold, since Playwright refuses to drive a browser build it does not recognise), and playwright keeps allow_failure:true with needs:[preflight] so a broken browser means the suite does not run at all instead of running, failing and being forgiven. Verified negatively against an empty browser path: exits 1 in seconds naming the executable it wanted. Dropping allow_failure outright was rejected because it lands two neglected suites' accumulated drift on consumers' merges on day one; an input was rejected because it defers the decision, and the current default is what hid three failures. D7 (NEW, found while writing the self-test) — THE E2E GATE BECOMES AN INPUT BECAUSE IT WAS UNTESTABLE: svelte-test's self-test only ever covered the when: never disabled path, and could not do otherwise — the e2e rule was HARDCODED to $RUN_E2E on merge-request/default-branch pipelines, while a component self-test runs in a CHILD pipeline whose CI_PIPELINE_SOURCE is parent_pipeline. A hardcoded gating expression made the component's most fragile path permanently unreachable by its own tests — not why the job broke, but why nothing caught it. e2e_if now defaults to exactly the old expression (no consumer behaviour change) and the new tests/svelte-test/e2e.gitlab-ci.yml sets it true against a real browser. IMPLEMENTATION MEASURED on the built image: /usr 711 MB, browsers 267 MB, and the 27 Chromium runtime packages introduce ZERO new OS scan findings over the node-tools baseline. SEQUENCING: playwright-tools cannot publish its first tag until ci-base -> node-tools clears two INHERITED busybox HIGHs (CVE-2026-38753/4) that node-tools has been failing scheduled rebuilds on since 08-20; current wolfi-base already carries the fixed busybox 1.38.0-r1. Rollout note: krites/keryx e2e suites have not run since 08-18 and should be EXPECTED to fail on accumulated drift before they pass. | svelte-test, node-tools → new playwright-tools; krites, keryx |
How to read this¶
- Status mirrors each spec's frontmatter:
draft(under discussion, not yet implementable) →approved(safe to implement) →rejected(considered and declined — kept, not deleted) →implemented(shipped). Most specs here go straight fromapprovedto shipped without an explicitimplementedflip; treatapprovedentries whose described behaviour matches the currenttemplates/as shipped. - This table is generated by hand and kept current as part of the contributing workflow — every new spec gets a row here when it lands.