Working with catladder
catladder generates this repository's CI/CD pipelines, deployments and
environment files from the TypeScript config catladder.ts at the repo
root. It is installed per project (@catladder/cli) — always use the
locally installed version (yarn catenv, yarn catladder).
Golden rules
- Never hand-edit generated files. Everything under
.catladder-generated/, the generated parts of.gitlab-ci.yml, and catladder-generated workflows under.github/workflows/are overwritten on every regeneration (they carry a "generated by catladder" header). To change them, changecatladder.tsand regenerate. - Regenerate after every
catladder.tschange by runningyarn catenv. With direnv set up, re-entering the project directory does this automatically. - Commit the regenerated files together with the config change. Generated files are checked in; CI includes them directly.
Structure of catladder.ts
import type { Config } from "@catladder/cli";
const config = {
appName: "my-app",
customerName: "pan",
pipelines: { gitlab: true, github: true }, // which CI systems to generate
environments: {
// optional: project-wide env declarations/tuning
review: { autoStop: "3 days" },
stage: false, // the project has no stage env (any component)
next: { type: "dev", on: { branch: "next" } }, // extra branch-tracking env
},
components: {
www: {
dir: "frontend", // working directory of this component
vars: { public: {}, secret: [] }, // env vars (see catladder-secrets skill)
build: { type: "node" }, // node | rails | meteor | custom — see catladder-builds
deploy: { type: "kubernetes" }, // kubernetes | google-cloudrun | dockerTag | custom — see catladder-deploys
env: {
// enable/customize per environment
review: {},
dev: {},
stage: false, // false disables the env for this component only
prod: {},
},
},
},
} satisfies Config;
export default config;
Key concepts:
- Components are the sub-apps/services of the project (frontend, api, ...). Each has a build config and a deploy config.
- Environments:
dev(deployed on push to the main branch),review(per merge/pull request),stageandprod(deployed on tagged releases),local(only for local development via catenv). Environments are project-wide: the top-levelenvironmentsconfig declares and tunes them consistently for all components, while per-componentenv.<name>entries only carry component-local overrides (vars, deploy settings, host, autoStop) orfalseto opt the component out. Inenvironments:<name>: false— disables the env for the whole project: no component has it, so no setup contexts (catladder project setup), secrets, pipeline jobs/workflows or catenv for it. This is how to say "this project has no stage":environments: { stage: false }(noton: false).localcannot be disabled; inheriting from a disabled env is an error.on— when the env deploys:"mainBranch","mr","taggedRelease",{ branch: "next" }(a stable env deploying on pushes to that branch), orfalse(never deployed by a pipeline — the env still exists for setup, secrets and catenv; useenvironments.<name>: falseto remove it).on: "mr"makes an env a review app (one instance per MR/PR, torn down on close). Defaults from the env type.autoStop— how long a stoppable environment stays up after its last deploy before gitlab stops it (e.g."3 days";falsedisables). Defaults: review"1 week", dev"4 weeks", others never. GitLab only — github review apps stop when their PR closes. Components can override it per env (env.<name>.autoStop).- Extra envs are declared with any name plus a
type— the env type acts as a preset: it supplies all defaults (trigger, autoStop, deploy policy) and the env's identity for tooling (ENV_TYPE), everything overridable. Every component deploys to a declared env unless it opts out:environments: { next: { type: "dev", on: { branch: "next" } } }. inherit— make an env inherit from another env:inherit: "dev"gives it dev's per-component config overrides (merged below its own;hostandtypenever inherit, the env type is implied from the inherited env) AND dev's secret values — all-or-nothing: its jobs reference dev'sCL_DEV_*variables and it has no secret store of its own (manage secrets via dev; rotating affects both). The object form separates the axes:inherit: { config: "dev" }inherits config only,inherit: { config: "dev", secrets: "dev" }both.
- Review apps: top-level
reviewApps: { deploy, label, skipLabel, drafts }decides whether merge/pull requests deploy their review apps — every MR/PR (default), opt-in or opt-out by label, or on request; and what drafts run. See thecatladder-pipelinesskill. - Pipelines:
pipelines: { gitlab: true, github: true }selects which CI systems get generated files. Options objects instead oftrueallow per-pipeline settings (e.g.runnerVariables,gitRemote, andrepository— GitHub only, for lowercasing GHCR image paths; see thecatladder-pipelinesskill). - Package manager: yarn and pnpm are supported and autodetected
(from the
packageManagerfield in package.json or the lockfile; yarn is the fallback).packageManager: "pnpm" | "yarn"at the top level overrides the detection. In pnpm projects, run the CLIs aspnpm catenv/pnpm catladder.
Common tasks
- Add a component: add an entry under
components, runyarn catenv, commit config + generated files. - Change resources / deployment settings: edit the component's
deploy(or per-env override), regenerate. - Inspect the resulting pipeline: read the generated files (e.g.
.catladder-generated/gitlab/,.github/workflows/) — but never edit them.
Escape hatches — last resort, and a signal to file an issue
catladder has two escape hatches for things it doesn't (yet) support natively:
customJobs(per component) — hand-written CI job specs injected into the pipeline.- transform
hooks(top-levelhooks) —transformFileBeforeWrite/transformYamlBeforeWriterewrite the generated files just before they're written to disk.
Both work, but they are fragile by design: they depend on the exact
shape of catladder's generated output, so a catladder upgrade can break
them silently, and they sit outside everything the rest of the config
gives you (rules, caching, images, per-env logic, snapshots). Prefer a
first-class config option whenever one exists — check the build,
deploy, releases, and env/vars surfaces (and the sibling skills)
first.
When you do reach for one, treat it as a missing-feature signal. If
a dev needs customJobs or a transform hook, it almost always means
catladder is missing a capability that other projects will need too.
Open an issue at
https://github.com/panter/catladder/issues describing the use
case (what you're trying to achieve and why the existing config can't
express it) so it can be generalized into a supported feature — and,
where possible, offer to help the dev write that issue. The goal is for
escape hatches to shrink over time, not accumulate.
Related skills
catladder-builds— the componentbuildconfig (build types, Docker)catladder-deploys— the componentdeployconfig (k8s, Cloud Run, …)catladder-secrets— env vars and secret managementcatladder-releases— releasing (semantic-release / changesets)catladder-pipelines— how the generated pipelines work, debugging CIcatladder-cli— non-interactive CLI usage and command referencecatladder-migrate-ci-backend— moving a project between GitLab CI and GitHub Actionscatladder-migrate-release-method— switching between semantic-release and changesetscatladder-migrate-package-manager— switching a project between yarn and pnpm