# Gitea Actions CI. # # Gitea Actions is workflow-compatible with GitHub Actions, so this runs on either # with no changes. It needs a registered runner with the `ubuntu-latest` label. # # The gates, in the order they fail fastest: # fmt — formatting, seconds # clippy — lints, denied rather than warned # test — 160 unit + integration tests # deny — RustSec advisories, licence policy, source policy # musl — the artifact §8 actually ships: one static binary name: CI on: push: branches: [main, master] pull_request: # Advisories appear without any code changing, so the dependency audit also # runs on a schedule rather than only on push. schedule: - cron: "0 6 * * 1" env: CARGO_TERM_COLOR: always # Fail the build on warnings. The tree is warning-clean, so keeping it that way # is cheaper than letting warnings accumulate. RUSTFLAGS: "-D warnings" jobs: check: name: fmt, clippy, test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust run: | # rustup is not guaranteed present on a self-hosted Gitea runner. if ! command -v rustup >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --profile minimal --component rustfmt,clippy echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" else rustup component add rustfmt clippy fi - name: Cache cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo- - name: Formatting run: cargo fmt --all -- --check - name: Clippy run: cargo clippy --all-targets --all-features - name: Tests run: cargo test --all-features deny: name: advisories and licences runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust run: | if ! command -v rustup >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --profile minimal echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" fi - name: Cache cargo-deny uses: actions/cache@v4 with: path: ~/.cargo/bin/cargo-deny key: ${{ runner.os }}-cargo-deny - name: Install cargo-deny run: | command -v cargo-deny >/dev/null 2>&1 || cargo install cargo-deny --locked # Advisories, licences, bans and sources — see deny.toml for why the licence # allow-list is closed rather than a deny-list. - name: cargo deny run: cargo deny check musl: name: static musl binary runs-on: ubuntu-latest # Only gate merges on the artifact build once the cheaper checks have passed. needs: check steps: - uses: actions/checkout@v4 - name: Install Rust and musl target run: | if ! command -v rustup >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ | sh -s -- -y --profile minimal echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" export PATH="$HOME/.cargo/bin:$PATH" fi rustup target add x86_64-unknown-linux-musl sudo apt-get update && sudo apt-get install -y musl-tools - name: Cache cargo uses: actions/cache@v4 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-musl-${{ hashFiles('Cargo.lock') }} restore-keys: ${{ runner.os }}-musl- # §8: "Ship a single static binary (musl target) plus the SQLite file." # rusqlite is built with `bundled`, so SQLite is compiled in; reqwest uses # rustls rather than OpenSSL, so there is no system TLS dependency to link. - name: Build run: cargo build --release --target x86_64-unknown-linux-musl - name: Verify the binary is actually static run: | BIN=target/x86_64-unknown-linux-musl/release/jray-server file "$BIN" # A dynamically-linked result would defeat §8's deployment story, so this # is asserted rather than assumed. # # Checked with `file`, not `ldd`: the musl target produces a static-PIE, # and `ldd` prints the musl loader for one — an `ldd`-based check reports # a perfectly static binary as dynamic. if ! file "$BIN" | grep -qE 'static-pie linked|statically linked'; then echo "::error::binary is not statically linked" >&2 exit 1 fi - name: Upload binary uses: actions/upload-artifact@v3 with: name: jray-server-x86_64-musl path: target/x86_64-unknown-linux-musl/release/jray-server if-no-files-found: error # ── Debian package ──────────────────────────────────────────────────────── # # DR-015. The .deb is not a second build of the software: it packages the very # binary the job above already proved static, so the artifact an operator # installs is byte-identical to the one CI verified. Building it twice would # let the two diverge silently. deb: name: debian package runs-on: ubuntu-latest needs: musl steps: - uses: actions/checkout@v4 - name: Fetch the verified musl binary uses: actions/download-artifact@v3 with: name: jray-server-x86_64-musl path: prebuilt - name: Build the package run: | chmod +x prebuilt/jray-server scripts/build-deb.sh --binary prebuilt/jray-server # The install is where packaging actually fails, so it is exercised rather # than assumed: an unattended preseed proves the debconf path works without # a terminal, which is the case a release must not break. `dpkg -i` runs as # root on the runner, and the maintainer scripts skip the systemd wiring # when /run/systemd/system is absent. - name: Install it unattended and check what it configured run: | set -e sudo apt-get update -qq sudo apt-get install -y -qq debconf-utils cat <<'SEED' | sudo debconf-set-selections jray-server jray-server/server-id string ci.example.org jray-server jray-server/bind string 127.0.0.1:8080 jray-server jray-server/trusted-proxies string 127.0.0.1 jray-server jray-server/tmdb-api-key password ci-key SEED sudo DEBIAN_FRONTEND=noninteractive dpkg -i dist/*.deb sudo test -f /etc/jray-server/env [ "$(sudo stat -c '%a' /etc/jray-server/env)" = "600" ] \ || { echo "::error::env file is not mode 600"; exit 1; } sudo grep -q '^JRAY_SERVER_ID=ci.example.org$' /etc/jray-server/env \ || { echo "::error::debconf answer did not reach the env file"; exit 1; } # The key must land in the file and not linger in debconf's database. sudo grep -q '^JRAY_TMDB_API_KEY=ci-key$' /etc/jray-server/env \ || { echo "::error::API key missing from the env file"; exit 1; } if sudo debconf-show jray-server | grep -q 'ci-key'; then echo "::error::API key still present in the debconf database"; exit 1 fi sudo /usr/bin/jray-server --version >/dev/null 2>&1 || true echo "installed cleanly" # DR-016. Reconfigure is the operation that silently destroys a working # install: press Enter through the password prompt and a naive postinst # blanks the key, after which every upload stays pending forever and the # server looks fine. Asserted, not trusted. # # The second half guards the inverse mistake — the debconf `config` script # seeds unanswered questions from the env file, and an unguarded seed would # overwrite the preseed above, so an unattended install would reconfigure # itself back to whatever was on disk. - name: Reconfigure keeps the key, and updates what it was told to run: | set -e printf 'jray-server jray-server/tmdb-api-key password\n' | sudo debconf-set-selections printf 'jray-server jray-server/contact string changed@example.org\n' | sudo debconf-set-selections echo 'JRAY_JOB_BATCH=32' | sudo tee -a /etc/jray-server/env >/dev/null sudo dpkg-reconfigure -f noninteractive jray-server sudo grep -q '^JRAY_TMDB_API_KEY=ci-key$' /etc/jray-server/env \\ || { echo "::error::a blank answer wiped the configured API key"; exit 1; } sudo grep -q '^JRAY_CONTACT=changed@example.org$' /etc/jray-server/env \\ || { echo "::error::preseeded value was overwritten by the env-file seed"; exit 1; } # A setting the package does not manage must survive untouched. sudo grep -q '^JRAY_JOB_BATCH=32$' /etc/jray-server/env \\ || { echo "::error::reconfigure discarded a hand-added setting"; exit 1; } - name: Purge, and check the database is not collateral run: | set -e sudo mkdir -p /var/lib/jray-server && sudo touch /var/lib/jray-server/jray.db sudo DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq jray-server sudo test ! -f /etc/jray-server/env \ || { echo "::error::configuration survived purge"; exit 1; } # Deliberate deviation from "purge removes everything": manifests are # real CV compute and federation is not a backup. See postrm. sudo test -f /var/lib/jray-server/jray.db \ || { echo "::error::purge destroyed the database"; exit 1; } - name: Upload the package uses: actions/upload-artifact@v3 with: name: jray-server-deb path: dist/*.deb if-no-files-found: error # ── Publishing, on tags only ────────────────────────────────────────── # # Two channels, deliberately. The apt registry is the one that gives # operators upgrades; the release asset is for people who would rather not # add a third-party apt source to their machine. # # NOTE: this uses the automatic Actions token. If your Gitea build does not # grant it package:write, replace it with a PAT held in a repository secret # — the symptom is a 401 from the upload below, not a silent no-op. - name: Publish to the Gitea Debian registry if: startsWith(github.ref, 'refs/tags/v') env: TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -e DEB=$(ls dist/*.deb) code=$(curl -sS -o /tmp/up.log -w '%{http_code}' \ --user "${{ github.repository_owner }}:$TOKEN" \ --upload-file "$DEB" \ "${{ github.server_url }}/api/packages/${{ github.repository_owner }}/debian/pool/stable/main/upload") echo "upload HTTP $code"; cat /tmp/up.log case "$code" in 201|409) ;; *) echo "::error::registry upload failed"; exit 1 ;; esac - name: Attach the package to the release if: startsWith(github.ref, 'refs/tags/v') env: TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -e TAG="${GITHUB_REF#refs/tags/}" API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}" id=$(curl -sS -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" \ | sed -n 's/.*"id":[ ]*\([0-9]*\).*/\1/p' | head -1) if [ -z "$id" ]; then id=$(curl -sS -X POST -H "Authorization: token $TOKEN" \ -H 'Content-Type: application/json' \ -d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\"}" "$API/releases" \ | sed -n 's/.*"id":[ ]*\([0-9]*\).*/\1/p' | head -1) fi [ -n "$id" ] || { echo "::error::could not resolve a release for $TAG"; exit 1; } DEB=$(ls dist/*.deb) curl -sS -X POST -H "Authorization: token $TOKEN" \ -F "attachment=@$DEB" \ "$API/releases/$id/assets?name=$(basename "$DEB")" >/dev/null echo "attached $(basename "$DEB") to release $TAG"