Appearance
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 truthThe 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.0Checking the running version:
bash
curl -s https://console-stg.exto360.com/healthz | jq .version
# → "0.1.0"When to Bump VERSION
| Change type | Example | Bump |
|---|---|---|
| Breaking API change | Remove/rename endpoint | Major (1.0.0) |
| New feature | New endpoint, new UI page | Minor (0.2.0) |
| Bug fix, patch | Fix invite email, tweak query | Patch (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| Branch | Purpose | Lifetime |
|---|---|---|
main | Production-ready code. All merges go here. | Permanent |
feat/*, fix/*, chore/* | Feature/bugfix work branches | Short-lived (days) |
Rules
- No long-lived branches. No
develop,staging, orrelease/*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 prod3. Image Tagging Strategy
Never use mutable tags like latest for deployments.
| Tag | When | Purpose |
|---|---|---|
git-<sha7> | Every CI build | Immutable, traceable to exact commit |
v0.1.0 | Release (git tag) | Human-readable release version |
stg-latest | After each main build | Convenience: "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-latest4. 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 + testWorkflow: .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):
| Secret | Description |
|---|---|
AZURE_CLIENT_ID | Service principal with ACR push access |
AZURE_TENANT_ID | Azure AD tenant |
AZURE_SUBSCRIPTION_ID | Subscription 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-onlyThe --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 .versionNo 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>
