Skip to content

Upgrades & version pinning

Releases are cut by release-please watching main. Conventional commits drive the version bump:

Commit prefixTriggersEffect (pre-1.0)
feat:releaseMinor bump (0.X.0) — bump-minor-pre-major: true.
fix:releasePatch bump.
docs:, refactor:, chore:noneNo release.
feat!: or BREAKING CHANGE: footerreleaseMinor bump pre-1.0, major bump post-1.0.

When release-please opens its release PR and it merges, a semver tag is pushed (v0.7.2), which triggers the docker-publish.yml workflow to build and publish images tagged :0.7.2, :0.7, :0, and update :latest.

TagWhat it points at
:latestThe most recent push to main. Use only for staging or aggressive dev.
:sha-<short>A specific commit. Useful for pinning to a known-good build.
:1.2.3A specific semver release. The safe production default.
:1.2Tracks the most recent patch within minor 1.2.x.
:1Tracks the most recent minor within major 1.x.x.
  1. Pin a version in .env:

    SCANI_IMAGE_TAG=1.2.3
  2. Read the changelog. The CHANGELOG.md in the repo is generated by release-please; it lists every feature and fix in the new version, plus any breaking changes (marked with !).

  3. Back up Postgres. Always. See Backup & restore.

  4. Pull the new images.

    Terminal window
    SCANI_IMAGE_TAG=1.3.0 docker compose -f docker-compose.prod.yml pull

    This pulls the new scani/api, scani/worker, scani/data-provider, scani/frontend-app, AND scani/migrate images at the pinned tag.

  5. Apply migrations. This is an explicit step, not part of up -d. Always run it before recreating the long-running services — a new image with new code against an unmigrated schema is the most common cause of post-upgrade 500s.

    Terminal window
    SCANI_IMAGE_TAG=1.3.0 docker compose -f docker-compose.prod.yml \
    --profile migrate run --rm migrate

    Expected output ends with ✅ Migrations completed successfully and exit code 0. Idempotent — already-applied migrations are skipped, so re-running is safe.

  6. Recreate the long-running services.

    Terminal window
    SCANI_IMAGE_TAG=1.3.0 docker compose -f docker-compose.prod.yml up -d

    Watch the boot:

    Terminal window
    docker compose -f docker-compose.prod.yml logs -f api worker

    The api’s /readyz will return 503 until both the migration step has run AND the new container has come up. If it stays 503 after the api logs 🐘 Connected to PostgreSQL database, the migration step was probably skipped — re-run step 5.

  7. Verify. Hit the SPA. Trigger a manual sync on a connected integration. Check the dashboard headline.

  8. Pin the new version in .env so subsequent pulls don’t surprise you.

If something is wrong:

Terminal window
SCANI_IMAGE_TAG=1.2.3 docker compose -f docker-compose.prod.yml pull
SCANI_IMAGE_TAG=1.2.3 docker compose -f docker-compose.prod.yml up -d

Rollback caveat: if the new release included a Drizzle migration that was applied, rolling back the image will leave Postgres on the newer schema. Most migrations are additive (new columns, new tables, new indexes) and the older code will read the newer schema fine. Migrations that change types or drop columns are flagged in the changelog and require a pg_restore from the pre-upgrade backup if you need to roll back.

What to do when an upgrade introduces a breaking change

Section titled “What to do when an upgrade introduces a breaking change”

Breaking changes are flagged in the changelog and (when meaningful) have their own migration notes. The pattern is usually:

  1. The new release ships the new schema and supports both the old and new shape in code.
  2. You upgrade in place. Both shapes work; new writes go to the new shape.
  3. A future release removes the old-shape support, after enough time has passed for everyone to upgrade.

For this to be safe, don’t skip releases. Upgrading from 1.2.x directly to 2.0.x is supported but pays the full migration cost in one step. Upgrading one release at a time costs less and surfaces problems earlier.

The ci.yml workflow runs the migration suite on every PR that touches packages/infra/db/. Migrations that fail at all-up are caught before merge. The docker-publish.yml workflow builds multi-arch images on every push to main; production images are only tagged from semver-tag pushes, which only happen after the release-please PR merges (which itself only fires on green CI).