Skip to content

Carry the environment on a config write - #593

Merged
incognick merged 1 commit into
mainfrom
feat/app-env-environment
Aug 19, 2026
Merged

Carry the environment on a config write#593
incognick merged 1 commit into
mainfrom
feat/app-env-environment

Conversation

@incognick

Copy link
Copy Markdown
Contributor

Adds env to controlplane.Database.SetAppEnv and UnsetAppEnv. An embedder implementing the Database seam cannot tell which environment a config write belongs to, and needs to in order to act per-environment on one — the engine knows (it re-applies that environment's workload straight afterwards) and used to drop the fact at the seam.

The read stays app-global and nothing an app sees changes: AppEnv carries no environment because the store has none, so narrowing it would be a schema change and a change of meaning rather than a record of one. Postgres accepts the parameter and does not store it.

`controlplane.Database` took a config write as (app, key, value) and a removal as
(app, key). Both are made IN an environment: `Engine.SetConfig` and
`Engine.UnsetConfig` resolve one, and re-apply exactly that environment's workload
once the store holds the value. The environment stopped at the seam, so an
implementation of `Database` saw a staging change and a production one as the same
call, with nothing to act on per-environment.

Add `env` to the two writes, positioned after the app as every other
environment-carrying method on the seam positions it, and pass the canonical name
(ADR-0067 §2) so an implementation never has to know that an unnamed environment
and "prod" are the same one.

Leave the READ app-global. `AppEnv` takes no environment because the store has
none: the `app_env` table is keyed by (app, key) with no environment column,
docs/CAPABILITIES.md states that a config value applies in every environment, and
deploy, rollback, a re-apply and a one-off command all render a workload from that
one read. An environment on `AppEnv` would not carry information — it would narrow
what a running app sees to the values that happened to be written while pointed at
the environment it is deployed to, and silently drop the rest. Postgres therefore
takes the parameter and does not store it, and says so where a reader of the row
would ask.

The fake records each write with its environment, in a log kept apart from the
config it stores, so a test can assert the environment reached the seam while the
fake stays exactly as app-global as Postgres is.

Signed-off-by: Nicholas Phillips <nsphilli@gmail.com>
@incognick
incognick merged commit 3b274cc into main Aug 19, 2026
5 checks passed
@incognick
incognick deleted the feat/app-env-environment branch August 19, 2026 23:39
incognick added a commit that referenced this pull request Aug 20, 2026
* Key an app's config on the environment

The `app_env` table was keyed (app, key) with no environment column, so an app had
exactly one set of config values and a deploy rendered that one set into whichever
environment it targeted. Config was the one part of an app that could not differ
between staging and production: an app needing a different API endpoint, log level or
feature flag per environment had nowhere to say so, and a value set while trying
something in staging reached production on that app's next deploy there.

Migration 00037 adds the environment column and moves the primary key to
(app, environment, key), backfilling every existing row to the default environment
`prod` — the only environment an install that never added a second one has, and
therefore the one those values were already being rendered into. PR #593 had already
put the environment on the two writes as provenance; it is now part of what identifies
the row, and the read takes it too.

`Database.AppEnv` gains the environment, positioned after the app as every other
environment-carrying method on the seam positions it. The five call sites each pass the
environment they are acting on: deploy and run render the environment they target,
rollback renders the environment being rolled back, a re-apply rolls the environment
whose config was written, and `ListConfig` lists the environment asked for. There is
deliberately no wildcard scope and no resolution order — a read returns that
environment's rows and nothing else — so the comments at those sites, which asserted the
app-global read, are rewritten to state what is now true rather than left as a trap.

The direct consequence is that a newly added environment starts with no config at all,
and an app deployed into one comes up on its image's own defaults. That is stated where
a user looks: the environments section and the config table in docs/CAPABILITIES.md
(whose "the same values apply in every environment" promise is now false and is
replaced), the `app config` command help, and the empty listing, which now names the
environment it is empty in.

The store test asserts both directions of isolation against a real database — the same
key in two environments holds two values, and a removal in one leaves the other alone —
and the engine tests assert the read path where it decides what a running app sees: a
deploy into staging renders staging's value and not the default environment's, a deploy
into the default environment still renders its own, and a fresh environment renders
nothing.

Signed-off-by: Nicholas Phillips <nsphilli@gmail.com>

* Correct the comments and CLI help that still say config is app-global

Four comments and two agent command descriptions outlived the schema change in the
commit before this one: the seam's SetAppEnv and UnsetAppEnv still told an implementer
the write lands everywhere, DeployRequest still explained Env's absence by an app-global
store, the fake still justified its storage shape by matching one, and burrow-agent's
deploy and build help still told an agent operator that config applies across
environments.

A comment asserting a property is a hypothesis, and one asserting the property the code
no longer has is worse than none — it is what the next reader trusts instead of reading
the query. Each now states what is true: the write lands in the environment it was made
in, the read takes an environment, and an agent sets config in the environment it is
deploying to.

Signed-off-by: Nicholas Phillips <nsphilli@gmail.com>

---------

Signed-off-by: Nicholas Phillips <nsphilli@gmail.com>
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