feather-reader 0.2.1

A minimalist, atproto-native RSS/Atom reader in Rust — your feed subscriptions live in your own PDS.
Documentation
name: CI

# Build / test / lint / audit gate. Runs entirely on a SELF-HOSTED runner
# (labelled `self-hosted, linux, x64`) to spend zero GitHub-hosted Actions minutes.
# CodeQL, dependency-review, and OSSF Scorecard live in their own workflows
# (they are designed for GitHub-hosted runners — see those files + the README).

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:  # allow manual runs from the Actions tab / gh CLI

concurrency:
  # One in-flight CI run per ref. With a SINGLE self-hosted runner and 13+ open
  # Dependabot PRs, every superseded push would otherwise queue a full 5-job run
  # behind the others. Keying on the PR number (falling back to the ref for
  # push/dispatch) and cancelling in-progress runs means a new push to a PR
  # kills its own stale run instead of piling up. Distinct PRs still run
  # serially on the one runner, but no PR wastes the queue on outdated commits.
  group: ci-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always

# Least privilege: every job only checks out the tree and hits package registries
# — none push, tag, comment, or upload — so trim the default GITHUB_TOKEN to read.
permissions:
  contents: read

jobs:
  rust:
    name: Rust
    # FORK-PR GUARD (applied to every job): never run untrusted fork code on the
    # self-hosted, tailnet-connected runner — a slipped-in build.rs / proc-macro /
    # npm lifecycle script would be RCE on the homelab. Same-repo PRs + push /
    # dispatch only. Fork PRs get NO CI here; add a hosted ubuntu-latest lane if
    # they should. Harmless while private (all PRs are same-repo); closes the hole
    # before the public flip.
    if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
    runs-on: [self-hosted, linux, x64]  # self-hosted CI VM — GitHub free-tier minutes are precious
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

      - name: Install stable toolchain (clippy + rustfmt)
        uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
        with:
          toolchain: stable  # required — SHA-pinned, so the @ref no longer selects the toolchain
          components: clippy, rustfmt

      - name: Cache cargo
        uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

      - name: Build
        run: cargo build --all-targets --locked

      - name: Test
        run: cargo test --locked

      - name: Clippy
        run: cargo clippy --all-targets -- -D warnings

      - name: Rustfmt
        run: cargo fmt --all --check

  cargo-deny:
    name: cargo deny (licenses + bans + sources)
    if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}  # fork-PR guard (see rust job)
    runs-on: [self-hosted, linux, x64]  # self-hosted CI VM
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

      # cargo-deny shells out to `cargo metadata` to build the crate graph, so
      # this job needs a Rust toolchain (cargo on PATH) even though it only runs
      # the license/ban/source gates.
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
        with:
          toolchain: stable  # required — SHA-pinned ref no longer selects the toolchain

      # Install a pinned cargo-deny binary directly (rather than
      # EmbarkStudios/cargo-deny-action@v2, whose bundled RustSec advisory-DB
      # fetch failed on this self-hosted runner and broke the gate every run).
      # Config is deny.toml at the repo root; feature evaluation is set there
      # (`[graph] all-features = true`).
      - name: Install cargo-deny
        uses: taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
        with:
          tool: cargo-deny@0.20.2

      # We deliberately DO NOT run the `advisories` check here: it needs the
      # RustSec advisory DB, which the dedicated `cargo audit` job below already
      # covers (same DB, same fail-on-vuln semantics). This job enforces the
      # AGPL-compatible license allowlist, duplicate/ban hygiene, and the
      # crates.io-only source policy. (Not `--offline`: `cargo metadata` needs
      # the registry index to resolve the dependency graph.)
      - name: cargo-deny (licenses + bans + sources)
        run: cargo-deny check bans licenses sources

  cargo-audit:
    name: cargo audit (RustSec)
    if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}  # fork-PR guard (see rust job)
    runs-on: [self-hosted, linux, x64]  # self-hosted CI VM
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

      - name: Install stable toolchain
        uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
        with:
          toolchain: stable  # required — SHA-pinned ref no longer selects the toolchain

      - name: Cache cargo
        uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2

      # Dedicated RustSec scan of Cargo.lock. Fails the build on any known
      # vulnerability. Un-actionable transitive advisories (no fixed release, or
      # a fix that needs a semver-major bump) are whitelisted in deny.toml's
      # `[advisories] ignore = [...]`; mirror any such id here as an explicit
      # `--ignore RUSTSEC-YYYY-NNNN` flag. Keep the list SHORT and documented.
      - name: Install cargo-audit
        run: cargo install cargo-audit --locked || true

      - name: cargo audit
        # `-D warnings` promotes unmaintained/unsound advisories to failures too.
        run: cargo audit -D warnings

  sidecar:
    name: OAuth sidecar
    if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}  # fork-PR guard (see rust job)
    runs-on: [self-hosted, linux, x64]  # self-hosted CI VM — GitHub free-tier minutes are precious
    defaults:
      run:
        working-directory: oauth-sidecar
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

      - name: Setup Node
        uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
        with:
          node-version: 24
          cache: npm
          cache-dependency-path: oauth-sidecar/package-lock.json

      - name: Install
        run: npm ci

      - name: Build
        run: npm run build

      - name: Typecheck
        run: npm run typecheck

      - name: Lint (ESLint, --max-warnings 0)
        run: npm run lint

      - name: Format check (Prettier)
        # Scoped to the tooling/config files the CI pipeline owns. The hand-
        # authored src/ + test/ predate Prettier and are enforced for
        # CORRECTNESS by ESLint above; a repo-wide `prettier --write` is left as
        # a deliberate one-time follow-up (`npm run format:write`) so this gate
        # never fights the code-fix workflows over whitespace. See
        # .github/workflows/README.md.
        run: npm run format:check

      - name: npm audit (prod deps, high+)
        # Sidecar runtime deps only (--omit=dev); dev tooling churn shouldn't
        # break CI. Fails on high/critical advisories.
        run: npm audit --omit=dev --audit-level=high

  secrets:
    name: Secret scan (gitleaks)
    if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}  # fork-PR guard (see rust job)
    runs-on: [self-hosted, linux, x64]  # self-hosted CI VM — scans fine here now
    env:
      GITLEAKS_VERSION: 8.28.0
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
        with:
          fetch-depth: 0  # full history so gitleaks can scan past commits too

      # Run the gitleaks BINARY directly rather than gitleaks/gitleaks-action@v2.
      # The action gates on a GITLEAKS_LICENSE (required for any repo owned by a
      # GitHub *org*/account it deems non-free) and does GITHUB_TOKEN-scoped PR
      # bookkeeping — both of which trip on a personal-account private repo and
      # failed the job regardless of findings. The binary is licence-free for
      # this use and has none of that PR/org coupling.
      - name: Install gitleaks
        run: |
          set -euo pipefail
          url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
          curl -sSfL "$url" -o /tmp/gitleaks.tar.gz
          tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
          install -m 0755 /tmp/gitleaks "$RUNNER_TEMP/gitleaks"
          "$RUNNER_TEMP/gitleaks" version

      # Scan the full git history (checkout above sets fetch-depth: 0). Config is
      # .gitleaks.toml at the repo root (extends the default ruleset + allowlists
      # the documented test fixtures/placeholders). --redact keeps any match out
      # of the logs; a non-zero exit (leak found) fails the job.
      - name: gitleaks (history + tree)
        run: |
          "$RUNNER_TEMP/gitleaks" git --config .gitleaks.toml --redact --exit-code 1