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: false

Job 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-smoke (abridged)
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 1
done
docker logs "$cid"
exit 1

3. 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}"
done

4. 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

  1. Update VERSION (or rely on the workflow's "Inject version from git tag" step).
  2. Open and merge a PR with the changelog entry / version bump.
  3. From main: git tag v0.1.0 && git push origin v0.1.0.
  4. Workflow runs end-to-end: test → smoke → push → cosign → Trivy → proto bundle.
  5. 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 -race over everything except the driver-specific tree.
  • A Conformance / <driver> matrix — memory, postgres (via the postgres:16.13-alpine3.23 service container), entdb (via ghcr.io/elloloop/tenant-shard-db:2.0.5 service container with the realentdb build 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 -v
go test ./store/postgres/... -race -count=1 -v # needs Docker for testcontainers
NOTIFY_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:dev
curl -fsS http://127.0.0.1:19090/healthz
docker rm -f notify-smoke

Related