Skip to content

Versioning, CI/CD & Branching Playbook

1. Versioning Strategy

Single Version for All Components

Console is a monorepo with tightly coupled components (api, worker, web). All share one version tracked in VERSION at the repo root.

VERSION          # e.g. 0.1.0 — single source of truth

The version is injected into Go binaries at build time via -ldflags:

go
// internal/version/version.go
var Version = "dev"   // overridden by: -X github.com/extohub/exto-console/internal/version.Version=0.1.0

Checking the running version:

bash
curl -s https://console-stg.exto360.com/healthz | jq .version
# → "0.1.0"

When to Bump VERSION

Change typeExampleBump
Breaking API changeRemove/rename endpointMajor (1.0.0)
New featureNew endpoint, new UI pageMinor (0.2.0)
Bug fix, patchFix invite email, tweak queryPatch (0.1.1)

How to bump:

bash
echo "0.2.0" > VERSION
git add VERSION
git commit -m "chore: bump version to 0.2.0"

2. Branching Strategy

Branch Model

main ─────●────●────●────●────●──── (always deployable)
           \       /      \       /
            feat-x        fix-y
BranchPurposeLifetime
mainProduction-ready code. All merges go here.Permanent
feat/*, fix/*, chore/*Feature/bugfix work branchesShort-lived (days)

Rules

  • No long-lived branches. No develop, staging, or release/* branches.
  • All changes go through PRs into main.
  • CI must pass before merge (Go lint+test, web lint+build+test).
  • Squash merge preferred for clean history.
  • Environments are controlled by which image tag is deployed, not by branches.

Environment Promotion

main (merge) → CI builds git-<sha> → deploy to stg
                                    ↓ (manual, after validation)
git tag v0.1.0 → CI builds v0.1.0 → deploy to prod

3. Image Tagging Strategy

Never use mutable tags like latest for deployments.

TagWhenPurpose
git-<sha7>Every CI buildImmutable, traceable to exact commit
v0.1.0Release (git tag)Human-readable release version
stg-latestAfter each main buildConvenience: "what's currently latest for stg"

Image Repositories

gaeadev.azurecr.io/console-api:git-48d64e3
gaeadev.azurecr.io/console-api:v0.1.0
gaeadev.azurecr.io/console-api:stg-latest

gaeadev.azurecr.io/console-worker:git-48d64e3
gaeadev.azurecr.io/console-worker:v0.1.0
gaeadev.azurecr.io/console-worker:stg-latest

4. CI/CD Pipeline

Workflow: .github/workflows/ci.yml

Trigger: PRs and pushes to mainPurpose: Validate code quality — does NOT build images

PR opened / push to main
  ├── Go: lint + build + test
  └── Web: lint + build + test

Workflow: .github/workflows/build.yml

Trigger: Push to main or push a v* tag Purpose: Build Docker images and push to ACR

Push to main:
  1. Checkout code
  2. Read VERSION file, compute short SHA
  3. az login (OIDC) → az acr build
  4. Build console-api:git-<sha7>   (remote build on ACR)
  5. Build console-worker:git-<sha7>
  6. Tag both as stg-latest (az acr import)

Push v* tag:
  1–5. Same as above
  6. Tag both as v0.1.0 (az acr import)

Required GitHub Configuration

Environment: gg-usa-nonprod

Secrets (per environment):

SecretDescription
AZURE_CLIENT_IDService principal with ACR push access
AZURE_TENANT_IDAzure AD tenant
AZURE_SUBSCRIPTION_IDSubscription containing the ACR

5. Dockerfiles

Both use multi-stage builds with version injection:

dockerfile
# deploy/docker/Dockerfile.api
FROM golang:1.25-alpine AS build
ARG VERSION=dev
# ...
RUN CGO_ENABLED=0 go build \
    -ldflags "-X github.com/extohub/exto-console/internal/version.Version=${VERSION}" \
    -o /bin/api ./cmd/api

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /bin/api /api
ENTRYPOINT ["/api"]

Local build:

bash
VERSION=$(cat VERSION)
docker build --build-arg VERSION=$VERSION -f deploy/docker/Dockerfile.api -t console-api:local .
docker build --build-arg VERSION=$VERSION -f deploy/docker/Dockerfile.worker -t console-worker:local .

6. Deployment

Deploy Script

bash
# Deploy staging with a specific image
./infra/deploy.sh stg --image-tag git-48d64e3

# Deploy prod with a release tag
./infra/deploy.sh prod --image-tag v0.1.0

# Preview changes (dry run)
./infra/deploy.sh prod --image-tag v0.1.0 --what-if

# Deploy infra + upload web SPA
./infra/deploy.sh stg --image-tag git-48d64e3 --deploy-web

# Upload web SPA only (no infra changes)
./infra/deploy.sh stg --deploy-web-only

The --image-tag flag overrides consoleApiImageTag and consoleWorkerImageTag in the Bicep parameters.

Deployment Checklist

Staging Deploy

bash
# 1. Merge PR to main
# 2. Wait for CI + build.yml to complete
# 3. Find the image tag from the workflow summary or:
git log --oneline -1 main   # → 48d64e3 feat: ...

# 4. Deploy
./infra/deploy.sh stg --image-tag git-48d64e3

# 5. Verify
curl -s https://console-stg.exto360.com/healthz | jq .
# → {"status":"alive","version":"0.1.0"}

Production Release

bash
# 1. Ensure main is stable on staging

# 2. Bump version if needed
echo "0.2.0" > VERSION
git add VERSION && git commit -m "chore: bump version to 0.2.0"
git push

# 3. Tag the release
git tag v0.2.0
git push origin v0.2.0

# 4. Wait for build.yml to complete

# 5. Deploy
./infra/deploy.sh prod --image-tag v0.2.0

# 6. Verify
curl -s https://console.exto360.com/healthz | jq .
# → {"status":"alive","version":"0.2.0"}

7. Rollback

Since every build produces an immutable git-<sha> tag, rollback is just redeploying the previous tag:

bash
# Find the previously deployed tag (check deployment history or ACR)
az acr repository show-tags --name gaeadev --repository console-api --orderby time_desc --top 5

# Redeploy the previous version
./infra/deploy.sh prod --image-tag v0.1.0

# Verify
curl -s https://console.exto360.com/healthz | jq .version

No git reverts needed. No branch manipulation. Just point to a known-good image.


8. Quick Reference

Repo layout:
  VERSION                          ← semver (0.1.0)
  internal/version/version.go      ← Go var, injected via ldflags
  deploy/docker/Dockerfile.api     ← ARG VERSION=dev
  deploy/docker/Dockerfile.worker  ← ARG VERSION=dev
  .github/workflows/ci.yml         ← PR checks (lint, test, build)
  .github/workflows/build.yml      ← Image build + push to ACR
  infra/deploy.sh                  ← Deploy with --image-tag

Day-to-day:
  PR → CI passes → merge to main → build.yml → git-<sha> image → deploy stg
  Ready for prod → bump VERSION → git tag v0.2.0 → build.yml → deploy prod

Rollback:
  ./infra/deploy.sh prod --image-tag <previous-tag>