v5.0.0
Major Changes
-
customJobsjob model:needsStagesis removed in favour of semantic requirements — declarerequires: [{ capability: "deployment" }]instead ofneedsStages: [{ stage: "deploy" }](capabilities:build,qualityGate,deployment, withartifacts,fromandstrict). Jobenvironmentkeys are now platform-neutral:on_stop→onStop,auto_stop_in→autoStopIn, andactionis narrowed tostart|stop|access. Requirements that consume artifacts (artifacts: true) now fail pipeline generation when no job provides them, instead of silently deploying without them. -
Cloud Run
DATABASE_URL/DATABASE_JDBC_URLnow default todbConnectionStringVariablesMode: "embedded": connection strings contain the component's final values instead of$DB_USER/$DB_PASSWORDplaceholders, making them referenceable from other components (${otherComponent:DATABASE_URL}). Embedded mode also respectsvars.publicoverrides of theDB_*vars — a component reusing another component's database viadbBaseNameplusDB_PASSWORD: "${owner:DB_PASSWORD}"now gets the owner's password in its connection string (previously the component's own, usually unset, secret was embedded, producing a password-less URL). The deployed runtime values are unchanged for the common cases, since legacy placeholders were expanded at deploy time anyway — but the generated pipeline YAML changes for every project withcloudSql. SetdbConnectionStringVariablesMode: "legacy"to restore the old behaviour; that escape hatch will be removed in a future major. -
@catladder/pipelineis no longer published to npm. It was always an implementation detail compiled into the CLI;@catladder/cliis the only published package. Projects importing@catladder/pipelinedirectly must switch to the@catladder/cliexports. -
Job images are now always built from your repository (
jobImages: "repo"behavior); the central image registry mode has been removed. Image definitions are materialized into.catladder-generated/images/, tagged by content hash and built by the pipeline only when missing. Projects still configured with the central mode must drop that setting and commit the materialized image definitions on the next regeneration. -
Job images slim down. The
jobs-testing-chromeimage is removed and test/verify jobs now default tojobs-default: if your tests need a browser, pointtest.jobImage(orverify.jobImage) at an image that carries one — best the official playwright image matched to your dependency, e.g.jobImage: "mcr.microsoft.com/playwright:v1.49.0-jammy"— or declare a project image viaimages. The failure mode without this is a loud "browser not found" in the test job.jobs-defaultitself is trimmed: basenode:22(was the EOLnode:18) with node 20/22/24 pre-installed (was 12–24). Pre-installed node versions are a cache, not a contract — a job with an.nvmrcalways gets its version (pre-warmed ornvm installed at runtime), a job without one runs the shipped LTS base, which may bump in future minors. Pin with an.nvmrcif your project depends on a specific node version.
Minor Changes
-
Agent skills: catladder ships agent-facing documentation as skills (
catladder-config,catladder-builds,catladder-deploys,catladder-secrets,catladder-releases,catladder-pipelines,catladder-cliwith a generated command reference). They are materialized into.claude/skills/on every generation (cross-agent.agents/skills/opt-in), so AI coding agents always see documentation matching the installed catladder version. -
catci, catladder's CI companion: a tree-shaken bundle materialized into.catladder-generated/catci/on generation, running the release, security-audit and npm-publish logic in CI with plainnode. Job images no longer need catladder installed, and CI always runs exactly the version that generated the pipeline. -
catenv is interactive when a human runs it: attached to a terminal it can now run the gitlab token setup and vault unlock prompts, instead of aborting with a
NonInteractiveErrormid-wizard. Under direnv, turbo or CI (stdout not a TTY) it stays non-interactive automatically, and--non-interactiveforces that on a terminal too. In non-interactive runs a missing gitlab token now fails fast with the remedy ("runcatenvonce in a terminal") rather than starting a prompt it cannot answer, and the bitwarden unlock prompt is suppressed as well. -
The catladder store:
.catladder-store/store.ymlis a committed, non-secret record of machine-fetched facts (currently gcloud project numbers), populated bycatladder project setup. It makes Cloud Run URLs deterministic at generation time, so generating a pipeline no longer requires cloud access or authentication. -
Changesets guardrails: merge-request pipelines get a
🦋 changeset checkjob reporting what merging would release (changesets added by the MR, everything pending, the resulting version with a changelog preview) — as a warning-only job with an exposed artifact on GitLab (sticky MR comment whenGL_TOKENis available to MR pipelines) and a sticky PR comment on GitHub. The⚠️ force create releasejob now releases a patch bump even without pending changesets, as the recovery path for changes merged without one. -
The github review workflow gains a generated
catladder ✅job that succeeds exactly when every other review job succeeded (needs:all of them,if: always(), skipped counts as failed;continue-on-errorjobs like the audit stay non-blocking). It exists to be the one required status check in branch protection: requiring real job names goes stale on every component change — a renamed required job blocks its own PR forever, a new component's jobs are silently not required — while this context never changes. Combined with the repo's "Allow auto-merge" setting this restores gitlab's built-in "merge when pipeline succeeds" on github. -
GitHub Actions backend:
pipelines: { gitlab: true, github: true }generates workflows for both CI systems in parallel from one config, including build, deploy, review-environment and release jobs. Job scripts are externalized into committed files instead of being inlined into the workflow yaml. -
GitHub environment secrets: secrets are scoped per environment instead of per repository, so
stageandprodcan hold different values for the same variable.secrets-sync-githubtakes an--envfilter to sync a single environment. -
Manual gates on GitHub: manually gated jobs (e.g. a prod deploy) and their build closure are routed into a
workflow_dispatch"manual tasks" workflow, where each gated job becomes a dropdown choice — GitLab's manual jobs without needing GitHub Environments (Enterprise-only approvals). -
The
pagesdeploy type now works on the github backend. The deploy job hands the built site toactions/upload-pages-artifact+actions/deploy-pages, targets thegithub-pagesenvironment and reports the published url from the deploy step, withpages: write/id-token: writegranted on the job. The repository's Pages source must be set to "GitHub Actions" once. Per-merge-request previews stay gitlab-only: github serves one site per repository anddeploy-pagesreplaces all of it, so review environments log why they were skipped instead of generating a deploy job. -
Two new agent skills guide migrations that used to be tribal knowledge:
catladder-migrate-release-methodwalks a project from semantic-release to changesets (or back), including backfilling changesets for everything merged since the last release tag so the changelog and version bump stay correct — asking how to group the changes when the history is large or ambiguous.catladder-migrate-ci-backendguides a move between GitLab CI and GitHub Actions: the blockers to check up front, running both backends in parallel viapipelines, mirroring secrets, the vault and registry consequences, and the manual cleanup a cut-over needs. -
New
npmPackagedeploy type: publishes a component to npm, deriving version and dist-tag from the pipeline trigger — tagged releases publish the tag version aslatest, branches and merge requests publish0.0.0-<slug>-<sha>canaries (withnext/betabranches getting their own dist-tag). Works in workspace monorepos and authenticates via theNPM_TOKENsecret. -
npmPackagecomponents can publish to npm without a stored token, using npm trusted publishing (OIDC). On github the publish job now declaresid-token: write, and when the workflow can mint an OIDC token catladder skips the.npmrcentirely and lets npm exchange the token for short-lived credentials — provenance is attested automatically.NPM_TOKENstays supported and still wins when it is set, so gitlab and untrusted workflows are unaffected.Publishing moved to its own
npm-publishjob image, because trusted publishing needs npm >= 11.5.1, which thejobs-defaultnode does not carry.To use it, register the package's trusted publisher on npmjs.com with the repository and the workflow filename that publishes it (
catladder-release.ymlfor tagged releases). npm allows one trusted publisher per package, so canary publishes from the main-branch and review workflows still needNPM_TOKEN. -
New agent skill
catladder-migrate-package-managerguides a project from yarn to pnpm (or back). It covers what catladder switches on its own once it detects the package manager — install commands, caches, docker prod install, audit and build/start defaults — and, more importantly, the repository work it does not do: the blockers to check first (meteor is yarn-only, Yarn PnP, yarn plugins), writingpnpm-workspace.yamlbeforepnpm importso the resolved versions survive the migration, and the fallout to fix locally rather than in a pipeline round-trip. A reference page maps every yarn command and config key to its pnpm equivalent and catalogs the behavioral traps that broke real migrations: phantom dependencies, opt-in install scripts, the rootpreparerunning in--prodinstalls, thepnpm runbanner,shnot expanding**, the missing root-script fallback, and pnpm 11 reading settings only frompnpm-workspace.yaml. -
New
pagesdeploy type: publishes a static site (docs, storybook, coverage reports) to GitLab Pages, with per-environment previews. Custom deploys additionally gainedartifactsPathsandallowFailure. -
pnpm support for node builds: the package manager is autodetected (the
packageManagerfield in package.json, then the lockfile, falling back to yarn) and can be pinned with the new top-levelpackageManager: "pnpm" | "yarn". Install commands, the default build/lint/test/audit commands, the docker production install (pnpm install --prod --frozen-lockfile --filter <component>..., the equivalent ofyarn workspaces focus) and the CI caches all follow the detected manager. pnpm projects cache a project-local.pnpm-store(keyed per pnpm major, since store formats change on majors) alongside a lockfile-keyednode_modulescache; workspace manifests,pnpm-workspace.yamland patch files travel into the docker build context as a single layer, and job images ship pnpm. Yarn projects generate byte-identical pipelines. Themeteorbuild type remains yarn-only. -
catladder project doctordetects drift betweencatladder.tsand the actual infrastructure — missing or misconfigured access tokens, service accounts, IAM bindings, registries, namespaces and store entries — and reports what to run to fix each finding. -
Project-declared job images: declare Docker images under
imagesin catladder.ts and reference them in anyjobImagefield via{ image: "<name>" }— build, test, custom/pages deploy, and verify jobs alike. An image is declared either with a directory ({ dir }, used in place — nothing is copied into.catladder-generated) or with an inline Dockerfile ({ dockerfile: string | string[] }, materialized into.catladder-generated/images/project/<name>/Dockerfile); both supportcontext,buildArgsandhashExtraPaths. Catladder generates a🐳 image <name>job that builds the image content-hashed into the project registry underjob-images/and skips when the tag already exists, on both the GitLab and GitHub backends. Catladder's own built-in image build jobs are renamed from🐳 job image <name>to🐳 catladder image <name>to tell the two apart. -
Configurable release method:
releases.methodchooses between"semantic-release"(default, commit-message driven) and"changesets"(intentional releases declared in.changeset/*.md, version derived from git tags — no root package.json required). Both methods share the same release job, changelog maintenance and mandatory security-audit gate. -
requiresInstallreplacesrequiresYarnInstallon thecustomandpagesdeploy configs, since the install now runs with whichever package manager the project uses.requiresYarnInstallkeeps working as a deprecated alias with identical behavior. -
Per-pipeline-type
runnerVariables:pipelines: { gitlab: { runnerVariables: {…} } }sets CI variables for one backend only, for tuning that differs between GitLab runners and GitHub runners (memory requests, concurrency limits) without leaking into the other. -
Scoped, non-interactive secrets commands:
catladder secrets pull,push,setandlistmanage individual secrets or whole environments from the command line without prompts — scriptable in CI and usable by AI coding agents, unlike the interactiveproject config-secretsflow. -
Secrets vault: the source of truth for secrets is configurable (
gitlabfor the legacy behavior,bitwarden), with a local cache and mirroring into CI. Vault values are classified as secret or variable, so non-sensitive configuration is no longer masked and hidden like a password. -
project setupnow configures github merge gating, andproject doctorverifies it. A fresh github repository lets PRs merge while checks are still running and never shows the auto-merge button — gitlab ships this behavior built-in. Setup enables auto-merge (+ delete-merged-branches) and adds a branch protection rule on the default branch requiring the generatedcatladder ✅check, merging non-destructively into any existing protection. Admins can still bypass per PR. On github-only projects, setup now also skips the gitlab-specific provisioning (access token, agent webhooks) instead of failing. -
The generated yarn audit job actually audits the project.
yarn npm auditchecks only the current workspace's direct dependencies, so in a monorepo it checked the root manifest — which usually has no production dependencies — and the 🛡 audit job passed without looking at a single app. It now runs with--all --recursive(every workspace, transitive dependencies), which is whatpnpm audit --prodand classicyarn auditdo on their own; classic additionally gets--groups dependenciesso--environment productionand its behaviour match. Expect a previously green audit job on a yarn monorepo to start reporting long-standing vulnerabilities — they were always there, nothing was checking.The severity threshold is configurable now:
audit: { level: "high" }on a build config (info|low|moderate|high|critical, defaultcritical), translated to each package manager's own flag —pnpm audit --audit-level,yarn npm audit --severity, classicyarn audit --level. A customaudit: { command }still wins over both. -
pnpm projects now run CI without any dependency cache, and the pnpm
store no longer travels into docker images.Measured on a 3800-package monorepo, with every configuration run as
jobs in the same pipeline so they shared fleet conditions:- node_modules cache: restoring it (1.29 GB, 348k files) cost ~125s
to save 6s of install; writing it cost ~190s more. - store cache: on gitlab a job with the store took 74s against 35s
with no cache at all — the restore alone (53s) exceeded a full
from-registry install (30-42s), because the runner zips the store file
by file. On github (single zstd stream) it was a wash: ~60s vs ~64s. - store in the docker image: the worst of the three. The store is
copied into a layer below node_modules, so pnpm cannot hardlink
across the overlay boundary and copies every package instead. The
in-image install was slower that way (28.4s vs 9.6s from the
registry), while adding 3 GB to the build context and to the shipped
image: build 339s → 27s, image 3.35 GB → 0.73 GB.
So pnpm builds now: install with no cache, use pnpm's own store location
instead of a project-local.pnpm-store(nothing to cache, nothing to
leak into a build context), and let the docker--prodinstall download
from the registry — into scratch space that is dropped in the same layer,
so packages ship once.Generated pipelines lose all pnpm
cache:blocks and the
COPY .pnpm-storeline. Yarn caching is unchanged. - node_modules cache: restoring it (1.29 GB, 348k files) cost ~125s
-
Manual releases can now be queued while the pipeline is still running: the
create releasebutton is clickable from the first second of a main-branch pipeline and the release then runs automatically as soon as every other job succeeded (clicking after a green pipeline releases right away; a failed pipeline skips the queued release). On GitLab this is a no-op button job plus an automatic🚀 release once pipeline succeedsexecutor; on GitHub releasing is the new🚀 catladder create releasedispatch workflow (with forcing as a checkbox), which queues a marker that the new🛠️ catladder release on greenworkflow picks up when the main workflow completes — step summaries explain each run. The GitHubcatladder manual tasksworkflow is replaced by per-kind dispatch workflows (🚀 create release,▶️ deploy,🛑 stop,↩️ rollback), each a first-class Actions entry with a short same-kind dropdown; the pipeline workflows get a🛠️name prefix so the triggerable workflows sort to the top of the Actions sidebar. Review apps still stop automatically when their pull request closes, but that workflow (🛑 catladder stop review app) can now also be dispatched with a PR number to tear a review app down while its pull request is still open (gitlab parity). Side effect on GitLab: main-branch pipelines now end as passed (with a skipped executor) instead of blocked when nobody clicks the release button (the manual release jobs carryallow_failure: truenow).
Patch Changes
-
The changeset check's "how to add one" hint now names the project's own
package manager, instead of always sayingyarn changeset— which does
not exist in a pnpm project. Same for the release job's "nothing to
release" message. -
Cloud Run: keep the generated url resolvable by shortening long service names.
ROOT_URL/ROOT_URL_INTERNALfor a cloud run service are its deterministic url
https://<service>-<projectNumber>.<region>.run.app. Cloud run only serves that
url while the hostname's first dns label — service name plus project number —
stays within the 63 characters dns allows; past that it falls back to a legacy
url built from a random identifier that cannot be computed in advance.Until now catladder generated the deterministic url regardless, so a component
whose name pushed the label over 63 characters got a hostname that can never
resolve, with no warning at generation time. Anything consuming it failed with
an opaque dns error — a cloud tasks queue targeting such a service retried
forever withHTTP status code 0.The component part of the service name is now shortened just enough to fit. The
env and review slug parts stay intact, and names that already fit are never
touched, so existing deployments keep their names and generated output is
unchanged for them. Because review environments resolve their slug at runtime
(mr<iid>/pr<number>), 8 characters are reserved for it — a name can
therefore fitdev/prodand still be shortened forreview.Two cases fail generation with a clear error instead of being shortened: when
two components would end up with the same service name, and when the fixed
parts (customerName, appName, env, project number) already leave no usable room
for the component name. -
The changeset check no longer marks its GitHub job as failed when a pull request adds no changeset. GitHub has no yellow "allow_failure" state like GitLab, so the non-blocking nudge showed up as a red ❌ with "Process completed with exit code 1", which reads like something broke. On GitHub the job now stays green and reports a warning annotation plus the report in the run summary (the sticky PR comment is unchanged); on GitLab the job still turns yellow.
-
GitHub: merge gating no longer blocks the release job's push.
project setupused to require thecatladder ✅aggregate check via
classic branch protection. The release job pushes the release commit and
tag straight to the default branch with the workflow token — and that
fresh commit can never have a passing check yet, so catladder's own
gating rejected its own release (GH006: Protected branch update failed).Setup now configures the gating as a repository ruleset
(catladder merge gating) instead: same required check for PRs and
humans, plus a bypass for the GitHub Actions app so the release job can
push. Classic-protection gating from earlier versions is migrated away
automatically (only the aggregate check is touched; other protection
settings are preserved).project doctorverifies the ruleset and flags
the legacy setup, and the release job now explains the fix when its push
is rejected by branch protection. -
GitHub: the release job pushes with a provisioned deploy key, making it
compatible with merge gating.The previous fix put the required
catladder ✅check into a ruleset
with a bypass for the GitHub Actions app — but github refuses the
built-in actions app on bypass lists by design (any collaborator could
push anywhere by authoring a workflow), so the release push was still
blocked.project setupnow provisions a release deploy key: an ed25519
write deploy key on the repository, its private half stored as the
CATLADDER_RELEASE_KEYactions secret, and a deploy-key bypass on the
merge-gating ruleset. The generated release jobs hand the secret to
catci, which pushes the release commit and tag over ssh with it — the
only actor that can pass the gating. Deploy keys are repo-scoped, only
admins can add them, and they never expire (no yearly renewal like
gitlab'sGL_TOKEN). Without the secret the job falls back to pushing
with the workflow token, which keeps working for repositories without
merge gating.Because deploy-key pushes trigger workflows (unlike workflow-token
pushes), the release commit now carries[skip ci]on github — the
explicitly dispatched taggedRelease workflow stays the only release
run, and the release commit no longer triggers a redundant main
pipeline. On gitlab the commit message is unchanged (the tag pipeline
must fire natively there).project doctorverifies the key, the secret and the ruleset bypass,
and the changesets job image now ships an ssh client.project setup
also no longer tries to set the gitlab project topic on github-only
projects (it crashed with "not found" before reaching the github
steps). -
Faster pipelines through better caching: node caches are keyed by lockfile content on GitHub, the yarn cache download is skipped entirely when
node_moduleshits exactly, and lint/test jobs pull the node caches without pushing them back. -
Disabling a pipeline backend now cleans up after itself.
catenvonly ever removed the files of the backends that were still enabled, so switchingpipelinesfrom gitlab to github left.gitlab-ci.ymland.catladder-generated/gitlab/behind — and a stale.gitlab-ci.ymlkeeps running a stale pipeline. A full generation now also runs the cleanup of every backend that is not enabled. A.gitlab-ci.ymlis only deleted when it carries catladder's generated-file marker, so a hand-maintained one is left untouched. -
Fixes across both backends: fully-literal dotenv values are no longer bash-escaped, in-workspace component jobs use the workspace's cache slots, hidden files are included in GitHub artifacts, npm publish authentication works in workspace monorepos, cross-component secret references are injected into GitHub job env, stale materialized image definitions are cleaned on every generation, an invalid store file is treated as missing instead of crashing, and Cloud Run database-delete retries are capped instead of looping forever.
-
allowFailurenow works on the github backend. The flag was lowered for gitlab only and silently dropped for github, so a job marked as non-blocking failed the whole workflow — and with it every job depending on it. Thepagesdeploy type is the worst case: it isallowFailureby default precisely so a broken site publish cannot block the pipeline, but on github a failed publish also skipped the release job. It now lowers tocontinue-on-error. -
catladder no longer assumes it runs against one specific GitLab instance. The host was hardcoded as
https://git.panter.chin the security-audit commands, in catci, in theproject cijob-trace call and in thegitlabUrlof the generated semantic-release config — so on any other GitLab those calls silently talked to the wrong server. They now resolve the instance fromCI_SERVER_URLin CI and from the git remote locally, and the semantic-release GitLab plugin derives its url from the CI environment like it is meant to.Also removes the GitLab Visual Review toolbar from the kubernetes helm chart: it was off by default, hardcoded the same host, and the GitLab feature behind it was removed upstream in 17.0.
-
Kubernetes deploys now create a working image-pull secret on the github backend. The
kubernetesCreateSecretscript readCI_REGISTRY_USER/CI_REGISTRY_PASSWORDdirectly, which do not exist on GitHub Actions — the secret was created with empty credentials and pulls from a private ghcr package failed. The deploy job now passes the credentials asDOCKER_REGISTRY_USER/DOCKER_REGISTRY_PASSWORD, resolved per backend through the CI-variable abstraction (newregistryPassword), so gitlab keeps the exact same values it used before. -
Two pnpm detection fixes. Standalone (non-workspace) node components generated
yarn lint/yarn testfor their lint and test jobs regardless of the detected package manager — they now follow it, like the build and start commands and like workspace builds already did. And apackageManagerfield written bycorepack use(pnpm@11.6.0+sha512.…) had its integrity suffix carried intonpm install -g pnpm@<version>in the generated docker build, where it is not a valid npm spec; the suffix is now stripped where the field is parsed. -
The queued-release executor (
🚀 release once pipeline succeeds) now onlyneeds:the sink jobs of the main-branch pipeline — the deploy/verify tips and terminal quality jobs — instead of every automatic job.needs:is transitive, so the coverage is identical (a failed ancestor still skips its descendants and with them the executor), but the needs list stays far below GitLab's 50-entry cap: large multi-component pipelines that previously fell back to the legacy after-the-pipeline release button get the click-any-time queueing back. -
The semantic-release release method runs semantic-release v25 in a self-contained, exactly-pinned job image, dropping the previously required
@semantic-release/gitfork.