Skip to content

Immutable application releases

Source: docs/RELEASES.md. This page renders that file unchanged.

The application version remains 0.1.0. Each successful publication from main creates a prerelease named build-<full Git commit SHA>. This is a build identity, not a semantic version increment or a production deployment.

Publication contract

  1. A push to main, or a manual run on main, validates the source reference.
  2. An existing complete immutable release is verified and reused without rebuilding.
  3. Otherwise CI must pass, including secret scanning and production-image Trivy scans, before building candidates. The same image gate already runs on PRs and pushes to develop, so vulnerable images block promotion before reaching main.
  4. Four Linux AMD64 images are built with SBOM and provenance attestations. Each candidate tag includes the commit, run ID and attempt; no moving tag is written.
  5. Trivy 0.74.0 scans the exact digests. HIGH/CRITICAL vulnerabilities, including unfixed vulnerabilities, block the release. Scanner/download errors also fail. Reports are retained in Actions for 30 days. There are no vulnerability exceptions.
  6. All four image records must agree on the source commit and expected repositories.
  7. A draft receives release.json, release.json.sha256, and docker-compose.release.yml. Uploaded bytes/checksums are checked before publishing.
  8. Publication succeeds only if GitHub reports the release as immutable and its tag points to the expected commit. Missing or mutable releases are not accepted.

GitHub release immutability is enabled for this repository. The workflow token does not have repository administration permission and cannot toggle that setting. If an administrator disables it, the final check fails; the download helper also refuses the resulting mutable release. Restore the setting before publishing again.

The new workflow does not update latest or publish on v* tag pushes. Existing latest images remain unchanged. Semantic release naming/promotion is separate work; creating a version tag alone does not publish images now.

Manifest and consumption

release.json contains schema version 1, repository, source commit, release ID, platform, and exactly these images: frontend, api, backend and ops-agent. Image references use ghcr.io/owner/image@sha256:..., never mutable tags.

With Python, GitHub CLI and Docker Buildx installed, authenticate to GitHub and GHCR as appropriate for repository/package visibility, then download and verify:

Terminal window
python .github/scripts/release_publish.py download \
--repository UlloaSP/mlsuite \
--commit <full-40-character-commit> \
--directory <new-empty-directory>

The helper verifies GitHub immutability, the release tag, asset hashes, canonical manifest, Compose/image agreement, and registry availability. It does not overwrite an existing download directory. It uses GitHub’s authenticated metadata checks; it does not perform independent cryptographic verification of attestations.

The Compose asset provides all application service references and platform; the base production override deliberately contains no fallback application images. The single API image is reused by db-migrate, artifact-migrate, and spring-app so migrations always run from the exact digest being promoted. It is intended to accompany docker-compose.yml and docker-compose.prod.yml; PostgreSQL, MinIO, secrets, volumes, networking and host requirements are outside this application manifest. You can inspect the selected images without starting services:

Terminal window
docker compose --env-file .env -f docker-compose.yml -f docker-compose.prod.yml \
-f <download-directory>/docker-compose.release.yml config --images

This step supplies release artifacts, not the deployment procedure. Keep the downloaded override inside the repository path named by RELEASE_COMPOSE; both the cutover wizard and ops-agent then consume the same digest-pinned configuration. The base production Compose file gates the API on a one-shot Flyway migration and deep readiness checks. The operator must still provide environment-specific backup, restore, promotion and rollback orchestration; follow the production database and artifact rollout in README.md before starting the release. The initial on-premise contract is local-single-disk. The cutover wizard creates a coordinated local PostgreSQL plus MinIO backup, verifies it through an isolated restore rehearsal, rejects a rehearsal that exceeds RECOVERY_RTO_MINUTES, and requires explicit acceptance that loss of the only disk is unrecoverable. Its RPO/RTO apply only to logical recovery; this is not PITR or disaster recovery. Add another physical failure domain before making either claim.

Failure and retry behavior

  • External commands in the release helper have a 300-second timeout. A timeout fails the operation with the executable name, without logging arguments or captured output. The publication job retains its ten-minute overall limit.
  • A failed build or scan leaves only unpromoted candidate images. It cannot create a complete release because publication requires every matrix job to succeed.
  • Upload failure leaves a draft. Rerunning can replace only its expected draft assets, verify the complete set again, and publish. Unknown draft assets or a mismatched target commit require investigation rather than automatic deletion.
  • Published release assets are never overwritten. Retry verifies and reuses the completed release, including when main has subsequently advanced.
  • A superseded run that has not passed preparation cannot start another build. A build already admitted may finish after main advances; it has its own SHA identity and cannot replace a newer release or latest.
  • Publication runs are serialized and are not cancelled mid-publication. GitHub concurrency is not a FIFO queue; intermediate pending commits may be superseded.
  • Retain every image referenced by a retained release. Candidate-looking tags may point to published release digests and must not be deleted by a blanket cleanup. Immutability prevents content substitution, not deletion of GHCR packages.

Docker base images and Actions are pinned to reviewed digests/SHAs. Build cache is scoped by service. Package repositories and build metadata can still change; this does not claim byte-for-byte reproducible rebuilding. Promotion must reuse the manifest digests rather than rebuild from the same source commit.

Verification limits for the introducing PR

Contract/lifecycle tests exercise complete releases, malformed or missing records, checksum failures, missing images, partial uploads, immutable retries and downloads. They use mocked GitHub/registry writes. Required PR CI validates these tests and workflow syntax alongside application tests. The actual GHCR scan and immutable release publication path first runs after this feature is promoted to main. No production deployment or vulnerability-free image claim follows from PR CI.

References: GitHub immutable releases, Docker build attestations.