Last updated 2026-05-28
GitHub Actions release pipeline
notify publishes container images via
.github/workflows/release.yml. A push to any
v* tag triggers test → docker-smoke → build-and-push →
cosign sign → Trivy scan → proto bundle release. The exact same
workflow is also workflow_dispatch-triggerable for
one-off rebuilds.
When you'd care
- You're cutting a release.
- You're debugging a failed publish run.
- You want to mirror the same pipeline for an internal fork.
Trigger shape
on: push: tags: ['v*'] workflow_dispatch: inputs: ref: description: Branch or tag to build (defaults to current ref). required: falseJob graph
test ──► docker-smoke ──► build-and-push ──► scan-image └──► release-protos (only on tag pushes)1. test
Re-runs every gate that PRs run. Plain
go build ./... + go vet ./... +
go test -race over everything except the
driver-specific tree. A broken build must not publish.
2. docker-smoke
Builds the server target locally
(docker buildx build --load), runs the container with
NOTIFY_AUTH_DEV_MODE=true +
NOTIFY_STORE_DRIVER=memory, and curls
/healthz until it sees
{"status":"ok"}. Fails the run if the container
crashes during boot.
docker run -d \ -e NOTIFY_AUTH_DEV_MODE=true \ -e NOTIFY_STORE_DRIVER=memory \ -p 127.0.0.1:19090:9090 \ notify:release-smoke
for i in $(seq 1 30); do if curl -fsS http://127.0.0.1:19090/healthz > /tmp/notify-health.json; then grep -q '"status":"ok"' /tmp/notify-health.json exit 0 fi sleep 1donedocker logs "$cid"exit 13. build-and-push
Cross-builds for linux/amd64 + linux/arm64
via docker/setup-qemu-action +
docker/setup-buildx-action, then publishes to
ghcr.io/elloloop/notify tagged from
docker/metadata-action — semver components, branch
name, sha, and latest on a tag push.
Permissions are scoped: contents: read,
packages: write (to push to ghcr),
id-token: write (for cosign keyless OIDC).
- name: Sign published image (keyless OIDC) env: DIGEST: ${{ steps.build.outputs.digest }} TAGS: ${{ steps.meta.outputs.tags }} run: | for tag in $TAGS; do cosign sign --yes "${tag}@${DIGEST}" done4. scan-image
Post-publish, Trivy is pinned by SHA (defending against the March 2026 advisory) and scans the published image. HIGH and CRITICAL CVEs cause the workflow to fail; results are uploaded as SARIF to GitHub Code Scanning.
- name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ steps.tag.outputs.image }} format: sarif output: trivy-results.sarif severity: CRITICAL,HIGH ignore-unfixed: true exit-code: '1'5. release-protos
On a tag push only. Packs the proto/ tree plus
buf.yaml + buf.gen.yaml into a tarball +
zip + sha256 file and attaches them to the GitHub Release. Consumers
who don't want to vendor a git ref can pin against the proto
bundle for the exact version.
gh release create "$VERSION" \ --title "$VERSION" \ --notes-file "$notes_file" \ "dist/notify-protos-${VER}.tar.gz" \ "dist/notify-protos-${VER}.zip" \ "dist/notify-protos-${VER}.sha256"Cutting a release
- Update
VERSION(or rely on the workflow's "Inject version from git tag" step). - Open and merge a PR with the changelog entry / version bump.
- From
main:git tag v0.1.0 && git push origin v0.1.0. - Workflow runs end-to-end: test → smoke → push → cosign → Trivy → proto bundle.
- The published image is reachable at
ghcr.io/elloloop/notify:0.1.0.
Conformance workflow (PR gate)
.github/workflows/conformance.yml is the PR-side gate.
It runs:
- A Unit job —
go build / vet / test -raceover everything except the driver-specific tree. - A Conformance / <driver> matrix —
memory,postgres(via thepostgres:16.13-alpine3.23service container),entdb(viaghcr.io/elloloop/tenant-shard-db:2.0.5service container with therealentdbbuild tag).
Branch protection pins the Conformance / memory,
Conformance / postgres, Conformance / entdb,
and Unit checks. Nothing lands without all four green.
Local equivalent
# Run the same gates the workflows run.go build ./...go vet ./...go test -race -count=1 -timeout=600s \ $(go list ./... | grep -vE '/(store/(postgres|entdb))(/|$)')
# Per-driver conformance.go test ./store/memory/... -race -count=1 -vgo test ./store/postgres/... -race -count=1 -v # needs Docker for testcontainersNOTIFY_ENTDB_ADDRESS=localhost:50051 \ go test -tags=realentdb ./store/entdb/... -race -count=1 -v # needs entdb at that addr
# Build + smoke the container locally.docker build -t notify:dev .docker run --rm -d --name notify-smoke \ -e NOTIFY_AUTH_DEV_MODE=true \ -e NOTIFY_STORE_DRIVER=memory \ -p 19090:9090 notify:devcurl -fsS http://127.0.0.1:19090/healthzdocker rm -f notify-smoke