14.7 KiB · text History 4e85662
name: Rust CI and Docker Build

on:
  push:
    branches: [ master ]
    tags: [ 'v*' ]
  pull_request:
    branches: [ master ]

env:
  CARGO_TERM_COLOR: always
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

# A newer push supersedes this run: on a branch or PR the run in
# progress is cancelled; on master it finishes, and only a run still
# waiting is replaced.
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}

jobs:
  build:
    strategy:
      matrix:
        include:
          - runner: arc-runner-set
            arch: x86_64
          - runner: ubuntu-24.04-arm
            arch: aarch64
    runs-on: ${{ matrix.runner }}
    permissions:
      contents: read
      packages: write

    steps:
      - uses: actions/checkout@v4

      - name: Install system dependencies (Linux)
        if: runner.os == 'Linux'
        run: |
          if command -v apt-get &> /dev/null; then
            sudo apt-get update
            # cmake and git are for paho-mqtt-sys, which vendors the
            # Paho C library and builds it with cmake from a git
            # submodule. Both are preinstalled on GitHub-hosted
            # runners and absent from a bare self-hosted container,
            # which is why this only started failing on arc-runner-set.
            sudo apt-get install -y build-essential pkg-config cmake git libssl-dev libgtk-3-dev libgdk-pixbuf2.0-dev libpango1.0-dev libglib2.0-dev
          elif command -v yum &> /dev/null; then
            sudo yum install -y pkgconfig cmake git openssl-devel gtk3-devel gdk-pixbuf-devel pango-devel
          elif command -v dnf &> /dev/null; then
            sudo dnf install -y pkgconfig cmake git openssl-devel
          elif command -v pacman &> /dev/null; then
            sudo pacman -Sy --noconfirm pkgconf openssl
          elif command -v apk &> /dev/null; then
            sudo apk add --no-cache pkgconfig openssl-dev
          else
            echo "Package manager not found. Please install pkg-config and openssl-dev manually."
            exit 1
          fi

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: "1.98"
          components: rustfmt, clippy

      - name: Setup Rust cache
        uses: Swatinem/rust-cache@v2

      - name: Build
        run: cargo build --verbose

      - name: Run tests
        run: cargo test --verbose

      - name: Check formatting
        run: cargo fmt -- --check

      - name: Run clippy
        run: cargo clippy --workspace --all-targets --locked -- -D warnings

      - name: Check
        run: cargo check

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

      - name: Strip binary
        run: strip target/release/koco

      - name: Install cargo-deb
        run: cargo install cargo-deb

      - name: Install cargo-generate-rpm
        run: cargo install cargo-generate-rpm

      # Every CI build has to carry a distinct version, or the second
      # one publishes the same filename with different bytes and the
      # registry correctly refuses it with a 409 — and no machine that
      # installed the first ever sees the second.
      #
      # `+` rather than `-`: RPM forbids `-` in a version outright, and
      # in both ecosystems `0.1.2+dev.<stamp>` sorts *after* 0.1.2,
      # which is what you want from a repository you added in order to
      # run the latest build. A tag publishes the bare Cargo version.
      #
      # One stamp for the whole job, so the .deb and the .rpm from a
      # single run agree about what they are.
      - name: Stamp the package version
        id: version
        run: |
          set -euo pipefail
          base=$(grep -m1 '^version = ' Cargo.toml | cut -d'"' -f2)

          if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
            version="$base"
          else
            version="$base+dev.$(date -u +%s)"
          fi

          echo "version: $version"
          echo "version=$version" >> "$GITHUB_OUTPUT"

          # Rewritten after the release build, so the binary is cached
          # on the unstamped manifest and only the packaging sees this.
          sed -i "0,/^version = .*/s//version = \"$version\"/" Cargo.toml

      - name: Build test Debian package
        run: cargo deb --no-build --no-strip

      - name: Build RPM package
        run: cargo generate-rpm

      # `vars.CI_FORGE_URL` is what the forge injects into a run; fall
      # back to what the runner already knows, and say so plainly if
      # neither resolves rather than handing curl an empty URL.
      - name: Resolve the forge URL
        env:
          CI_FORGE_URL: ${{ vars.CI_FORGE_URL }}
        run: |
          set -euo pipefail
          forge_url=${CI_FORGE_URL:-${GITHUB_SERVER_URL:-}}

          if [ -z "$forge_url" ]; then
            echo "No forge URL available." >&2
            echo "Set a CI_FORGE_URL variable on this repository or its org." >&2
            exit 1
          fi

          echo "FORGE_URL=$forge_url" >> "$GITHUB_ENV"

      # `${{ github.token }}` is minted for this job and authorized to
      # publish to THIS repository's packages and nowhere else, so
      # there is no secret to create first.
      #
      # Pull requests don't publish: a PR build is a check, and a
      # registry that carries every proposed change is not a registry
      # anyone can install from.
      - name: Publish the .deb and the .rpm to the forge
        if: github.event_name != 'pull_request'
        env:
          REPOSITORY: ${{ github.repository }}
          TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail

          for deb in target/debian/*.deb; do
            echo "publishing $deb"
            curl --fail-with-body -sS \
              -u "gitgud-actions:$TOKEN" \
              -H "Content-Type: application/vnd.debian.binary-package" \
              --data-binary "@$deb" \
              "$FORGE_URL/pkg/deb/$REPOSITORY/upload?dist=stable&component=main"
            echo
          done

          for rpm in target/generate-rpm/*.rpm; do
            echo "publishing $rpm"
            curl --fail-with-body -sS \
              -u "gitgud-actions:$TOKEN" \
              -H "Content-Type: application/x-rpm" \
              --data-binary "@$rpm" \
              "$FORGE_URL/pkg/rpm/$REPOSITORY/upload"
            echo
          done

      - name: How to install it
        if: github.event_name != 'pull_request'
        env:
          REPOSITORY: ${{ github.repository }}
          OWNER: ${{ github.repository_owner }}
        run: |
          cat <<EOF
          # Debian / Ubuntu
          curl -fsSL $FORGE_URL/pkg/_keys/deb/$OWNER | sudo tee /etc/apt/keyrings/$OWNER.asc >/dev/null
          echo "deb [signed-by=/etc/apt/keyrings/$OWNER.asc] $FORGE_URL/pkg/deb/$OWNER/_all stable main" | sudo tee /etc/apt/sources.list.d/$OWNER.list
          sudo apt update && sudo apt install koco

          # Fedora / RHEL / openSUSE — /etc/yum.repos.d/$OWNER.repo
          [$OWNER]
          name=$OWNER
          baseurl=$FORGE_URL/pkg/rpm/$OWNER/_all
          enabled=1
          repo_gpgcheck=1
          gpgcheck=0
          gpgkey=$FORGE_URL/pkg/_keys/rpm/$OWNER
          EOF

      - name: Upload test artifact
        uses: actions/upload-artifact@v4
        # Artifact storage is a GitHub feature; this forge has none, so
        # these steps have nothing to upload to. They used to run and
        # fail with `continue-on-error`, which is a red step on every
        # single run — and a build that is always a bit red teaches
        # everyone to stop reading it. Skipping says the same thing
        # honestly and leaves the badge meaningful.
        #
        # The packages are published to the forge's registries instead,
        # which is the thing anyone actually installs from.
        if: ${{ startsWith(github.server_url, 'https://github.com') }}
        with:
          name: koco-test-${{ matrix.arch }}-linux
          path: target/release/koco

      - name: Upload test Debian package
        uses: actions/upload-artifact@v4
        if: ${{ startsWith(github.server_url, 'https://github.com') }}
        with:
          name: koco-test-${{ matrix.arch }}-linux.deb
          path: target/debian/*.deb

      - name: Upload test RPM package
        uses: actions/upload-artifact@v4
        if: ${{ startsWith(github.server_url, 'https://github.com') }}
        with:
          name: koco-test-${{ matrix.arch }}-linux.rpm
          path: target/generate-rpm/*.rpm


  release:
    name: Release
    if: startsWith(github.ref, 'refs/tags/v')
    needs: build
    permissions:
      contents: write
      packages: write
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            runner: arc-runner-set
            name: koco-x86_64-linux-gnu.tar.gz
            archive_cmd: tar -czf
            binary_ext: ""
            rust: "1.98"
          - os: ubuntu-24.04-arm
            runner: ubuntu-24.04-arm
            name: koco-aarch64-linux-gnu.tar.gz
            archive_cmd: tar -czf
            binary_ext: ""
            rust: "1.98"
          - os: macos-latest
            runner: macos-latest
            name: koco-aarch64-macos.tar.gz
            archive_cmd: tar -czf
            binary_ext: ""
            rust: "1.98"
          - os: windows-latest
            runner: windows-latest
            name: koco-x86_64-windows.zip
            archive_cmd: 7z a
            binary_ext: ".exe"
            rust: "1.98"
    runs-on: ${{ matrix.runner }}

    steps:
      - uses: actions/checkout@v4

      - name: Install system dependencies (self-hosted Linux)
        if: runner.os == 'Linux'
        run: |
          if command -v apt-get &> /dev/null; then
            sudo apt-get update
            sudo apt-get install -y build-essential pkg-config cmake git libssl-dev libgtk-3-dev libgdk-pixbuf2.0-dev libpango1.0-dev libglib2.0-dev
          elif command -v yum &> /dev/null; then
            sudo yum install -y pkgconfig cmake git openssl-devel gtk3-devel gdk-pixbuf-devel pango-devel
          elif command -v dnf &> /dev/null; then
            sudo dnf install -y pkgconfig cmake git openssl-devel
          elif command -v pacman &> /dev/null; then
            sudo pacman -Sy --noconfirm pkgconf openssl
          elif command -v apk &> /dev/null; then
            sudo apk add --no-cache pkgconfig openssl-dev
          else
            echo "Package manager not found. Please install pkg-config and openssl-dev manually."
            exit 1
          fi

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: ${{ matrix.rust }}

      - name: Setup Rust cache
        uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.os }}-${{ matrix.rust }}

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

      - name: Strip binary (Unix)
        if: matrix.os != 'windows-latest'
        run: |
          strip target/release/koco${{ matrix.binary_ext }}

      - name: Create release archive (Unix)
        if: matrix.os != 'windows-latest'
        run: |
          cd target/release
          tar -czf ../../${{ matrix.name }} koco
          cd ../..

      - name: Create release archive (Windows)
        if: matrix.os == 'windows-latest'
        run: |
          cd target/release
          7z a ../../${{ matrix.name }} koco.exe
          cd ../..

      - name: Install cargo-deb (Linux)
        if: runner.os == 'Linux'
        run: cargo install cargo-deb

      - name: Build Debian package (Linux)
        if: runner.os == 'Linux'
        run: cargo deb --no-build --no-strip

      - name: Install cargo-generate-rpm (Linux)
        if: runner.os == 'Linux'
        run: cargo install cargo-generate-rpm

      - name: Build RPM package (Linux)
        if: runner.os == 'Linux'
        run: cargo generate-rpm

      - name: Upload release archive
        uses: actions/upload-artifact@v4
        if: ${{ startsWith(github.server_url, 'https://github.com') }}
        with:
          name: ${{ matrix.name }}
          path: ${{ matrix.name }}

      - name: Upload Debian package
        if: runner.os == 'Linux'
        uses: actions/upload-artifact@v4
        if: ${{ startsWith(github.server_url, 'https://github.com') }}
        with:
          name: koco-${{ matrix.os }}.deb
          path: target/debian/*.deb

      - name: Upload RPM package
        if: runner.os == 'Linux'
        uses: actions/upload-artifact@v4
        if: ${{ startsWith(github.server_url, 'https://github.com') }}
        with:
          name: koco-${{ matrix.os }}.rpm
          path: target/generate-rpm/*.rpm

  create-release:
    name: Create Release
    runs-on: arc-runner-set
    # Makes a *GitHub* Release out of artifacts a GitHub runner uploaded.
    # Off GitHub it would download nothing and then fail, which is the
    # permanently-red build this workflow is trying to stop having.
    # Tagged builds still publish their packages — that happens in the
    # build job, which runs everywhere.
    if: >-
      startsWith(github.ref, 'refs/tags/v') &&
      startsWith(github.server_url, 'https://github.com')
    needs: release
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        if: ${{ startsWith(github.server_url, 'https://github.com') }}
        with:
          path: artifacts

      - name: Create release notes
        run: |
          echo "# koco Release ${GITHUB_REF_NAME}" > release_notes.md
          echo "" >> release_notes.md
          echo "## Changes in this release" >> release_notes.md
          echo "" >> release_notes.md
          echo "## Supported Platforms" >> release_notes.md
          echo "" >> release_notes.md
          echo "- Linux (x86_64)" >> release_notes.md
          echo "- Linux (aarch64)" >> release_notes.md
          echo "- Windows (x86_64)" >> release_notes.md
          echo "- macOS (x86_64 and Apple Silicon)" >> release_notes.md
          echo "" >> release_notes.md
          echo "## Installation" >> release_notes.md
          echo "" >> release_notes.md
          echo "1. Download the appropriate archive for your platform" >> release_notes.md
          echo "2. Extract the binaries" >> release_notes.md
          echo "3. Place them in your PATH" >> release_notes.md
          echo "" >> release_notes.md

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v1
        with:
          files: |
            artifacts/*/*.tar.gz
            artifacts/*/*.zip
            artifacts/*/*.deb
            artifacts/*/*.rpm
          body_path: release_notes.md
          draft: false
          prerelease: ${{ contains(github.ref_name, '-') }}
          generate_release_notes: true
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}