Releases are cut by pushing a semver v* git tag. That triggers
.github/workflows/release.yml, which runs
GoReleaser to build every platform binary, checksum
and keyless-sign them, and publish a GitHub Release plus a multi-arch container
image to ghcr.io/authhound/probe.
There is no manual artifact upload and no signing key to guard — signing is keyless (Sigstore), so the trust root is the GitHub Actions OIDC identity of this repo's release workflow, not a secret someone can leak.
- Nothing to configure for signing or ghcr. The workflow uses the built-in
GITHUB_TOKEN(for the Release + ghcr push) and GitHub's OIDC token (for cosign keyless signing). No repository secrets are required. - After the first release, make the container image public so
docker run ghcr.io/authhound/probeworks without a login: GitHub → theauthhoundorg → Packages →probe→ Package settings → Change visibility → Public. (One-time; it stays public.)
From a clean main that's green in CI:
# 1. Pick the next version (semver, leading v). Example: v0.1.0.
$ git checkout main && git pull
# 2. (Optional) dry-run locally first — see "Dry run" below.
# 3. Tag and push. The tag is what triggers everything.
$ git tag -a v0.1.0 -m "authhound-probe v0.1.0"
$ git push origin v0.1.0Then watch Actions → Release. On success you'll have:
- A GitHub Release at
releases/tag/v0.1.0with:authhound-probe_{linux,darwin}_{amd64,arm64}.tar.gzauthhound-probe_windows_{amd64,arm64}.zip- one SPDX SBOM per archive (
*.sbom.json) — the exact module list inside each build, so anyone can check exposure to a published CVE in seconds checksums.txt,checksums.txt.sig,checksums.txt.pem(checksums cover the SBOMs too, so the one signature authenticates everything)
ghcr.io/authhound/probe:0.1.0and:latest(multi-arch amd64+arm64), signed.
The README's releases/latest/download/authhound-probe_linux_amd64.tar.gz link
resolves automatically once the release exists — the archive names are
intentionally version-less (name_template in .goreleaser.yaml) so the
latest/download/ URLs never change between versions.
Run the exact commands from the README's
Verify your download section against the new
release. If cosign verify-blob and sha256sum -c both pass, the published
artifacts match what the pipeline built. This is the acceptance check.
In CI: the Release dry-run workflow
runs the exact release steps (same SHA-pinned actions, same GoReleaser version)
with --snapshot — everything is built, nothing is tagged, signed, or
published. It triggers automatically on any PR touching the release path
(release workflow, .goreleaser.yaml, Dockerfile.goreleaser — including
Dependabot action bumps), and manually from Actions → Release dry-run → Run
workflow. A green dry-run is the strongest pre-tag signal available. Its
action pins must stay identical to release.yml; Dependabot bumps both files
in one PR.
Locally: you can also exercise the config without publishing anything.
# Validate the config (also catches deprecations):
$ goreleaser check
# Build + archive + checksum everything into ./dist, no push, no signing:
$ goreleaser release --snapshot --clean --skip=sign,docker,publish
$ ls dist/ # inspect archive names + checksums.txt
# Full docker/sign path locally needs a running Docker daemon + cosign + an
# OIDC token, so those steps only truly run in CI. `--skip` them locally.Snapshot builds stamp a version like 0.0.0-SNAPSHOT-<sha>; a real tag stamps
the clean version (v0.1.0 → authhound-probe 0.1.0).
signs:in.goreleaser.yamlsignschecksums.txtwithcosign sign-blob(keyless). Because that file contains the SHA256 of every archive, one verified signature over it authenticates every artifact.docker_signs:signs the pushed image manifests withcosign sign(keyless).- Keyless signing needs
id-token: write(OIDC) — already set in the workflow permissions. Certificates are issued by Fulcio and logged in Rekor; nothing is stored between runs.
- GoReleaser is pinned to the
~> v2.17.0patch line in the workflow for reproducibility. The Docker config uses the classicdockers/docker_manifestsblocks, which that version supports but has flagged as eventually superseded bydockers_v2. When you bump the pin, rungoreleaser checkand, if the classic blocks are dropped, migrate todockers_v2(single block:images/tags/platforms) and re-test the image build. - Distroless base is pinned by digest in
Dockerfile.goreleaser. To refresh it to the latestnonrootimage, resolve the multi-arch digest and update theFROMline:$ TOKEN=$(curl -s "https://gcr.io/v2/token?scope=repository:distroless/static:pull&service=gcr.io" \ | grep -o '"token":"[^"]*"' | cut -d'"' -f4) $ curl -sI -H "Authorization: Bearer $TOKEN" \ -H "Accept: application/vnd.oci.image.index.v1+json" \ https://gcr.io/v2/distroless/static/manifests/nonroot | grep -i docker-content-digest
- Adding a Docker Hub mirror later (if you claim
docker.io/authhound): adddocker.io/authhound/probeimage templates + a manifest in.goreleaser.yaml, add adocker/login-actionstep for Docker Hub in the workflow usingDOCKERHUB_USERNAME/DOCKERHUB_TOKENsecrets, and update the README's Docker line. ghcr stays primary either way.
Dependabot (.github/dependabot.yml) opens PRs for
Go modules and for the SHA-pinned GitHub Actions. Nothing auto-merges —
this is deliberate, not an oversight:
- Security-fix PRs (Dependabot security updates, or anything fixing a govulncheck finding): review and merge same week, then cut a patch release if the fix is in a shipped code path.
- Routine version bumps arrive Mondays as one grouped minor/patch PR (majors come separately). They wait for the weekly batch review plus a green test suite — no fast lane.
Rationale: real supply-chain attacks ride new versions (a hijacked
maintainer account publishes a malicious release and waits for auto-updaters).
A small cooldown plus human review is the mitigation that costs us minutes and
costs an attacker their window. Tampered re-publishes of existing versions
are already blocked by Go's checksum database (sum.golang.org), which go mod verify and every module download check against.
- Re-running: delete the tag locally and remotely, fix, re-tag:
If a partial GitHub Release was created, delete it in the UI first.
$ git tag -d v0.1.0 && git push origin :refs/tags/v0.1.0 - ghcr push denied: confirm the workflow has
packages: write(it does) and that the org allows Actions to publish packages (org → Settings → Actions → Workflow permissions).