8.9 KiB · text History aca7335
# Builds Icon Studio and publishes a .deb and an .rpm to this forge's
# package registries, the way the other neiam Rust projects do.
#
# x86_64 only: that is what the desktops this runs on are, and a second
# architecture is a second runner to keep alive for no one.
#
# Nothing to configure. `${{ github.token }}` is minted for the job and
# authorised to publish to THIS repository's packages and nowhere else,
# so there is no secret to create first.
name: packages

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

env:
  CARGO_TERM_COLOR: always

# 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:
  package:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4

      - name: Install system dependencies
        run: |
          set -euo pipefail

          # A hosted runner is an unprivileged user with sudo; a job
          # container is already root and often ships no sudo at all, so
          # hardcoding either one fails before the first package is
          # fetched. Ask what we are.
          if [ "$(id -u)" -eq 0 ]; then
            SUDO=
          elif command -v sudo >/dev/null 2>&1; then
            SUDO=sudo
          else
            echo "Not root and no sudo; cannot install system dependencies." >&2
            exit 1
          fi

          # eframe/winit need the windowing and GL headers; rfd is built
          # against the XDG portal rather than GTK, so there is no GTK
          # stack here. dpkg-dev matters more than it looks: without it
          # cargo-deb cannot run dpkg-shlibdeps, and the .deb ships with
          # an empty Depends that installs anywhere and then fails to
          # start on a machine missing libxkbcommon.
          if command -v apt-get >/dev/null 2>&1; then
            $SUDO apt-get update
            $SUDO apt-get install -y --no-install-recommends \
              build-essential pkg-config dpkg-dev rpm \
              libxkbcommon-dev libwayland-dev \
              libx11-dev libxcursor-dev libxrandr-dev libxi-dev \
              libgl1-mesa-dev
          elif command -v dnf >/dev/null 2>&1; then
            $SUDO dnf install -y gcc pkgconf-pkg-config rpm-build dpkg-dev \
              libxkbcommon-devel wayland-devel \
              libX11-devel libXcursor-devel libXrandr-devel libXi-devel \
              mesa-libGL-devel
          else
            echo "Unsupported package manager." >&2
            exit 1
          fi

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

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

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

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

      # Includes the comparison against the icons the original studio
      # shipped, so a rendering regression stops the release rather than
      # being packaged.
      - name: Run tests
        run: cargo test --verbose

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

      - name: Strip binary
        run: strip target/release/icon-studio

      # The icons and desktop entry are committed, but regenerating them
      # proves presets/icon-studio.toml still renders — the packaged icon
      # and the document that describes it cannot drift apart.
      - name: Regenerate the application icon
        run: |
          set -euo pipefail
          cargo run --release --example make_app_icon
          git --no-pager diff --stat -- assets/icons || true

      # Built with stable, not pinned: cargo-deb has repeatedly needed a
      # newer rustc than its own declared MSRV admits. These are
      # build-time utilities that never link into the binary, so holding
      # them to this crate's toolchain buys nothing and breaks whenever
      # the ecosystem moves. `--locked` keeps two runs a month apart
      # installing the same bytes.
      - name: Install the packaging tools
        run: |
          set -euo pipefail
          cargo install cargo-deb --locked
          cargo 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 nothing 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 to run the latest build.
      # A tag publishes the bare Cargo version.
      #
      # One stamp for the whole job, so the .deb and .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
          # against the unstamped manifest and only packaging sees this.
          sed -i "0,/^version = .*/s//version = \"$version\"/" Cargo.toml

      # The build cache can restore a target/ that still holds packages
      # from an earlier run. The publish step globs these directories, so
      # a stale .deb would be re-uploaded under its old version alongside
      # the new one.
      - name: Clear previous package output
        run: rm -rf target/debian target/generate-rpm

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

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

      - name: Show what was built
        run: ls -l target/debian/*.deb target/generate-rpm/*.rpm

      # `vars.CI_FORGE_URL` is a forge built-in, but an older forge build
      # will not inject it; fall back to what the runner already knows.
      - 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; not publishing."
            echo "PUBLISH=false" >> "$GITHUB_ENV"
            exit 0
          fi

          echo "publishing to $forge_url"
          echo "FORGE_URL=$forge_url" >> "$GITHUB_ENV"
          echo "PUBLISH=true" >> "$GITHUB_ENV"

      # Pull requests do not publish: a PR build is a check, and a
      # registry carrying every proposed change is not one anyone can
      # install from.
      - name: Publish the .deb and the .rpm
        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 "ci:$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 "ci:$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-icon-studio

          # 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
          EOF