Repository navigation
publish-staged: restore the version that already serves back to Azure (#1061) - #1110
Merged
Merged
Conversation
After serving moves on-prem the lake's current.json names the release's own version, which publish-staged refuses (it wants the rollback). A restore mode (--restore) takes that case: current.json must name the release's version and the indexes of its pointer, and the copy is the same one, with the same skips, retries and checks. It writes neither the pending pointer nor current.json. Default behaviour and the Published result are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
restore runs publish-staged --restore in the run directory that built the version. ready polls /readyz and /v1/meta of the app at --app-url until it serves --index-version, with bounded tries, and exits 1 if it never does. The optional arguments of the publish call are expanded so an empty list is safe on older bash too. Tests use a stub usnm-ingest and a loopback server. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
09 section 9.6 now restores with release.sh --step restore and checks the app with --step ready before the Worker is pointed at it. The operations doc gets the commands, what a restore checks and writes, its limits (one local index root, reference after Phase 5, no Cosmos write) and how to rehearse it before Phase 4. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…1061) The index account, its endpoint and the Azure apps are deleted at Phase 4 and provisioned again in a disaster recovery, so publish-staged and its restore mode stay in the code while the runbook restores to Azure. The Phase 4 rollback row names release.sh --step restore, and the gate list gains a rehearsed restore. ADR-0027 was accepted on 2026-10-24: these edits are for the maintainer's review. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
State when current.json names the release's own version (after Phase 4), say the run directories must be kept on tank instead of implying they are, and drop two phrasings that read as marketing or were wrong. The publish error now says the plain publish copies the version to Azure; the clap help for --restore says "hasn't promoted". Text only, no logic change. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The readiness check can report success despite a failed metadata transfer, allowing an incorrect cutover decision.
1 open issue
What changed in this PR
Adds a disaster-recovery path to restore the currently serving on-prem version to Azure without changing the lake’s pointers, addressing part of #1061.
Changes:
- Adds
publish-staged --restorewith serving-version and index checks while preserving normal publish behavior. - Adds release-script restore and bounded readiness checks, with Rust and Python regression coverage.
- Updates recovery runbooks and architecture decisions; the operator rehearsal remains outstanding.
| File | Description |
|---|---|
| scripts/onprem/test_release.py | Tests publish arguments, restore prerequisites, and readiness polling. |
| scripts/onprem/release.sh | Adds restore and readiness steps. |
| docs/operations/rebuild-and-index-storage.md | Documents restore commands, prerequisites, limits, and rehearsal. |
| docs/design/adr/0027-onprem-serving-behind-cloudflare.md | Retains restore tooling and adds a recovery rehearsal gate. |
| docs/design/adr/0021-separate-index-storage.md | Clarifies index-account reprovisioning for recovery. |
| docs/design/09-operations-security-cost.md | Updates recovery sequencing and readiness verification. |
| crates/usnm-ingest/src/onprem.rs | Implements restore guards, pointer preservation, and regression tests. |
| crates/usnm-ingest/src/cli/onprem.rs | Exposes and tests the restore flag. |
Review effort: Balanced
The ready step read the version with "|| true", which kept the output of a curl that failed after sending a complete JSON body (a response that closed before its Content-Length) and could match the version. The fetch is now the condition of an if, so under pipefail a failure of curl or jq is a failed try with its own message. The test app can now cut /v1/meta short. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The production recovery procedure and accepted architecture decision amendments need maintainer sign-off, with the empty-account Azure restore rehearsal still outstanding.
No open issues 🎉
1 resolved since last review
Review effort: Balanced
tgoodyear
added a commit
that referenced
this pull request
Oct 25, 2026
After the rebase onto #1110, the publish record runs for the publish step only: a restore writes nothing to Cosmos and needs no endpoint. The usage range covers the whole header again, test_release.py's publish test gives the endpoint a record needs and keeps the copy's arguments apart from the record's, and the new restore test fails if restore records a run. The publish tests no longer skip on bash 3.2, since #1110 expands publish's optional arrays safely; only the seed test still does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tgoodyear
added a commit
that referenced
this pull request
Oct 25, 2026
…#955) (#1118) * Grant the release host Cosmos index_runs write (Part of #1096) onprem-release.bicep gives the release container's Arc identity the built-in Cosmos DB Data Contributor role scoped to the index_runs container only, next to its curated, reference, qw-index and AcrPull grants (ADR-0027 settled decision 5). main.bicep passes the Cosmos account to the module. Nothing opens the Cosmos firewall: that stays with onPremServing and cosmosOnPremAccess, and the comments say so. The role only takes effect after a provision with USNM_ONPREM_RELEASE on. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * release.sh publish records the run in index_runs (Part of #1096) After publish-staged copies a version, release.sh runs usnm-ingest import-staged-run for it against Cosmos, from the snapshot's index_run.json, so /v1/versions and /datasets list a version built on-prem. It takes --cosmos-endpoint (or USNM_COSMOS_ENDPOINT), checks the endpoint and the staged pointer before copying anything, and records nothing on a dry run. The record call uses Cosmos alone and no --current: the host's role covers index_runs only, so ops/current stays the ingest job's step. usnm-ingest refuses --cosmos with a state file and reads both from the environment, so the script unsets USNM_COSMOS_ENDPOINT and USNM_STATE_FILE after reading the endpoint. test_release_record.py runs the script against a stub usnm-ingest. The runbook now describes the record and the post-release check, and the RUN_FILE comment no longer says the host has no Cosmos access. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Tighten runbook wording for the index_runs grant (Part of #1096) Drop the repeated "no other container", make the delete note a caveat, put the "on in prod" note next to USNM_COSMOS_ONPREM_ACCESS (the only setting infra/env/prod.json sets), and fix the grammar of the publish retry sentence. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Keep restore and ready beside the index_runs record (Part of #1096) After the rebase onto #1110, the publish record runs for the publish step only: a restore writes nothing to Cosmos and needs no endpoint. The usage range covers the whole header again, test_release.py's publish test gives the endpoint a record needs and keeps the copy's arguments apart from the record's, and the new restore test fails if restore records a run. The publish tests no longer skip on bash 3.2, since #1110 expands publish's optional arrays safely; only the seed test still does. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> Co-authored-by: agent <agent@example.invalid>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

This adds a restore mode to
usnm-ingest publish-staged(--restore) andrestoreandreadysteps toscripts/onprem/release.sh, so the disaster recovery runbook can copy the versioncurrent.jsonalready names back to Azure and check the app before the Worker is pointed at it. Part of #1061; the issue stays open for the rehearsal, which is an operator exercise this PR documents and does not run.As built,
publish-stagedrefuses unless the lake'scurrent.jsonnames the release'sprevious_version. After Phase 4 it names the release's own version, so the runbook'srelease.sh --step publishstops before copying anything. With--restorethe copy is refused unlesscurrent.jsonnames this release's version and the same indexes as the release's pending pointer; if it names the rollback, the error says to publish without--restore, and any other version is refused. Past that guard it is the same copy with the same checks, skips, retries and create-only writes. It writes neither the pending pointer norcurrent.json, and--dry-runwrites nothing to Azure. Without--restorenothing changes: the checks, the writes, the log lines and the serializedPublishedresult are as before (the result gains no field, and a test pins its keys).release.sh --step restoreispublishplus the flag, run in the directory that built the version; it stops first if that directory has noreference/index-storage-pending.json.release.sh --run-dir DIR --step ready --app-url URL --index-version Vpolls/readyzand/v1/metawith bounded tries (USNM_READY_TRIES,USNM_READY_WAIT_SECS) and exits 1 if the app never serves V. It needs only curl and jq, and the URL comes from the flag, so no hostname is committed. The optional arguments of the publish call are now expanded so an empty list is safe on bash before 4.4; the argumentsusnm-ingestreceives are the same.While Azure still serves, before Phase 4, the apps and the index account hold the serving version, so a restore copies nothing. It still runs every check against the run directory, and a test against local copies of the lake and the account shows that restoring what they already hold changes nothing. A real copy into an empty account is a separate exercise, on dev or at Phase 4, with the maintainer's approval for anything provisioned. The docs state the limits: the indexes must all sit under one
--local-index-root, which will matter once releases have a delta mode; Phase 5's restore point must cover reference too; and the restore needs the release's run directory (state.json,reference/index-storage-pending.json,qw/), which must be kept ontank. They also correct the runbook to provision the index account again, since Phase 4 deletes it.ADR-0027 was accepted on 2026-10-24, so these four edits, all in their own commit, are for the maintainer's review. ADR-0021's "Amended by" line and ADR-0027 decision 13 said
publish-stagedretires after Phase 4. ADR-0021's line now says the index account, its endpoint and the Azure serving apps are deleted at Phase 4 and provisioned again in a disaster recovery, wherepublish-staged --restorecopies the index back. Decision 13 says the same and adds thatpublish-stagedand its restore mode stay in the code while the runbook restores serving to Azure. ADR-0027's Phase 4 rollback row namesrelease.sh --step restoreinstead ofrelease.sh publish, which could not restore. Decision 10's Phase 4 gate list gains "a rehearsed restore of the serving version to Azure (#1061)". Expect a small textual conflict with the open #1042 inrelease.sh, at the usagesedrange line (the two ranges combine to 2,56); the header comments merge cleanly. Whichever PR merges second resolves it.Checks run locally: cargo fmt --all --check; cargo clippy -p usnm-ingest -p usnm-boinc -p usnm-ocr-worker -p usnm-ocr-dash -p usnm-ocr-adapter --all-targets --locked -- -D warnings; cargo test -p usnm-ingest --locked; bash -n and shellcheck -x -S warning on scripts/onprem/release.sh; release.sh --help; python3 -m unittest scripts/onprem/test_release.py (11 tests, on macOS bash 3.2); prettier 3.9.10 --check on the four changed Markdown files; scripts/ci/no-private-addresses.sh, no-shared-keys.sh and no-positional-args.py. A mutation check that ignored
s.restorein the guard and always wrote the pending pointer failed the five new restore tests, as intended, and the source was restored afterwards. After merging origin/main into the branch I reran cargo fmt --all --check; cargo clippy -p usnm-ingest -p usnm-boinc -p usnm-ocr-worker --all-targets --locked -- -D warnings; cargo test -p usnm-ingest --locked; shellcheck on release.sh; test_release.py; no-private-addresses.sh, no-shared-keys.sh, no-positional-args.py and check-names.py --base origin/main, and all passed (prettier and the clippy runs for usnm-ocr-dash and usnm-ocr-adapter were not repeated). scripts/ci/run-unit-tests.py ran 1259 tests in 65 files; four files fail (scripts/boinc/test_acceptance.py, scripts/boinc/test_amd_device_map.py, scripts/boinc/test_boinc_scripts.py and scripts/ci/test_changes.py), and the same four fail the same way on a clean origin/main (2e9c646). I did not run cargo test --workspace, because onlyusnm-ingestusespublish_staged,onprem::StagedandPublished, and I did not run bicep lint or the web npm checks, because no.biceporweb/file changed.🤖 Generated with Claude Code