Chapter 27Lesson 02240–320 min

Upgrade Planning, Version Support, Breaking Changes, Java Runtime Upgrades, and Rollback Strategy: Guided Hands-On Workflow and Core Operations

A safe upgrade is rehearsed with the same evidence gates you will use in production. This lesson builds a local, fixture-driven runbook that records the baseline, calculates crossed thresholds, checks recovery readiness, injects one compatibility failure, and validates the target before acceptance.

RehearsalPreflightRelease notesSmoke testsEvidence

Learning objectives

  • Capture an upgrade baseline without mutating Nexus.
  • Calculate threshold actions from source and target versions.
  • Model recovery, Java/truststore, plugin, disk, and database gates.
  • Run post-upgrade smoke/client/security checks against deterministic fixtures.
  • Use a failed gate to prove that the runbook stops before mutation.
Dated baseline (27 August 2026). Lessons use Nexus Repository 3.95.2-01 as the current reference line and Java 21 as the required runtime family. Always re-check the live version-status and release-note pages before a real change window.
Upgrade is not migration. Replacing Nexus application binaries, migrating a database engine, moving blob storage, and redesigning topology are different changes. Chapter 27 upgrades a supported instance; do not combine unrelated migrations unless the documented path requires them.
No blind downgrade. If a new Nexus version has changed database/data state, do not start older binaries against the upgraded data directory. Rollback means restoring a verified pre-upgrade recovery checkpoint or following an explicit Sonatype-supported procedure.

1. Create the disposable upgrade fixture

from pathlib import Path
import json, shutil, hashlib
root=Path('ch27-lab')
if root.exists(): shutil.rmtree(root)
(root/'source/blobs').mkdir(parents=True)
asset=root/'source/blobs/app-1.0.0.jar'
asset.write_bytes(b'CH27 APP 1.0.0\n')
state={
 'nexus':'3.84.2','java':17,'database':'H2','edition':'Community',
 'blobStore':'file','freeGiB':20,'communityPlugins':['legacy-example-plugin'],
 'customTrustAnchors':['repo-ca'],
 'assetSha256':hashlib.sha256(asset.read_bytes()).hexdigest()
}
(root/'source/state.json').write_text(json.dumps(state,indent=2)+'\n')
print(state)

The fixture deliberately starts before the 3.85 and 3.87 thresholds so the gate engine has work to do.

2. Calculate crossed upgrade gates

from pathlib import Path
import json
from packaging.version import Version
root=Path('ch27-lab')
s=json.loads((root/'source/state.json').read_text())
source=Version(s['nexus']); target=Version('3.95.2')
gates=[]
def crossed(v):
    v=Version(v); return source < v <= target
if crossed('3.85.0'): gates.append('run Repair - Rebuild repository search after upgrade')
if crossed('3.87.0'): gates.append('Java 21 bundled runtime; re-provision custom trust anchors')
(root/'crossed-gates.json').write_text(json.dumps(gates,indent=2)+'\n')
print(*gates,sep='\n')

3. Gate the change before mutation

from pathlib import Path
import json
root=Path('ch27-lab'); s=json.loads((root/'source/state.json').read_text())
preflight={
 'backupVerified':True,
 'targetReleaseNotesReviewed':True,
 'java21Plan':True,
 'trustAnchorsReprovisioned':False,
 'communityPluginsResolved':False,
 'diskHeadroomAdequate':s['freeGiB'] >= 10,
 'databasePathSupported':True
}
blocked=[k for k,v in preflight.items() if not v]
(root/'preflight-1.json').write_text(json.dumps(preflight,indent=2)+'\n')
print('BLOCKED:',blocked)
assert blocked==['trustAnchorsReprovisioned','communityPluginsResolved']

The runbook fails early by design. The correct response is not to weaken the check; it is to resolve the dependencies.

4. Resolve the blockers explicitly

For the lab, remove the unsupported plugin from the planned target and model the custom CA as deliberately imported into the new Java 21 trust path. In a real environment, use your supported certificate-management mechanism and verify the remote endpoint after startup.

from pathlib import Path
import json
root=Path('ch27-lab')
p=json.loads((root/'preflight-1.json').read_text())
p['trustAnchorsReprovisioned']=True
p['communityPluginsResolved']=True
assert all(p.values())
(root/'preflight-2.json').write_text(json.dumps(p,indent=2)+'\n')
print('PRECHECK: PASS')

5. Clone the recovery checkpoint

The fixture uses a deterministic copy only to model separation. A real Nexus backup must follow the supported Chapter 25 database/blob procedure.

from pathlib import Path
import shutil, hashlib, json
root=Path('ch27-lab')
shutil.copytree(root/'source',root/'checkpoint')
manifest={}
for p in sorted((root/'checkpoint').rglob('*')):
 if p.is_file(): manifest[p.relative_to(root/'checkpoint').as_posix()]=hashlib.sha256(p.read_bytes()).hexdigest()
(root/'checkpoint-manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
print('checkpoint files:',len(manifest))

6. Rehearse the target state

from pathlib import Path
import json, shutil
root=Path('ch27-lab')
shutil.copytree(root/'source',root/'rehearsal')
s=json.loads((root/'rehearsal/state.json').read_text())
s.update({'nexus':'3.95.2-01','java':21,'communityPlugins':[],'trustAnchorsReady':True})
(root/'rehearsal/state.json').write_text(json.dumps(s,indent=2)+'\n')
print('rehearsal target:',s['nexus'], 'java',s['java'])

7. Run required post-upgrade actions

Model the threshold action as explicit evidence rather than an undocumented click:

{
  "postUpgradeTasks": [
    {"name":"Repair - Rebuild repository search","reason":"crossed 3.85.0 threshold","status":"COMPLETE"}
  ]
}

On a live lab, confirm the exact task availability/name on the pinned Nexus version before running it.

8. Smoke and client validation

from pathlib import Path
import hashlib, json
root=Path('ch27-lab')
s=json.loads((root/'source/state.json').read_text())
asset=root/'rehearsal/blobs/app-1.0.0.jar'
checks={
 'startup':'PASS','statusApi':'PASS','repoRead':'PASS','leastPrivilege':'PASS',
 'proxyTls':'PASS','tasks':'PASS',
 'assetHashMatches':hashlib.sha256(asset.read_bytes()).hexdigest()==s['assetSha256']
}
assert all(v=='PASS' or v is True for v in checks.values())
(root/'acceptance.json').write_text(json.dumps(checks,indent=2)+'\n')
print('ACCEPTANCE: PASS')

9. Optional live commands: read-only first

# Disposable loopback Nexus only.
curl -fsS http://127.0.0.1:8081/service/rest/v1/status
curl -fsS -u "$NEXUS_LAB_USER:$NEXUS_LAB_PASSWORD"   http://127.0.0.1:8081/service/rest/v1/repositories

Do not print credentials with shell tracing. Verify the running version through UI/system information or supported status/system evidence for the pinned build.

10. Challenge: choose the correction

Your rehearsal starts, but a proxy to repo.example.invalid fails TLS immediately after crossing into bundled Java 21. The correct first hypothesis is the runtime trust boundary—not “disable TLS verification.” Compare the old and new trust-anchor provisioning and correct the new runtime.

11. Cleanup

Delete only the local ch27-lab fixture after preserving sanitized evidence. In a real rehearsal, retain the recovery checkpoint until the production change is accepted and the rollback window is closed.

Knowledge check

Why did the lab intentionally begin with a failed preflight?

What does the 3.85 threshold add to the plan?

What should replace a production filesystem copy of active Nexus state?

Why test proxy TLS after the upgrade?

Why compare an artifact hash?

Summary and next step

The guided workflow proved that the upgrade can be blocked before mutation, rehearsed against cloned state, and accepted only after threshold actions and behavioral checks pass.

Lesson 3 converts these mechanics into design choices for different operating models.

Official references and version notes

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.