name: ci
# CI for souveraine — Rust crate. Tests + clippy, then the aarch64 artifact,
# all on `primary`.
#
# There used to be a second branch, `public`, which was the only ref that
# published, so a green run on `primary` meant the gates passed and NOTHING
# SHIPPED — indistinguishable from a run that did. It existed to mirror to
# Codeberg; the mirror never worked and nothing else consumed it. Unified
# 2026-07-31 after it stranded three commits, including TASK-48's fix. One
# branch, one meaning: green on primary means it built and it shipped.
on:
  push:
    branches: [primary, public]
  pull_request:
    branches: [primary]

jobs:
  rust-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      - name: Install system deps
        # Retried because the runner's ephemeral job containers lose DNS for
        # minutes at a time (four builds lost on 2026-08-13). A transient
        # resolver blip must not decide whether our build passes, for the same
        # reason the tuie pin exists below.
        #
        # The retry wraps *both* commands: `apt-get update` exits 0 when every
        # index fails to download — it warns "old ones used instead" — so only
        # the install reports the failure.
        run: |
          for attempt in 1 2 3; do
            if sudo apt-get update -qq \
               && sudo apt-get install -y -qq libasound2-dev libchafa-dev libwayland-dev; then
              exit 0
            fi
            echo "system deps attempt $attempt failed; retrying in $((attempt * 15))s"
            sleep $((attempt * 15))
          done
          echo "system deps unavailable after 3 attempts — check runner DNS"
          exit 1
      # Pinned fork on our Gitea (rgb patch committed there) — upstream HEAD
      # must never decide whether our build passes.
      - name: Clone tuie (pinned fork)
        run: |
          git -c http.extraheader="Authorization: token ${{ secrets.LOCAL_GITEA_TOKEN }}" \
            clone "${GITHUB_SERVER_URL}/Fimeg/tuie.git" /workspace/Fimeg/tuie
          git -C /workspace/Fimeg/tuie checkout aa43e335c8f5a3cd2c78e397a1a8dfe534a6bc35
      # Pinned to a MASTER commit, deliberately, not to a `stable`-branch tip.
      # The old pin (29eef336, "toolchain: stable") was the tip of that repo's
      # `stable` branch when it was taken. dtolnay force-moves `stable` on every
      # Rust release, which orphaned the commit: it still exists as an object on
      # GitHub, but is reachable from no ref, so act's clone resolves it as
      # "reference not found" and rust-test dies before it compiles anything.
      # That failure skipped aarch64-artifact and silently stopped publishing to
      # `edge` — the phone sat on an old build with nothing saying why.
      # master is append-only, so a commit on it stays reachable. The toolchain
      # is chosen by the `toolchain:` input below, never by the branch.
      - uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c
        with:
          toolchain: "1.94"
          components: clippy
      - name: cargo test
        run: cargo test
      # `souveraine-sessiond` is `required-features = ["sessiond"]`, which is
      # not in `default` — so the line above never compiled the device state
      # authority, let alone ran its tests. Every sessiond change since the
      # daemon existed has been type-checked only by the aarch64 cross-build,
      # which builds and does not test. A green rust-test meant nothing for the
      # one binary that decides whether the phone is locked. Same trap as the
      # old primary/public split: a signal that reads like coverage and isn't.
      - name: cargo test (sessiond)
        run: cargo test --features sessiond --bin souveraine-sessiond
      - name: cargo test (USB KVM receiver)
        run: cargo test --features usb-kvm --bin souveraine-usb-kvm
      - name: cargo clippy
        run: cargo clippy -- -D warnings
      # Advisory, deliberately. Turning this on for the first time surfaced 9
      # pre-existing warnings in code clippy had never seen (dead
      # `update_sensors`, unused re-exports in device_state, a nul-terminated
      # string built by hand). None are from the change that added this step,
      # and blocking the phone's only shipping path on them would be the wrong
      # trade. Read it, burn the list down, then make it -D warnings and hard.
      - name: cargo clippy (sessiond)
        continue-on-error: true
        run: cargo clippy --features sessiond --bin souveraine-sessiond -- -D warnings
      - name: cargo clippy (USB KVM receiver)
        run: cargo clippy --features usb-kvm --bin souveraine-usb-kvm -- -D warnings

  no-ai-attribution:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          fetch-depth: 0
      - name: Check commit messages for AI attribution
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            RANGE="${{ github.event.pull_request.base.sha }}..${{ github.sha }}"
          else
            RANGE="${{ github.event.before }}..${{ github.sha }}"
            if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
              RANGE="HEAD~10..HEAD"
            fi
          fi
          # CO assembled from fragments so this guard file does not itself trip
          # the public-branch pre-commit scanner (which bans the literal token).
          CO="Co-Authored""-By:"
          PATTERNS="${CO}.*[Cc]laude|${CO}.*OpenAI|${CO}.*ChatGPT|${CO}.*Copilot|${CO}.*Letta|${CO}.*Cursor|Generated by|Generated with|AI-assisted|Auto-generated by"
          FAIL=0
          while IFS= read -r msg; do
            if echo "$msg" | grep -qiE "$PATTERNS"; then
              echo "::error::AI attribution found in commit: $msg"
              FAIL=1
            fi
          done < <(git log --format='%s%n%b' $RANGE 2>/dev/null)
          if [ "$FAIL" -eq 1 ]; then
            echo "::error::Commits contain AI attribution lines. Remove them before merging."
            exit 1
          fi
          echo "No AI attribution found in commits."

  action-pins:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      - name: Check for floating action refs
        run: |
          if grep -rE 'uses:.*@(v[0-9]+|stable|main|master)(\s|$)' .gitea/workflows/; then
            echo "::error::Floating action refs found — pin every action to a commit SHA"
            exit 1
          fi
          echo "All action refs are SHA-pinned."

  # The projection. Gitea is the only thing that publishes.
  sync-public:
    runs-on: ubuntu-latest
    needs: [rust-test, no-ai-attribution, action-pins]
    if: github.ref == 'refs/heads/public'
    env:
      PUBLIC_FORGE_TOKEN: ${{ secrets.PUBLIC_FORGE_TOKEN }}
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          fetch-depth: 0
      - name: Publish and verify exact SHA
        run: |
          set -euo pipefail
          : "${PUBLIC_FORGE_TOKEN:?PUBLIC_FORGE_TOKEN is required}"
          expected="${GITHUB_SHA}"
          forge_url='https://forge.caseytunturi.com/Fimeg/souveraine.git'
          remote_sha="$(git ls-remote "$forge_url" refs/heads/public | awk '{print $1}')"
          if [ -n "$remote_sha" ]; then
            git cat-file -e "${remote_sha}^{commit}" 2>/dev/null || {
              echo "[publish] remote public SHA is not in tested history" >&2; exit 1; }
            git merge-base --is-ancestor "$remote_sha" "$expected" || {
              echo "[publish] refusing non-fast-forward public history" >&2; exit 1; }
          fi
          auth="$(printf 'publisher-souveraine:%s' "$PUBLIC_FORGE_TOKEN" | base64 -w0)"
          git -c "http.https://forge.caseytunturi.com/.extraheader=Authorization: Basic $auth" \
            push "$forge_url" public:public
          test "$(git ls-remote "$forge_url" refs/heads/public | awk '{print $1}')" = "$expected"
          echo "[publish] exact SHA is anonymous on Forgejo: $expected"
