feather-reader 0.2.2

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 on GitHub-hosted `ubuntu-latest` — free
# and unlimited for this public repo, with the 5 jobs running in parallel. (They
# used to be pinned to the single self-hosted ci-VM runner to save private-repo
# minutes; that rationale went away when the repo went public.)
# 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: a new push to a PR cancels that PR's own
  # in-progress run rather than letting a superseded 5-job build finish. Keyed on
  # the PR number (falling back to the ref for push/dispatch). On GitHub-hosted
  # runners the jobs also run in parallel — so this is purely about not wasting a
  # run 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): same-repo PRs + push / dispatch only;
    # fork PRs get NO CI. Originally to keep untrusted fork code (a slipped-in
    # build.rs / proc-macro / npm lifecycle script) off the self-hosted homelab
    # runner. Now that CI is GitHub-hosted (sandboxed; secrets aren't exposed to
    # fork-PR workflows) that RCE risk is gone — so this is now a POLICY choice:
    # drop this `if:` to give contributor PRs CI.
    if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
    runs-on: ubuntu-latest  # GitHub-hosted: free + parallel now the repo is public
    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: ubuntu-latest  # GitHub-hosted: free + parallel now the repo is public
    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 broke the gate on the old self-hosted runner 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: ubuntu-latest  # GitHub-hosted: free + parallel now the repo is public
    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: ubuntu-latest  # GitHub-hosted: free + parallel now the repo is public
    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: ubuntu-latest  # GitHub-hosted: free + parallel now the repo is public
    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