retch-cli 0.9.10

A fast, feature-rich system information fetcher written in Rust (similar to fastfetch or neofetch)
Documentation
name: Packaging Verification

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]
    paths:
      - '**/*.rs'
      - '**/Cargo.toml'
      - 'Cargo.lock'
      - 'flake.nix'
      - 'flake.lock'
      - 'packaging/**'
      # The recipes that RENDER packaging/aur (aur-bump / aur-srcinfo / aur-check) live in
      # the Justfile, so a change there can alter the AUR tooling without touching anything
      # under packaging/ — which is exactly how #203 shipped with every workflow's paths
      # filter skipping it, CI green having never looked at the change. This job cannot run
      # those recipes (no podman, no `just` in the container), so what the entry buys is
      # artifact-level coverage: the committed PKGBUILD/.SRCINFO pair is re-verified against
      # the real tarball, rebuilt with makepkg, and its packaged man page inspected.
      - 'Justfile'
      # .copr/Makefile is what GENERATES the SRPM, and until v0.9.10 no PR-triggered
      # workflow watched it: copr.yml watches '.copr/**' but only on push to main, so a PR
      # touching only that file got no packaging verification whatsoever. That is not
      # hypothetical — it is how the v0.9.9 bug (`rpmdev-setuptree` assuming %{_topdir},
      # which mock moves) reached main and failed on COPR instead of in CI. Same hole as
      # the Justfile one above, one directory over.
      - '.copr/**'
      - '.github/workflows/packaging.yml'
  workflow_dispatch:

permissions:
  contents: read

jobs:
  nixpkgs:
    runs-on: ubuntu-latest
    if: false  # disabled: nixpkgs PR declined due to lack of popularity
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

      - name: Install Nix
        uses: cachix/install-nix-action@v31
        with:
          nix_path: nixpkgs=channel:nixos-unstable

      - name: Build Nixpkgs Derivation
        run: nix-build packaging/nixpkgs/test.nix

      - name: Build Nix Flake
        run: nix build .#default

  aur:
    runs-on: ubuntu-latest
    container: archlinux:latest
    steps:
      - name: Install system dependencies
        run: |
          # mandown is deliberately NOT installed: the PKGBUILD no longer regenerates the man
          # page (it installs the committed docs/retch.1), so mandown is not in makedepends
          # either. Pre-installing it here would mask a future build() that calls it without
          # declaring it — `makepkg -s` installs what makedepends asks for, and nothing else.
          # `cargo` is still pre-installed, so that one makedepend remains masked.
          pacman -Syu --noconfirm git base-devel cargo curl sudo

      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

      # Runs BEFORE the build step, which rewrites source= and sha256sums= to build from local
      # sources. Without this, the declared checksum is never checked by anything: a stale or
      # wrong sha256 in the reference PKGBUILD would sail through CI and only fail for someone
      # actually installing from the AUR.
      - name: Verify declared source checksum
        shell: bash
        run: |
          set -euo pipefail
          cd packaging/aur
          # A PKGBUILD is a bash script of assignments; sourcing defines the functions without
          # running them.
          # shellcheck disable=SC1091
          source ./PKGBUILD
          url_field="${source[0]#*::}"
          declared="${sha256sums[0]}"
          echo "pkgver:   $pkgver"
          echo "url:      $url_field"
          echo "declared: $declared"

          if [ "$declared" = "SKIP" ]; then
            echo "::error::sha256sums is SKIP in the committed PKGBUILD; it must carry a real checksum"
            exit 1
          fi

          # The reference copy tracks the last RELEASED version, so its tag normally exists. On
          # a branch that has bumped it ahead of the last release it will not yet — that is a
          # legitimate state, not a failure, so skip rather than go red.
          if ! curl -fsSL --retry 3 -o upstream.tar.gz "$url_field"; then
            echo "::notice::tag not published yet ($url_field) — checksum not verifiable at this commit"
            exit 0
          fi

          actual=$(sha256sum upstream.tar.gz | cut -d' ' -f1)
          echo "actual:   $actual"
          if [ "$actual" != "$declared" ]; then
            echo "::error::sha256 mismatch for $url_field — declared $declared, actual $actual"
            exit 1
          fi
          echo "checksum OK"
          rm -f upstream.tar.gz

      - name: Build AUR PKGBUILD
        run: |
          # Create builduser
          useradd -m builduser
          echo "builduser ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
          
          cd packaging/aur
          
          # Package local source to avoid downloading tag that doesn't exist yet
          tar --exclude=packaging/aur/cargo-home --exclude=target --exclude=.git -czf /tmp/retch.tar.gz -C ../.. .
          mv /tmp/retch.tar.gz retch.tar.gz
          
          # Patch PKGBUILD for local build
          sed -i 's|source=.*|source=("retch.tar.gz")|' PKGBUILD
          sed -i 's|sha256sums=.*|sha256sums=("SKIP")|' PKGBUILD
          sed -i 's|"$pkgname-\$pkgver"|"$srcdir"|g' PKGBUILD
          
          chown -R builduser:builduser .
          
          # Run makepkg (unsetting any runner-inherited cargo linker configs)
          sudo -u builduser env -u CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER makepkg -s --noconfirm

      # Regression test for two real defects that shipped in this PKGBUILD for months, both
      # silent — the package built fine and the installed page was wrong:
      #   1. `\$DATE` / `\$pkgver` are LITERAL inside a bash double-quoted string, so the .TH
      #      footer read `$DATE` / `retch $pkgver` instead of the date and version.
      #   2. the `s/\\fB\\fB/\\fB/g` font-strip never matched on any platform (GNU sed reads
      #      `\\f` as a form feed), so it was dead code pretending to clean the output.
      # Both are fixed by installing the committed docs/retch.1 rather than regenerating it.
      # Nothing else inspects the packaged man page, so without this the defects could return.
      - name: Verify packaged man page
        shell: bash
        run: |
          set -euo pipefail
          cd packaging/aur

          # NOTE: every inspection below reads a materialised FILE, never `producer | grep -q`.
          # Under `set -o pipefail` that idiom is a check that fails for the wrong reason:
          # `grep -q` (and `head -1`, and `grep -m1`) exit on the first match, the producer
          # takes SIGPIPE and exits 141, and pipefail turns the whole pipeline nonzero — so the
          # check reports "not found" precisely WHEN IT MATCHES. The first version of this step
          # did exactly that and failed CI while the package was perfectly correct.
          pkg=$(find . -maxdepth 1 -name 'retch-*.pkg.tar.*' ! -name '*-debug-*' -print -quit)
          if [ -z "$pkg" ]; then
            echo "::error::no built package found in packaging/aur"
            ls -1
            exit 1
          fi
          echo "inspecting $pkg"

          bsdtar -tf "$pkg" > pkg-listing.txt

          # makepkg's `zipman` option is ON by default, so the packaged page is
          # `retch.1.gz`, not `retch.1` — match either, and any other compressor a
          # makepkg.conf might select, rather than assuming one name.
          man_entry=$(grep -m1 -E '^usr/share/man/man1/retch\.1(\.gz|\.zst|\.xz|\.bz2)?$' pkg-listing.txt || true)
          if [ -z "$man_entry" ]; then
            echo "::error::package contains no usr/share/man/man1/retch.1"
            echo "--- what it did install under usr/share:"
            grep -E '^usr/share' pkg-listing.txt || echo "(nothing)"
            exit 1
          fi
          echo "man page entry: $man_entry"

          bsdtar -xOf "$pkg" "$man_entry" > packaged-man.raw
          case "$man_entry" in
            *.gz)  gzip -dc  < packaged-man.raw > packaged-retch.1 ;;
            *.zst) zstd -dc  < packaged-man.raw > packaged-retch.1 ;;
            *.xz)  xz -dc    < packaged-man.raw > packaged-retch.1 ;;
            *.bz2) bzip2 -dc < packaged-man.raw > packaged-retch.1 ;;
            *)     cp packaged-man.raw packaged-retch.1 ;;
          esac

          th=$(grep -m1 '^\.TH' packaged-retch.1)
          echo "TH line: $th"

          # Defect 1: an unexpanded shell variable in the footer.
          case "$th" in
            *'$'*)
              echo "::error::.TH line contains a literal \$ — unexpanded variable in the footer"
              exit 1
              ;;
          esac

          # The footer must name the program and a real date. Deliberately NOT asserted against
          # $pkgver: this job builds from the local tree, whose committed page carries the
          # in-development version while pkgver tracks the last release. Coupling them here
          # would fail for a reason that has nothing to do with the packaging.
          case "$th" in
            *'"retch '[0-9]*) : ;;
            *)
              echo "::error::.TH line does not carry a 'retch <version>' footer"
              exit 1
              ;;
          esac

          # Defect 2: doubled font escapes the dead sed claimed to strip.
          if grep -q '\\fB\\fB\|\\fP\\fP' packaged-retch.1; then
            echo "::error::packaged man page contains doubled font escapes (\\fB\\fB / \\fP\\fP)"
            exit 1
          fi

          rm -f pkg-listing.txt packaged-retch.1 packaged-man.raw
          echo "packaged man page OK"

  copr:
    runs-on: ubuntu-latest
    container: fedora:latest
    steps:
      # Installed BEFORE checkout: fedora:latest ships no git, and without it
      # actions/checkout silently falls back to a REST tarball download. Same ordering and
      # the same reason as the aur job above.
      - name: Install system dependencies
        run: dnf -y install git make rpm-build rpmdevtools

      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

      # Offline consistency, the same half `just check` runs. CI invokes cargo directly and
      # never `just` (see NOTES.md), so the checker has to be called explicitly here or the
      # guard only ever runs on the machine of whoever remembered to type `just check`.
      - name: Verify the spec has not drifted
        run: python3 scripts/copr_check.py

      # This is the artifact-level half, and it is deliberately the SRPM step rather than a
      # full `rpmbuild -ba`: COPR itself does the full build on every packaging commit, so
      # duplicating a ~6-minute compile here would buy a second copy of a signal we already
      # get. What COPR does NOT give us is a signal *before* merge, which is the gap this
      # closes.
      - name: Build the SRPM exactly as COPR does
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p /tmp/srpm-plain
          make -f .copr/Makefile srpm outdir=/tmp/srpm-plain
          ls -1 /tmp/srpm-plain

      # THE REGRESSION TEST, and the run with the actual diagnostic value.
      #
      # COPR does not run .copr/Makefile in a plain container: it runs it inside MOCK, which
      # sets HOME=/builddir and redefines rpm's %{_topdir} to /builddir/build. v0.9.8 claimed
      # its Makefile was "verified by running exactly what COPR runs" — true of the COMMAND,
      # false of the ENVIRONMENT, and the environment was the half that mattered. The build
      # died on COPR in 60 seconds with
      #   cp: cannot create regular file '/builddir/rpmbuild/SPECS/': No such file or directory
      #
      # BOTH conditions are required to reproduce it. Setting %{_topdir} alone does NOT fail
      # on the pre-fix Makefile (rpmdev-setuptree simply creates the tree wherever _topdir
      # points); it is the combination with HOME=/builddir that separates the two paths. This
      # step was written by reconstructing the pre-fix Makefile from a606bbe and confirming it
      # FAILS here with that exact message, then confirming the current one passes — a check
      # nobody has watched fail is not yet a check.
      #
      # Both runs are kept, and neither is redundant: the plain run catches a Makefile that
      # hardcodes /builddir/build (which would work on COPR and break every dev box), and the
      # mock run catches one that assumes the default. NOTES.md's v0.9.9 entry names exactly
      # that pair as the reason not to hardcode.
      - name: Build the SRPM under mock's environment
        shell: bash
        run: |
          set -euo pipefail
          rm -rf .copr-sources
          mkdir -p /builddir /tmp/srpm-mock
          echo "%_topdir /builddir/build" > /builddir/.rpmmacros
          export HOME=/builddir
          echo "HOME=$HOME  _topdir=$(rpmbuild --eval '%{_topdir}')"
          make -f .copr/Makefile srpm outdir=/tmp/srpm-mock
          ls -1 /tmp/srpm-mock

      # The SRPM's name encodes the version rpm actually resolved, so this closes the loop
      # between the text copr_check.py reads and the artifact the build produced.
      - name: Verify the SRPM matches the version the spec pins
        shell: bash
        run: |
          set -euo pipefail
          version=$(sed -n 's/^Version:[[:space:]]*//p' packaging/copr/retch.spec | head -1)
          # find -print -quit, never `ls | head`: under pipefail a producer killed by SIGPIPE
          # makes the pipeline fail, so that idiom reports "not found" precisely when it
          # matched. The aur job's man-page step went red on a correct package for exactly
          # this reason; see NOTES.md.
          srpm=$(find /tmp/srpm-mock -maxdepth 1 -name '*.src.rpm' -print -quit)
          if [ -z "$srpm" ]; then
            echo "::error::no .src.rpm was produced"
            ls -la /tmp/srpm-mock || true
            exit 1
          fi
          echo "spec Version: $version"
          echo "built:        $(basename "$srpm")"
          case "$(basename "$srpm")" in
            retch-"$version"-*.src.rpm) ;;
            *)
              echo "::error::SRPM name does not carry the spec's Version ($version)"
              exit 1
              ;;
          esac
          echo "SRPM matches the pinned version"