Apply schema scripts from a console command, rehearsed in CI (task 45) #19

Merged
rob merged 2 commits from feat/schema-in-ci into main 2026-08-03 12:20:25 +00:00
Owner

dotnet run --project src/PlaceMark.Database applies pending scripts, taking its connection string from ConnectionStrings__PlaceMark (ADR-0014's key, double-underscore form, so no password in a process list). Also closes a gap the README admitted — the Compose database can now be created from a clone.

CI gains a Postgres service container and one run:-only step invoking the command twice: the first must apply something under the journal-key prefix, the second must find nothing pending. Both assertions verified to fire.

Two acceptance criteria deliberately not delivered

There is no test environment and no production — task 42 is not started — so "applies to test on merge to main" and "manual approval gate" have nothing to point at. No workflow was written against them. ADR-0023 fixes their shape for when #42 lands: test is the same command with the environment's connection string as a secret; production is the same command behind workflow_dispatch, where the dispatch is the gate. Ticket 45 stays open.

What the CI step proves — and does not

Proves: the scripts apply cleanly from empty, in resource-name order, through the entry point a deployment invokes rather than a library call a fixture makes, and that they are journalled so a second application does nothing.

Does not prove: anything about applying a future script to a database already holding a previous schema and real rows. That is the residual risk and it is what a provisioned environment buys.

The journal-key prefix grep is a tripwire for the rename hazard — someone who renames the project and updates the grep still gets green; it catches the accident, not the intent.

Watch on the first run

The service container is the one construct here never run on act_runner v6.4.0. It is not an action, so the uses: defect should not apply — but if the run dies before any step starts, that block is the suspect. Deleting services: and the schema step restores the pipeline exactly as it was; the workflow header says so.

If postgres does not resolve from the job container you will see ten attempts over ~30s then a connect failure — that is service networking, not the schema.

If the second run ever reports something applied, the journal did not record the first. That is a real finding, not flakiness.

Also

Fixes SchemaUpgrader reporting failed on '' whenever the failure was not a script's — an unreachable database, a bad credential and a malformed connection string all arrive as a failed result with no error script.

ADR index row will conflict with PR #18 — both insert after the 0021 row. Keep both, in numeric order.

`dotnet run --project src/PlaceMark.Database` applies pending scripts, taking its connection string from `ConnectionStrings__PlaceMark` (ADR-0014's key, double-underscore form, so no password in a process list). Also closes a gap the README admitted — the Compose database can now be created from a clone. CI gains a Postgres service container and one `run:`-only step invoking the command **twice**: the first must apply something under the journal-key prefix, the second must find nothing pending. Both assertions verified to fire. ## Two acceptance criteria deliberately not delivered There is no test environment and no production — **task 42 is not started** — so "applies to test on merge to main" and "manual approval gate" have nothing to point at. No workflow was written against them. ADR-0023 fixes their shape for when #42 lands: test is the same command with the environment's connection string as a secret; production is the same command behind `workflow_dispatch`, where the dispatch *is* the gate. **Ticket 45 stays open.** ## What the CI step proves — and does not **Proves:** the scripts apply cleanly from empty, in resource-name order, through the entry point a deployment invokes rather than a library call a fixture makes, and that they are journalled so a second application does nothing. **Does not prove:** anything about applying a *future* script to a database already holding a previous schema and real rows. That is the residual risk and it is what a provisioned environment buys. The journal-key prefix grep is a tripwire for the rename hazard — someone who renames the project *and* updates the grep still gets green; it catches the accident, not the intent. ## Watch on the first run **The service container is the one construct here never run on `act_runner v6.4.0`.** It is not an action, so the `uses:` defect should not apply — but if the run dies before any step starts, that block is the suspect. Deleting `services:` and the schema step restores the pipeline exactly as it was; the workflow header says so. If `postgres` does not resolve from the job container you will see ten attempts over ~30s then a connect failure — that is service networking, not the schema. **If the second run ever reports something applied, the journal did not record the first.** That is a real finding, not flakiness. ## Also Fixes `SchemaUpgrader` reporting `failed on ''` whenever the failure was not a script's — an unreachable database, a bad credential and a malformed connection string all arrive as a failed result with no error script. **ADR index row will conflict with PR #18** — both insert after the `0021` row. Keep both, in numeric order.
Apply schema scripts from a console command, and rehearse it in CI
All checks were successful
CI / build (pull_request) Successful in 43s
91e52bf8fc
Adds a deployment entry point: dotnet run --project src/PlaceMark.Database
takes its connection string from ConnectionStrings__PlaceMark, prints
each applied script's full resource name or reports nothing pending, and
exits 0 or 1. The project is now an executable as well as the library the
test fixture calls, which also closes the gap the README admitted — the
Compose database can be created from a clone.

CI gains a Postgres service container and one run:-only step invoking the
command twice: the first must apply something under the journal-key
prefix, the second must find nothing pending. Both assertions were
verified to fire.

Two of the three acceptance criteria are not delivered and deliberately
not faked. There is no test environment and no production — task 42 is
not started — so applying on merge and a manual approval gate have
nothing to point at. ADR-0023 fixes their shape for when that lands.

What the CI step proves: the scripts apply cleanly from empty, in
resource-name order, through the entry point a deployment invokes rather
than a library call a fixture makes, and that they are journalled. What
it does not: anything about applying a future script to a database
already holding a previous schema and real rows.

Fixes SchemaUpgrader reporting failed on '' whenever the failure was not
a script's — an unreachable database, a bad credential and a malformed
connection string all arrive as a failed result with no error script.
rob force-pushed feat/schema-in-ci from 91e52bf8fc
All checks were successful
CI / build (pull_request) Successful in 43s
to 5b60ebcd89
All checks were successful
CI / build (pull_request) Successful in 44s
2026-08-03 12:08:36 +00:00
Compare
rob left a comment

Verdict: mergeable

Independent review of 91e52bf, in a detached worktree. Nothing blocking. Everything below that is not marked nit or observation was executed rather than read.

What I ran

All four CI commands, on the head commit: dotnet restore, dotnet build --configuration Release (0 warnings, TreatWarningsAsErrors on), dotnet test (32 passing — 27 Infrastructure, 4 Api, 1 WebUI), dotnet format --verify-no-changes (exit 0).

The entry point against a postgres:18.4-trixie container, every path:

Scenario Result
Empty database Applied PlaceMark.Database.Scripts.0001-initial-schema.sql, exit 0
Run again No pending scripts: the database is already up to date., exit 0
Variable unset names ConnectionStrings__PlaceMark, exit 1
Unreachable (Port=1) failed. + Failed to connect to 127.0.0.1:1, exit 1
Wrong password 28P01: password authentication failed, exit 1
Malformed connection string Format of the initialization string does not conform…, exit 1
Failing script failed on 'PlaceMark.Database.Scripts.0001-initial-schema.sql' + 42710: collation "case_insensitive" already exists, exit 1

The failing-script case was produced by clearing schemaversions while leaving the objects, which is also the state the transaction strategy is supposed to prevent. schemaversions confirms the journal key is the full resource name.

1. Is the honesty right?

Yes, and I would have objected to the alternative. Task 42 is confirmed unstarted in Vikunja, so criteria 1 and 2 name a test database, a production database and a credential to each, none of which exist; a workflow against them is a file whose first execution is the day it matters, which is exactly what the ADR says. Criterion 3 is delivered and demonstrated, not asserted. Task 45 carries the partial-delivery comment and the 00220023 correction, and stays open. Correct call.

Two observations on ADR-0023, raised now only because its body freezes on merge:

  • The forward shape — "production is the same command behind workflow_dispatch, where the dispatch is the gate… needs no further record, only wiring" — rests on a mechanism that has never run on this instance either, which is the objection the record itself uses to reject writing the workflows. A clause saying so, or saying that Forgejo has no GitHub-style environment protection rules and so workflow_dispatch is the only option rather than the chosen one, would stop a later reader having to work out which it was.
  • "Spelling the prefix out here is what makes such a rename fail in the pipeline instead of in a database" slightly overstates it: SchemaUpgraderTests.Upgrade_Always_JournalsEachScriptByItsFullEmbeddedResourceName already pins the exact full resource name, so on a rename the Test step goes red first. The grep is a second, prefix-only tripwire at the deployment entry point — still worth having, just not the only one.

2. Does the CI step prove what it claims?

Yes, and it is not vacuous. Run 154's log shows the service container pulled, created and run on FORGEJO-ACTIONS-TASK-171_WORKFLOW-CI_JOB-build-build-network at 11:57:08, before the job container — so services: does work on act_runner v6.4.0. The step itself logged Applied PlaceMark.Database.Scripts.0001-initial-schema.sql at 11:57:42.647 and No pending scripts… at 11:57:43.172: half a second apart, no retry, and the grep's subject line was produced by an actual apply against an actually empty database.

I then ran the step's shell verbatim against a local container with mutations injected. All four branches fire:

  • Journal-key grep — republished the project with -p:RootNamespace=PlaceMark.SchemaRenamed (confirmed the embedded resource became PlaceMark.SchemaRenamed.Scripts.0001-initial-schema.sql). The run succeeded and applied a script, and the step still failed: Nothing was applied under the expected journal key 'PlaceMark.Database.Scripts.'., exit 1. That is the real hazard reproduced, not a doctored log.
  • Second run applied something — schema reset between invocations, so both runs succeed and apply: The second run applied something, so the first was not journalled., exit 1.
  • Second run failed — journal wiped with objects left standing: the DbUp error is printed, then The second run failed…, exit 1.
  • Retry exhaustion — pointed at a dead port: exactly 10 attempts in 31s, the connect error printed, exit 1. The ATTEMPT arithmetic gives the 10 the message claims.

3. ConnectionStrings__PlaceMark and leak risk

Consistent with ADR-0014: it is the double-underscore form of the key that record names, environment variables are its stated mechanism for deployed environments, and taking it from the environment rather than argv keeps the password out of the process list.

No leak. The value in the workflow is the Compose default, and it is the credential of the container it targets, which lives for one job; no step enables set -x, and the run 154 log dumps no environment. I specifically tried the failure shapes that tend to echo the string back — unknown keyword and unparsable port, both with a password present — and got Couldn't set bogus / Couldn't set port with no value. Under ADR-0023's future shape the string becomes a repository secret and is masked as well.

Observation, non-blocking: Run reads the raw environment variable rather than going through Microsoft.Extensions.Configuration, so ADR-0014's layering does not reach this command — a developer whose connection string lives in user secrets, as user-secrets.example.json prescribes, has to supply it a second time. The README's copy-paste hard-codes the Compose defaults two paragraphs above the one that invites you to change them in .env; half a sentence would close that.

4. OutputType=Exe on a project a fixture references

No problem found, and I looked for the three that usually bite. The test project's output now also carries PlaceMark.Database (apphost), .deps.json and .runtimeconfig.json — inert, VSTest discovery is unaffected, 27 tests pass. There is no Program ambiguity: PlaceMark.Api does not reference PlaceMark.Database (checked every .csproj), so WebApplicationFactory<Program> in PlaceMark.Api.Tests is untouched, and both generated Program types are internal in any case. CA1515 (public types in an executable) is not firing at AnalysisLevel 10.0 / Recommended — the build is clean with warnings as errors. The ADR's argument against a separate runner project holds.

5. The SchemaUpgrader fix

Correct diagnosis, and the test genuinely pins it. Reverting the line to $"...failed on '{result.ErrorScript?.Name}'." and rerunning turns Run_DatabaseUnreachable_ReportsTheFailureAndFails red with Applying the PlaceMark schema scripts failed on ''. — the exact string the fix exists to remove. Restored and re-ran green. result.ErrorScript being null for a connection-level failure is confirmed against three distinct causes (unreachable, rejected credential, malformed string), and the script-failure path still names the script.

6. ADR-0023 and the index

Conforms to ADR-0001 and template.md: Status, Date and Source; Context / Decision / Alternatives considered / Consequences; NNNN-short-title.md; imperative title; six alternatives with the substantive rejection argued rather than listed. Index row present, its title matches the record's, in numeric order after 0021. PR #18 does add 0022-problem-details-in-every-environment.md (checked its file list), so the number is taken rather than skipped and the stated conflict resolution is right. No 0022 reference anywhere in the tree. ADR-0021 is correctly left unedited despite now being answered.

7. Tests

<Method>_<Scenario>_<Expected> throughout, Shouldly throughout, no ?. in front of a Should…, Case.Sensitive where the string matters. Two nits, neither worth a push:

  • Run_DatabaseIsEmpty_AppliesEveryScriptAndSucceeds asserts one script name, so the name over-promises the moment a second script lands — AppliesTheSchemaAndSucceeds would age better.
  • output.ToString().ShouldContain("already up to date") omits Case.Sensitive while its neighbours in the same file pass it.
Verdict: mergeable Independent review of `91e52bf`, in a detached worktree. Nothing blocking. Everything below that is not marked *nit* or *observation* was executed rather than read. ## What I ran All four CI commands, on the head commit: `dotnet restore`, `dotnet build --configuration Release` (0 warnings, `TreatWarningsAsErrors` on), `dotnet test` (32 passing — 27 Infrastructure, 4 Api, 1 WebUI), `dotnet format --verify-no-changes` (exit 0). The entry point against a `postgres:18.4-trixie` container, every path: | Scenario | Result | | --- | --- | | Empty database | `Applied PlaceMark.Database.Scripts.0001-initial-schema.sql`, exit 0 | | Run again | `No pending scripts: the database is already up to date.`, exit 0 | | Variable unset | names `ConnectionStrings__PlaceMark`, exit 1 | | Unreachable (`Port=1`) | `failed.` + `Failed to connect to 127.0.0.1:1`, exit 1 | | Wrong password | `28P01: password authentication failed`, exit 1 | | Malformed connection string | `Format of the initialization string does not conform…`, exit 1 | | Failing script | `failed on 'PlaceMark.Database.Scripts.0001-initial-schema.sql'` + `42710: collation "case_insensitive" already exists`, exit 1 | The failing-script case was produced by clearing `schemaversions` while leaving the objects, which is also the state the transaction strategy is supposed to prevent. `schemaversions` confirms the journal key is the full resource name. ## 1. Is the honesty right? Yes, and I would have objected to the alternative. Task 42 is confirmed unstarted in Vikunja, so criteria 1 and 2 name a test database, a production database and a credential to each, none of which exist; a workflow against them is a file whose first execution is the day it matters, which is exactly what the ADR says. Criterion 3 is delivered *and* demonstrated, not asserted. Task 45 carries the partial-delivery comment and the `0022`→`0023` correction, and stays open. Correct call. Two observations on ADR-0023, raised now only because its body freezes on merge: - The forward shape — "production is the same command behind `workflow_dispatch`, where the dispatch *is* the gate… needs no further record, only wiring" — rests on a mechanism that has never run on this instance either, which is the objection the record itself uses to reject writing the workflows. A clause saying so, or saying that Forgejo has no GitHub-style environment protection rules and so `workflow_dispatch` is the only option rather than the chosen one, would stop a later reader having to work out which it was. - "Spelling the prefix out here is what makes such a rename fail in the pipeline instead of in a database" slightly overstates it: `SchemaUpgraderTests.Upgrade_Always_JournalsEachScriptByItsFullEmbeddedResourceName` already pins the exact full resource name, so on a rename the **Test** step goes red first. The grep is a second, prefix-only tripwire at the deployment entry point — still worth having, just not the only one. ## 2. Does the CI step prove what it claims? Yes, and it is not vacuous. Run 154's log shows the service container pulled, created and run on `FORGEJO-ACTIONS-TASK-171_WORKFLOW-CI_JOB-build-build-network` at `11:57:08`, before the job container — so `services:` does work on act_runner v6.4.0. The step itself logged `Applied PlaceMark.Database.Scripts.0001-initial-schema.sql` at `11:57:42.647` and `No pending scripts…` at `11:57:43.172`: half a second apart, no retry, and the grep's subject line was produced by an actual apply against an actually empty database. I then ran the step's shell verbatim against a local container with mutations injected. All four branches fire: - **Journal-key grep** — republished the project with `-p:RootNamespace=PlaceMark.SchemaRenamed` (confirmed the embedded resource became `PlaceMark.SchemaRenamed.Scripts.0001-initial-schema.sql`). The run succeeded and applied a script, and the step still failed: `Nothing was applied under the expected journal key 'PlaceMark.Database.Scripts.'.`, exit 1. That is the real hazard reproduced, not a doctored log. - **Second run applied something** — schema reset between invocations, so both runs succeed and apply: `The second run applied something, so the first was not journalled.`, exit 1. - **Second run failed** — journal wiped with objects left standing: the DbUp error is printed, then `The second run failed…`, exit 1. - **Retry exhaustion** — pointed at a dead port: exactly 10 attempts in 31s, the connect error printed, exit 1. The `ATTEMPT` arithmetic gives the 10 the message claims. ## 3. `ConnectionStrings__PlaceMark` and leak risk Consistent with ADR-0014: it is the double-underscore form of the key that record names, environment variables are its stated mechanism for deployed environments, and taking it from the environment rather than `argv` keeps the password out of the process list. No leak. The value in the workflow is the Compose default, and it is the credential of the container it targets, which lives for one job; no step enables `set -x`, and the run 154 log dumps no environment. I specifically tried the failure shapes that tend to echo the string back — unknown keyword and unparsable port, both with a password present — and got `Couldn't set bogus` / `Couldn't set port` with no value. Under ADR-0023's future shape the string becomes a repository secret and is masked as well. *Observation, non-blocking:* `Run` reads the raw environment variable rather than going through `Microsoft.Extensions.Configuration`, so ADR-0014's layering does not reach this command — a developer whose connection string lives in user secrets, as `user-secrets.example.json` prescribes, has to supply it a second time. The README's copy-paste hard-codes the Compose defaults two paragraphs above the one that invites you to change them in `.env`; half a sentence would close that. ## 4. `OutputType=Exe` on a project a fixture references No problem found, and I looked for the three that usually bite. The test project's output now also carries `PlaceMark.Database` (apphost), `.deps.json` and `.runtimeconfig.json` — inert, VSTest discovery is unaffected, 27 tests pass. There is no `Program` ambiguity: `PlaceMark.Api` does not reference `PlaceMark.Database` (checked every `.csproj`), so `WebApplicationFactory<Program>` in `PlaceMark.Api.Tests` is untouched, and both generated `Program` types are internal in any case. `CA1515` (public types in an executable) is not firing at `AnalysisLevel 10.0` / `Recommended` — the build is clean with warnings as errors. The ADR's argument against a separate runner project holds. ## 5. The `SchemaUpgrader` fix Correct diagnosis, and the test genuinely pins it. Reverting the line to `$"...failed on '{result.ErrorScript?.Name}'."` and rerunning turns `Run_DatabaseUnreachable_ReportsTheFailureAndFails` red with `Applying the PlaceMark schema scripts failed on ''.` — the exact string the fix exists to remove. Restored and re-ran green. `result.ErrorScript` being null for a connection-level failure is confirmed against three distinct causes (unreachable, rejected credential, malformed string), and the script-failure path still names the script. ## 6. ADR-0023 and the index Conforms to ADR-0001 and `template.md`: Status, Date and Source; Context / Decision / Alternatives considered / Consequences; `NNNN-short-title.md`; imperative title; six alternatives with the substantive rejection argued rather than listed. Index row present, its title matches the record's, in numeric order after 0021. PR #18 does add `0022-problem-details-in-every-environment.md` (checked its file list), so the number is taken rather than skipped and the stated conflict resolution is right. No `0022` reference anywhere in the tree. ADR-0021 is correctly left unedited despite now being answered. ## 7. Tests `<Method>_<Scenario>_<Expected>` throughout, Shouldly throughout, no `?.` in front of a `Should…`, `Case.Sensitive` where the string matters. Two nits, neither worth a push: - `Run_DatabaseIsEmpty_AppliesEveryScriptAndSucceeds` asserts one script name, so the name over-promises the moment a second script lands — `AppliesTheSchemaAndSucceeds` would age better. - `output.ToString().ShouldContain("already up to date")` omits `Case.Sensitive` while its neighbours in the same file pass it.
Action review on the schema command
All checks were successful
CI / build (pull_request) Successful in 45s
f2a67ee9f8
ADR-0023 no longer exempts itself from its own objection: nothing here
has been triggered by workflow_dispatch, so what the record fixes is the
shape of the gate — a person decides, and the decision is an act rather
than a merge — not the mechanism. If dispatch proves unusable the gate is
somebody running the command by hand.

Narrows the journal-key grep claim in all three places it appeared.
SchemaUpgraderTests pins the exact resource name, so the Test step
reddens first; the grep is the second line, earning its place where the
tests are not what is running or where a rename updates the test but not
the grep.

States plainly that the command reads the environment variable only,
rather than routing through Microsoft.Extensions.Configuration. A command
that alters a schema must not be ambiguous about which database, and a
layered lookup lets it run with nothing set against whatever a lower
layer named. User secrets are keyed by UserSecretsId, which belongs to
the API, so reaching them means a second store or borrowing another
project's identity.

Asserts the whole set of applied scripts rather than one, so a script the
wildcard fails to embed fails the test.
rob left a comment

Verdict: mergeable

Re-review at f2a67ee, from a clean archive of the head rather than my earlier worktree, so the rebase is included in what follows. All four points are actioned; nothing new is blocking. Two optional clauses in ADR-0023 are noted under 3, worth deciding before that body freezes and not worth another round.

Re-verified

CI run 158 is on this head: checked out f2a67ee9f83a78e913e5bbe33e9ed337a3d38cce, service container created before the job container as before, 47 tests (1 WebUI, 19 Api, 27 Infrastructure), then Applied PlaceMark.Database.Scripts.0001-initial-schema.sql followed by No pending scripts: the database is already up to date., job succeeded. The step is still doing real work on the rebased base.

Locally: Release build 0 warnings, 47 tests, format exit 0. I re-extracted the step from the rebased YAML and ran it under dash against a fresh postgres:18.4-trixie — applied, then no-op, exit 0 — and re-ran the entry point's failure paths (variable unset, dead port, wrong password, malformed string): all exit 1, none echoing the connection string.

Rebase itself: the index carries 0022 and 0023 as separate rows in numeric order, and 0015's Accepted (partly superseded by 0022) comes from #18 in the base rather than from here. PlaceMark.Database's sources are byte-identical to 91e52bf apart from the new XML-doc paragraph; CONTRIBUTING.md and SchemaUpgraderTests untouched. I reviewed none of #18's content — it is base now.

1. The deferred gate

Accepted, and it goes further than I asked. Naming the fallback — somebody runs the command against production by hand — means the record stays true whichever way workflow_dispatch turns out, so there is no longer an outcome that falsifies it. The instruction to treat a never-dispatched dispatch workflow with the same suspicion as a never-deployed deployment workflow is the right thing to leave for #42.

2. The grep claim

Verified rather than read, by reintroducing the rename two ways.

Through the tests: -p:RootNamespace=PlaceMark.Renamed on the project reddens the Test step with three failures — Upgrade_Always_JournalsEachScriptByItsFullEmbeddedResourceName, Upgrade_ScriptFailsHalfWay_LeavesNothingBehindAndDoesNotJournalIt and Run_DatabaseIsEmpty_AppliesEveryScriptAndSucceeds. So "the Test step above reddens on such a rename first" is true as written.

Through something the tests are not running: published the renamed project separately and handed the binary to the step, which is the case the narrowed claim reserves for itself. The run succeeded and applied a script, and the step still failed — Nothing was applied under the expected journal key 'PlaceMark.Database.Scripts.'., exit 1. Both halves hold. I re-ran the second-run assertion on this head too (schema reset between invocations): The second run applied something, so the first was not journalled., exit 1.

3. Reading the variable directly — judged

The conclusion is right, keep it. The two arguments are not equally strong, though, and it is worth knowing which one is carrying it.

The UserSecretsId argument is decisive, and it is a fact rather than a preference. AddUserSecrets needs a UserSecretsId on the assembly and PlaceMark.Database has none, so making user secrets work here means either a second store — and ADR-0014 has already written down what a store whose template drifts does, "a stale template is the predictable failure, and it presents as a null value rather than an error", which would now be two stores drifting — or borrowing PlaceMark.Api's identity, which couples a schema tool to a web project for no gain. That alone settles it.

The ambiguity argument is directionally right but stated more strongly than the tree supports. "A layered lookup would let it run with nothing set and silently upgrade whichever database some lower layer named" describes something that cannot happen today: PlaceMark.Database has no appsettings.json, so a builder with AddJsonFile(optional: true) plus AddEnvironmentVariables() would resolve to null and produce the same "set this variable" message the command already produces. The hazard becomes real the first time somebody adds a settings file to the project — which is an argument for the rule rather than against it, since the value of "one source, read directly" is precisely that it survives that commit. Optional: a clause along the lines of "there is no such layer in this project today, and this decision is to keep it that way rather than to fix a present bug" would make the record exact instead of predictive.

Optional, and the one I would actually take: the Consequences do not name the cost. ADR-0014's delivery is "a fresh clone is one command from a working local configuration"; this command sits outside that, so the Compose credential now appears in three places — docker-compose.yml, user-secrets.example.json and the README block — and a developer who changes .env has to change two of them. The README comment you added covers the reader who is looking at it; the ADR is where somebody asks later why the schema tool is not configured like everything else. One line under Consequences closes it.

For the record, the third option was considered and rejected correctly: a command-line argument would put the password in the process list. I checked again on this head that no failure path — including the unknown-keyword and unparsable-port forms — echoes the string back.

4. The empty-database test

Verified: added a second .sql to Scripts/, and the assertion failed with the two-line actual against the one-line expected, naming the extra line. Removed it, suite green. ShouldBe([...]) over the split lines is the right shape, and the comment is precise about the mechanism — adding a script means adding a line here, which is what turns a script the wildcard failed to embed into a red test rather than a silent pass. Case.Sensitive on the up-to-date assertion is consistent with its neighbours now.

Nothing further from me.

Verdict: mergeable Re-review at `f2a67ee`, from a clean archive of the head rather than my earlier worktree, so the rebase is included in what follows. All four points are actioned; nothing new is blocking. Two optional clauses in ADR-0023 are noted under 3, worth deciding before that body freezes and not worth another round. ## Re-verified CI run 158 is on this head: `checked out f2a67ee9f83a78e913e5bbe33e9ed337a3d38cce`, service container created before the job container as before, 47 tests (1 WebUI, 19 Api, 27 Infrastructure), then `Applied PlaceMark.Database.Scripts.0001-initial-schema.sql` followed by `No pending scripts: the database is already up to date.`, job succeeded. The step is still doing real work on the rebased base. Locally: Release build 0 warnings, 47 tests, `format` exit 0. I re-extracted the step from the rebased YAML and ran it under `dash` against a fresh `postgres:18.4-trixie` — applied, then no-op, exit 0 — and re-ran the entry point's failure paths (variable unset, dead port, wrong password, malformed string): all exit 1, none echoing the connection string. Rebase itself: the index carries `0022` and `0023` as separate rows in numeric order, and 0015's `Accepted (partly superseded by 0022)` comes from #18 in the base rather than from here. `PlaceMark.Database`'s sources are byte-identical to `91e52bf` apart from the new XML-doc paragraph; `CONTRIBUTING.md` and `SchemaUpgraderTests` untouched. I reviewed none of #18's content — it is base now. ## 1. The deferred gate Accepted, and it goes further than I asked. Naming the fallback — somebody runs the command against production by hand — means the record stays true whichever way `workflow_dispatch` turns out, so there is no longer an outcome that falsifies it. The instruction to treat a never-dispatched dispatch workflow with the same suspicion as a never-deployed deployment workflow is the right thing to leave for #42. ## 2. The grep claim Verified rather than read, by reintroducing the rename two ways. Through the tests: `-p:RootNamespace=PlaceMark.Renamed` on the project reddens the **Test** step with three failures — `Upgrade_Always_JournalsEachScriptByItsFullEmbeddedResourceName`, `Upgrade_ScriptFailsHalfWay_LeavesNothingBehindAndDoesNotJournalIt` and `Run_DatabaseIsEmpty_AppliesEveryScriptAndSucceeds`. So "the Test step above reddens on such a rename first" is true as written. Through something the tests are not running: published the renamed project separately and handed the binary to the step, which is the case the narrowed claim reserves for itself. The run succeeded and applied a script, and the step still failed — `Nothing was applied under the expected journal key 'PlaceMark.Database.Scripts.'.`, exit 1. Both halves hold. I re-ran the second-run assertion on this head too (schema reset between invocations): `The second run applied something, so the first was not journalled.`, exit 1. ## 3. Reading the variable directly — judged **The conclusion is right, keep it.** The two arguments are not equally strong, though, and it is worth knowing which one is carrying it. The `UserSecretsId` argument is decisive, and it is a fact rather than a preference. `AddUserSecrets` needs a `UserSecretsId` on the assembly and `PlaceMark.Database` has none, so making user secrets work here means either a second store — and ADR-0014 has already written down what a store whose template drifts does, "a stale template is the predictable failure, and it presents as a null value rather than an error", which would now be two stores drifting — or borrowing `PlaceMark.Api`'s identity, which couples a schema tool to a web project for no gain. That alone settles it. The ambiguity argument is directionally right but stated more strongly than the tree supports. "A layered lookup would let it run with nothing set and silently upgrade whichever database some lower layer named" describes something that cannot happen today: `PlaceMark.Database` has no `appsettings.json`, so a builder with `AddJsonFile(optional: true)` plus `AddEnvironmentVariables()` would resolve to null and produce the same "set this variable" message the command already produces. The hazard becomes real the first time somebody adds a settings file to the project — which is an argument *for* the rule rather than against it, since the value of "one source, read directly" is precisely that it survives that commit. **Optional:** a clause along the lines of "there is no such layer in this project today, and this decision is to keep it that way rather than to fix a present bug" would make the record exact instead of predictive. **Optional, and the one I would actually take:** the Consequences do not name the cost. ADR-0014's delivery is "a fresh clone is one command from a working local configuration"; this command sits outside that, so the Compose credential now appears in three places — `docker-compose.yml`, `user-secrets.example.json` and the README block — and a developer who changes `.env` has to change two of them. The README comment you added covers the reader who is looking at it; the ADR is where somebody asks later why the schema tool is not configured like everything else. One line under Consequences closes it. For the record, the third option was considered and rejected correctly: a command-line argument would put the password in the process list. I checked again on this head that no failure path — including the unknown-keyword and unparsable-port forms — echoes the string back. ## 4. The empty-database test Verified: added a second `.sql` to `Scripts/`, and the assertion failed with the two-line actual against the one-line expected, naming the extra line. Removed it, suite green. `ShouldBe([...])` over the split lines is the right shape, and the comment is precise about the mechanism — adding a script means adding a line here, which is what turns a script the wildcard failed to embed into a red test rather than a silent pass. `Case.Sensitive` on the up-to-date assertion is consistent with its neighbours now. Nothing further from me.
rob merged commit 00a6ff3155 into main 2026-08-03 12:20:25 +00:00
rob deleted branch feat/schema-in-ci 2026-08-03 12:20:25 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/PlaceMark!19
No description provided.