This document describes the release workflow for DRAForge.
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.
$PATH.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.
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.
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.
A releasable tag must:
vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-rc.NUMBER;main;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.
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}
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.
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:
checksums.txt;latest GHCR tags;# 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"
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
| 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 |
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.
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.
The following are generated by GoReleaser and must not be committed
(already covered by .gitignore):
dist/ — GoReleaser output directorybin/ — local task build output*.sbom.json — SBOM filescoverage.out — test coverage outputAdditionally, never commit:
dist/ or bin/| 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 |