Repository navigation
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 intoOct 7, 2026
Conversation
…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
approved these changes
Oct 7, 2026
igorlamos
merged commit Oct 7, 2026
b07d9e0
into
feature/estate-consolidation-p0-foundations
7 checks passed
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 intomain, and tag v2.9.0 and movev2to 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) anddb-drift-env.yml(105 / 91), and bepluscloud has neither. This PR adds both here once, as reusable workflows. They run the repository's ownbeplus dbcommands, and everything each copy wrote out by hand now comes frombeplus.estate.json.scripts/verify-callers.mjs dbchecks that each estate's caller plus these workflows reproduce the copy it replaces, step by step: 0 divergent across the three estates.BE-232 — Reusable workflows
db-migrations-pr.yml+db-drift-env.yml(Linear)EXTRACTION-PLAN R2-4 was blocked until
beplus dbhad the commands these copies need. P2 adds them:@beplus/database-toolsplus@beplus/cli-plugin-database, withdb check pending | rebuild | chainsanddb 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_callonly) is the pull-request gate:pgvector/pgvector:0.8.7-pg16(the copies used the floatingpg16tag)be auto, BE-213)beplus npm auth, followed by the stub-200 registry check (BE_NPM_CHECK, below)rush installandrush build --to <database project>beplus db check,db check pending,db check chainsanddb check rebuild --compare. Each is the command the copies'npm run db:check:*ran, or now resolves to.BE_POSTGRES_URLis the job's own localhost service.BE_NPM_TOKENis the only secret it reads, and it needs no AWS credentials..github/workflows/db-drift-env.yml(new,workflow_callonly) is the drift check:beplus db diff migrations <env>for each environment, then compares each environment with the next.planjob derives what each copy hard-coded, and validates every value before a step uses it:database.package, then that package.json'sname. Theprojectinput overrides it.naming.prefix+_test, which givesmrkit_test,mcl_testandbe_testbeplus.estate.json→environments. Theenvironmentsinput overrides it; MRKIT passesdev produntildb diffsupportsstage.github-environmentnames the GitHub environment that holds the secrets when they are environment secrets (MRKIT:dev). Left empty, the job claims no environment.database.dumpImage, otherwise the tools' pin,pgvector/pgvector:pg16, which is the value the copies set.cli/action.ymlgainsBE_NPM_CHECK, opt-in. It lists package directories whose@beplusdependencies must resolve to real packages after the auth.{"status":200,"message":"OK"}.npm viewprints nothing and exits 0, and pnpm only fails minutes later withERR_PNPM_TARBALL_INTEGRITY.common/config/rush/.npmrc), otherwise npm's own, and skipsworkspace:dependencies..github/workflows/test-cli.ymlgains the leg "the registry check fails on the anonymous stub", with fixturetest/cli/npm-check/package.json. It maps@bepluswith 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.npmandbeplus, 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.mjslibrary mode no longer crashes. It usedHEAD~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_REFstill overrides it.README.mdandcli/README.mddocument both workflows (inputs table, caller examples, the check-name rename) andBE_NPM_CHECK, in a new section, "The registry never says no".Gates
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, Mountaineerd8d997d3c, bepluscloud3065b04)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, sincedb diffreads 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 copyBE_POSTGRES_URL; a connection secret in the PR gate;db migratein 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, forgetgithub-environment, or letstagein.node scripts/verify-callers.mjs infra-difffor all 3 estates (the P0 callers)verify-callerslibrary modeBE_NPM_CHECK, run locally against npm.beplus.cloud on the three estates' P3 database packages@beplusdependency 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.db-migrations-pr.yml,db-drift-env.yml,test-cli.ymlandcli/action.ymlDeviations
db diffcannot 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.project.github-environmentis needed because MRKIT's secrets aredevenvironment secrets.environmentsis needed because MRKIT must passdev produntildb diffsupportsstage. Everything else comes from the manifest.cliaction (BE_NPM_CHECK, opt-in), not inline in the workflow, so any job that runsbeplus npm authcan use it. Leaving the input empty keeps the old behaviour.db check chains. Its copy only ran the chains check locally (db:check), never in CI. The drift job'srush buildstep is dropped, becausedb diffreads no compiled schema.0.8.7-pg16, as the issue asked). The copies used the floatingpg16tag.migration integrity + rebuildtochecks / 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.verify-callerslibrary-mode fix (HEAD~1→ the parent of the commit that made the file a caller) is a fix to the same script that the newdbmode needed. It is not part of the issue's scope.Waiting on Igor (in order)
.github/workflowschanges; the push was refused with "refusing to allow a GitHub App to create or update workflow.github/workflows/db-drift-env.ymlwithoutworkflowspermission".git -C ~/Git/github.com/beplus/setup-beplus-estate-consolidation-p2 push -u origin feature/estate-consolidation-p2-database-toolsThen the PR, with Estate Consolidation P0 —
cliinstalls 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 cliruns, and its new leg "the registry check fails on the anonymous stub" is green; the action's outcome is failure, as asserted.cliinstalls the CLI the repository pins, and the estates' infra-diff becomes one reusable workflow #12 (P0) first, then this PR (squash). Then movev2:git tag -f v2 <merge-sha> && git push -f origin v2. After that, callers'@v2getsdb-migrations-pr.yml,db-drift-env.ymlandBE_NPM_CHECK. Until then, an oldercli@v2only warns about the unknown input and runs no check.@beplus/database0.1.0 and@beplus/database-tools, and release CLI 2.12.0 bundling@beplus/cli-plugin-databasewithdb check pending | rebuild | chains; this is what the estates pin in P3. Until@beplus/databaseis published,BE_NPM_CHECKcorrectly fails with a 404 for it.scratchpad/p2/db-callers/{mountaineer,mrkit,bepluscloud}/.db-drift-envrun on Mountaineer and MRKIT shows✓ migrations, dev, and prod are structurally 1:1:1.migration integrity + rebuild, rename it tochecks / migration integrity + rebuild.github-environment. Confirm GitHub treatsenvironment: ''as no environment: the job should run with no environment and no deployment record.BE_POSTGRES_URL_DEV/_STAGE/_PROD(read-capable) asdevenvironment secrets."database": {"package": "packages/data/database"}tobeplus.estate.json.stageleg also needs@beplus/database-toolsto support thestagediff target. Until then, passenvironments: dev prod.