Skip to content

feat(cli): take instance bootstrap values from the environment - #336

Open
Lob26 wants to merge 1 commit into
theam:mainfrom
Lob26:feat/bundle-instance-bootstrap
Open

feat(cli): take instance bootstrap values from the environment#336
Lob26 wants to merge 1 commit into
theam:mainfrom
Lob26:feat/bundle-instance-bootstrap

Conversation

@Lob26

@Lob26 Lob26 commented Sep 7, 2026

Copy link
Copy Markdown

What changes

facility instance bootstrap reads each option from its FACILITY_<OPTION>
environment variable — --org-slug from FACILITY_ORG_SLUG — so a one-shot
container task can carry the binding without a command line. A bootstrap
Compose profile runs it:

docker compose --profile bootstrap run --rm bootstrap

Why

Binding the first owner is the one step between a started bundle and a usable
instance, and today it needs a local Node toolchain, the published CLI, and ten
flags typed correctly. The CLI already travels inside the api image for exactly
this reason — the Dockerfile says so and installs a facility wrapper on PATH
— but nothing in docker-compose.yml reached it.

Environment rather than arguments is the design decision, not a convenience.
An organization name and a GitHub login are operator input. The shape that
otherwise fits a Compose service is
sh -c "facility instance bootstrap --org-name $FACILITY_ORG_NAME …", which
interpolates that input into a command. Passing values as variables to an
exec-form command removes the shell from the path instead of quoting around it —
the same lesson as #214 and #186, where a discovered directory name and a git ref
reached a shell unquoted.

Precedence and validation are deliberate: an explicit option wins over its
variable, a malformed option fails rather than being rescued by an ambient
one, and a value is validated identically whichever way it arrived — a variable
is not more trusted for having come from the environment. Missing values are
reported under both spellings, because the operator reading that error in a
container log has only the variable.

The profile keeps bootstrap out of docker compose up, and its variables use
:- rather than :? on purpose: a required-variable interpolation is evaluated
for the whole file and would fail up itself. The CLI names what is missing.

Verification

Five new cases, deterministic without PostgreSQL or the network — the injected
client refuses at the first database call, so a run that reaches it has passed
every validation:

  • an environment-only run reaches the database;
  • an explicit option overrides its variable;
  • a blank option still defers to the variable, and the variable is validated
    (--org-slug must be a lowercase URL slug, not accepted);
  • a malformed option does not fall back to the environment; and
  • the missing-value message is pinned in full, both spellings.
pnpm --filter @theagilemonkeys/facility test        14 passed, 0 skipped

That includes the pre-existing transactional/idempotency test, which normally
skips: I ran it against a real PostgreSQL rather than leaving it skipped, so the
advisory-lock and conflict paths were exercised.

The Compose profile was resolved, not assumed:

docker compose config --services
  postgres api migrate runner-image web worker          (no bootstrap)
docker compose --profile bootstrap config --services
  postgres api bootstrap migrate runner-image web worker
node guards/run.mjs                                     2 guards, 0 failed
pnpm --filter @facility/docs build                      [SUCCESS]
  • pnpm verify passes locally
  • Behaviour verified beyond the test suite (say how)
  • Documentation updated, or no user-facing change

pnpm verify does not pass on this machine and not because of this change:
test:dev reports 116 tests, 92 pass, 24 fail here, and I measured the
identical 92/24 on a clean main — Windows noise in the patched image-size
cases, tar failing to resolve C:, and the registry publication tests. Beyond
the suite, the profile was resolved with real docker compose config runs, which
is what confirmed that :? would have broken up for everyone.

Not done on purpose: the master key is not auto-generated. It looks like the
obvious companion, and it is the wrong feature. A generated key that is not
persisted makes every stored project secret undecryptable on the next start, and
one written to a file is a secret on disk with whatever mode the writer chose.
${SECRET_MASTER_KEY:?set SECRET_MASTER_KEY} is already a good error for a value
the operator must own.

Related: #19, which asks for the operator bootstrap to be automated in a
cloud-agnostic bundle. This is that step; it does not close the issue.

🤖 Claude Code helped

Binding the first owner is the one step between a started bundle and a
usable instance, and today it needs a local Node toolchain, the published
CLI, and ten flags typed correctly. The CLI already travels inside the api
image for exactly this reason, but nothing in docker-compose.yml reaches
it.

`facility instance bootstrap` now reads each option from its
`FACILITY_<OPTION>` variable — `--org-slug` from `FACILITY_ORG_SLUG` — so
a one-shot container task carries the binding in its environment and needs
no command line at all. A `bootstrap` Compose profile runs it:

    docker compose --profile bootstrap run --rm bootstrap

Environment rather than arguments is the point, not a convenience. An
organization name and a GitHub login are operator input, and the shape
that would otherwise fit a Compose service is `sh -c "facility instance
bootstrap --org-name $FACILITY_ORG_NAME ..."`, which interpolates that
input into a command. Passing values as variables to an exec-form command
removes the shell from the path instead of quoting around it.

Precedence and validation are deliberate: an explicit option wins over its
variable, a malformed option fails rather than being rescued by an ambient
one, and a value is validated identically whichever way it arrived — a
variable is not more trusted for having come from the environment. Missing
values are reported under both spellings, because the operator reading
that error in a container log has only the variable.

The profile keeps `bootstrap` out of `docker compose up`, and its
variables use `:-` rather than `:?` on purpose: a required-variable
interpolation is evaluated for the whole file and would fail `up` itself.
The CLI names what is missing.

Covered without Postgres or the network by refusing at the first database
call: an environment-only run reaches it, an explicit option overrides a
variable, a blank option still defers to one, a malformed option does not,
and the missing-value message is pinned in full.
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.

1 participant