Release #140
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # Auto-release on merge to main: when the CI workflow succeeds on a push to | |
| # main, compute the next semantic version from the merged commits, build and | |
| # publish the container images, smoke-boot the runtime image against a real | |
| # Postgres so a broken build can't ship, then cut the GitHub Release with | |
| # cross-compiled CLI binaries and move the `latest` image tag. | |
| # | |
| # Manual runs are supported via workflow_dispatch (e.g. to re-cut after a | |
| # transient failure). Use dry_run to compute the version without publishing. | |
| on: | |
| workflow_run: | |
| workflows: ["CI"] | |
| types: [completed] | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: "Compute the version but do not publish" | |
| type: boolean | |
| default: false | |
| concurrency: | |
| group: release | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| prepare: | |
| name: Compute version | |
| # Only release after CI passed on a push to main (or a manual dispatch). | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event.workflow_run.conclusion == 'success' && | |
| github.event.workflow_run.head_branch == 'main' && | |
| github.event.workflow_run.event == 'push') | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_release: ${{ steps.compute.outputs.should_release }} | |
| version: ${{ steps.compute.outputs.version }} | |
| tag: ${{ steps.compute.outputs.tag }} | |
| prerelease: ${{ steps.compute.outputs.prerelease }} | |
| sha: ${{ steps.sha.outputs.sha }} | |
| steps: | |
| - id: sha | |
| run: echo "sha=${{ github.event.workflow_run.head_sha || github.sha }}" >> "$GITHUB_OUTPUT" | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ steps.sha.outputs.sha }} | |
| fetch-depth: 0 | |
| - id: compute | |
| name: Next version from conventional commits | |
| run: | | |
| set -euo pipefail | |
| # Base the next version on the latest STABLE tag (vX.Y.Z). Pre-release | |
| # tags like v0.1.0-rc.1 are deliberately ignored so the first real | |
| # auto-release starts a clean stable series. | |
| last_stable="$(git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -1 || true)" | |
| if [ -n "$last_stable" ]; then | |
| base="${last_stable#v}" | |
| range="${last_stable}..HEAD" | |
| else | |
| base="0.0.0" | |
| range="HEAD" | |
| fi | |
| IFS=. read -r MA MI PA <<< "$base" | |
| subjects="$(git log --format='%s' $range)" | |
| bodies="$(git log --format='%B' $range)" | |
| bump=none | |
| # `type!:` or a `BREAKING CHANGE` footer => major. | |
| if printf '%s\n' "$subjects" | grep -qE '^[a-zA-Z]+(\([^)]*\))?!:' \ | |
| || printf '%s\n' "$bodies" | grep -qE 'BREAKING[ -]CHANGE'; then | |
| bump=major | |
| elif printf '%s\n' "$subjects" | grep -qE '^feat(\([^)]*\))?:'; then | |
| bump=minor | |
| elif printf '%s\n' "$subjects" | grep -qE '^fix(\([^)]*\))?:'; then | |
| bump=patch | |
| fi | |
| echo "base=$base range=$range bump=$bump" | |
| case "$bump" in | |
| major) MA=$((MA+1)); MI=0; PA=0 ;; | |
| minor) MI=$((MI+1)); PA=0 ;; | |
| patch) PA=$((PA+1)) ;; | |
| none) | |
| echo "No feat/fix/breaking commits since ${last_stable:-<start>}; nothing to release." | |
| echo "should_release=false" >> "$GITHUB_OUTPUT" | |
| exit 0 ;; | |
| esac | |
| version="${MA}.${MI}.${PA}" | |
| echo "Releasing v${version}" | |
| { | |
| echo "should_release=true" | |
| echo "version=${version}" | |
| echo "tag=v${version}" | |
| echo "prerelease=false" | |
| } >> "$GITHUB_OUTPUT" | |
| images: | |
| name: Build & push images | |
| needs: prepare | |
| if: needs.prepare.outputs.should_release == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| matrix: | |
| include: | |
| - name: runtime | |
| dockerfile: services/runtime/Dockerfile | |
| - name: dashboard | |
| dockerfile: apps/dashboard/Dockerfile | |
| - name: customer-app | |
| dockerfile: apps/customer-app/Dockerfile | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ needs.prepare.outputs.sha }} | |
| - uses: docker/setup-buildx-action@v3 | |
| - uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push ${{ matrix.name }} | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| push: true | |
| tags: ghcr.io/agent-field/af-stack-${{ matrix.name }}:${{ needs.prepare.outputs.version }} | |
| build-args: | | |
| VERSION=${{ needs.prepare.outputs.version }} | |
| cache-from: type=gha,scope=${{ matrix.name }} | |
| cache-to: type=gha,mode=max,scope=${{ matrix.name }} | |
| provenance: false | |
| smoke: | |
| name: Smoke — boot runtime image | |
| needs: [prepare, images] | |
| if: needs.prepare.outputs.should_release == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: read | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ needs.prepare.outputs.sha }} | |
| - uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Boot the freshly built runtime image and assert /ready | |
| env: | |
| AF_STACK_VERSION: ${{ needs.prepare.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| docker compose -f docker-compose.release-smoke.yml up -d | |
| echo "Waiting for runtime /ready (migrations must apply on the built image)..." | |
| ok="" | |
| for _ in $(seq 1 60); do | |
| code="$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/ready || true)" | |
| if [ "$code" = "200" ]; then echo "runtime /ready = 200"; ok=1; break; fi | |
| sleep 3 | |
| done | |
| if [ "$ok" != "1" ]; then | |
| echo "::error::runtime image did not become ready — refusing to publish the release" | |
| docker compose -f docker-compose.release-smoke.yml logs runtime | tail -80 | |
| exit 1 | |
| fi | |
| curl -fsS http://localhost:8080/health >/dev/null && echo "health OK" | |
| - name: Tear down | |
| if: always() | |
| run: docker compose -f docker-compose.release-smoke.yml down -v || true | |
| publish: | |
| name: Tag, release & move latest | |
| needs: [prepare, images, smoke] | |
| if: >- | |
| needs.prepare.outputs.should_release == 'true' && | |
| (github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| ref: ${{ needs.prepare.outputs.sha }} | |
| fetch-depth: 0 | |
| - name: Create and push the release tag | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git tag -a "${{ needs.prepare.outputs.tag }}" -m "Release ${{ needs.prepare.outputs.tag }}" | |
| git push origin "${{ needs.prepare.outputs.tag }}" | |
| - uses: actions/setup-go@v5 | |
| with: | |
| go-version-file: go.mod | |
| - name: GoReleaser — GitHub Release + CLI binaries | |
| uses: goreleaser/goreleaser-action@v7 | |
| with: | |
| version: "~> v2" | |
| args: release --clean | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Move :latest to this release (stable only) | |
| if: needs.prepare.outputs.prerelease != 'true' | |
| run: | | |
| set -euo pipefail | |
| for svc in runtime dashboard customer-app; do | |
| docker buildx imagetools create \ | |
| --tag "ghcr.io/agent-field/af-stack-$svc:latest" \ | |
| "ghcr.io/agent-field/af-stack-$svc:${{ needs.prepare.outputs.version }}" | |
| done |