draforge

Release Process

This document describes the release workflow for DRAForge.


Version Injection

All three binaries embed version and commit information at build time via GoReleaser ldflags:

Binary LDFLAGS
draforge -X main.versionVal= -X main.commitSHA=
draforge-controller -X main.versionVal= -X main.commitSHA=
draforge-sim-driver -X main.versionVal= -X main.commitSHA=

These values are set by GoReleaser during goreleaser release and are visible via go version -m on the compiled binary. The CLI also exposes them via draforge version.

Snapshot builds use a synthetic *-SNAPSHOT-<commit> version and do not publish anything. The pull-request GoReleaser Dry-Run job builds both target architectures with pinned QEMU and Buildx, validates Docker v2 artifact metadata, and still does not push images. task release:local is the lighter Docker-free workstation check.

Prerequisites

Production publication runs only in GitHub Actions. Do not export or store a personal GitHub token, registry credential, or Cosign private key for the normal release path.


Snapshot release (local dry-run)

Use the repository task for a Docker-free workstation check:

task release:local
# Equivalent to: goreleaser release --snapshot --clean --skip=docker,sbom,sign

This produces binaries, archives, and checksums in dist/ without publishing, signing, generating SBOMs, or building containers. Verify the resulting payload with:

task release:verify

The pull-request GoReleaser Dry-Run job is the authoritative container check. It additionally builds the server, controller, and simulator-driver images for linux/amd64 and linux/arm64 and validates all Docker v2 snapshot artifacts.


Required install E2E gate

The tagged release workflow calls .github/workflows/e2e-matrix.yml with the full profile before GoReleaser starts. The full profile installs and verifies the complete chart on the Kubernetes versions pinned in tests/install-e2e/kubernetes-versions.json. The goreleaser job depends on this gate, so release candidates and final releases do not publish when any required cluster target fails.

Run the same matrix locally before creating a tag:

task e2e:install-kind-full

Failure artifacts from GitHub Actions include rendered manifests, effective values, Kubernetes resources, events, component logs, API payloads, metrics, and SSE output.


Tagged release flow

Tag provenance and immutability policy

A releasable tag must:

The active release-tag-immutability repository ruleset blocks updates and deletions for refs/tags/v*. The historical v0.1.0 and v0.2.0 lightweight tags predate this policy. They remain unchanged because rewriting published tags would break provenance; every new release must satisfy the annotated-tag gate.

Maintainers with a configured Git signing identity may use git tag -s instead of git tag -a. A Git tag signature is additional provenance, not a substitute for the workflow gates. The release workflow uses GitHub OIDC and Cosign to sign checksums and container images so published payloads remain independently verifiable.

1. Prepare the release commit

Update CHANGELOG.md, chart version and appVersion, web package metadata, compatibility statements, and supported-version documentation in a pull request. Merge only after required CI passes.

Choose the new version after the release commit is on main:

NEXT_VERSION="${NEXT_VERSION:?set NEXT_VERSION to the intended SemVer, for example 0.3.1}"
release_tag="v${NEXT_VERSION}"
release_version=${release_tag#v}

2. Create and verify the annotated tag

Create the tag on the reviewed main commit and verify it locally before pushing:

git switch main
git pull --ff-only origin main
git tag -a "$release_tag" -m "DRAForge $release_tag"
RELEASE_TAG="$release_tag" \
  RELEASE_MAIN_REF=main \
  bash scripts/verify-release-tag.sh

A signed annotated tag is also accepted:

git tag -s "$release_tag" -m "DRAForge $release_tag"

Use only one of the two tag commands.

3. Push once and let GitHub Actions publish

git push origin "$release_tag"

The tag push starts .github/workflows/release.yml. The workflow first validates the tag object and main ancestry, then runs the full install E2E matrix. GoReleaser publishes only after those gates pass. Do not run a second manual production GoReleaser publish for the same version.

If the workflow stops before GoReleaser publishes any release asset or image, merge the workflow-only correction to main and dispatch Release from main with the existing immutable release_tag and operation: recover. The recovery path checks out that tag, repeats provenance and the full install matrix, and publishes the same reviewed commit. Never move or recreate the tag.

If publication succeeds but the post-publish registry or signature check fails, dispatch Release from main with the same release_tag and operation: verify. Verification downloads and checks the existing release assets, retries anonymous manifest inspection, and validates checksum and container signatures without rebuilding or publishing anything. If published payloads are actually invalid, use a new patch version instead of replacing them.

The publish job:

  1. builds all three binaries for their configured operating systems and architectures;
  2. creates archives and checksums.txt;
  3. generates CycloneDX archive SBOMs;
  4. builds multi-platform server, controller, and simulator-driver images;
  5. publishes release, minor, and latest GHCR tags;
  6. signs checksums and container images with Cosign using the workflow identity;
  7. creates the GitHub release and attached assets;
  8. validates Docker v2 artifact metadata and anonymously inspects the public multi-platform manifests.

4. Verify the published release

# Check the GitHub release and attached assets
gh release view "$release_tag"

# Verify chart metadata and public multi-platform image references
scripts/verify-chart-images.sh "$release_version"
docker logout ghcr.io
VERIFY_REMOTE_IMAGES=1 scripts/verify-chart-images.sh "$release_version"

# Verify the published server version
docker run --rm "ghcr.io/oaslananka/draforge-server:$release_version" version

# Download SBOM assets for inspection
gh release download "$release_tag" -p "*.sbom"

SBOM generation

SBOMs are generated by GoReleaser via Syft during the release pipeline. Each archive gets a CycloneDX JSON SBOM (.sbom).

To generate an SBOM manually outside the release pipeline:

# Full project SBOM
task sbom
# Equivalent to: syft dir:. -o cyclonedx-json > draforge.sbom.json

# Per-binary SBOM
syft dist/draforge_linux_amd64_v1/draforge -o cyclonedx-json > draforge.sbom.json

Artifacts

Artifact Location Description
Binaries dist/ GoReleaser staging directory (not committed)
Archives dist/*.tar.gz, dist/*.zip Compressed release archives
Checksums dist/checksums.txt SHA-256 checksums of all archives
SBOMs dist/*.sbom CycloneDX JSON software bills of materials
Docker images ghcr.io/oaslananka/ Multi-arch container images
GitHub release GitHub Releases, keyed by the immutable annotated tag Release with assets

Rollback and superseding releases

Before pushing the tag

Delete the local, unpublished tag and update the release commit through the normal pull-request workflow:

git tag -d "$release_tag"

Do not reset or force-push shared main.

After pushing the tag

A pushed v* tag is immutable even when the workflow fails. Do not delete, move, or recreate it. If no public asset or image exists, fix the workflow through a pull request and use operation: recover for the same tag. If publication completed and only post-publish verification failed, use operation: verify. Create a new patch or release-candidate tag only when published payloads are invalid or must be superseded.

After publication

  1. Mark the affected GitHub release as superseded or document the limitation without deleting provenance.
  2. Leave the original tag, release assets, and container digests intact.
  3. Fix the issue through a pull request.
  4. Increment the patch or release-candidate number and publish a new release.
  5. Verify the replacement release and clearly link it from the superseded release notes.

What not to commit

The following are generated by GoReleaser and must not be committed (already covered by .gitignore):

Additionally, never commit:


Troubleshooting

Problem Likely cause Fix
goreleaser not found Not installed or not on $PATH Install the repository-pinned GoReleaser v2.16.0 release
Docker build fails Docker daemon not running Start Docker Desktop / dockerd
syft command not found Syft not installed Install from https://github.com/anchore/syft
gh auth failure during local inspection GitHub CLI is not authenticated Run gh auth login; do not create a long-lived production publish token
Cosign signing fails GitHub OIDC or package permissions unavailable Verify id-token: write, package permissions, and the failed workflow logs
Release created but no assets Protected workflow publish step failed or lacked repository permissions Inspect the tag workflow and verify contents: write, packages: write, and id-token: write