From 1a8f9424a53b7a78746abd4d7bd0fd0f4435791e Mon Sep 17 00:00:00 2001 From: Dave Liu <7david12liu@gmail.com> Date: Mon, 13 Jul 2026 10:15:51 -0700 Subject: [PATCH] ci: gate commerce journal transactions (#167) --- .github/workflows/ci.yml | 31 ++++++- .github/workflows/deploy.yml | 2 + GITHUB_ISSUES.md | 8 +- GITHUB_ISSUE_SLICES.md | 3 +- OFFICER_START_HERE.md | 1 + OPERATIONS_RUNBOOK.md | 4 +- SECURITY.md | 4 +- docs/officers/PUBLISH_AND_CHECK.md | 15 ++-- tests/ci-workflow.test.js | 130 +++++++++++++++++++++++++++++ tests/release-workflow.test.js | 4 + 10 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 tests/ci-workflow.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8acbd075..f7184a8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,8 @@ jobs: - name: Validate protected release workflow run: | ruby -e 'require "yaml"; YAML.safe_load(File.read(".github/workflows/deploy.yml"), permitted_classes: [], permitted_symbols: [], aliases: true)' - node --test tests/release-workflow.test.js tests/firebase-release-verification.test.js + ruby -e 'require "yaml"; YAML.safe_load(File.read(".github/workflows/ci.yml"), permitted_classes: [], permitted_symbols: [], aliases: true)' + node --test tests/ci-workflow.test.js tests/release-workflow.test.js tests/firebase-release-verification.test.js - run: npm run lint:fix -- --max-warnings=0 || true - name: Build env: @@ -58,6 +59,34 @@ jobs: # firebase-admin so no emulator is needed. - run: npm run test:run + commerce-command-journal: + name: Commerce command journal emulator + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: '20' + cache: 'npm' + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + - name: Install committed root dependencies + run: npm ci --legacy-peer-deps --ignore-scripts + - name: Install committed Functions dependencies + run: npm --prefix functions ci --ignore-scripts + - name: Run commerce command journal concurrency tests + env: + REQUIRE_COMMERCE_COMMAND_JOURNAL_EMULATOR: '1' + run: >- + npx --no-install firebase emulators:exec + --project demo-pay002b2-test + --only firestore + "npm --prefix functions run test:run -- + --runInBand commerceCommandJournal.emulator.test.js" + firestore-rules: name: Firestore security-rules tests runs-on: ubuntu-latest diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dbe336e6..bca7a43f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -112,6 +112,7 @@ jobs: for required_job in \ "Frontend lint + build" \ "Cloud Functions lint + test" \ + "Commerce command journal emulator" \ "Firestore security-rules tests" do if ! jq -e --arg name "$required_job" ' @@ -391,6 +392,7 @@ jobs: for required_job in \ "Frontend lint + build" \ "Cloud Functions lint + test" \ + "Commerce command journal emulator" \ "Firestore security-rules tests" do if ! jq -e --arg name "$required_job" ' diff --git a/GITHUB_ISSUES.md b/GITHUB_ISSUES.md index d33288c2..a9d6207a 100644 --- a/GITHUB_ISSUES.md +++ b/GITHUB_ISSUES.md @@ -137,12 +137,12 @@ Do not redesign routing or hosting in this issue; WEB-001 owns that. Optimized p ## CI-001 — Repair test gates and secure the deployment pipeline **Labels:** `priority:P0`, `type:security`, `type:testing`, `area:ci`, `size:L`, `needs-external-config` -**Status:** Published as tracker [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105). [#103](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/103), [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124), and [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126) provide the local/hosted frontend and SPA baselines. [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135) owns the manual exact-commit, backend-first source gate and Git-triggered Netlify production containment. [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133) owns protected OIDC/environment configuration, and [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136) owns the actual profile-recovery release. Fail-closed lint, required checks, scanning, staging/live evidence, and protected Netlify publication remain open, so the pipeline is still non-compliant overall. +**Status:** Published as tracker [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105). [#103](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/103), [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124), and [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126) provide the local/hosted frontend and SPA baselines. CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) owns the dedicated hosted command-journal Firestore transaction gate. [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135) owns the manual exact-commit, backend-first source gate and Git-triggered Netlify production containment. [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133) owns protected OIDC/environment configuration, and [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136) owns the actual profile-recovery release. Fail-closed lint, branch-required-check configuration, scanning, staging/live evidence, and protected Netlify publication remain open, so the pipeline is still non-compliant overall. **Depends on:** SAFETY-001 ### Problem -Before #103, frontend tests failed because the Jest environment lacked `TextEncoder`. That child repaired the local baseline, and #124 adds it to hosted CI. CI still runs a mutating `lint:fix` command and ignores its failure with `|| true`. #135 removes the automatic/fail-open release source defects: merge and release are separate, exact CI checks are re-read, release targets are fixed, OIDC replaces JSON-key wiring, lockfile tooling is used, backend precedes Pages, and Git-triggered Netlify production builds stop. The protected identity/environments and actual staged/live release remain outside that source slice. +Before #103, frontend tests failed because the Jest environment lacked `TextEncoder`. That child repaired the local baseline, and #124 adds it to hosted CI. #167 adds a separate named Node 20/Java 17 job for the PAY-002B2A Firestore transaction suite and makes both protected-release CI reads require it. The job uses one explicit demo project, Firestore only, committed lockfiles, and no cloud/provider authority. CI still runs a mutating `lint:fix` command and ignores its failure with `|| true`. #135 removes the automatic/fail-open release source defects: merge and release are separate, exact CI checks are re-read, release targets are fixed, OIDC replaces JSON-key wiring, lockfile tooling is used, backend precedes Pages, and Git-triggered Netlify production builds stop. The protected identity/environments and actual staged/live release remain outside that source slice. ### Scope @@ -179,7 +179,7 @@ Before #103, frontend tests failed because the Jest environment lacked `TextEnco ### Agent handoff -Repository changes can implement tests/workflow shape, but OIDC/IAM/environment protection requires an authorized owner. CI-001A/#103 owns test reliability, CI-001B1/#124 owns hosted frontend Jest, CI-001B1A/#126 owns hosted SPA safety, CI-001B2/#135 owns the protected release source gate, #133 owns cloud/environment authority, and #136 owns staged/target release evidence. Keep fail-closed lint, scanning, required checks, and Netlify hosting work in separately claimed children. +Repository changes can implement tests/workflow shape, but OIDC/IAM/environment protection requires an authorized owner. CI-001A/#103 owns test reliability, CI-001B1/#124 owns hosted frontend Jest, CI-001B1A/#126 owns hosted SPA safety, CI-001B2/#135 owns the protected release source gate, CI-001B3/#167 owns the focused command-journal emulator gate, #133 owns cloud/environment authority, and #136 owns staged/target release evidence. Keep fail-closed lint, scanning, branch-required-check configuration, and Netlify hosting work in separately claimed children. --- @@ -633,7 +633,7 @@ PAY-002A1 defines only a dependency-free version-1 target: separate payment, reg The next boundaries remain separate. A later inventory/migration child must use #113-approved data categories and a dry-run before writes. PAY-002B1 [#163](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/163) owns only pure caller-scoped command keys, payload fingerprints, and deterministic Stripe keys with closed environment/mode pairing. A provider attempt is one logical generation, not an HTTP retry. PAY-002B2A [#165](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/165) owns only the atomic `checkoutRequests/{commandKeyHash}` registered record plus deterministic revision-1 `auditEvents` partner. Exact existing pairs are read-only; same-key command-type/schema/payload conflicts and malformed pairs reject. Environment and caller scope derive different B1 keys; there is no cross-scope UUID query. B2A adds no lease, execute/send permission, result replay, provider plan/attempt, or reconciliation. PAY-002B2B owns leases/fencing, stale-worker rejection, terminal pointers/fixed final failure, and transition audit without provider-send permission. PAY-002B2C owns immutable provider-plan binding, safe-send cutoff, ambiguous outcomes, reconciliation, and safe provider-attempt advancement. PAY-002C/D, PAY-003B, RACE-001B, MERCH-001B, PAY-004, and PAY-005 depend on B2C. -B2A adds no TTL. Deletion could re-enable an old command, so [#110](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/110) must approve retention and a server-only tombstone or equivalent durable duplicate barrier before any journal pair is removed. #165's concurrent proof is dedicated local-emulator evidence; hosted Functions CI does not start its exact demo project or set the explicit suite opt-in, and the standard differently named demo run safely skips it. No #165 endpoint/index export, Firebase deployment, Stripe/provider configuration, production read/write, or officer behavior exists. +B2A adds no TTL. Deletion could re-enable an old command, so [#110](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/110) must approve retention and a server-only tombstone or equivalent durable duplicate barrier before any journal pair is removed. CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) runs #165's exact demo-project/opt-in transaction suite as a separate hosted job and requires it in protected-release CI evidence. That hosted result remains synthetic source proof only. No #165 endpoint/index export, Firebase deployment, Stripe/provider configuration, production read/write, or officer behavior exists. ### Problem diff --git a/GITHUB_ISSUE_SLICES.md b/GITHUB_ISSUE_SLICES.md index ac38b7b0..607b4ca3 100644 --- a/GITHUB_ISSUE_SLICES.md +++ b/GITHUB_ISSUE_SLICES.md @@ -20,6 +20,7 @@ An agent-ready child has one observable outcome, one trust boundary, explicit de | CI-001B1 | merged/closed as #124; CI-001A | Run the complete committed frontend Jest suite as a named blocking step in the hosted frontend CI job; update exact officer/engineering truth. | Exact pull-request and post-merge hosted steps passed; Firebase skipped and Netlify exact production commit remained unverified. | | CI-001B1A | merged/closed as #126; #99 and CI-001B1 | Run the standalone same-origin SPA callback suite as a separate blocking hosted frontend step. | Local deliberate-failure proof plus the named hosted step passing on the exact pull-request and post-merge commits; no provider callback claim. | | CI-001B2 | published as #135; CI-001B1 | Provide the tested manual exact-commit source gate: fixed Rules/Functions, lockfile CLI, pinned Actions, short-lived-auth wiring, backend-first Pages dependency, missing-config failure, no frontend credential, and Netlify Git-production containment. | Source/static negative tests and exact PR evidence. #133 separately owns protected OIDC/environment configuration; #136 owns staged/target release; WEB-001 owns protected Netlify publication. | +| CI-001B3 | source/tests tracked in live [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167); PAY-002B2A/#165 | Run the dedicated command-journal Firestore transaction suite as one named blocking Node 20/Java 17 job with exact demo project, explicit opt-in, Firestore-only scope, committed tooling, and no secrets/provider authority; require it at both protected-release CI reads. | Static negative workflow tests plus exact pull-request and post-merge hosted six-test evidence; no Firebase deployment, Stripe, website, or live claim. | | SUPPLY-001A | published as [#127](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/127); CI-001A | Prove `gpxparser` unused and remove its obsolete transitive chain. | `rg`/tree proof, before/after audits, root lockfile diff, full frontend checks. | | SUPPLY-001B | ready; CI-001A | Patch React Router within compatible 6.x and resolve/test future flags without route redesign. | Navigation/callback tests, audit delta, build. | | SUPPLY-001C | proposed; CI-001A | Upgrade Firebase web SDK within one compatibility boundary. | Auth/Firestore/Functions/App Check/emulator tests and bundle/audit diff. | @@ -63,7 +64,7 @@ An agent-ready child has one observable outcome, one trust boundary, explicit de | PAY-002A1 | source/tests tracked in live [#161](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/161); PAY-001A; no runtime adoption/migration/deploy | Build pure version-1 payment/registration/fulfillment/confirmed-refund/per-dispute reducers, cross-dimension validation, and a synthetic legacy classifier/redacted aggregate report. | Exhaust every state pair, retry/reorder/terminal/composite/legacy/hostile-input case; no Firebase/Stripe/logger call or record patch. | | PAY-002A2 | proposed; PAY-002A1, #113 owner-approved inventory | Use an owner-approved private read-only inventory for any real legacy/provider references (no mutation/export or copying into tests/issues), define one context-aware compatibility derivation, and use only synthetic emulator fixtures for the dry-run/idempotent migration plan. | Redacted counts/reasons for every approved fixture; repeat synthetic dry-run is identical; no production write until separately approved. | | PAY-002B1 | source/tests tracked in live [#163](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/163); PAY-002A1; no journal/runtime/deploy | Define the pure caller-scoped command-key, UUID/payload-fingerprint, and deterministic Stripe-key contract. The command key excludes command type so B2 can reject cross-operation ID reuse; environment/mode pairs fail closed; provider attempt means one logical generation and survives lease/HTTP retry. | Canonical UUID, golden length-framed digests, environment/mode and dimension separation, hostile frozen payload, key-length/raw-value absence, immutability, and no-side-effect tests. | -| PAY-002B2A | source/tests tracked in live [#165](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/165); PAY-002B1; unused/no runtime or deploy | Atomically create one immutable `registered` command and deterministic revision-1 audit partner, return a fixed read-only exact-existing classification, and reject same-key type/schema/payload conflicts or malformed pairs. Environment/caller changes derive another B1 key; stored mismatches are corruption. No lease, execute/send permission, result replay, provider, or reconciliation API. | Mocked transaction and dedicated opt-in demo-emulator concurrent pair/conflict/orphan/failure tests; hosted Functions CI does not run that opt-in suite and the standard demo run safely skips it. | +| PAY-002B2A | source/tests tracked in live [#165](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/165); PAY-002B1; unused/no runtime or deploy | Atomically create one immutable `registered` command and deterministic revision-1 audit partner, return a fixed read-only exact-existing classification, and reject same-key type/schema/payload conflicts or malformed pairs. Environment/caller changes derive another B1 key; stored mismatches are corruption. No lease, execute/send permission, result replay, provider, or reconciliation API. | Mocked transaction plus dedicated opt-in demo-emulator concurrent pair/conflict/orphan/failure tests; CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) adds the exact suite as a hosted release prerequisite. | | PAY-002B2B | proposed; PAY-002B2A | Add leases, monotonic fence epochs, stale-worker rejection, terminal server-only pointers/fixed final failure, and append-oriented transition audit without provider-send permission. | Concurrent lease/takeover/stale fence, terminal replay-pointer, fixed-failure, and transaction-retry emulator tests. | | PAY-002B2C | proposed; PAY-002B2B | Bind an immutable Stripe account/sandbox/API-version/endpoint/operation/mode/parameters plan, attempt/key, and pre-POST first-send evidence; stop old/unknown automatic POST, record ambiguity, reconcile verified evidence, and advance only a proven-safe logical attempt. | Exact-plan/key reuse, timeout/5xx/unknown-send cutoff, conflicting-plan, old-key, reconciliation, and safe/unsafe attempt-advance emulator/provider-fixture tests. | | PAY-002C | proposed; PAY-001B, PAY-002B2C, RACE-001B | Convert race checkout to persistence-first record/hold plus deterministic Stripe Session saga. | One record/Session on retry at every failure boundary. | diff --git a/OFFICER_START_HERE.md b/OFFICER_START_HERE.md index e7dc6b18..119ad03c 100644 --- a/OFFICER_START_HERE.md +++ b/OFFICER_START_HERE.md @@ -36,6 +36,7 @@ Use the club's approved password manager for access. Share only a public link or - **Code changed:** What changed? - **Hosted frontend tests passed:** Did the `Frontend lint + build` job show a green `Run frontend Jest tests` step? Jest is the automated frontend behavior test. +- **Hosted payment-safety test passed:** Did the `Commerce command journal emulator` job finish green? This check uses made-up records in a closed test database. - **Other tests passed:** What else was checked for this change? - **Code merged:** Which pull request was approved? - **Release approved:** Which environment, exact commit, and named approver were recorded? diff --git a/OPERATIONS_RUNBOOK.md b/OPERATIONS_RUNBOOK.md index e63efa9b..dd87f9c7 100644 --- a/OPERATIONS_RUNBOOK.md +++ b/OPERATIONS_RUNBOOK.md @@ -62,7 +62,7 @@ Maintain an internal inventory with project/account IDs and console links: | Environment | Firebase project | Stripe mode/account | Web domain | GitHub environment | Status | | --- | --- | --- | --- | --- | --- | | Local source runtime | `demo-mprc-local` emulators | Providers remain separately gated | `localhost:3000` | None | Available for synthetic Firebase work after all three loopback emulators report ready | -| CI | Demo/emulator project | Fixtures/test only | None | CI | Partially implemented | +| CI | Separate named demo/emulator projects | Fixtures/test only | None | CI | Frontend, Functions, Rules, SPA, and commerce-journal checks run; broader gates remain partial | | Staging | **Create dedicated project** | Test/sandbox + unique endpoint | `dev.runmprc.com` | `staging` | Launch blocker | | Production | `mid-peninsula-running-club` (verify) | Live + unique endpoint | `runmprc.com` | `production` | Informational site only until gates close | @@ -265,7 +265,7 @@ CI=true npm test -- --watchAll=false --runInBand npm run test:spa-navigation ``` -The frontend Jest command is a dependable local baseline and hosted CI runs it as the blocking `Run frontend Jest tests` step under [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124). Hosted CI also runs the separate Node SPA suite as `Run SPA callback safety tests` under [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126), plus the manual-release source invariants under [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135). Protected environment/OIDC configuration, actual staged/live deployment, fail-closed lint, required checks, and controlled Netlify publication remain **NOT AVAILABLE YET** under [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105), [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133), [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136), and WEB-001. +The frontend Jest command is a dependable local baseline and hosted CI runs it as the blocking `Run frontend Jest tests` step under [#124](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/124). Hosted CI also runs the separate Node SPA suite as `Run SPA callback safety tests` under [#126](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/126). CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) adds the blocking `Commerce command journal emulator` job. That job uses exact demo project `demo-pay002b2-test`, Firestore only, an explicit opt-in, and the focused transaction suite. The protected release source gate requires that named job both before artifact work and after approval. Its green result proves synthetic transaction behavior, not Firebase deployment or Stripe behavior. The manual-release source invariants remain under [#135](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/135). Protected environment/OIDC configuration, actual staged/live deployment, fail-closed lint, branch-required-check configuration, and controlled Netlify publication remain **NOT AVAILABLE YET** under [#105](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/105), [#133](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/133), [#136](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/136), and WEB-001. ### Firestore Rules diff --git a/SECURITY.md b/SECURITY.md index c036371e..def4f834 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -97,7 +97,7 @@ These entries are implementation evidence, not a production risk-acceptance deci | --- | --- | --- | | Foundation only for parts of RISK-011, RISK-020, RISK-021 | PAY-002A1 is tracked in live [#161](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/161). The source-only child defines versioned pure payment, registration, fulfillment, confirmed-refund, and per-dispute structural reducers; fixed cross-dimension review/rejection; and a deterministic synthetic legacy classifier/aggregate report with no IDs. Orders/registrations carry no singular canonical dispute state. Forbidden regressions fail closed, refunds/disputes do not overwrite payment state, and `paid_after_cancellation` remains paid plus cancelled plus review rather than becoming a payment enum. | This does not yet mitigate those risks in a runtime. An unchanged enum does not prove duplicate Event/command/evidence, authorization, amount monotonicity, provider state, or club policy. No endpoint imports the library and no business record is read/written or migrated. PAY-002B1 [#163](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/163) supplies pure command/attempt keys; PAY-002B2A [#165](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/165) supplies only unused registration/audit persistence; PAY-002B2B/C must add lease/fence/terminal and provider-plan/recovery behavior; PAY-002C/D persistence-first checkout; PAY-003B Event/version/evidence adoption including same-state amount changes and separate dispute records; PAY-004/PAY-005 cancellation/refund operations; MERCH-002/LEGAL-001 return/policy gates; #113 and a later migration child real inventory/dry-run. Firebase/Stripe deployment and live behavior remain unproven. | | Foundation only for retry/duplication parts of RISK-003 and RISK-020 | PAY-002B1 is tracked in live [#163](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/163). The pure source child defines a canonical UUID v4 boundary, a domain-separated caller/environment command hash, a command-type plus canonical frozen-payload fingerprint, and a deterministic environment/mode/operation/command/provider-attempt Stripe key. Invalid production/test or non-production/live pairs fail. Raw caller, UUID, operation, and payload values never appear in outputs. | Hashes are pseudonymous server-only identifiers—not anonymization—and equality is neither authorization nor durable replay proof. No endpoint imports the library. PAY-002B2A/#165 registers only a command/audit pair; it does not add a lease, provider plan, send permission, or result replay. PAY-002B2B must add leases/fencing and terminal pointers; PAY-002B2C must preserve ambiguous provider attempts/keys, stop old/unknown-send-time automatic POST retries, reconcile evidence, and allow only safe attempt advancement. PAY-002C/D and PAY-005 must adopt B2C with endpoint schemas and failure-boundary tests. Deployment, provider configuration, and live behavior remain unproven. | -| Foundation only for registration/audit parts of RISK-003, RISK-020, and RISK-032 | PAY-002B2A is tracked in live [#165](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/165). The unused server-only transaction creates one immutable `checkoutRequests/{commandKeyHash}` registered record and deterministic revision-1 `auditEvents` partner atomically. Exact retries are read-only. Same-key type/schema/payload conflicts and corrupt, orphan, future, or unexpected stored pairs fail closed without repair. Environment/caller-scope changes derive different B1 keys; a stored mismatch under one key is corruption, not a cross-scope lookup. Fixed results expose no raw identity, UUID, payload, hash, path, or Firestore detail. | Registration is neither authorization nor execution permission. There is no endpoint/index export, lease/fence, provider send, result replay, Stripe object, attempt transition, or reconciliation. Local emulator concurrency evidence is not a hosted gate: current Functions CI does not run the dedicated project/opt-in suite, and the standard differently named demo run safely skips it. No TTL is safe: [#110](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/110) must approve retention and a server-only tombstone or equivalent durable duplicate barrier before deletion. Source/tests/merge do not prove Firebase deployment, Stripe configuration, or live behavior. | +| Foundation only for registration/audit parts of RISK-003, RISK-020, and RISK-032 | PAY-002B2A is tracked in live [#165](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/165). The unused server-only transaction creates one immutable `checkoutRequests/{commandKeyHash}` registered record and deterministic revision-1 `auditEvents` partner atomically. Exact retries are read-only. Same-key type/schema/payload conflicts and corrupt, orphan, future, or unexpected stored pairs fail closed without repair. Environment/caller-scope changes derive different B1 keys; a stored mismatch under one key is corruption, not a cross-scope lookup. Fixed results expose no raw identity, UUID, payload, hash, path, or Firestore detail. CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) adds its dedicated exact demo-project/opt-in suite as a named hosted job and protected-release prerequisite. | Registration is neither authorization nor execution permission. There is no endpoint/index export, lease/fence, provider send, result replay, Stripe object, attempt transition, or reconciliation. The hosted emulator result is still synthetic source proof, not deployment or provider proof. No TTL is safe: [#110](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/110) must approve retention and a server-only tombstone or equivalent durable duplicate barrier before deletion. Source/tests/merge do not prove Firebase deployment, Stripe configuration, or live behavior. | | RISK-008 | ABUSE-001A1 is tracked in live [#159](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/159). The source-only slice replaces the browser reCAPTCHA v3 constructor with the pinned Firebase SDK's Enterprise provider and preserves local/test/capability-callback shutdown, missing-key behavior, token refresh, Analytics shutdown, and fixed redacted failure diagnostics. Synthetic tests use no real key or provider call. | No Enterprise key, allowed-domain policy, provider console setting, protected build variable, token exchange, staged metric, callable enforcement, website/Firebase publication, or live behavior is configured or proven. Release `29254280177` failed closed before build because the public site-key variable was absent. Complete #113/#133, ABUSE-001A2, #136/WEB-001, staged metrics, and provider readback before enforcement or a live claim. | | RISK-017 | PAY-001A is tracked in live [#157](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/157). The source slice adds dependency-free strict-root-object, root-budget, bounded string/array, integer-cents, fixed-currency, calendar-date, canonical-URL, conservative email, fixed-error, and closed-enum safe-log primitives. Synthetic hostile-input tests prove deep immutable copies and reject prototypes, accessors, and unknown configured root keys. The libraries contain no network, Firebase, Stripe, logger, clock, or random dependency and make no such call themselves; a supplied array item parser must be trusted, pure, and synchronous. | This library is not imported by an endpoint and is not deployed. PAY-001B/C/D must define exact race, merchandise, lookup, refund, admin, nested-object, and custom-field schemas; apply smaller business limits before every write/provider call; store immutable schema/price snapshots; and add endpoint no-side-effect tests. Safe-log primitives do not replace approved observability purpose, retention, access, transport, or provider controls. | | RISK-001, RISK-002, RISK-034 | PAY-003A [#101](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/101) source merged as `87bd1210575882cdd06214bcb98ed38ce00f71c2`, adding durable Stripe Event deduplication, transactional transition/processed marking, paid/amount/currency/Checkout-mode/reference-shape and explicitly configured livemode checks, async outcomes, monotonic refund/terminal guards, quarantine, generic signature failures, and a webhook-secret-only binding. | Firebase was not deployed. Complete CONFIG/PAY-001/PAY-002 contracts, metadata schema allowlist/migration, canonical reducers/reservations/outbox, retry/dead-letter/TTL/alerts, emulator integration, Stripe test-mode rehearsal, protected deploy, and reconcile. | @@ -110,7 +110,7 @@ These entries are implementation evidence, not a production risk-acceptance deci | Part of RISK-014 | SUPPLY-001A [#127](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/127) removes the unused direct `gpxparser` dependency and 50 lockfile entries without changing any retained package version, source URL, or integrity hash. A fresh Node 20 install lowers the root production audit from 33 findings (3 critical, 9 high) to 22 (1 critical, 8 high), and the optimized production asset hashes remain identical. | Remediate or time-bound the remaining root and Functions findings through the separately tested router, Firebase, Stripe, build-system, and scanning slices; do not use a forced audit upgrade. | | Part of RISK-024 | SEC-001 protects token documents from every browser client. | OAUTH-001 must still add transactional refresh/versioning, scope/account binding, disconnect/revocation, IAM/encryption decision, and redacted lifecycle audit. | | Part of RISK-027 | #99 blocks App Check, Firebase Analytics, and Sentry in local/test and on initial capability-callback startup. OBS-001A1 [#134](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/134) source disables Sentry replay, tracing, browser session tracking, client reports, member user context, breadcrumbs, attachments, transactions, and unapproved default integrations. OBS-001A2 [#139](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/139) source removes every application runtime Firebase Analytics import, initialization, and emission, preserves existing wrapper calls as provider-free no-ops, and removes the direct Waiver SDK bypass. OBS-001A3 [#142](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/142) source routes the remaining render, Auth-email, members-only, and App Check browser diagnostics through five fixed outcome identifiers and prohibits direct console calls elsewhere in application/public runtime source. Synthetic tests use mocks/canaries and make no provider call. | #110/#111 still own any future approved telemetry purpose, consent, event/parameter schema, provider settings, and residual hosted boundary. Source/tests and merge evidence do not prove website publication, the exact `runmprc.com` revision, provider transport/collection, IP/cookie behavior, console history, historical data deletion, retention, access, consent, deletion, or vendor terms. Sentry provider behavior is likewise unverified. Callback protected handoffs remain with ABUSE-001A, DATA-001A, and OAUTH-001C in #88. | -| Part of RISK-035 | The Jest environment is repaired, the stale frontend smoke test is replaced, #124 adds the complete frontend suite as a named blocking hosted step, and #126 adds the standalone SPA callback suite as its own named blocking step. | Keep exact hosted evidence on #124/#126; make lint non-mutating/fail-closed, prove required branch checks, and add the broader TEST-001 suite. | +| Part of RISK-035 | The Jest environment is repaired, #124 adds the complete frontend suite as a named blocking hosted step, and #126 adds the standalone SPA callback suite. CI-001B3 [#167](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/167) adds one lockfile-bound Node 20/Java 17 `Commerce command journal emulator` job using exact demo project `demo-pay002b2-test`, Firestore only, and explicit opt-in. Static negative tests require that exact job at both protected-release CI rechecks. | Record exact pull-request and post-merge hosted evidence for #167. A green demo-emulator job proves synthetic source behavior only; it does not deploy Firebase or prove Stripe/live behavior. Make lint non-mutating/fail-closed, configure and prove branch-required checks, and add the broader TEST-001 suite. | | RISK-026, RISK-036 | #135 merged through PR #138 as `9eafab1217aff7058c42240aaba72d7b93f8ed24`, replacing automatic frontend-first/fail-open GitHub deployment with a tested manual exact-current-commit gate. Post-merge staging and synthetic production probes failed closed before authentication or mutation, and published neither Firebase nor Pages. | #133 must configure protected environments and least-privilege OIDC/WIF; #136 must prove staged/target deployment and clearing/readback of the existing Pages `runmprc.com` claim; a protected WEB-001 child must establish Netlify publication and rollback. No Firebase, Pages, live-host, or provider-setting change is proven by source/static tests alone. | | RISK-039 | The #118 source slice uses an empty authenticated callable request, bounded Firebase Auth identity fields, one transactional create-only helper shared with signup, constant responses, generic failures, and a UI that hides Edit until setup and the Rules-protected read succeed. Signup and recovery never change custom claims; browser profile creation remains denied. | Source review/merge is not deployment. Under #105, deploy the exact #100 Rules plus both `createMemberOnSignUp` and `ensureMemberProfile` before the website, prove App Check policy, use a synthetic staged account, verify rollback, then record website, Function, Rules, and live behavior separately. Never repair a real profile manually. | | RISK-040 | AUTH-MAIL-002A [#145](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/145) merged as `46557c7`: account creation returns `accepted` or `unavailable` without exposing provider details. AUTH-MAIL-002B [#153](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/153) merged as `23bca8c8`: My Account makes no false “sent” claim, blocks rapid repeats, and applies the same 60-second browser cooldown after either outcome. Its protected release run `29252492614` stopped before build because the required public App Check key was absent, so neither frontend revision is published. AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) tracks one byte-equivalent password-reset result after provider success or failure, an immediate repeat guard, and the same browser wait. Synthetic tests use mocked calls and canary values only. | Publish and verify each exact frontend revision separately. #118 profile source is merged but live behavior is unproven. Keep delivery, Spam, DNS, templates, and private Firebase email-enumeration-protection readback under #119. Identical browser copy does not prove the direct Firebase API suppresses account-state differences. An accepted verification request does not prove delivery, and browser cooldowns can reset; they are not server abuse protection. Incoming action-code recovery remains AUTH-MAIL-002C2 under #120. | diff --git a/docs/officers/PUBLISH_AND_CHECK.md b/docs/officers/PUBLISH_AND_CHECK.md index 6bd39948..9362d52d 100644 --- a/docs/officers/PUBLISH_AND_CHECK.md +++ b/docs/officers/PUBLISH_AND_CHECK.md @@ -44,7 +44,7 @@ As of **2026-07-13**: - `main` is the canonical branch. - A merge starts CI checks. It does not start `.github/workflows/deploy.yml`. - The release workflow accepts only a full commit already merged into `main`. -- It requires successful frontend, Functions, and Firestore Rules checks for that commit. +- It requires successful frontend, Functions, commerce command journal, and Firestore Rules checks for that commit. - Its only current release plan is the reviewed profile-recovery set: Firestore Rules, `createMemberOnSignUp`, and `ensureMemberProfile`. - A caller cannot type a Firebase project or deployment target into the release form. - Missing environment configuration or cloud authority makes the release red before backend dependencies, cloud authentication, or deployment. A public website artifact may be prepared without cloud authority, but it cannot be published. @@ -96,18 +96,19 @@ If a member or officer sees **Server configuration is unavailable**: 5. Open the `Frontend lint + build` job. 6. Confirm `Run frontend Jest tests` is present and green. 7. Confirm `Run SPA callback safety tests` is present and green. -8. Confirm the Functions and Firestore Rules jobs are green. -9. Use a preview only for public, read-only pages. -10. Do not sign in, open private/admin pages, submit forms, or test signup, checkout, refund, email, or Strava in a preview. -11. Confirm the officer guide and undo note are present. -12. Approve or reject the merge. Do not describe merge approval as release approval. +8. Confirm `Commerce command journal emulator` is present and green. It uses made-up payment-command records and does not contact Stripe. +9. Confirm the Functions and Firestore Rules jobs are green. +10. Use a preview only for public, read-only pages. +11. Do not sign in, open private/admin pages, submit forms, or test signup, checkout, refund, email, or Strava in a preview. +12. Confirm the officer guide and undo note are present. +13. Approve or reject the merge. Do not describe merge approval as release approval. ## After merge 1. Record the pull request number. 2. Record the full merged commit. 3. Wait for that commit's CI jobs. -4. Confirm the required jobs are green again. +4. Confirm all four named jobs are green again: Frontend, Functions, commerce command journal, and Firestore Rules. 5. Mark the result **merged — not released**. 6. Do not expect GitHub Pages, Firebase, Netlify, or `runmprc.com` to change from the merge. 7. If Netlify unexpectedly publishes the merge, stop and treat it as a hosting incident. diff --git a/tests/ci-workflow.test.js b/tests/ci-workflow.test.js new file mode 100644 index 00000000..88810ff5 --- /dev/null +++ b/tests/ci-workflow.test.js @@ -0,0 +1,130 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const ROOT = path.resolve(__dirname, '..'); +const CI_PATH = path.join(ROOT, '.github/workflows/ci.yml'); +const RELEASE_PATH = path.join(ROOT, '.github/workflows/deploy.yml'); +const ciWorkflow = fs.readFileSync(CI_PATH, 'utf8'); +const releaseWorkflow = fs.readFileSync(RELEASE_PATH, 'utf8'); + +const JOB_ID = 'commerce-command-journal'; +const JOB_NAME = 'Commerce command journal emulator'; +const DEMO_PROJECT = 'demo-pay002b2-test'; +const OPT_IN = 'REQUIRE_COMMERCE_COMMAND_JOURNAL_EMULATOR'; +const TEST_FILE = 'commerceCommandJournal.emulator.test.js'; +const NEVER_RUN = ['$', '{{ false }}'].join(''); +const ALWAYS_TRUE = ['$', '{{ true }}'].join(''); + +function jobBlock(workflow, jobId) { + const startMarker = ` ${jobId}:\n`; + const start = workflow.indexOf(startMarker); + if (start < 0) return ''; + + const remainder = workflow.slice(start + startMarker.length); + const nextJob = /\n {2}[a-z0-9-]+:\n/.exec(remainder); + const end = nextJob === null + ? workflow.length + : start + startMarker.length + nextJob.index; + return workflow.slice(start, end); +} + +function ciErrors(workflow) { + const errors = []; + const block = jobBlock(workflow, JOB_ID); + if (!block) return ['missing job']; + + const checks = [ + [`name: ${JOB_NAME}`, 'wrong job name'], + ["node-version: '20'", 'wrong Node version'], + ["distribution: 'temurin'", 'missing Java distribution'], + ["java-version: '17'", 'wrong Java version'], + ['npm ci --legacy-peer-deps --ignore-scripts', 'root install is not locked'], + ['npm --prefix functions ci --ignore-scripts', 'Functions install is not locked'], + [`${OPT_IN}: '1'`, 'missing explicit opt-in'], + ['npx --no-install firebase emulators:exec', 'Firebase CLI is not lockfile-bound'], + [`--project ${DEMO_PROJECT}`, 'wrong demo project'], + ['--only firestore', 'emulator scope is not Firestore-only'], + ['--runInBand', 'Jest is not in band'], + [TEST_FILE, 'wrong focused test'], + ]; + checks.forEach(([required, message]) => { + if (!block.includes(required)) errors.push(message); + }); + + if ((workflow.match(new RegExp(`name: ${JOB_NAME}`, 'g')) ?? []).length !== 1) { + errors.push('job name must occur exactly once'); + } + if (/^\s+if\s*:/m.test(block)) errors.push('job or step may be skipped'); + if (/continue-on-error\s*:/.test(block)) errors.push('job may ignore failure'); + if (/\|\|\s*true\b|;\s*true\b|\bset\s+\+e\b/.test(block)) { + errors.push('job may swallow command failure'); + } + if (/firebase-tools@|npm install -g/.test(block)) errors.push('uncommitted CLI install'); + if (/--only\s+(?:[^\n]*,|auth|functions)/.test(block)) errors.push('broad emulator scope'); + if (/secrets\.|FIREBASE_SERVICE_ACCOUNT|FIREBASE_TOKEN|STRIPE_SECRET/.test(block)) { + errors.push('job receives a secret or cloud credential'); + } + if (/mid-peninsula-running-club|runmprc-97922/.test(block)) { + errors.push('job names a hosted project'); + } + return errors; +} + +function releaseErrors(workflow) { + const errors = []; + const requiredName = `"${JOB_NAME}"`; + const preflight = jobBlock(workflow, 'preflight'); + const backend = jobBlock(workflow, 'deploy-backend'); + if ((preflight.match(new RegExp(requiredName, 'g')) ?? []).length !== 1) { + errors.push('preflight must require the job exactly once'); + } + if ((backend.match(new RegExp(requiredName, 'g')) ?? []).length !== 1) { + errors.push('post-approval check must require the job exactly once'); + } + return errors; +} + +test('journal emulator CI job is exact, isolated, and blocking', () => { + assert.deepEqual(ciErrors(ciWorkflow), []); + assert.deepEqual(releaseErrors(releaseWorkflow), []); +}); + +test('guard rejects missing, renamed, broadened, or unsafe CI wiring', () => { + const mutations = [ + ciWorkflow.replace(` ${JOB_ID}:\n`, ' removed-journal-job:\n'), + ciWorkflow.replace(`name: ${JOB_NAME}`, 'name: Generic emulator'), + ciWorkflow.replace(`--project ${DEMO_PROJECT}`, '--project demo-other-test'), + ciWorkflow.replace(`${OPT_IN}: '1'`, `${OPT_IN}: '0'`), + ciWorkflow.replace('--only firestore', '--only auth,firestore'), + ciWorkflow.replace('npx --no-install firebase', 'npx firebase-tools@latest'), + ciWorkflow.replace(TEST_FILE, 'another.test.js'), + ciWorkflow.replace('timeout-minutes: 10', `continue-on-error: ${ALWAYS_TRUE}`), + ciWorkflow.replace( + ' run: >-\n npx --no-install firebase emulators:exec', + ` if: ${NEVER_RUN}\n` + + ' run: >-\n npx --no-install firebase emulators:exec', + ), + ciWorkflow.replace(TEST_FILE, `${TEST_FILE} || true`), + ]; + + mutations.forEach((mutated) => assert.notDeepEqual(ciErrors(mutated), [])); +}); + +test('guard rejects omission from either protected-release recheck', () => { + const requiredLine = ` "${JOB_NAME}" \\\n`; + const withoutFirst = releaseWorkflow.replace(requiredLine, ''); + assert.notDeepEqual(releaseErrors(withoutFirst), []); + + const secondIndex = releaseWorkflow.lastIndexOf(requiredLine); + const withoutSecond = releaseWorkflow.slice(0, secondIndex) + + releaseWorkflow.slice(secondIndex + requiredLine.length); + assert.notDeepEqual(releaseErrors(withoutSecond), []); + + const duplicatedOnlyInPreflight = withoutSecond.replace( + requiredLine, + requiredLine + requiredLine, + ); + assert.notDeepEqual(releaseErrors(duplicatedOnlyInPreflight), []); +}); diff --git a/tests/release-workflow.test.js b/tests/release-workflow.test.js index 9e2d4c97..ba2ec24d 100644 --- a/tests/release-workflow.test.js +++ b/tests/release-workflow.test.js @@ -45,6 +45,10 @@ test('release workflow is manual, exact-commit, and fixed-scope', () => { assert.match(workflow, /\.conclusion == "success"/); assert.match(workflow, /Frontend lint \+ build/); assert.match(workflow, /Cloud Functions lint \+ test/); + assert.equal( + (workflow.match(/"Commerce command journal emulator"/g) ?? []).length, + 2, + ); assert.match(workflow, /Firestore security-rules tests/); assert.match(workflow, /options:\n - profile-recovery/); assert.match(workflow, /RELEASE_PLAN: \$\{\{ inputs\.release_plan \}\}/);