Skip to content

Add CI check that the previous release works against the new schema#3506

Draft
jonathangreen wants to merge 2 commits into
mainfrom
chore/migration-backcompat-ci
Draft

Add CI check that the previous release works against the new schema#3506
jonathangreen wants to merge 2 commits into
mainfrom
chore/migration-backcompat-ci

Conversation

@jonathangreen

@jonathangreen jonathangreen commented Jun 25, 2026

Copy link
Copy Markdown
Member

Description

Adds a CI job, backwards-compatibility-test, that verifies the previous released version of the application still works against the database schema produced by the current code. This enforces our online-migration requirement: a release's database must keep working with version N-1 of the code, because new migrations run while the previous version's webservers are still serving traffic during a deploy.

The check works by:

  1. Resolving the previous release (the latest GitHub release) and its published circ-webapp image.
  2. Initializing a fresh database with the current image, which builds the new schema.
  3. Running the previous release's database tests (pytest -m db) against that schema in external-schema mode (PALACE_TEST_DATABASE_EXTERNAL_SCHEMA), so the older code exercises the new schema. If those tests fail, the migration is not backwards compatible.

New files:

  • docker/ci/test_backwards_compatibility.sh — orchestrates the check.
  • docker/ci/test_backwards_compatibility.yml — compose overlay adding a webapp-prev service that runs the previous release's image in external-schema mode.

The push job now depends on backwards-compatibility-test so images are not tagged and pushed unless the check passes.

Motivation and Context

Our migrations are run online, while the previous version of the application is still serving traffic, so a release's database must work with version N-1 of the code. Until now nothing in CI verified that invariant — a migration that broke backwards compatibility would only be caught during a deploy. This job catches such regressions before images are pushed.

How Has This Been Tested?

The check is itself a CI job and runs as part of this PR's build against the latest published release image. The script also supports a PREV_RELEASE_IMAGE override for running it locally against a chosen previous image.

Checklist

  • I have updated the documentation accordingly.
  • All new and existing tests passed.

Add a backwards-compatibility CI job that runs the previous release's database
tests (pytest -m db) against the schema produced by the current code, exercising
the online-migration requirement that a release's database must keep working
with version N-1 of the code while its webservers are still serving traffic.

- docker/ci/test_backwards_compatibility.yml: compose overlay defining a
  webapp-prev service (the previous release's image) in external-schema mode.
- docker/ci/test_backwards_compatibility.sh: resolves the previous release from
  the latest GitHub release, builds the new schema with the current image, then
  runs the previous release's db tests against it. Skips when the previous
  release predates the external-schema seam.
- build.yml: new backwards-compatibility-test job, added to the push job's needs.

This is the second of two PRs and depends on the test seam from the first. Until
a release containing the seam becomes the previous release, the check skips.
@claude

claude Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Claude finished @jonathangreen's task in 5m 53s —— View job


Review

  • Read changed files and surrounding CI scripts
  • Understand external-schema test infrastructure
  • Analyze the new CI job, script, and compose overlay
  • Post review feedback

Summary

Nice addition — this closes a real gap by enforcing the online-migration N-1 invariant in CI before images are pushed. The digest/artifact handling and the run_in_container / compose-overlay patterns correctly mirror the existing migration-test job, and I confirmed the published circ-webapp image ships tests/, uv.lock, and uv (the common stage's COPY . /var/www/circulation), so the uv sync + pytest tests approach is sound. My main concern is the second commit removing the seam-skip guard: that's only safe if the current latest release already contains the external-schema seam, which the PR description still says it does not. A couple of smaller robustness/diagnostic notes follow. (Note: I couldn't query the live release list from this environment, so the first finding is conditioned on that fact.)

Details

docker/ci/test_backwards_compatibility.sh:80-82

With the seam-skip guard removed in 18b6dbb, this block runs unconditionally against whatever image the latest release published. If that release predates the external-schema seam — which the PR description still asserts ("the seam ships in a later release than this check … the job currently short-circuits with a skip") — the older image's tests/fixtures/database.py has no external_schema branch, so it ignores PALACE_TEST_DATABASE_EXTERNAL_SCHEMA and still calls drop_existing_schema() / _initialize_database(), rebuilding the schema from the old models. The job would then pass while testing the old schema (a false green) rather than skipping. The commit message claims the previous release now ships the seam, but that directly contradicts the PR body. Please confirm the current latest release actually contains the seam; if it doesn't yet, restore the skip guard (or gate the run on the image containing external_schema) until a release that has it exists, and reconcile the now-stale PR description either way.

if ! run_in_container webapp-prev \
"uv sync --frozen --active && pytest --no-cov -n0 -m db --ignore=tests/migration tests"; then
fail "Previous release tests failed against the current schema: the migration is not backwards compatible."

Minor: docker/ci/test_backwards_compatibility.sh:51-59

gh release view … 2>/dev/null discards stderr, so any failure (auth hiccup, transient network/API error) collapses to an empty prev_version and is treated identically to "no releases exist," skipping the entire check with exit 0. Because push gates on this job succeeding, a flaky gh call silently disables the backwards-compatibility gate for that run and lets images push through. Consider capturing the exit status separately and only skipping on the genuine "no release found" case, failing otherwise:

prev_version="$(gh release view --repo "${GITHUB_REPOSITORY}" --json tagName --jq '.tagName')" \
  || fail "Could not query the latest GitHub release."

if [[ -n "${GITHUB_REPOSITORY:-}" ]]; then
prev_version="$(gh release view --repo "${GITHUB_REPOSITORY}" --json tagName --jq '.tagName' 2>/dev/null | sed 's/^v//')"
else
prev_version="$(gh release view --json tagName --jq '.tagName' 2>/dev/null | sed 's/^v//')"
fi
if [[ -z "${prev_version}" ]]; then
echo "No previous GitHub release found; nothing to check. Skipping."
exit 0
fi

Minor: docker/ci/test_backwards_compatibility.sh:80-82

uv sync --frozen --active && pytest … is run as one command, so a dependency-resolution failure in uv sync (e.g. a lockfile mismatch in the old image) short-circuits before any test runs, yet the failure is reported as "the migration is not backwards compatible." That misattributes an environment/setup problem to a schema regression and would send a debugger down the wrong path. Consider running uv sync as its own checked step with a distinct error message, so only an actual pytest failure produces the backwards-compatibility error.

if ! run_in_container webapp-prev \
"uv sync --frozen --active && pytest --no-cov -n0 -m db --ignore=tests/migration tests"; then
fail "Previous release tests failed against the current schema: the migration is not backwards compatible."

Nit: .github/workflows/build.yml:433

This step pins actions/checkout@v6, while the sibling unit-test and migration-test jobs added/kept in this file use actions/checkout@v7. Bump to v7 for consistency across the workflow.

- uses: actions/checkout@v6

…bility job

The previous release now ships the external-schema test seam, so the
job no longer needs to detect older images that lack it and skip.
@greptile-apps

greptile-apps Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a backwards-compatibility-test CI job that verifies the previously-released circ-webapp image still runs its database tests successfully against the schema produced by the current code, enforcing the online-migration contract before images are pushed.

  • A new shell script (test_backwards_compatibility.sh) resolves the latest GitHub release, initialises a fresh database with the current image, pulls the previous release's image, and runs pytest -m db against that schema using PALACE_TEST_DATABASE_EXTERNAL_SCHEMA=true.
  • A Compose overlay (test_backwards_compatibility.yml) defines the webapp-prev service by extending the base webapp service and injecting the external-schema environment variables.
  • The push job now lists backwards-compatibility-test as a required dependency, so images are only tagged and pushed once the check passes.

Confidence Score: 3/5

The push job is gated on this new check — a wrong action version in the workflow step could cause the new job to fail immediately on every run, breaking the push gate for all branches until fixed.

The actions/checkout@v6 pin in the new job is inconsistent with @v7 used everywhere else in the file; if the tag resolves differently or doesn't exist, the job fails on checkout, permanently blocking the push job. The missing seam-detection step means the skip behaviour described in the PR depends on implicit pytest mechanics that aren't visible in the script, making the first post-release CI run harder to reason about.

.github/workflows/build.yml — the actions/checkout version mismatch on line 433 is the highest-risk item and should be corrected before merge.

Important Files Changed

Filename Overview
.github/workflows/build.yml Adds the backwards-compatibility-test job and gates the push job on it; contains a wrong actions/checkout@v6 pin (should be @v7 to match the rest of the file).
docker/ci/test_backwards_compatibility.sh New orchestration script; well-structured with explicit error handling, a cleanup trap, and graceful skip when no prior release exists; the described seam-detection step is absent, leaving skip logic implicit in test behaviour.
docker/ci/test_backwards_compatibility.yml New Compose overlay; correctly extends the webapp service, overrides the image with PREV_RELEASE_IMAGE, and sets the two external-schema env-vars needed by the previous release's test fixtures.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[backwards-compatibility-test job starts] --> B[Checkout repo & download digests]
    B --> C[Set WEBAPP_IMAGE from digest artifact]
    C --> D{PREV_RELEASE_IMAGE set?}
    D -- No --> E[gh release view → get latest release tag]
    E --> F{Release found?}
    F -- No --> G[Skip: no previous release\nexit 0]
    F -- Yes --> H[Build PREV_RELEASE_IMAGE from tag]
    D -- Yes --> H
    H --> I[Start pg / os / minio / redis containers]
    I --> J[run_in_container webapp\n./bin/util/initialize_instance\nbuild new schema]
    J --> K[compose pull webapp-prev\nprevious release image]
    K --> L[run_in_container webapp-prev\nuv sync && pytest -m db\nagainst new schema]
    L --> M{Tests pass?}
    M -- Yes --> N[✅ Migration is backwards compatible]
    M -- No --> O[❌ fail: migration breaks N-1]
    N --> P[push job: tag & push images]
    O --> Q[push job blocked]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[backwards-compatibility-test job starts] --> B[Checkout repo & download digests]
    B --> C[Set WEBAPP_IMAGE from digest artifact]
    C --> D{PREV_RELEASE_IMAGE set?}
    D -- No --> E[gh release view → get latest release tag]
    E --> F{Release found?}
    F -- No --> G[Skip: no previous release\nexit 0]
    F -- Yes --> H[Build PREV_RELEASE_IMAGE from tag]
    D -- Yes --> H
    H --> I[Start pg / os / minio / redis containers]
    I --> J[run_in_container webapp\n./bin/util/initialize_instance\nbuild new schema]
    J --> K[compose pull webapp-prev\nprevious release image]
    K --> L[run_in_container webapp-prev\nuv sync && pytest -m db\nagainst new schema]
    L --> M{Tests pass?}
    M -- Yes --> N[✅ Migration is backwards compatible]
    M -- No --> O[❌ fail: migration breaks N-1]
    N --> P[push job: tag & push images]
    O --> Q[push job blocked]
Loading

Reviews (1): Last reviewed commit: "Remove the external-schema test seam che..." | Re-trigger Greptile

Comment on lines +433 to +435
- uses: actions/checkout@v6
with:
persist-credentials: false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Every other actions/checkout step in this workflow uses @v7 (lines 45, 264, 335, 384), but the new job pins @v6. If @v6 resolves to a different major-version tag (or doesn't exist), the checkout step will either fail outright or silently use an older version with different semantics.

Suggested change
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/checkout@v7
with:
persist-credentials: false

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread docker/ci/test_backwards_compatibility.sh
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.44%. Comparing base (2a04c63) to head (18b6dbb).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3506   +/-   ##
=======================================
  Coverage   93.44%   93.44%           
=======================================
  Files         511      511           
  Lines       46465    46465           
  Branches     6339     6339           
=======================================
  Hits        43419    43419           
- Misses       1969     1970    +1     
+ Partials     1077     1076    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jonathangreen jonathangreen marked this pull request as draft June 25, 2026 16:11
jonathangreen added a commit that referenced this pull request Jun 26, 2026
…3507)

## Description

Makes four tests robust to running serially with pytest-xdist disabled
(`-n0`). They pass under the normal xdist test run but fail under `-n0`,
which is how the backwards-compatibility CI check (#3506) runs a
previous release's database suite against the current schema.

- **Three script tests** (`test_equivalents`, `test_novelist`,
`test_search`) constructed a `Script` with no `cmd_args` and called
`do_run()`, so the script's argparse fell back to `sys.argv[1:]`. Under
xdist each worker has a clean execnet `sys.argv`, but under `-n0`
`sys.argv` is the real pytest command line. That caused argparse to
error on unrecognized arguments (`equivalents`, `novelist`), and the
leaked `-m db` marker matched `RebuildSearchIndexScript`'s
`-m/--migration` flag, sending it down the migration path so
`search_reindex.delay` was never called (`search`). Each now passes an
explicit `cmd_args=[]` to express "no arguments" independent of
`sys.argv`.
- **The OIDC logout test**
(`test_oidc_logout_initiate_no_stored_id_token`) asserted on `caplog`
without setting a capture level. Under serial execution the effective
log level is left raised by an earlier test, so the warning was not
captured and `caplog.text` was empty. It now sets the level explicitly,
matching its passing sibling
`test_oidc_logout_initiate_revocation_only`.

No production code changes — scripts intentionally default `cmd_args` to
`sys.argv` for real CLI use via `Script.run()`.

## Motivation and Context

The backwards-compatibility check added in #3506 runs the previous
release's `-m db` suite under `-n0` (external-schema mode requires
serial execution — the single shared database can't be used by parallel
xdist workers). Its first real run surfaced these four latent
test-isolation bugs, which xdist had been masking. None are
schema-compatibility failures; all four fail identically against the
previous release's own schema. Fixing them here means that once a
release containing these fixes becomes the "previous release", the check
will pass against it.

## How Has This Been Tested?

Reproduced the failures on the unfixed tests by running them under `-n0`
in the docker test environment (`tox -e py312-docker -- -n0 ...`),
observing the argparse `unrecognized arguments` errors. With the fixes
applied, all four pass under `-n0` (including with `-m db` present,
which exercises the `search` flag-collision case): `60 passed, 19
deselected`.

## Checklist

- [x] I have updated the documentation accordingly.
- [x] All new and existing tests passed.
@jonathangreen

Copy link
Copy Markdown
Member Author

This is going to fail until #3507 gets into a release, so I'll leave it in draft for now.

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