aviso-server 0.5.0

Notification service for data-driven workflows with live and replay APIs.
name: Release

on:
  push:
    tags:
      - '*'   # Trigger on every tag push but issue a release only if the tag matches the Cargo.toml version
  workflow_dispatch:  # Allows manual triggering for testing

permissions:
  contents: write # Required for GitHub Actions to push a release

jobs:
  #################################################################
  # Job: release-setup
  # This job checks out the code, extracts the package name and version
  # from Cargo.toml, and verifies that the pushed tag matches the version.
  #################################################################
  release-setup:
    runs-on: ubuntu-latest
    outputs:
      package_name: ${{ steps.get_version.outputs.package_name }}
      package_version: ${{ steps.get_version.outputs.package_version }}
      should_release: ${{ steps.verify_tag.outputs.should_release }}
      major_minor: ${{ steps.release_tags.outputs.major_minor }}
      short_sha: ${{ steps.release_tags.outputs.short_sha }}
      is_stable: ${{ steps.release_tags.outputs.is_stable }}
    steps:
      - name: Check out code
        uses: actions/checkout@v4

      - name: Extract package version and name
        id: get_version
        run: |
          # Extract the package name from Cargo.toml (first occurrence)
          PACKAGE_NAME=$(grep '^name\s*=' Cargo.toml | head -n 1 | cut -d'"' -f2)
          # Extract the package version from Cargo.toml (first occurrence)
          PACKAGE_VERSION=$(grep '^version\s*=' Cargo.toml | head -n 1 | cut -d'"' -f2)
          # Set the outputs for later steps
          echo "package_name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT
          echo "package_version=${PACKAGE_VERSION}" >> $GITHUB_OUTPUT
          # Set CARGO_VERSION for the Skaffold build
          echo "CARGO_VERSION=${PACKAGE_VERSION}" >> $GITHUB_ENV
          echo "Extracted package: ${PACKAGE_NAME} version -> ${PACKAGE_VERSION}"

      - name: Verify Tag Equals Cargo Version
        id: verify_tag
        run: |
          # Accept both `0.4.1` and `v0.4.1` as the pushed tag; strip a
          # leading `v` before comparing to the Cargo.toml version.
          PUSHED="${{ github.ref_name }}"
          NORMALIZED="${PUSHED#v}"
          CARGO_VERSION="${{ steps.get_version.outputs.package_version }}"
          echo "Pushed Tag:        ${PUSHED}"
          echo "Normalized Tag:    ${NORMALIZED}"
          echo "Cargo Version:     ${CARGO_VERSION}"
          if [ "${NORMALIZED}" = "${CARGO_VERSION}" ]; then
            echo "should_release=true" >> $GITHUB_OUTPUT
          else
            echo "should_release=false" >> $GITHUB_OUTPUT
            echo "Tag does not match Cargo version. Skipping release steps."
          fi

      - name: Compute additional release tags
        id: release_tags
        if: steps.verify_tag.outputs.should_release == 'true'
        run: |
          # Derive the major.minor rolling tag (`0.4` for `0.4.1`) so
          # consumers can pin to a minor line that picks up patch
          # releases automatically. Pre-release versions (`-alpha`,
          # `-rc`, etc.) are excluded from `latest` and from the
          # rolling tag to avoid surprising stable consumers.
          VERSION="${{ steps.get_version.outputs.package_version }}"
          MAJOR_MINOR="$(echo "$VERSION" | cut -d. -f1,2)"
          SHORT_SHA="$(echo '${{ github.sha }}' | cut -c1-7)"
          if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            IS_STABLE="true"
          else
            IS_STABLE="false"
          fi
          echo "major_minor=${MAJOR_MINOR}" >> $GITHUB_OUTPUT
          echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
          echo "is_stable=${IS_STABLE}" >> $GITHUB_OUTPUT
          echo "Resolved tags: version=${VERSION} major_minor=${MAJOR_MINOR} short_sha=${SHORT_SHA} is_stable=${IS_STABLE}"

  #################################################################
  # Job: build-multiarch
  # This job builds and pushes Docker images for both amd64 and arm64
  # using a matrix strategy. Each image is tagged uniquely with the
  # architecture (e.g., v1.2.3-amd64 and v1.2.3-arm64) via Skaffold.
  #################################################################
  build-multiarch:
    needs: release-setup
    if: needs.release-setup.outputs.should_release == 'true'
    strategy:
      matrix:
        arch: [amd64, arm64]  # Build for both architectures.
    # Self-hosted runner based on the architecture:
    # ECMWF has self-hosted runners for both amd64 and arm64.
    # We will use Ubuntu for amd64, macOS for arm64.
    runs-on: ${{ matrix.arch == 'amd64' && 'platform-builder-Ubuntu-22.04' || 'platform-builder-MacOSX-13.4.1-arm64' }}
    env:
      # Repository to push images.
      SKAFFOLD_DEFAULT_REPO: eccr.ecmwf.int/aviso
      # Set the CARGO_VERSION based on Cargo.toml
      CARGO_VERSION: ${{ needs.release-setup.outputs.package_version }}
    steps:
      - name: Check out code
        uses: actions/checkout@v4

      - name: Docker login
        run: |
          # Log in to the Docker registry using stored secrets.
          echo "${{ secrets.ECMWF_DOCKER_REGISTRY_ACCESS_TOKEN }}" | \
            docker login eccr.ecmwf.int --username '${{ secrets.ECMWF_DOCKER_REGISTRY_USERNAME }}' --password-stdin

      - name: Install Skaffold
        run: |
          if [[ "$(uname)" == "Linux" && "${{ matrix.arch }}" == "amd64" ]]; then
            curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64
          elif [[ "$(uname)" == "Linux" && "${{ matrix.arch }}" == "arm64" ]]; then
            curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-arm64
          elif [[ "$(uname)" == "Darwin" && "${{ matrix.arch }}" == "amd64" ]]; then
            curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-darwin-amd64
          elif [[ "$(uname)" == "Darwin" && "${{ matrix.arch }}" == "arm64" ]]; then
            curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-darwin-arm64
          else
            echo "Unknown OS or architecture: $(uname) / ${{ matrix.arch }}"
            exit 1
          fi
          chmod +x skaffold
          mkdir -p $HOME/bin
          mv skaffold $HOME/bin/
          echo "$HOME/bin" >> $GITHUB_PATH

      - name: Build and push Docker image for ${{ matrix.arch }}
        env:
          # Set TARGETARCH to ensure the tag becomes "<CARGO_VERSION>-<TARGETARCH>"
          TARGETARCH: ${{ matrix.arch }}
        run: |
          # Run the Skaffold build, which uses TARGETARCH to generate an architecture-specific tag.
          skaffold build

  #################################################################
  # Job: create-manifest-and-release
  # This job creates a multi-arch manifest that maps the architecture-specific
  # images (e.g., 1.2.3-amd64 and 1.2.3-arm64) to a common tag (v1.2.3),
  # pushes the manifest to the registry, and then creates a GitHub release.
  #################################################################
  create-manifest-and-release:
    needs: [release-setup, build-multiarch]
    if: needs.release-setup.outputs.should_release == 'true'
    runs-on: ubuntu-latest
    strategy:
      matrix:
        image: [aviso_server, aviso_server-debug]
    steps:
      - name: Docker login
        run: |
          echo "${{ secrets.ECMWF_DOCKER_REGISTRY_ACCESS_TOKEN }}" | \
            docker login eccr.ecmwf.int --username '${{ secrets.ECMWF_DOCKER_REGISTRY_USERNAME }}' --password-stdin

      - name: Create and push multiarch manifest for ${{ matrix.image }}
        env:
          PACKAGE_VERSION: ${{ needs.release-setup.outputs.package_version }}
          MAJOR_MINOR:     ${{ needs.release-setup.outputs.major_minor }}
          SHORT_SHA:       ${{ needs.release-setup.outputs.short_sha }}
          IS_STABLE:       ${{ needs.release-setup.outputs.is_stable }}
          REPO: eccr.ecmwf.int/aviso/${{ matrix.image }}
        run: |
          # Each per-arch build pushed a single-arch image manifest
          # (<version>-amd64 from the Ubuntu runner, <version>-arm64
          # from the macOS-arm64 runner). `imagetools create` combines
          # those source manifests into a multi-arch manifest list and
          # attaches every human-friendly tag below to the same digest,
          # so consumers can pick the pinning level that matches their
          # risk tolerance:
          #
          #   :<version>           always (e.g. 0.4.1) — exact pin
          #   :<version>-<sha>     always              — exact-build pin for incident response
          #   :<major>.<minor>     stable releases     — picks up patch releases
          #   :latest              stable releases     — newest stable
          #
          # Pre-release versions (-alpha, -rc, etc.) are deliberately
          # excluded from the rolling tags so consumers tracking
          # :latest or :0.4 are not surprised by a beta build.
          TAGS=(
            "--tag $REPO:$PACKAGE_VERSION"
            "--tag $REPO:$PACKAGE_VERSION-$SHORT_SHA"
          )
          if [ "$IS_STABLE" = "true" ]; then
            TAGS+=(
              "--tag $REPO:$MAJOR_MINOR"
              "--tag $REPO:latest"
            )
          fi
          docker buildx imagetools create \
            ${TAGS[@]} \
            $REPO:$PACKAGE_VERSION-amd64 \
            $REPO:$PACKAGE_VERSION-arm64

    # Add a separate job for GitHub release creation
  github-release:
    needs: [release-setup, create-manifest-and-release]
    if: needs.release-setup.outputs.should_release == 'true'
    runs-on: ubuntu-latest
    steps:
      - name: Create GitHub Release
        id: create_release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.ref_name }}
          name: "${{ needs.release-setup.outputs.package_name }} v${{ needs.release-setup.outputs.package_version }}"
          generate_release_notes: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}