Skip to content

publish-staged: restore the version that already serves back to Azure (#1061) - #1110

Merged
tgoodyear merged 7 commits into
mainfrom
onprem/1061-restore-to-azure
Oct 25, 2026
Merged

tgoodyear merged 7 commits into
mainfrom
onprem/1061-restore-to-azure

Conversation

@tgoodyear

Copy link
Copy Markdown
Owner

This adds a restore mode to usnm-ingest publish-staged (--restore) and restore and ready steps to scripts/onprem/release.sh, so the disaster recovery runbook can copy the version current.json already 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-staged refuses unless the lake's current.json names the release's previous_version. After Phase 4 it names the release's own version, so the runbook's release.sh --step publish stops before copying anything. With --restore the copy is refused unless current.json names 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 nor current.json, and --dry-run writes nothing to Azure. Without --restore nothing changes: the checks, the writes, the log lines and the serialized Published result are as before (the result gains no field, and a test pins its keys).

release.sh --step restore is publish plus the flag, run in the directory that built the version; it stops first if that directory has no reference/index-storage-pending.json. release.sh --run-dir DIR --step ready --app-url URL --index-version V polls /readyz and /v1/meta with 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 arguments usnm-ingest receives 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 on tank. 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-staged retires 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, where publish-staged --restore copies the index back. Decision 13 says the same and adds that publish-staged and its restore mode stay in the code while the runbook restores serving to Azure. ADR-0027's Phase 4 rollback row names release.sh --step restore instead of release.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 in release.sh, at the usage sed range 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.restore in 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 only usnm-ingest uses publish_staged, onprem::Staged and Published, and I did not run bicep lint or the web npm checks, because no .bicep or web/ file changed.

🤖 Generated with Claude Code

tgoodyear and others added 6 commits October 24, 2026 20:33
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>
Brings in #1092 (import-staged-run --current). It and this branch each
added a tests module to crates/usnm-ingest/src/cli/onprem.rs; the
resolution keeps #1092's module and adds the publish-staged --restore
parsing test and its helper to it.

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>
Copilot AI balanced review requested due to automatic review settings October 25, 2026 02:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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 --restore with 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

Comment thread scripts/onprem/release.sh Outdated
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 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
tgoodyear merged commit 79889a4 into main Oct 25, 2026
49 checks passed
@tgoodyear
tgoodyear deleted the onprem/1061-restore-to-azure branch October 25, 2026 09:29
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>
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.

2 participants