Skip to content

Estate Consolidation P2 — the estates' database gates (the migrations PR check and the nightly drift) become two reusable workflows - #13

Merged
igorlamos merged 1 commit into
feature/estate-consolidation-p0-foundationsfrom
feature/estate-consolidation-p2-database-tools
Oct 7, 2026
Merged

igorlamos merged 1 commit into
feature/estate-consolidation-p0-foundationsfrom
feature/estate-consolidation-p2-database-tools

Conversation

@beplus-agent-claude

Copy link
Copy Markdown
Contributor

Stacked on #12. This PR targets feature/estate-consolidation-p0-foundations, the head branch of #12. Merge order: squash-merge this PR first, so it lands on #12's branch. Then squash-merge #12 into main, and tag v2.9.0 and move v2 to that commit. One tag move covers both PRs, and nobody has to re-push a workflow branch in between.

MRKIT and Mountaineer each carry their own db-migrations-pr.yml (146 / 137 lines) and db-drift-env.yml (105 / 91), and bepluscloud has neither. This PR adds both here once, as reusable workflows. They run the repository's own beplus db commands, and everything each copy wrote out by hand now comes from beplus.estate.json. scripts/verify-callers.mjs db checks that each estate's caller plus these workflows reproduce the copy it replaces, step by step: 0 divergent across the three estates.

Part of Linear initiative I-34 "Estate Consolidation — One Copy of Every Helper", project P2 — @beplus/database-tools + @beplus/cli-plugin-database (https://linear.app/bepluscloud/project/estate-consolidation-p2-beplusdatabase-tools-bepluscli-plugin-database-4eddb7faa1e8).
I-34 uses one branch per project per repository and one commit per issue. Please review commit by commit, then squash-merge.

Commit Issue
ff11bf5 feat(db): the estates' database gates — the migrations PR check and the nightly drift — as two reusable workflows (BE-232) BE-232

Stacked: base is feature/estate-consolidation-p0-foundations (#12, BE-213 + BE-218). This PR's own diff is only ff11bf5. Merge #12 first. Once #12 is squash-merged, this branch gets rebased onto main and the base changes to main. The commit is signed.


BE-232 — Reusable workflows db-migrations-pr.yml + db-drift-env.yml (Linear)

EXTRACTION-PLAN R2-4 was blocked until beplus db had the commands these copies need. P2 adds them: @beplus/database-tools plus @beplus/cli-plugin-database, with db check pending | rebuild | chains and db diff. Both are resolved from the repository's own install and never bundled into the CLI (D3).

What changed

  • .github/workflows/db-migrations-pr.yml (new, workflow_call only) is the pull-request gate:
    • a pgvector Postgres service pinned to pgvector/pgvector:0.8.7-pg16 (the copies used the floating pg16 tag)
    • the repository's pinned CLI (be auto, BE-213)
    • beplus npm auth, followed by the stub-200 registry check (BE_NPM_CHECK, below)
    • rush install and rush build --to <database project>
    • then beplus db check, db check pending, db check chains and db check rebuild --compare. Each is the command the copies' npm run db:check:* ran, or now resolves to.
    • BE_POSTGRES_URL is the job's own localhost service. BE_NPM_TOKEN is the only secret it reads, and it needs no AWS credentials.
  • .github/workflows/db-drift-env.yml (new, workflow_call only) is the drift check:
    • It runs beplus db diff migrations <env> for each environment, then compares each environment with the next.
    • All results are aggregated, so one drift does not hide another.
    • The schedule and the manual trigger stay in each repository's caller.
  • A plan job derives what each copy hard-coded, and validates every value before a step uses it:
    • the Rush project: database.package, then that package.json's name. The project input overrides it.
    • the test database: naming.prefix + _test, which gives mrkit_test, mcl_test and be_test
    • the environments: beplus.estate.json → environments. The environments input overrides it; MRKIT passes dev prod until db diff supports stage.
    • github-environment names the GitHub environment that holds the secrets when they are environment secrets (MRKIT: dev). Left empty, the job claims no environment.
    • The job no longer sets the dump image. It is the repository's database.dumpImage, otherwise the tools' pin, pgvector/pgvector:pg16, which is the value the copies set.
  • cli/action.yml gains BE_NPM_CHECK, opt-in. It lists package directories whose @beplus dependencies must resolve to real packages after the auth.
    • Why: npm.beplus.cloud answers an unauthenticated read with HTTP 200 and {"status":200,"message":"OK"}. npm view prints nothing and exits 0, and pnpm only fails minutes later with ERR_PNPM_TARBALL_INTEGRITY.
    • The check fails the step first and names the cause: the stub, or npm's own error, such as a 404.
    • It reads the registry Rush hands pnpm (common/config/rush/.npmrc), otherwise npm's own, and skips workspace: dependencies.
  • .github/workflows/test-cli.yml gains the leg "the registry check fails on the anonymous stub", with fixture test/cli/npm-check/package.json. It maps @beplus with no token and asserts that the action fails.
  • scripts/verify-callers.mjs db: a new mode that compares each estate's caller plus these workflows against the copy they replace.
    • It compares every job key and every step.
    • It does not re-implement the derivation: it runs the shared plan step on the estate's own manifest and package.json.
    • The drift job's guard and comparison are now loops over the environments. It runs them side by side with the copies' versions, with a stub npm and beplus, in every scenario that changes their output (6 for the guard, 5 for the comparison). It requires the same stdout, stderr and exit code.
  • scripts/verify-callers.mjs library mode no longer crashes. It used HEAD~1, which compared each caller with itself once the callers had landed. It now takes the parent of the commit that turned each file into a caller; $BEFORE_REF still overrides it.
  • README.md and cli/README.md document both workflows (inputs table, caller examples, the check-name rename) and BE_NPM_CHECK, in a new section, "The registry never says no".

Gates

Command Result
node scripts/verify-callers.mjs db --repo …/mrkithq/monorepo --ref dev --callers p2/db-callers/mrkit --repo …/ironmountain-cz/monorepo --ref origin/dev --callers p2/db-callers/mountaineer --repo …/bepluscloud/ai --ref origin/dev --callers p2/db-callers/bepluscloud --manifest p2/db-callers/bepluscloud/beplus.estate.json
(re-run when this PR was prepared: MRKIT 1c28cd1d1, Mountaineer d8d997d3c, bepluscloud 3065b04)
exit 0, 0 divergent. 12 identical, 6 identical once derived, 11 intended, 4 run side by side, 1 added, 1 removed. The 11 intended changes: the CLI pin, the auth step's name in Mountaineer, and npm run db:check:* → beplus db check …. The 1 added: MRKIT's chains check, which its CI never ran. The 1 removed: MRKIT's drift build, since db diff reads no compiled schema. bepluscloud gains both workflows: 2 + 2 jobs, 12 + 9 steps.
python3 p2/mutate.py: 18 mutations of the shared workflows and the callers, each on a scratch copy 18/18 go red. The mutations: an edited or dropped step; a remote BE_POSTGRES_URL; a connection secret in the PR gate; db migrate in the drift job; id-token: write; a wrong test database; a floating service image; and callers that drop a trigger, pass an undeclared input, rename their job, forget github-environment, or let stage in.
node scripts/verify-callers.mjs infra-diff for all 3 estates (the P0 callers) exit 0, 0 divergent: 18 identical, 3 identical once derived, 6 intended, 3 added. Unchanged from P0.
verify-callers library mode It no longer crashes. ci: 12/12 identical. publish: 12 workflows report library-publish's deliberate later changes (#8–#10). These are not regressions.
BE_NPM_CHECK, run locally against npm.beplus.cloud on the three estates' P3 database packages With the token: every @beplus dependency resolves except @beplus/database, which returns a 404 because it is unpublished; that is correct until 0.1.0 is out. Anonymous: the stub is detected and the step goes red.
YAML parse of db-migrations-pr.yml, db-drift-env.yml, test-cli.yml and cli/action.yml all parse
Independent review re-run after the commit: verify-callers db, infra-diff and library same counts as above; db and infra-diff 0 divergent

Deviations

  • No OIDC read role for the drift check. The issue said "OIDC read role". No account has a database-read OIDC role yet, and db diff cannot reach a private instance through the tunnel. So the connection strings stay secrets (BE_POSTGRES_URL_DEV / _STAGE / _PROD), as in the copies, and the workflow assumes no AWS role. The workflow header says this and lists what the role would take.
  • Two inputs beyond project. github-environment is needed because MRKIT's secrets are dev environment secrets. environments is needed because MRKIT must pass dev prod until db diff supports stage. Everything else comes from the manifest.
  • The registry check is in the cli action (BE_NPM_CHECK, opt-in), not inline in the workflow, so any job that runs beplus npm auth can use it. Leaving the input empty keeps the old behaviour.
  • MRKIT's PR gate gains db check chains. Its copy only ran the chains check locally (db:check), never in CI. The drift job's rush build step is dropped, because db diff reads no compiled schema.
  • The Postgres service image is pinned (0.8.7-pg16, as the issue asked). The copies used the floating pg16 tag.
  • The check name changes from migration integrity + rebuild to checks / migration integrity + rebuild. A reusable workflow's job is prefixed with the caller's job id. Branch protection that requires the old name needs a one-time rename.
  • The verify-callers library-mode fix (HEAD~1 → the parent of the commit that made the file a caller) is a fix to the same script that the new db mode needed. It is not part of the issue's scope.
  • Old copies are not touched here. The estates' callers replace their copies in their adoption issues (MCL-242, MRK-923, BE-234).

Waiting on Igor (in order)

  1. Push this branch. The agent App cannot push .github/workflows changes; the push was refused with "refusing to allow a GitHub App to create or update workflow .github/workflows/db-drift-env.yml without workflows permission".
    git -C ~/Git/github.com/beplus/setup-beplus-estate-consolidation-p2 push -u origin feature/estate-consolidation-p2-database-tools
    Then the PR, with Estate Consolidation P0 — cli installs the CLI the repository pins, and the estates' infra-diff becomes one reusable workflow #12's branch as its base: beplus gh pr create --repo beplus/setup-beplus --base feature/estate-consolidation-p0-foundations --head feature/estate-consolidation-p2-database-tools …. The agent opens it once the branch is on origin.
    → Test cli runs, and its new leg "the registry check fails on the anonymous stub" is green; the action's outcome is failure, as asserted.
  2. Merge Estate Consolidation P0 — cli installs the CLI the repository pins, and the estates' infra-diff becomes one reusable workflow #12 (P0) first, then this PR (squash). Then move v2: git tag -f v2 <merge-sha> && git push -f origin v2. After that, callers' @v2 gets db-migrations-pr.yml, db-drift-env.yml and BE_NPM_CHECK. Until then, an older cli@v2 only warns about the unknown input and runs no check.
  3. Publish @beplus/database 0.1.0 and @beplus/database-tools, and release CLI 2.12.0 bundling @beplus/cli-plugin-database with db check pending | rebuild | chains; this is what the estates pin in P3. Until @beplus/database is published, BE_NPM_CHECK correctly fails with a 404 for it.
  4. Estate adoption (MCL-242, MRK-923, BE-234):
    • Copy the callers from scratchpad/p2/db-callers/{mountaineer,mrkit,bepluscloud}/.
    • Then: one real migration PR per estate shows the same check results as before.
    • One manual db-drift-env run on Mountaineer and MRKIT shows ✓ migrations, dev, and prod are structurally 1:1:1.
    • Where branch protection requires migration integrity + rebuild, rename it to checks / migration integrity + rebuild.
  5. Mountaineer's first run passes no github-environment. Confirm GitHub treats environment: '' as no environment: the job should run with no environment and no deployment record.
  6. bepluscloud (BE-234):
    • Create BE_POSTGRES_URL_DEV / _STAGE / _PROD (read-capable) as dev environment secrets.
    • Add "database": {"package": "packages/data/database"} to beplus.estate.json.
    • The stage leg also needs @beplus/database-tools to support the stage diff target. Until then, pass environments: dev prod.

…he nightly drift — as two reusable workflows (BE-232)

mrkithq/monorepo and ironmountain-cz/monorepo each carried db-migrations-pr.yml (146 / 137 lines)
and db-drift-env.yml (105 / 91); bepluscloud/ai had neither. Both now live here once and run the
repository's own `beplus db` commands — @beplus/database-tools, resolved from the repository's
install, never bundled in the CLI (D3) — over beplus.estate.json:

- db-migrations-pr.yml: a pgvector service pinned to pgvector/pgvector:0.8.7-pg16 (the copies
  floated on pg16), the pinned CLI, `beplus npm auth` with the stub-200 registry check, rush
  install / build --to the database project, then `beplus db check`, `db check pending`,
  `db check chains`, `db check rebuild --compare` — each the command the copies' `npm run
  db:check:*` ran or now resolves to. BE_POSTGRES_URL is its own localhost service; BE_NPM_TOKEN
  is its only secret.
- db-drift-env.yml: `beplus db diff migrations <env>` for each environment, then each environment
  against the next, aggregated so one drift does not hide another; scheduled + on demand stay in
  the caller. Connection strings stay secrets (BE_POSTGRES_URL_DEV / _STAGE / _PROD): no account
  has a database-read OIDC role yet, and `db diff` does not reach a private instance through the
  tunnel, so it assumes no AWS role — said in the header, with what it takes.

A plan job derives what each copy wrote out: the rush project (database.package → its
package.json name; `project` wins), the test database (naming.prefix + _test: mrkit_test,
mcl_test, be_test), the environments (`environments`; the input wins — MRKIT passes `dev prod`
until `db diff` knows stage). All validated before a step pastes them. `github-environment` says
which GitHub environment holds the secrets when they are environment secrets (MRKIT: dev);
empty claims none. The dump image is no longer set in the job: the repository's
database.dumpImage, else the tools' pin — pgvector/pgvector:pg16, the value the copies set.

The registry check is the `cli` action's, opt-in: BE_NPM_CHECK names package directories whose
@beplus dependencies must come back as real packages after the auth. npm.beplus.cloud answers an
unauthenticated read with HTTP 200 and {"status":200,"message":"OK"}; `npm view` prints nothing
and exits 0, and pnpm fails minutes later with ERR_PNPM_TARBALL_INTEGRITY. The check fails the
step first, naming the stub or npm's error (a 404). Locally against npm.beplus.cloud: with the
token every dependency of the three estates' P3 database packages resolves but @beplus/database
(unpublished → 404, as it must until 0.1.0 is out); anonymous → the stub, red. test-cli.yml gains
the anonymous leg.

scripts/verify-callers.mjs gains `db`: the shared jobs, resolved for an estate, against the copy's
jobs — every key, every step — with the derivation not re-implemented but RUN (the plan step, on
the estate's own manifest and package.json), and the drift job's guard and comparison, which are
loops over the environments now, run side by side with the copies' (stub npm / beplus, every
scenario that changes their output: 6 for the guard, 5 for the comparison) — same stdout, stderr,
exit. Callers for MRKIT (dev), Mountaineer (origin/dev) and bepluscloud (origin/dev, gains both,
with the database section BE-234 adds): 0 divergent — 12 identical, 6 identical once derived,
11 intended (CLI pin, the auth step's name in Mountaineer, `npm run db:check:*` → `beplus db
check …`), 4 run side by side, 1 added (MRKIT's chains check, which its CI never ran), 1 removed
(MRKIT's drift build: `db diff` reads no compiled schema). Eighteen mutations (an edited or
dropped step, a remote BE_POSTGRES_URL, a connection secret in the PR gate, a migrate in the drift
job, id-token, a wrong test database, a floating service image, a caller that drops a trigger,
passes an undeclared input, renames its job, forgets its environment or lets stage in) all go
red. infra-diff mode unchanged (30 steps, 0 divergent).

The library mode no longer crashes: HEAD~1 compared each caller with itself once the callers had
landed; it now takes the parent of the commit that made each file a caller ($BEFORE_REF still
wins). ci: 12/12 identical; publish: 12 report library-publish's deliberate later changes (#8–#10).

Co-Authored-By: Igor Lamos <igor@be.plus>
@igorlamos igorlamos self-assigned this Oct 7, 2026
@igorlamos
igorlamos merged commit b07d9e0 into feature/estate-consolidation-p0-foundations Oct 7, 2026
7 checks passed
@igorlamos
igorlamos deleted the feature/estate-consolidation-p2-database-tools branch October 7, 2026 21:59
igorlamos added a commit that referenced this pull request Oct 7, 2026
…ff becomes one reusable workflow (#12)

* feat(cli): installs the beplus CLI the repository pins, via `be auto` (BE-213)

A CLI release must never change what a repository's CI does without a commit in that
repository, and `cli` defaulted to `latest`. Now, with no `BE_CLI_VERSION`, it installs the
version pinned in the nearest beplus.estate.json → cli.version by running `be auto` — the same
command a laptop, a Docker build stage or a buildspec runs, so the pin is resolved in one
place — and then fails unless `beplus --version` is exactly that pin. An inexact or unreadable
pin fails the run; it never falls back to `latest`.

An explicit `BE_CLI_VERSION` still wins (with a warning when it differs from the pin). With no
pin the action keeps today's behaviour, `be latest`, and says with a `::warning::` that the
repository floats. `BE_WORKING_DIRECTORY` says where the nearest manifest is looked up from
(default: the workspace).

`cdk-package` installs the CLI through `cli`; its `cli-version` default goes from `latest` to
empty so it honours the pin too (its note about `latest` being v1 is stale: latest is 2.11.0).
The four reusable workflows stop passing an explicit `latest`, which would override a pin;
`vars.BE_CLI_VERSION` still wins when it is set.

`test-cli.yml` runs this branch's action against fixtures in test/cli: a pin installs exactly
it (2.10.0 — not the newest release, so a run that floated cannot pass), also from a package
directory below the manifest; BE_CLI_VERSION wins; no pin and no manifest float on latest; an
inexact pin (`2`) fails the run. The pinned legs go green once `be auto` reads
beplus.estate.json on beplus/be@main.

Co-Authored-By: Igor Lamos <igor@be.plus>

* feat(infra-diff): the estates' pull-request cdk diff, as one reusable workflow (BE-218)

bepluscloud/ai, ironmountain-cz/monorepo and mrkithq/monorepo each carried a 186-line
infra-diff.yml. Their jobs differed in three values, and all three are derived now, so the
workflow has no inputs:

- the role github-actions-<owner>-<repo>-<env>-cdk-diff, from the calling repository
  (github.repository_owner + github.event.repository.name), as each estate's
  GitHubOIDCStack names it; the account is still vars.BE_AWS_ACCOUNT_ID of the environment;
- the region, from beplus.estate.json defaultRegion (vars.AWS_REGION still wins);
- the sticky-comment marker, from naming.product lower-cased: be / mountaineer / mrkit,
  exactly the markers the copies used, so the switch edits a PR's existing comment.

A plan step reads the last two from the manifest, validates them, and hands the marker to
the comment script through env, never pasted into it. Every other step body is
byte-identical to the copies, bar the CLI version: `vars.BE_CLI_VERSION || '2'` floated on
the newest 2.x; empty now installs the repository's pin through `be auto` (BE-213).

It cannot deploy: the composite always runs `mode: diff` as the read-only …-cdk-diff role,
both literals, and with no input declared a caller that passes `mode` is refused by GitHub.
The trigger paths, the permissions and the concurrency group stay in each repository's
~30-line caller.

scripts/verify-callers.mjs gains `infra-diff`: it compares the shared jobs, resolved for an
estate, with the file its caller replaced — every job key, every step, every run/script
byte — allowing only declared differences (three derivations, the BE-213 pin, the added
manifest step/outputs/env), and checks the shared workflow can only diff. The three callers
against bepluscloud origin/dev, Mountaineer origin/dev and MRKIT dev: 30 steps, 18
identical, 3 identical once derived, 6 the intended pin, 3 added, 0 divergent. Nine
mutations (deploy mode, deploy role, a mode input, an edited or dropped step, a caller
passing mode, a lost path, an extra job, the wrong repository) all go red. The R2-1
library mode is unchanged.

Co-Authored-By: Igor Lamos <igor@be.plus>

* feat(db): the estates' database gates — the migrations PR check and the nightly drift — as two reusable workflows (BE-232) (#13)

mrkithq/monorepo and ironmountain-cz/monorepo each carried db-migrations-pr.yml (146 / 137 lines)
and db-drift-env.yml (105 / 91); bepluscloud/ai had neither. Both now live here once and run the
repository's own `beplus db` commands — @beplus/database-tools, resolved from the repository's
install, never bundled in the CLI (D3) — over beplus.estate.json:

- db-migrations-pr.yml: a pgvector service pinned to pgvector/pgvector:0.8.7-pg16 (the copies
  floated on pg16), the pinned CLI, `beplus npm auth` with the stub-200 registry check, rush
  install / build --to the database project, then `beplus db check`, `db check pending`,
  `db check chains`, `db check rebuild --compare` — each the command the copies' `npm run
  db:check:*` ran or now resolves to. BE_POSTGRES_URL is its own localhost service; BE_NPM_TOKEN
  is its only secret.
- db-drift-env.yml: `beplus db diff migrations <env>` for each environment, then each environment
  against the next, aggregated so one drift does not hide another; scheduled + on demand stay in
  the caller. Connection strings stay secrets (BE_POSTGRES_URL_DEV / _STAGE / _PROD): no account
  has a database-read OIDC role yet, and `db diff` does not reach a private instance through the
  tunnel, so it assumes no AWS role — said in the header, with what it takes.

A plan job derives what each copy wrote out: the rush project (database.package → its
package.json name; `project` wins), the test database (naming.prefix + _test: mrkit_test,
mcl_test, be_test), the environments (`environments`; the input wins — MRKIT passes `dev prod`
until `db diff` knows stage). All validated before a step pastes them. `github-environment` says
which GitHub environment holds the secrets when they are environment secrets (MRKIT: dev);
empty claims none. The dump image is no longer set in the job: the repository's
database.dumpImage, else the tools' pin — pgvector/pgvector:pg16, the value the copies set.

The registry check is the `cli` action's, opt-in: BE_NPM_CHECK names package directories whose
@beplus dependencies must come back as real packages after the auth. npm.beplus.cloud answers an
unauthenticated read with HTTP 200 and {"status":200,"message":"OK"}; `npm view` prints nothing
and exits 0, and pnpm fails minutes later with ERR_PNPM_TARBALL_INTEGRITY. The check fails the
step first, naming the stub or npm's error (a 404). Locally against npm.beplus.cloud: with the
token every dependency of the three estates' P3 database packages resolves but @beplus/database
(unpublished → 404, as it must until 0.1.0 is out); anonymous → the stub, red. test-cli.yml gains
the anonymous leg.

scripts/verify-callers.mjs gains `db`: the shared jobs, resolved for an estate, against the copy's
jobs — every key, every step — with the derivation not re-implemented but RUN (the plan step, on
the estate's own manifest and package.json), and the drift job's guard and comparison, which are
loops over the environments now, run side by side with the copies' (stub npm / beplus, every
scenario that changes their output: 6 for the guard, 5 for the comparison) — same stdout, stderr,
exit. Callers for MRKIT (dev), Mountaineer (origin/dev) and bepluscloud (origin/dev, gains both,
with the database section BE-234 adds): 0 divergent — 12 identical, 6 identical once derived,
11 intended (CLI pin, the auth step's name in Mountaineer, `npm run db:check:*` → `beplus db
check …`), 4 run side by side, 1 added (MRKIT's chains check, which its CI never ran), 1 removed
(MRKIT's drift build: `db diff` reads no compiled schema). Eighteen mutations (an edited or
dropped step, a remote BE_POSTGRES_URL, a connection secret in the PR gate, a migrate in the drift
job, id-token, a wrong test database, a floating service image, a caller that drops a trigger,
passes an undeclared input, renames its job, forgets its environment or lets stage in) all go
red. infra-diff mode unchanged (30 steps, 0 divergent).

The library mode no longer crashes: HEAD~1 compared each caller with itself once the callers had
landed; it now takes the parent of the commit that made each file a caller ($BEFORE_REF still
wins). ci: 12/12 identical; publish: 12 report library-publish's deliberate later changes (#8–#10).

Co-authored-by: bea-claude <bea+claude@be.plus>
Co-authored-by: Igor Lamos <igor@be.plus>

---------

Co-authored-by: bea-claude <bea+claude@be.plus>
Co-authored-by: Igor Lamos <igor@be.plus>
Co-authored-by: beplus-agent-claude[bot] <300068699+beplus-agent-claude[bot]@users.noreply.github.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.

2 participants