Skip to content

feat(env): environment profiles, per-env state + tokens, protected-env guardrail, promotion docs - #56

Merged
2000game merged 4 commits into
mainfrom
feat/environments-22
Jul 9, 2026
Merged

2000game merged 4 commits into
mainfrom
feat/environments-22

Conversation

@2000game

@2000game 2000game commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Introduces an explicit environment concept so one config repo drives several ChurchTools instances (e.g. eqrm-dev rehearsal + prod), Terraform-workspace-style, with no file edits when switching. Closes #22.

What landed (all scope items)

  1. Environment profiles — a committed ct.envs.json (default path; CT_ENVS override) declares named (host, state file, token reference) triples. Every state/host-touching command takes --env <name> (-e). No --env → current single-host behaviour, byte-identical.
  2. Multi-host token store — tokenStore now keys Keychain accounts by host, so one machine holds logins for several instances at once. Backward-compatible: an existing single blob is still read (as a fallback when its host matches). ct auth login writes both the per-host account and the default pointer. Token order per env: CT_LOGINTOKEN (CI; a profile tokenEnv is copied here) → host-keyed Keychain entry.
  3. State file per env — ct-state.<env>.json convention, overridable per profile via state. Both committed.
  4. Protected-env guardrail — "protected": true makes apply/destroy always require typed confirmation of the env name, even with --auto-approve/--force. --confirm-env <name> (must match exactly) substitutes for the typed input in CI.
  5. Promotion workflow documented — README: plan dev → apply dev → verify (round-trip/--refresh) → plan prod → apply prod.
  6. Version gate per env — ct plan --env <name> header surfaces the env name and the target instance's live CT version, so a dev/prod skew is visible before promoting.

Design

prepareEnv resolves a named profile and writes its host (and, for CI, token) into process.env, so the unchanged resolveConfig / authedSession / resolveStatePath pick them up — no host/token/state triple threaded through every helper, and the --env-less path is untouched. Cross-contamination is impossible: the existing state host-check (Refusing to mix instances) binds each state file to its host; a test covers the env path specifically.

Test evidence

  • npm test → 382 passed | 4 skipped
  • npm run typecheck → clean
  • npm run lint → clean

New tests: envs (profile load/validate), env-context (prepareEnv wiring + no-env passthrough), prompt (confirmEnv), tokenStore-multihost (per-host + legacy fallback), plan-env-command (two hosts/two state files, version header, cross-contamination refusal), apply-env-command + destroy-env-command (protected-env guardrail incl. --auto-approve/--force refusal and --confirm-env match), plus cli registration.

Merge-coordination note

Per the task brief, other agents are concurrently editing src/commands/get.ts (pagination + API-client error rendering). This PR makes a minimal touch there — option plumbing only: adds -e, --env <name> to the get <resource> subcommands and get raw, and a one-line prepareEnvHost(opts) call before authedSession(). No logic in the GET request/response path is changed. Flagging for the merge coordinator.

2000game added 4 commits July 9, 2026 13:34
Add ct.envs.json environment profiles (host, per-env state file, tokenEnv
reference, protected flag) and a prepareEnv wiring helper that resolves a
named profile into the existing single-host resolution by writing its host and
(for CI) token into the process env — so resolveConfig/authedSession/
resolveStatePath pick them up and the --env-less path stays byte-identical.

Extend the Keychain token store to per-host accounts (account name = host) with
a backward-compatible fallback to the legacy single blob when its host matches,
so one machine can hold logins for several instances. authedSession now resolves
the token for the target host; the host<->token binding check is preserved.

Also: resolveStatePath takes a per-env fallback; CtClient exposes its resolved
CT version; confirmEnv gates protected environments (no force/assumeYes escape).
…22)

Wire --env <name> into plan, apply, destroy, adopt, adopt grants, state list,
and get (host-only). plan --env surfaces the target env name + its CT version in
the header (per-env version gate). apply/destroy against a protected env always
require typed confirmation of the env name — --auto-approve/--force never bypass
it; --confirm-env <name> substitutes for the typed input in CI.
Env profile loading/validation, prepareEnv wiring (host/token/state, no-env
passthrough), confirmEnv guardrail, multi-host tokenStore fallback, and
command-level plan --env (two hosts/two state files, version header,
cross-contamination refusal) + apply/destroy protected-env guardrail.
@2000game
2000game force-pushed the feat/environments-22 branch from 53b10f8 to e1c2a7e Compare July 9, 2026 11:36
@2000game
2000game merged commit e92a849 into main Jul 9, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: environments — per-instance state + dev→prod promotion workflow

1 participant