Skip to content

Repository files navigation

savegame-compat — keep every player journey loadable

CI CodeQL Release MIT license Zero runtime dependencies

Prevent a game update from turning yesterday's progress into today's support ticket.

savegame-compat is a zero-dependency Node.js CLI and GitHub Action that runs real, committed JSON save files through your actual migration chain. It checks the schema after every step, verifies game-specific invariants, runs every fixture twice to expose random or time-dependent migrations, and fails CI when an old save no longer reaches the current format.

简体中文 · configuration · migration operations · repair guide · research

The 60-second proof

git clone https://github.com/KanadeK/savegame-compat.git
cd savegame-compat
npm ci
node bin/savegame-compat.js verify \
  --config examples/skyfarer/savegame.config.json \
  --json artifacts/report.json \
  --html artifacts/report.html

Expected result:

PASS Skyfarer: 3/3 fixtures compatible with save version 3
  [ok] v1-veteran.json  1->3  2 steps
  [ok] v2-early-access.json  2->3  1 step
  [ok] v3-current.json  3->3  0 steps
Migration executions: 3; fixture versions: 1, 2, 3

That output is not mocked. The v1 fixture uses a declarative migration to move currency and reshape every inventory entry. The v2→v3 step runs a JavaScript migration that splits player and world state into new structures. Each intermediate result is validated before the next step starts.

What it catches

  • A removed or renamed field with no migration.
  • A migration chain that skips a shipped save version.
  • A migration that emits data invalid for an intermediate or current schema.
  • Player IDs, progression, inventory counts, or economy rules lost in transit.
  • Duplicate inventory identifiers after a reshape.
  • Date.now(), Math.random(), unordered input, or other nondeterministic output.
  • A "current" fixture that no longer matches the current schema.
  • Missing fixtures for any configured save format version.
  • Unsafe JSON Pointer and merge keys that could mutate object prototypes.

How the gate works

flowchart LR
  A["Committed save fixtures<br>v1, v2, …, current"] --> B["Validate source schema"]
  B --> C["Run the real migration step"]
  C --> D["Validate intermediate schema"]
  D --> E{"Current version?"}
  E -- "No" --> C
  E -- "Yes" --> F["Check game invariants"]
  F --> G["Repeat and compare SHA-256"]
  G --> H["Exit code + JSON/HTML report"]
Loading

The HTML report contains file names, routes, counts, failure codes, and hashes. It deliberately does not embed save contents, player names, IDs, or inventory values.

Add it to a game

Install from the tagged GitHub source:

npm install --save-dev github:KanadeK/savegame-compat#v0.1.1
npx savegame-compat init
npx savegame-compat verify

init creates a valid one-version project:

savegame.config.json
fixtures/v1.json
schemas/v1.schema.json

When the save format changes:

  1. Keep the old fixture unchanged.
  2. Add the new schema and at least one current fixture.
  3. Add one migration edge from the previous format.
  4. Increment currentVersion.
  5. Add invariants for values the schema cannot prove.
  6. Run npx savegame-compat verify.

The Skyfarer example is a complete three-version reference.

Configuration at a glance

{
  "formatVersion": 1,
  "project": "My Game",
  "currentVersion": 3,
  "versionPath": "/meta/saveFormat",
  "fixturesDir": "fixtures",
  "schemas": {
    "1": "schemas/v1.schema.json",
    "2": "schemas/v2.schema.json",
    "3": "schemas/v3.schema.json"
  },
  "migrations": [
    { "from": 1, "to": 2, "file": "migrations/1-to-2.json" },
    { "from": 2, "to": 3, "file": "migrations/2-to-3.mjs" }
  ],
  "invariants": [
    {
      "id": "player-id-survives",
      "check": "preserved",
      "before": "/player/id",
      "after": "/profile/playerId",
      "sourceVersions": [1, 2]
    },
    {
      "id": "credits-never-negative",
      "check": "gte",
      "path": "/economy/credits",
      "value": 0
    }
  ]
}

Declarative migrations support add, replace, remove, move, copy, default, increment, multiply, coerce, map, merge, assert, and recursive map-array. Complex migrations can export an up(save, context) function from a trusted .mjs file. See migration operations.

GitHub Action

name: Save compatibility
on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  saves:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: KanadeK/savegame-compat@v0.1.1
        with:
          config: savegame.config.json
      - uses: actions/upload-artifact@v7
        if: always()
        with:
          name: savegame-compat-report
          path: artifacts/savegame-compat-report.*

The Action needs no package installation and adds the compatibility summary to the workflow job summary.

CLI

Command Real behavior
init [dir] Writes a valid starter config, schema, and fixture; refuses overwrite by default.
verify Migrates every fixture twice, validates every step and invariant, checks version coverage.
inspect save.json Validates the source save and prints the exact planned migration route.
migrate save.json Writes a validated migrated copy; refuses to overwrite unless --force is explicit.
--version Prints the package version.

Run npx savegame-compat --help for all output and report options.

Trust and scope

  • Save files must be JSON objects. Compressed, encrypted, binary, or engine-native saves need a deterministic JSON export/import adapter.
  • The built-in validator implements the documented JSON Schema subset; unsupported external $ref URLs are rejected instead of silently ignored.
  • JavaScript migration modules are repository code and run with the same local permissions as the CLI. Use declarative JSON migrations for untrusted input.
  • Forward migrations are supported. Downgrades are deliberately rejected.
  • No telemetry, network request, account, cloud service, or runtime dependency is used.

Acceptance and repair

The release gate used by this repository is:

npm ci
npm run verify

It runs formatting/syntax checks, 47 unit and integration tests, a healthy three-generation migration, a deliberately broken fixture that must exit 1, privacy assertions on the HTML report, a secret scan, deterministic release packaging, checksum generation, and a clean install from the generated .tgz.

When a gate fails, do not rewrite an old fixture to make it pass. Use the failure-code repair guide, then rerun:

npx savegame-compat inspect fixtures/the-failing-save.json
npx savegame-compat migrate fixtures/the-failing-save.json --out /tmp/repaired.json
npx savegame-compat verify --json artifacts/report.json --html artifacts/report.html
npm run verify

Project docs

License

MIT © KanadeK. See LICENSE.

About

Prevent game updates from breaking player saves with deterministic migration CI.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages