@uscreen.de/deploy-cli
v0.5.0
Published
CLI for compose-based deployments: config → compose generation, setup transfer, agent bootstrap (ADR-001)
Keywords
Readme
@uscreen.de/deploy-cli
CLI for compose-based deployments (ADR-001, docs/adr/): renders typed
compose structures from zod-validated config and serializes them with the
yaml package — no text templating engine. The Go deploy agent on the
hosts stays separate.
Commands
deploy-cli init [dir] # scaffolds a new deploy repo
deploy-cli validate # all configs against the schemas
deploy-cli render --env <env> [--app <s>] # compose files to dist/
deploy-cli deploy --env <env> [--app <s>] # render + SSH deploy (ADR-003)
deploy-cli deploy --transport agent ... # image roll via agent API (no
# config transfer; token via
# --token, DEPLOY_TOKEN, or the
# deploy-agent sops secrets)
# In deploy repos: `pnpm run deploy` — `pnpm deploy` (without run) is a
# built-in pnpm command and won't reach the script.All commands are fully scriptable via flags (CI-ready);
@clack/prompts only asks when a flag is missing and a TTY is present:
--env as single choice, --app as multiselect with an
"all apps" line (toggles the whole list, changesets pattern) and everything
preselected (default = all). --app accepts multiple flags and
comma lists (--app web,api); without a TTY and without a flag, "all" applies.
Deploy repo layout
envs/<env>.yml env = (VM, environment): host, uid/gid,
network, prefix, registry, default tag
envs/<env>.env.yml optional service defaults of the env
(service form, bottom layer)
envs/<env>.secrets.yml optional global secrets (service form,
sops; above defaults, below the app)
apps/<app>/app.yml base declaration of the services
apps/<app>/<env>.env.yml env overlay, deep-merged (arrays replace)
apps/<app>/<env>.secrets.yml sops-encrypted secrets (ADR-002)
known_hosts optional, used as UserKnownHostsFile
dist/<env>/<app>.compose.yml render output (mode 0600)All apps of an env run on the host as one compose project
(multi--f + -p <prefix>); relative host volumes are namespaced
per service (./data → ./<service>/data).
Routing can declare an IP allowlist (routing.allow), stripprefix
(routing.strip_prefix), and gzip: false per service; autoheal: false
leaves out the autoheal label (for the deploy agent itself).
A service with blue_green: true is rendered as two slot services
(<name>-blue/-green) with a shared Traefik load balancer
(retry middleware, quick-dial serversTransport, active LB healthcheck) —
the Go deploy agent keeps exactly one slot running.
A service with oneshot: true is a job, not a service (e.g. DB
migrations): rendered with restart: "no" and left out of the
compose up. The ssh transport runs it once via
docker compose run --rm before the up — a non-zero exit aborts
the deploy, so broken migrations never get new app containers.
oneshot excludes routing, blue_green and healthcheck. The agent
transport only rolls images and does not run oneshots — a schema
change needs deploy --transport ssh.
All config schemas are strict: an unknown key — a typo, or a feature of a newer CLI than the one installed in the deploy repo — is an error naming the key, never a silent drop. What gets checked is the merge result, so a finding may come from app.yml, the overlay or the secrets.
Development
pnpm install
pnpm test # vitest (schema, merge, compose snapshots)
pnpm typecheck
pnpm lint # biome
pnpm build # tsdown → dist/cli.js (one file)Secrets & transport
- Secrets (ADR-002): sops + age.
apps/<app>/<env>.secrets.ymlhas the same deep-partial form as the env overlay and gets merged as a third layer (base → overlay → secrets). Missing file → warning; failedsops -d→ hard error.validateruns without an age key. Secrets stay encrypted at rest:rendershows the secret keys with placeholder values («redacted», needs no age key — the structure is unencrypted in sops);--secretsforces plaintext into dist/.deployrenders in-memory and streams via ssh stdin — plaintext only ever lives on the target VM. - Transport (ADR-003): setup deploy via ssh/scp as in the PoC —
create/chown bind-mount directories, compose file + chmod 600,
one
docker compose upover all*.compose.yml(--wait,--remove-orphansonly on a full deploy). Optionalknown_hostsis used as UserKnownHostsFile.
Deliberately open
- Deploy trigger via the agent API (HTTPS) instead of SSH: the
Transportinterface is the seam, decision separate (ADR-003). - Registry choice for the CLI package (ADR-001).
