Releases on Github

We cut releases with semantic commits so the version bumps are automatic. Conventional commit types drive the semver change: feat: raises the minor version, fix: bumps the patch, and adding a breaking change marker (!) turns that into a major release. This keeps release automation predictable while still letting humans read the history.

Workflow overview

Create .github/workflows/release.yml alongside the CI workflow. It has two jobs: get-version determines the next semantic version without publishing anything, and release builds/tag/pushes the containers. Everything lives in a single file for easy copy/paste:

name: Release

on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  get-version:
    name: Determine semantic version
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.semver.outputs.version }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install semantic-release
        run: npm install -g semantic-release @semantic-release/commit-analyzer @semantic-release/release-notes-generator

      - name: Create .releaserc.json
        run: |
          cat > .releaserc.json <<'EOF'
          {
            "branches": [{ "name": "main" }],
            "tagFormat": "v${version}",
            "plugins": [
              "@semantic-release/commit-analyzer",
              "@semantic-release/release-notes-generator"
            ]
          }
          EOF

      - name: Calculate next version (dry-run)
        id: semver
        run: |
          set -euo pipefail
          npx semantic-release --dry-run | tee sr.log
          VER=$(grep -Eo 'next release version is [0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z\.-]+)?' sr.log | awk '{print $5}' || true)
          echo "version=${VER}" >> $GITHUB_OUTPUT

      - name: Print calculated version
        run: echo "Next version is ${{ steps.semver.outputs.version }}"

  release:
    name: Build and publish containers
    needs: get-version
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    env:
      FORCE_COLOR: 1
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push versioned images
        env:
          VERSION: ${{ needs.get-version.outputs.version }}
          WEB_REPO: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
          MIGRATIONS_REPO: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}-migrations
        run: |
          if [[ -z "$VERSION" ]]; then
            echo "No release-worthy commits detected. Skipping publish."
            exit 0
          fi
          cargo run --release -p infrastructure -- build \
            --web-tag "$WEB_REPO:$VERSION" \
            --migrations-tag "$MIGRATIONS_REPO:$VERSION"
  • The release job reuses the same Rust/Dagger pipeline as CI, but now it supplies explicit tags so the resulting containers are published with the semantic version.
  • If semantic-release does not find a new version (for example, only docs changes landed), the script exits early and nothing is published.
  • Once the images exist, you can follow up with semantic-release (without --dry-run) or a changelog workflow if you want GitHub Releases as well.