14.9 KiB · text History dc41247
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 }}

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
            sudo apt-get install -y build-essential pkg-config libssl-dev libgtk-3-dev libgdk-pixbuf2.0-dev libpango1.0-dev
          elif command -v yum &> /dev/null; then
            sudo yum install -y pkgconfig openssl-devel gtk3-devel gdk-pixbuf-devel pango-devel
          elif command -v dnf &> /dev/null; then
            sudo dnf install -y pkgconfig 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.92"
          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 -- -D warnings

      - name: Check
        run: cargo check

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

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

      # Built with `stable`, not this project's pinned toolchain. The
      # pin says what *this crate* compiles with; cargo-deb and
      # cargo-generate-rpm are build-time utilities that never link
      # into the binary, and constraining them by the project's MSRV
      # breaks the install every time the ecosystem moves past it —
      # cargo-deb 3.8.0 needs rustc 1.88 while declaring 1.85.
      # `--locked` keeps two runs a month apart installing the same
      # bytes.
      - name: Install the packaging tools
        run: |
          set -euo pipefail
          rustup toolchain install stable --profile minimal
          cargo +stable install cargo-deb --locked
          cargo +stable install cargo-generate-rpm --locked

      # 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.0+dev.<stamp>` sorts *after* 0.1.0,
      # which is what you want from a repository you added in order to
      # run the latest build. A tag publishes the bare Cargo version.
      - name: Stamp the package 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"
          # 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. Not publishing is a
      # legitimate outcome — github.com has no registry at these paths.
      - 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:-}}

          case "${forge_url:-}" in
            "")
              echo "No forge URL available; not publishing."
              echo "PUBLISH=false" >> "$GITHUB_ENV"
              ;;
            *://github.com|*://github.com/*|*://www.github.com*)
              echo "Running on GitHub, which has no package registry here."
              echo "PUBLISH=false" >> "$GITHUB_ENV"
              ;;
            *)
              echo "publishing to $forge_url"
              echo "FORGE_URL=$forge_url" >> "$GITHUB_ENV"
              echo "PUBLISH=true" >> "$GITHUB_ENV"
              ;;
          esac

      # `${{ 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.PUBLISH == 'true'
        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.PUBLISH == 'true'
        env:
          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 neiam-ppl

          # Fedora / RHEL / openSUSE — /etc/yum.repos.d/$OWNER.repo
          [$OWNER]
          name=$OWNER
          baseurl=$FORGE_URL/pkg/rpm/$OWNER/_all
          enabled=1
          repo_gpgcheck=1
          # cargo-generate-rpm produces unsigned packages; the forge
          # signs the index, not what it was handed.
          gpgcheck=0
          gpgkey=$FORGE_URL/pkg/_keys/rpm/$OWNER

          sudo dnf install neiam-ppl   # or: sudo zypper install neiam-ppl
          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: ppl-test-${{ matrix.arch }}-linux
          path: target/release/ppl

      - name: Upload test Debian package
        uses: actions/upload-artifact@v4
        if: ${{ startsWith(github.server_url, 'https://github.com') }}
        with:
          name: ppl-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: ppl-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: ppl-x86_64-linux-gnu.tar.gz
            archive_cmd: tar -czf
            binary_ext: ""
            rust: "1.92"
          - os: ubuntu-24.04-arm
            runner: ubuntu-24.04-arm
            name: ppl-aarch64-linux-gnu.tar.gz
            archive_cmd: tar -czf
            binary_ext: ""
            rust: "1.92"
          - os: macos-latest
            runner: macos-latest
            name: ppl-aarch64-macos.tar.gz
            archive_cmd: tar -czf
            binary_ext: ""
            rust: "1.92"
          - os: windows-latest
            runner: windows-latest
            name: ppl-x86_64-windows.zip
            archive_cmd: 7z a
            binary_ext: ".exe"
            rust: "1.92"
    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 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 openssl-devel gtk3-devel gdk-pixbuf-devel pango-devel
          elif command -v dnf &> /dev/null; then
            sudo dnf install -y pkgconfig 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/ppl${{ matrix.binary_ext }}

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

      - name: Create release archive (Windows)
        if: matrix.os == 'windows-latest'
        run: |
          cd target/release
          7z a ../../${{ matrix.name }} ppl.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: ppl-${{ 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: ppl-${{ 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 "# ppl 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 }}