edgefirst-schemas 3.3.0

Message schemas for EdgeFirst Perception - ROS2 Common Interfaces, Foxglove, and custom types
Documentation
name: Release

on:
  push:
    tags:
      - 'v[0-9]+.[0-9]+.[0-9]+'
      - 'v[0-9]+.[0-9]+.[0-9]+-*'

permissions:
  contents: write
  actions: read
  id-token: write

jobs:
  # ===========================================================================
  # Build C API binary packages for Linux x86_64 and aarch64
  # ===========================================================================
  build-c-api:
    name: Build C API (${{ matrix.arch }})
    runs-on: ${{ matrix.runner }}
    strategy:
      matrix:
        include:
          - arch: x86_64
            runner: ubuntu-22.04
          - arch: aarch64
            runner: ubuntu-22.04-arm
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

      - name: Install Rust toolchain
        run: rustup toolchain install stable --profile minimal

      - name: Build release
        run: cargo build --release

      - name: Extract version from tag
        id: version
        run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

      - name: Package C API
        run: |
          VERSION=${{ steps.version.outputs.version }}
          MAJOR=$(echo "$VERSION" | cut -d. -f1)
          MINOR=$(echo "$VERSION" | cut -d. -f2)
          PATCH=$(echo "$VERSION" | cut -d. -f3)
          PKG_NAME="edgefirst-schemas-linux_${{ matrix.arch }}-${VERSION}"
          mkdir -p "${PKG_NAME}/lib/pkgconfig" "${PKG_NAME}/include/edgefirst"
          cp README.md LICENSE "${PKG_NAME}/"
          cp include/edgefirst/schemas.h "${PKG_NAME}/include/edgefirst/"

          # Versioned shared library with chained symlinks (GNU/Linux convention).
          # The ELF DT_SONAME is libedgefirst_schemas.so.${MAJOR}; the runtime
          # loader opens that name and walks this chain to the real file:
          #   .so                               symlink -> .so.${MAJOR}
          #   .so.${MAJOR}                      symlink -> .so.${MAJOR}.${MINOR}
          #   .so.${MAJOR}.${MINOR}             symlink -> .so.${MAJOR}.${MINOR}.${PATCH}
          #   .so.${MAJOR}.${MINOR}.${PATCH}    real file
          cp target/release/libedgefirst_schemas.so "${PKG_NAME}/lib/libedgefirst_schemas.so.${MAJOR}.${MINOR}.${PATCH}"
          ln -s "libedgefirst_schemas.so.${MAJOR}.${MINOR}.${PATCH}" "${PKG_NAME}/lib/libedgefirst_schemas.so.${MAJOR}.${MINOR}"
          ln -s "libedgefirst_schemas.so.${MAJOR}.${MINOR}"          "${PKG_NAME}/lib/libedgefirst_schemas.so.${MAJOR}"
          ln -s "libedgefirst_schemas.so.${MAJOR}"                   "${PKG_NAME}/lib/libedgefirst_schemas.so"

          cp target/release/libedgefirst_schemas.a "${PKG_NAME}/lib/"
          sed "s/@VERSION@/${VERSION}/" \
            edgefirst-schemas.pc.in > "${PKG_NAME}/lib/pkgconfig/edgefirst-schemas.pc"
          zip -r --symlinks "${PKG_NAME}.zip" "${PKG_NAME}"

      - name: Upload C API artifact
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: c-api-${{ matrix.arch }}
          path: edgefirst-schemas-linux_${{ matrix.arch }}-${{ steps.version.outputs.version }}.zip

  # ===========================================================================
  # Collect Debian packages from ROS workflow
  # ===========================================================================
  collect-debian:
    name: Collect Debian Packages
    runs-on: ubuntu-latest
    steps:
      - name: Wait for ROS workflow
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "Waiting for ROS workflow on commit ${GITHUB_SHA}..."
          for i in $(seq 1 30); do
            RUN_ID=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow ros.yml \
              --commit "$GITHUB_SHA" --json databaseId --jq '.[0].databaseId')
            if [ -n "$RUN_ID" ]; then
              break
            fi
            echo "  Attempt $i/30 — ROS workflow not found yet, retrying in 10s..."
            sleep 10
          done

          if [ -z "$RUN_ID" ]; then
            echo "::error::ROS workflow run not found for commit ${GITHUB_SHA}"
            exit 1
          fi

          echo "Found ROS workflow run: ${RUN_ID}"
          gh run watch "$RUN_ID" --repo "$GITHUB_REPOSITORY" --exit-status

      - name: Download Debian packages
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          RUN_ID=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow ros.yml \
            --commit "$GITHUB_SHA" --json databaseId --jq '.[0].databaseId')
          gh run download "$RUN_ID" --repo "$GITHUB_REPOSITORY" \
            --pattern 'ros-*-edgefirst-msgs_*' --dir packages
          find packages -name '*.deb' -exec mv {} . \;
          echo "Downloaded Debian packages:"
          ls -lh *.deb

      - name: Upload Debian artifacts
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
        with:
          name: debian-packages
          path: "*.deb"

  # ===========================================================================
  # Create GitHub Release (waits for all artifacts, publishes in one shot)
  # ===========================================================================
  create-release:
    name: Create GitHub Release
    runs-on: ubuntu-latest
    needs: [build-c-api, collect-debian]
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
        with:
          fetch-depth: 0

      - name: Extract version from tag
        id: version
        run: |
          VERSION=${GITHUB_REF#refs/tags/}
          echo "version=$VERSION" >> $GITHUB_OUTPUT
          echo "Version: $VERSION"

      - name: Extract changelog for version
        run: |
          # Strip 'v' prefix — CHANGELOG.md uses bare version numbers
          VERSION="${{ steps.version.outputs.version }}"
          BARE_VERSION="${VERSION#v}"

          # Extract the section for this version from CHANGELOG.md
          CHANGELOG_CONTENT=$(awk "/## \[$BARE_VERSION\]/,/## \[/" CHANGELOG.md | sed '$d' | tail -n +2)

          if [ -z "$CHANGELOG_CONTENT" ]; then
            CHANGELOG_CONTENT="Release $VERSION"
          fi

          echo "$CHANGELOG_CONTENT" > release_notes.md

      - name: Generate SBOM
        run: |
          bash .github/scripts/generate_sbom.sh || echo "SBOM generation skipped"

      - name: Download all artifacts
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
        with:
          path: release-assets

      - name: Collect release files
        run: |
          mkdir -p release-files
          find release-assets -type f \( -name '*.zip' -o -name '*.deb' \) -exec mv {} release-files/ \;
          [ -f sbom.json ] && cp sbom.json release-files/
          echo "Release files:"
          ls -lh release-files/

      - name: Create GitHub Release
        uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
        with:
          name: ${{ steps.version.outputs.version }}
          body_path: release_notes.md
          draft: false
          prerelease: ${{ contains(steps.version.outputs.version, '-') }}
          files: release-files/*
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  # ===========================================================================
  # Publish to crates.io
  # ===========================================================================
  publish-crates:
    name: Publish to crates.io
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1

      - name: Install Rust toolchain
        run: rustup toolchain install stable --profile minimal

      - name: Install cargo-workspaces
        uses: taiki-e/install-action@0bc4cd8a3e21db4cb9519857377b0c8c5e150de5 # v2.67.2
        with:
          tool: cargo-workspaces

      - name: Publish to crates.io
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo workspaces publish --from-git

  # ===========================================================================
  # Wait for the wheel matrix to finish on this same commit, then download
  # the produced artifacts. Mirrors Studio/client's release.yml flow:
  # wheels.yml has already run on the tag push and uploaded one wheel per
  # platform plus an sdist; we just collect them and ship.
  # ===========================================================================
  wait-for-wheels:
    name: Wait for Wheels
    runs-on: ubuntu-latest
    steps:
      - name: Wait for wheels.yml
        uses: lewagon/wait-on-check-action@84840133801bb697fed25fd0e21078b85f5831a0 # v1.6.0
        with:
          ref: ${{ github.sha }}
          check-name: 'Verify Wheels'
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          wait-interval: 15
          # 'Verify Wheels' only registers after every matrix entry
          # finishes (Linux x86_64/aarch64, macOS x86_64/aarch64,
          # Windows x86_64). Allow up to 10 min for the aggregate check
          # to appear before failing.
          checks-discovery-timeout: 600

  # ===========================================================================
  # Publish wheels (and sdist) to PyPI
  # ===========================================================================
  publish-pypi:
    name: Publish to PyPI
    runs-on: ubuntu-latest
    needs: [wait-for-wheels]
    environment: pypi
    permissions:
      id-token: write
    steps:
      - name: Download wheel artifacts from wheels.yml
        uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
        with:
          workflow: wheels.yml
          commit: ${{ github.sha }}
          name: wheels-*
          name_is_regexp: true
          path: dist/
          merge_multiple: true

      - name: Download sdist artifact from wheels.yml
        uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
        with:
          workflow: wheels.yml
          commit: ${{ github.sha }}
          name: sdist
          path: dist/

      - name: Inventory artifacts
        run: |
          echo "Files to publish:"
          ls -lh dist/

      - name: Publish to PyPI
        # Trusted publishing via PyPI's id-token; no API token in secrets.
        uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1
        with:
          packages-dir: dist/

  # ===========================================================================
  # Attach the same wheels + sdist to the GitHub release for offline install.
  # ===========================================================================
  attach-wheels-to-release:
    name: Attach Wheels to GitHub Release
    runs-on: ubuntu-latest
    needs: [create-release, wait-for-wheels]
    steps:
      - name: Download wheel artifacts
        uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
        with:
          workflow: wheels.yml
          commit: ${{ github.sha }}
          name: wheels-*
          name_is_regexp: true
          path: wheels/
          merge_multiple: true

      - name: Download sdist artifact
        uses: dawidd6/action-download-artifact@8a338493df3d275e4a7a63bcff3b8fe97e51a927 # v19
        with:
          workflow: wheels.yml
          commit: ${{ github.sha }}
          name: sdist
          path: wheels/

      - name: Upload to release
        uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
        with:
          tag_name: ${{ github.ref_name }}
          files: |
            wheels/*.whl
            wheels/*.tar.gz
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}