diff --git a/.github/workflows/bulldozer-serialization-compat.yaml b/.github/workflows/bulldozer-serialization-compat.yaml index 2c821d30d..331fc3919 100644 --- a/.github/workflows/bulldozer-serialization-compat.yaml +++ b/.github/workflows/bulldozer-serialization-compat.yaml @@ -5,7 +5,10 @@ name: Bulldozer serialization compat # makes an older build unable to read data the new code writes, which matters for rollbacks) would # corrupt or lose data. This workflow proves both directions by having each code version read a dump # written by the other. The per-version golden-fixture unit test (serialization-compat.test.ts) is the -# first line of defence; this cross-version job is the belt-and-suspenders check. +# first line of defence; the two cross-version jobs below (backward-compat and forward-compat) are the +# belt-and-suspenders check. They are split into separate jobs on purpose: backward compat (upgrade) +# must always hold, whereas forward compat (rollback) may legitimately break on a non-additive format +# change — keeping them separate makes a forward-compat failure explicit without masking backward compat. on: push: @@ -64,8 +67,11 @@ jobs: echo "changed=true" >> $GITHUB_OUTPUT fi - cross-version: - name: Cross-version read (base ↔ current) + # Backward compatibility (production UPGRADE): the new code must read data the old code wrote. + # This is the direction that must ALWAYS hold — if it fails, deploying the new code would break + # reads of already-persisted production data. + backward-compat: + name: Backward compat (new code reads old data) needs: check-changed if: needs.check-changed.outputs.changed == 'true' runs-on: ubuntu-latest @@ -115,20 +121,71 @@ jobs: pnpm install --frozen-lockfile pnpm exec turbo run build --filter=@hexclave/shared - - name: Emit dump with current code - working-directory: current - run: pnpm -C apps/bulldozer-js exec tsx "$HARNESS_REL" emit "$GITHUB_WORKSPACE/new.json" - - name: Emit dump with base code working-directory: base run: pnpm -C apps/bulldozer-js exec tsx "$HARNESS_REL" emit "$GITHUB_WORKSPACE/old.json" - # Backward compatibility (production upgrade): the new code must read what the old code wrote. - name: Current code reads base-written dump working-directory: current run: pnpm -C apps/bulldozer-js exec tsx "$HARNESS_REL" verify "$GITHUB_WORKSPACE/old.json" - # Forward compatibility (production rollback): the old code must read what the new code wrote. + # Forward compatibility (production ROLLBACK): the old code must read data the new code wrote. + # This can legitimately fail when a change makes the new format unreadable by older builds (e.g. a + # non-additive format change). It runs as its OWN job so that such a failure is explicit and + # actionable, and does NOT mask the backward-compat result above. + forward-compat: + name: Forward compat (old code reads new data) + needs: check-changed + if: needs.check-changed.outputs.changed == 'true' + runs-on: ubuntu-latest + steps: + - name: Checkout current branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + path: current + + - name: Checkout base branch (${{ needs.check-changed.outputs.base_branch }}) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + ref: ${{ needs.check-changed.outputs.base_branch }} + path: base + + # See the backward-compat job: the harness is new, so overlay it onto the base checkout. + - name: Overlay harness onto base checkout + run: | + rm -rf "base/$DATA_STRUCTURES_DIR/__fixtures__" + cp -r "current/$DATA_STRUCTURES_DIR/__fixtures__" "base/$DATA_STRUCTURES_DIR/__fixtures__" + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 + with: + node-version: 22.x + + - name: Setup pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + # Both checkouts live in subdirectories, so there's no package.json at the workspace root; + # read the pnpm version from the current checkout's package.json (`packageManager`). + package_json_file: current/package.json + + - name: Install and build @hexclave/shared (current) + working-directory: current + run: | + pnpm install --frozen-lockfile + pnpm exec turbo run build --filter=@hexclave/shared + + - name: Install and build @hexclave/shared (base) + working-directory: base + run: | + pnpm install --frozen-lockfile + pnpm exec turbo run build --filter=@hexclave/shared + + - name: Emit dump with current code + working-directory: current + run: pnpm -C apps/bulldozer-js exec tsx "$HARNESS_REL" emit "$GITHUB_WORKSPACE/new.json" + - name: Base code reads current-written dump working-directory: base run: pnpm -C apps/bulldozer-js exec tsx "$HARNESS_REL" verify "$GITHUB_WORKSPACE/new.json"