Appearance
Release Pipeline
Code reaches the live host through a pull-based release: GitHub Actions builds and publishes an artifact, and a timer on the host pulls and applies it. There is no self-hosted runner and no inbound access from GitHub to the host.
Flow
Pull request check (.github/workflows/portal-typecheck.yml)
Added by pull request 269. On every pull request (no path filter, so it can be made a required status check) it runs npm ci and npx tsc --noEmit in cyberrange/portal on Node 20. It exists because next build type-checks the portal but Jest's TypeScript transform does not: a type error passed review and broke every release build until it was hot-fixed.
CI build (.github/workflows/ampere-release.yml)
- Triggers: push to
main(build only), or manual dispatch withpromote=trueto publish the release. - Steps: check the portal build configs exist, run
check_runtime_deps.py(fails the build ifsrc/provisioningimports a third-party package missing frompyproject.toml, or uses FastAPI uploads withoutpython-multipart),pack_tree.shbundles thecyberrange/tree,npm ci && npm run buildbuilds the portal, then a manifest is written with the commitsha, thepackage-lock.jsonSHA-256 and a build timestamp. - Artifact:
tree.tgz,portal-build.tgzandmanifest.json, kept 14 days. - Promotion: only a manual dispatch with
promote=trueonmainforce-tags and publishes theampere-liverelease.
No test suite in CI
The release workflow builds and packages and, since pull request 143, checks declared Python dependencies; the separate pull-request workflow type-checks the portal. Neither runs pytest or jest (F9), so a failing test does not stop a merge or a release (see Accuracy Notes for the suite status). A test gate in CI is a recommended improvement.
The docs site is not packaged
pack_tree.sh bundles only the cyberrange/ subdirectory. This docs-site/ folder lives at the repository root and is never included in a platform release, so adding these docs cannot affect the running platform.
Host apply (deploy/host/pull_release.sh)
Run by cyberrange-pull-release.timer (2 minutes after boot, then every 5 minutes). The step-by-step story of a release is also told in How the Web App Is Deployed.
PATH: since pull request 305 both the script and the unit file add/usr/sbin:/snap/bintoPATH. User units start with/usr/bin:/bin, wherelxcis missing, so before this the certificate sync failed and a stale pin stayed in place (issue 304). The sync now names thePATHin its error whenlxcis not found.Fetch: skip if the release env or token file is missing; otherwise fetch the
ampere-liverelease by tag from the GitHub API and download the assets.Decide:
release_sync.py --check-applycompares the manifestshaagainst~/cyberrange/DEPLOYED_SHA. Unchanged: logpull-release skip, re-sync the Wazuh certificate pin, run the CA smoke (step 5) and exit. A failed smoke exits 1 so the unit shows failed and the next timer run retries; a check that could not run is logged aswazuh cert recheck incomplete, and only when both ran is it loggedwazuh cert recheck OK(pull request 307). If the release SHA is marked bad (a previous failed apply), logpull-release STUCK: release <sha> failed to apply earlier; live site stays on <live sha>with the next step, and write~/cyberrange-data/rollback/STUCK. The GitHub Action and release still look green in this state, so check for theSTUCKfile.Snapshot: copy the current tree and portal build to
~/cyberrange-data/rollback/before changing anything.Integrity: verify the downloaded
package-lock.jsonhash matches the manifest; on mismatch,fail_apply. If the lockfile changed since last time, runnpm ci --omit=dev.Apply: extract the tree to
~/cyberrangeand the portal build to~/cyberrange/portal. The extract keeps an existingcerts/wazuh-api.crt(the host's runtime pin) instead of overwriting it with the repository's starter pin (pull request 152), thensync_wazuh_api_cert.pyrefreshes the pin from the livewazuh-managerwhen the manager's certificate fingerprint matches whatlocalhost:55000serves and its SAN includesDNS:localhost, backing up the old file under~/cyberrange-data/(pull request 155). The sync is best-effort and never fails a pull.Internal CA (pull request 275). Once
issue_wazuh_internal_ca.py --installhas writtencerts/wazuh-api-ca.crt, the sync no longer replaces any pin: it only checks that the live manager certificate chains to that CA. A CA smoke then runsopenssl s_clientagainstlocalhost:55000with-verify_hostname localhostand that CA, and signs in as the scoring user. A chain, hostname or sign-in failure isfail_apply "Wazuh CA smoke failed", which rolls the release back. A smoke that cannot run (nolxc, nowazuh-managerguest, nothing listening on 55000, scoring credentials unset) is logged and does not roll back. Hosts without the CA file stay on the leaf pin and skip the smoke.Python dependencies: if
pyproject.tomlchanged, install its[project].dependenciesinto~/cyberrange/.venv(fail_applyon error). Warn if the running API unit does not use that venv. Also warn (pull-release WARNING: ... WAZUH_PROVISION_USER) whenenv/.envsetsWAZUH_SCORING_USERbut not theprovisionerpair, because a student's next lab would be refused withWAZUH_STALE_AGENT(pull request 211). Values are checked for presence only, never printed.Restart and health-gate:
systemctl --user restartthe API, portal and bridge, then poll with backoff for up toHEALTH_TIMEOUT(default 90 s): API/health(200 andok), portal/login(200),/api/auth/providers(JSON) and the bridge WebSocket handshake (101). If the API'sNRestartsgrows by 2 or more since the restart, fail fast as a crash loop.fail_apply <reason>: write the reason, each unit's state and restart count, the API journal tail and the last 60 lines of/tmp/provision-api.logto stderr androllback/last-failure.log; record the SHA as bad; restore the snapshot and restart the units, so a bad release cannot leave the host down.Success: write the new
DEPLOYED_SHA, record the lockfile hash, and clearbad-shaandSTUCK.
Manual rollback
bash
~/cyberrange/deploy/host/pull_release.sh --rollbackThis restores the last snapshot from ~/cyberrange-data/rollback/ and restarts the services. It is the fast way back if a release is healthy enough to pass the gate but wrong in some other way.
Recovering a stuck release
bash
cat ~/cyberrange-data/rollback/STUCK # which release failed, which is live
cat ~/cyberrange-data/rollback/last-failure.log # why it failed
# fix the cause, then deliberately retry that same commit:
~/cyberrange/deploy/host/pull_release.sh --retry-badWithout --retry-bad a failed commit is skipped forever, so re-promoting the same commit does nothing. Hosts that got stuck before pull request 143 still run the old script on their first apply and need one manual recovery (install the missing Python packages into the venv, remove rollback/bad-sha, run pull_release.sh).
What a release does not carry
Release artifacts are code and the portal build only. They do not include the database, evidence images, Keycloak data, env files or golden images. Those live on the host (and inside containers) and are managed separately; see Data Stores for the backup gap.
Sources
.github/workflows/ampere-release.yml, portal-typecheck.yml, cyberrange/deploy/host/pull_release.sh, release_sync.py, sync_wazuh_api_cert.py, issue_wazuh_internal_ca.py, check_runtime_deps.py, test_pull_release.py, pack_tree.sh, cyberrange/certs/README.md, cyberrange/docs/WAZUH-ADMIN-DEGRADED-RUNBOOK.md, cyberrange/deploy/systemd/cyberrange-pull-release.service, .timer.