Skip to content

Stabilize the workspace and add clap-based CLI/env configuration - #1

Merged
stephane-segning merged 1 commit into
masterfrom
claude/project-stabilization-setup-b9e589
Aug 9, 2026
Merged

Stabilize the workspace and add clap-based CLI/env configuration#1
stephane-segning merged 1 commit into
masterfrom
claude/project-stabilization-setup-b9e589

Conversation

@stephane-segning

Copy link
Copy Markdown
Contributor

Summary

Makes the vpay scaffold actually start, run and stop correctly, and gives both binaries a real clap CLI whose every option auto-resolves from an environment variable. No payment capability is added — the 8 ProviderError::NotImplemented adapter tokens are untouched and vpay still cannot take a payment.

just ci could not pass before this change, and not for a flaky reason: pnpm -r test swept the Cypress package into the unit test sweep, so the recursive runner always hit cypress run with the binary deliberately not installed (CYPRESS_INSTALL_BINARY: 0 in CI). Alongside that, next build was broken, cargo deny check failed on a live advisory in a production dependency, and neither binary could be configured without editing source.

Source of truth. There is no tracking issue; this originated as a direct maintainer request. The load-bearing external references are the three advisories that drove the dependency work — RUSTSEC-2026-0009 (time, a production dependency), RUSTSEC-2025-0134 (rustls-pemfile) and RUSTSEC-2025-0111 (tokio-tar) — plus the in-repo decisions this change is bound by: ADR-0003 (a profile selects a config file, never a code path), ADR-0004 (static musl), ADR-0006 (no test doubles in shipping processes) and ADR-0007.

Intent

Six defects prevented the project from starting and working:

  1. just ci was structurally unpassable — the Cypress/unit sweep collision above. Not environment-dependent; arithmetically impossible.
  2. next build was broken and no test could see it. @vpay/ui imported with explicit .js suffixes; TypeScript's moduleResolution: "bundler" maps those back to the .ts source, so tsc and Vitest were green while webpack took the suffix literally and failed to resolve.
  3. cargo deny check failed, including on time — reachable from both shipping binaries, not just dev-dependencies.
  4. vpay-worker-bin exited immediately on boot, which an orchestrator reads as a crash loop.
  5. vpay-server had no signal handlingdocker compose down had to SIGKILL it — and hardcoded 0.0.0.0:8080.
  6. --shutdown-grace-seconds was parsed and consumed by nothing, advertising a bounded drain in --help that did not exist. Found by reading the code, not by any check.

Scope

Configuration (headline). clap 4.6.6 with derive/env/wrap_help. Options: VPAY_BIND, DATABASE_URL, VPAY_PROFILE, VPAY_CONFIG, VPAY_PUBLIC_BASE_URL, RUST_LOG, VPAY_LOG_FORMAT, VPAY_SHUTDOWN_GRACE_SECONDS. An explicit flag beats its env var. Shared options live in a flattened CommonArgs so the two binaries cannot drift, enforced by a test. --version reports a real 0.1.0 (workspace was 0.0.0). --profile's doc comment states outright that it selects a config file and never a code path.

Process lifecycle. vpay-server shuts down via with_graceful_shutdown on SIGINT/SIGTERM; serve_with_bounded_drain races the drain against a grace clock and exits non-zero if the clock wins, so a forced cutoff is distinguishable from a clean drain without parsing logs. vpay-worker-bin stays up answering the same signals while logging a startup banner and a 60s heartbeat stating the job loop is not implemented.

Pipeline/build. @vpay/e2e's script renamed teste2e with all four callers updated; .js-suffix imports fixed; backends/Dockerfile no longer hardcodes x86_64-unknown-linux-musl (Alpine's toolchain is already musl-native, so it builds the implicit host target — the old form could never build on an arm64 host); both runtime stages run as non-root UID 65532; new .dockerignore.

Supply chain — fixed by upgrading, not suppressing. deny.toml still has ignore = []. time 0.3.45→0.3.47; testcontainers 0.23→0.27 moves onto bollard 0.20, which drops rustls-pemfile entirely and replaces tokio-tar with the maintained astral-tokio-tar fork. Internal path deps carry versions, clearing the wildcard ban. rust-version 1.85→1.88.

schemas/vpay.cstack was written in a grammar that does not exist — Prisma syntax with an invented policy block, admitted as unverified in its own header because the vendor docs 404. Rewritten against the real CrateStack grammar. Notably payment_method_types String[] was a hard parse error (no SQL bind representation for a list scalar on a DB-backed model), so the old file could never have parsed; policy ledger_is_append_only on Charge named the ledger but attached to a table that is not the ledger, and no ledger model existed at all despite vpay-ledger having tested types.

Two documented claims were false and are corrected. docs/flows/configuration.md and docs/flows/ledger.md both asserted database CHECK constraints that the grammar cannot express (@db_enforce promotes only single-field validators; there is no cross-column @@check). Those invariants are enforced only in Rust.

Verification

just ci exits 0 end to end — fmt-checkclippy -D warnings → both self-checks → 64 Rust tests → typecheck → web tests → deny.

Check Before After
just ci could not pass exit 0
cargo nextest run --workspace 44 passed / 5 ignored 64 passed / 5 ignored
pnpm -r test aborted on Cypress 10 assertions pass
pnpm -r build next build failed 8 packages compile
cargo deny check advisories + bans FAILED all ok, ignore = []
cratestack check never run (invented grammar) schema OK

The ignored-test count is unchanged at 5 — nothing was un-ignored to inflate a green run.

Screenshots / Evidence

Server started from environment variables only, no flags:

$ VPAY_BIND=127.0.0.1:18099 VPAY_LOG_FORMAT=text ./vpay-server
INFO vpay_server: provider adapters linked rails=["mtn_momo", "orange_money"]
INFO vpay_server: deployment profile (selects a config file only) profile=sandbox
WARN vpay_server: vpay-server is a scaffold: only /healthz is implemented. See docs/STATUS.md
INFO vpay_server: listening addr=127.0.0.1:18099
INFO vpay_server: received SIGTERM, starting graceful shutdown
INFO vpay_server: graceful shutdown complete, exiting

/healthz200 ok; unknown route → the Stripe-shaped 404 envelope; SIGTERM → exit 0. The worker was confirmed still running after 3s (it previously exited instantly) and also exits 0 on SIGTERM. Both report 0.1.0 for --version.

Risk Assessment

Low-to-moderate, and the risk is concentrated in what could not be verified.

  • Unverified: the entire container path. No image was built and the compose stack was never brought up — Docker Hub is unreachable from the authoring environment (docker pull alpine:3.22 did not complete in five minutes; 4 of 5 required base images are uncached). The Dockerfiles were rewritten but never built. docs/STATUS.md records them as "revised, still never built". A reviewer with network access should build them before trusting them.
  • Unverified: MSRV 1.88 is derived from cargo metadata, never compiled against; 63 of 317 packages declare no rust_version, so the true floor could be higher. rust-toolchain.toml records this. The Dockerfile deliberately pins rust:1.95.0-alpine3.22 — the version actually known to build this workspace — rather than the unverified floor.
  • Unverified: the shutdown-grace timeout path. Covered by unit tests on the extracted grace_clock, but no live process test cuts off a genuinely slow request: the only route is /healthz, which answers instantly, and adding a slow test-only route would put a test double in the shipping router, which ADR-0006 and verify-no-mocks forbid. docs/STATUS.md says plainly the SIGTERM tests "would pass identically with the grace clock deleted."
  • Unverified: Cypress specs still have never executed (no binary, no stack).
  • Behavioural change worth a second opinion: the server exits non-zero when the grace period elapses before draining finishes. Reasoning is documented inline; an orchestrator already treats the container as stopped regardless, but reviewers may prefer exit 0.
  • Inert by design: --database-url, --config and --public-base-url are accepted but consumed by nothing — this is CLI plumbing, not the ADR-0003 config system. Their --help text says so. On the worker, --shutdown-grace-seconds also does nothing, and says so.

Both repo self-checks (verify-no-mocks, verify-status) pass, and the "vpay cannot take a payment / do not deploy it" banner remains prominent in both README.md and docs/STATUS.md.

AI Usage Declaration

AI (Claude Opus 5, via Claude Code) performed this work end to end: diagnosis, implementation across Rust/TypeScript/Docker/schema, and the documentation pass. Implementation was delegated to parallel Sonnet sub-agents on disjoint file sets; the orchestrating session independently re-ran every gate rather than accepting agent self-reports — which caught two defects the agents' own summaries did not surface (the inert --shutdown-grace-seconds flag, and a proposed unsafe env-mutation in tests that violates the workspace's unsafe_code = "forbid"). One agent hypothesis I passed on (that missing examples/* packages would break the frontend Docker install) was disproved by direct test and retracted.

  • A human is accountable for this change and has reviewed it.
  • Every claim in this description was verified by running the command, not inferred.
  • Limitations are stated explicitly rather than omitted — see Risk Assessment.
  • No functionality was fabricated; unimplemented paths still return NotImplemented and remain declared in docs/STATUS.md.

Reviewer Focus

  1. backends/apps/vpay-server/src/main.rs — the serve_with_bounded_drain / grace_clock oneshot race, and the std::process::exit(1) decision on timeout.
  2. The container path, which is the largest unverified surface. Please docker build both targets and bring up compose.yml -f compose.e2e.yml on a machine with registry access.
  3. schemas/vpay.cstack — its GAP comments assert that certain invariants cannot be expressed in the grammar. If that is wrong, the corrections to docs/flows/configuration.md and docs/flows/ledger.md are wrong too.
  4. docs/STATUS.md — is anything marked ✅ that would not fail a test if it broke?

`just ci` could not pass, `next build` was broken, `cargo deny check`
failed on a live advisory in a production dependency, and neither binary
could be configured without editing source. This makes the scaffold
actually start, run and stop correctly, without adding any payment
capability it does not have.

Configuration (the headline change):

* Both binaries now parse a clap CLI where every option auto-resolves
  from an environment variable, with an explicit flag beating its env
  var: VPAY_BIND, DATABASE_URL, VPAY_PROFILE, VPAY_CONFIG,
  VPAY_PUBLIC_BASE_URL, RUST_LOG, VPAY_LOG_FORMAT and
  VPAY_SHUTDOWN_GRACE_SECONDS. Shared options live in a flattened
  `CommonArgs` so the two binaries cannot drift.
* `--version` reports a real 0.1.0; the workspace was version 0.0.0.
* `--profile` selects a config *file* and never a code path, per
  ADR-0003. Its doc comment says so explicitly.
* `vpay-server` previously hardcoded 0.0.0.0:8080.

Process lifecycle:

* `vpay-server` now shuts down via `with_graceful_shutdown` on SIGINT or
  SIGTERM. It previously had to be SIGKILLed by `docker compose down`.
  `--shutdown-grace-seconds` bounds the drain and exits non-zero if the
  clock wins, so a forced cutoff is distinguishable from a clean drain.
* `vpay-worker-bin` no longer exits immediately on boot, which an
  orchestrator reads as a crash loop. It stays up answering the same
  signals while logging a startup banner and a 60s heartbeat stating the
  job loop is not implemented and no jobs are being processed.

Pipeline and build fixes:

* `pnpm -r test` swept the Cypress package into the unit sweep, so
  `just ci` and CI's `web` job were structurally incapable of passing.
  `@vpay/e2e`'s script is now `e2e`; all callers updated.
* `@vpay/ui` imported with explicit `.js` suffixes. TypeScript's
  `moduleResolution: "bundler"` maps those back to the `.ts` source, so
  `tsc` and Vitest passed while webpack took the suffix literally and
  `next build` failed outright. `pnpm -r build` now compiles all 8
  packages.
* `backends/Dockerfile` hardcoded `x86_64-unknown-linux-musl` and could
  never have built on an arm64 host; Alpine's toolchain is already
  musl-native, so it now builds the implicit host target. Both runtime
  stages run as non-root UID 65532. A `.dockerignore` was added.

Supply chain — fixed by upgrading, not suppressing (`ignore = []`):

* time 0.3.45 -> 0.3.47 for RUSTSEC-2026-0009, a *production* dependency.
* testcontainers 0.23 -> 0.27 moves onto bollard 0.20, which drops
  rustls-pemfile (RUSTSEC-2025-0134) and replaces tokio-tar with the
  maintained astral-tokio-tar fork (RUSTSEC-2025-0111).
* Internal path deps carry versions, clearing the wildcard ban.
* rust-version 1.85 -> 1.88, derived from cargo metadata. NOT verified by
  compiling; rust-toolchain.toml records that caveat.

schemas/vpay.cstack was written in a grammar that does not exist — Prisma
syntax with an invented policy block, admitted as unverified in its own
header. Rewritten against the real CrateStack grammar and verified with
`cratestack check` (0.7.8). It remains excluded from the build graph and
models only entities with a real, tested Rust type to mirror.

Two documented claims were false and are corrected: docs/flows/
configuration.md and docs/flows/ledger.md both asserted database CHECK
constraints that the schema grammar cannot express. Those invariants are
enforced only in Rust.

Deliberately NOT done, and recorded as such in docs/STATUS.md: no image
was built and the compose stack was never brought up (Docker Hub is
unreachable from the authoring environment); the Cypress specs have still
never executed; the shutdown-grace timeout path has no live test, because
the only route answers instantly and a slow test-only route would put a
test double in the shipping router; `--database-url`, `--config` and
`--public-base-url` are accepted but consumed by nothing. The 8
NotImplemented adapter tokens are untouched — vpay still cannot take a
payment.

Verified: `just ci` exits 0 (fmt-check, clippy -D warnings, both
self-checks, 64 Rust tests / 5 ignored, typecheck, web tests, deny).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 9, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 72d2b5b

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@stephane-segning
stephane-segning merged commit 237c716 into master Aug 9, 2026
3 of 6 checks passed
@stephane-segning
stephane-segning deleted the claude/project-stabilization-setup-b9e589 branch August 9, 2026 14:16
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