11.3 KiB · text History d69022d
name: Rust CI

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:
  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
        run: |
          if command -v apt-get &> /dev/null; then
            sudo apt-get update
            # build-essential/pkg-config for the C shims nix and blake3 build;
            # git because a bare self-hosted container has none and some build
            # scripts want it. No OpenSSL: the iroh stack is rustls all the way.
            sudo apt-get install -y build-essential pkg-config git
          elif command -v dnf &> /dev/null; then
            sudo dnf install -y gcc gcc-c++ pkgconfig git
          elif command -v yum &> /dev/null; then
            sudo yum install -y gcc gcc-c++ pkgconfig git
          elif command -v pacman &> /dev/null; then
            sudo pacman -Sy --noconfirm base-devel pkgconf git
          elif command -v apk &> /dev/null; then
            sudo apk add --no-cache build-base pkgconfig git
          else
            echo "Package manager not found. Install a C toolchain, pkg-config and git 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 --workspace --verbose

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

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

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

      - name: Build release binaries
        run: cargo build --workspace --release --verbose

      - name: Strip binaries
        run: strip target/release/devendra-agent target/release/devendra-controller

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

      # 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 `-`: 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
        id: version
        run: |
          set -euo pipefail
          # The crates all inherit version.workspace = true, so the one
          # version that matters lives under [workspace.package] here.
          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 binaries are cached
          # against the unstamped manifest and only packaging sees this.
          sed -i "0,/^version = .*/s//version = \"$version\"/" Cargo.toml

      # Only the agent has [package.metadata.deb] — it is the thing that
      # gets installed on every managed host. The controller is run from
      # the repo, so it has nothing to package yet.
      - name: Build Debian package
        run: cargo deb -p devendra-agent --no-build --no-strip

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

      - name: How to install it
        if: github.event_name != 'pull_request'
        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 devendra-agent

          # Then put a join ticket in /etc/devendra/agent.env and:
          sudo systemctl start devendra-agent
          EOF

      # 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.
      - name: Upload binaries
        uses: actions/upload-artifact@v4
        if: ${{ startsWith(github.server_url, 'https://github.com') }}
        with:
          name: devendra-${{ matrix.arch }}-linux
          path: |
            target/release/devendra-agent
            target/release/devendra-controller

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

  release:
    name: Release
    if: startsWith(github.ref, 'refs/tags/v')
    needs: build
    permissions:
      contents: write
      packages: write
    strategy:
      matrix:
        include:
          - runner: arc-runner-set
            arch: x86_64
          - runner: ubuntu-24.04-arm
            arch: aarch64
    runs-on: ${{ matrix.runner }}

    # Linux only, on purpose: the agent reconciles systemd units and
    # resolves uids through nix, so there is no macOS or Windows build
    # to ship.
    steps:
      - uses: actions/checkout@v4

      - name: Install system dependencies
        run: |
          if command -v apt-get &> /dev/null; then
            sudo apt-get update
            sudo apt-get install -y build-essential pkg-config git
          elif command -v dnf &> /dev/null; then
            sudo dnf install -y gcc gcc-c++ pkgconfig git
          else
            echo "Package manager not found. Install a C toolchain, pkg-config and git manually."
            exit 1
          fi

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable
        with:
          toolchain: "1.92"

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

      - name: Build release binaries
        run: cargo build --workspace --release --verbose

      - name: Strip binaries
        run: strip target/release/devendra-agent target/release/devendra-controller

      - name: Create release archive
        run: |
          tar -czf devendra-${{ matrix.arch }}-linux-gnu.tar.gz \
            -C target/release devendra-agent devendra-controller

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

      - name: Build Debian package
        run: cargo deb -p devendra-agent --no-build --no-strip

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

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

  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
        with:
          path: artifacts

      - name: Create release notes
        run: |
          cat > release_notes.md <<EOF
          # devendra ${GITHUB_REF_NAME}

          ## Contents

          - \`devendra-agent\` — host agent (systemd unit + quadlet reconciler)
          - \`devendra-controller\` — controller web UI

          ## Supported platforms

          - Linux (x86_64)
          - Linux (aarch64)

          ## Installation

          Install the \`.deb\` on each managed host, put a join ticket in
          \`/etc/devendra/agent.env\`, then \`systemctl start devendra-agent\`.
          EOF

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