fnox 1.25.1

A flexible secret management tool supporting multiple providers and encryption methods
Documentation
name: ci

on:
  workflow_dispatch:
  pull_request:
  push:
    tags: ["*"]
    branches: ["main"]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

permissions: {}

jobs:
  build:
    permissions:
      contents: read
    strategy:
      fail-fast: false
      matrix:
        os:
          - macos-latest
          - ubuntu-latest
    runs-on: ${{ matrix.os }}
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          submodules: recursive
          persist-credentials: false
      - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
        with:
          cache: false
      # save-if: false — this job never saves to cache, only restores from main's cache.
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 # zizmor: ignore[cache-poisoning]
        with:
          shared-key: debug-${{ matrix.os }}
          save-if: false
      - name: Install system dependencies (Ubuntu)
        if: matrix.os == 'ubuntu-latest'
        run: sudo apt-get update && sudo apt-get install -y libudev-dev
      - name: Install Rust components
        run: rustup component add rustfmt clippy
      - name: Build
        run: cargo build
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: fnox-${{ matrix.os }}
          path: target/debug/fnox

  ci-bats:
    needs: build
    permissions:
      contents: read
    strategy:
      fail-fast: false
      matrix:
        os:
          - macos-latest
          - ubuntu-latest
        tranche: [0, 1]
    runs-on: ${{ matrix.os }}
    timeout-minutes: 20
    steps:
      - run: brew install parallel vault
        if: ${{ matrix.os == 'macos-latest' }}
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          submodules: recursive
          persist-credentials: false
      - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
        with:
          cache: false
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          name: fnox-${{ matrix.os }}
          path: target/debug
      - run: chmod +x target/debug/fnox
      - name: Setup age key for fnox
        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
        run: mkdir -p ~/.config/fnox && echo "${{ secrets.AGE_SECRET }}" > ~/.config/fnox/age.txt
      - name: Setup services (Ubuntu)
        if: ${{ matrix.os == 'ubuntu-latest' }}
        run: |
          # Install gnome-keyring for keychain tests
          sudo apt-get update
          sudo apt-get install -y gnome-keyring libsecret-tools
          # Start D-Bus session daemon
          mkdir -p ~/.dbus-session
          dbus-daemon --session --fork --print-address=1 > ~/.dbus-session/bus-address
          DBUS_SESSION_BUS_ADDRESS=$(cat ~/.dbus-session/bus-address)
          export DBUS_SESSION_BUS_ADDRESS
          echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" >> "$GITHUB_ENV"
          # Start gnome-keyring daemon for keychain tests
          echo "foobar" | gnome-keyring-daemon --unlock --components=secrets --daemonize
          # Start Vaultwarden with HTTPS (self-signed certificate)
          # Generate self-signed certificate
          mkdir -p /tmp/vaultwarden-certs
          openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
            -keyout /tmp/vaultwarden-certs/key.pem \
            -out /tmp/vaultwarden-certs/cert.pem \
            -subj "/CN=localhost" \
            -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2>/dev/null || true
          # Start Vaultwarden with HTTPS enabled
          docker run -d --name vaultwarden \
            -p 8080:80 \
            -e DOMAIN=https://localhost:8080 \
            -e SIGNUPS_ALLOWED=true \
            -e DISABLE_ADMIN_TOKEN=true \
            -e I_REALLY_WANT_VOLATILE_STORAGE=true \
            -e ROCKET_TLS='{certs="/data/certs/cert.pem",key="/data/certs/key.pem"}' \
            -v /tmp/vaultwarden-certs:/data/certs:ro \
            vaultwarden/server:latest
          # Start Vault
          docker run -d --name vault --cap-add=IPC_LOCK \
            -p 8200:8200 \
            -e VAULT_DEV_ROOT_TOKEN_ID=fnox-test-token \
            -e VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 \
            -e VAULT_ADDR=http://127.0.0.1:8200 \
            hashicorp/vault:latest
          # Start Infisical (self-hosted with PostgreSQL and Redis)
          docker compose -f test/docker-compose.infisical-ci.yml up -d
          # Start LocalStack for AWS tests (STS leases, KMS, Secrets Manager, Parameter Store)
          docker run -d --name localstack \
            -p 4566:4566 \
            -e SERVICES=sts,iam,kms,secretsmanager,ssm \
            localstack/localstack:4
          # Wait for services to be ready
          # Poll LocalStack health endpoint (can take 10-30s on cold runners)
          for _i in $(seq 1 30); do
            curl -sf http://localhost:4566/_localstack/health && break
            sleep 2
          done
          # Wait for Vault to be ready
          for _i in $(seq 1 15); do
            curl -sf http://localhost:8200/v1/sys/health && break
            sleep 1
          done
          # Setup LocalStack resources for AWS provider tests
          export AWS_ACCESS_KEY_ID=test
          export AWS_SECRET_ACCESS_KEY=test
          export AWS_DEFAULT_REGION=us-east-1
          # Create KMS key for tests
          LOCALSTACK_KMS_KEY_ID=$(aws --endpoint-url http://localhost:4566 kms create-key \
            --region us-east-1 --query 'KeyMetadata.KeyId' --output text)
          aws --endpoint-url http://localhost:4566 kms create-alias \
            --alias-name alias/fnox-testing \
            --target-key-id "$LOCALSTACK_KMS_KEY_ID" \
            --region us-east-1
          # Create shared test secret in Secrets Manager
          aws --endpoint-url http://localhost:4566 secretsmanager create-secret \
            --name "fnox/test-secret" \
            --secret-string "This is a test secret in AWS Secrets Manager!" \
            --region us-east-1
          {
            echo "VAULT_ADDR=http://localhost:8200"
            echo "VAULT_TOKEN=fnox-test-token"
            echo "LOCALSTACK_ENDPOINT=http://localhost:4566"
          } >> "$GITHUB_ENV"
      - name: Setup Bitwarden for tests
        if: ${{ matrix.os == 'ubuntu-latest' }}
        run: |
          source ./test/setup-bitwarden-ci.sh
          echo "BW_SESSION=$BW_SESSION" >> "$GITHUB_ENV"
      - name: Install Infisical CLI
        if: ${{ matrix.os == 'ubuntu-latest' }}
        run: |
          curl -1sLf 'https://artifacts-cli.infisical.com/setup.deb.sh' | sudo -E bash
          sudo apt-get update && sudo apt-get install -y infisical
      - name: Setup Infisical for tests
        if: ${{ matrix.os == 'ubuntu-latest' }}
        run: |
          source ./test/setup-infisical-ci.sh
          {
            echo "INFISICAL_TOKEN=$INFISICAL_TOKEN"
            echo "INFISICAL_CLIENT_ID=$INFISICAL_CLIENT_ID"
            echo "INFISICAL_CLIENT_SECRET=$INFISICAL_CLIENT_SECRET"
            echo "INFISICAL_PROJECT_ID=$INFISICAL_PROJECT_ID"
            echo "INFISICAL_API_URL=http://localhost:8081/api"
          } >> "$GITHUB_ENV"
      - name: Setup services (macOS)
        if: ${{ matrix.os == 'macos-latest' }}
        run: |
          # Start Vault in dev mode in background
          vault server -dev -dev-root-token-id=fnox-test-token -dev-listen-address=127.0.0.1:8200 &
          {
            echo "VAULT_ADDR=http://127.0.0.1:8200"
            echo "VAULT_TOKEN=fnox-test-token"
          } >> "$GITHUB_ENV"
          # Wait for Vault to be ready
          sleep 2
      - name: Run bats tests - tranche ${{ matrix.tranche }}
        uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4
        env:
          TRANCHE: ${{ matrix.tranche }}
          TRANCHE_COUNT: 2
          KEEPASS_PASSWORD: fnox-test-password
          BATS_FILTER_TAGS: ${{ (github.event_name != 'pull_request' || !startsWith(github.head_ref, 'release-plz')) && '!expensive' || '' }}
        with:
          timeout_minutes: 10
          max_attempts: 3
          command: mise run test:bats

  ci-other:
    needs: build
    permissions:
      contents: read
    strategy:
      fail-fast: false
      matrix:
        os:
          - macos-latest
          - ubuntu-latest
    runs-on: ${{ matrix.os }}
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          submodules: recursive
          persist-credentials: false
      - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
        with:
          cache: false
      # save-if: only saves on main pushes, so PRs cannot poison the cache.
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 # zizmor: ignore[cache-poisoning]
        with:
          shared-key: debug-${{ matrix.os }}
          save-if: ${{ github.ref == 'refs/heads/main' }}
      - name: Install system dependencies (Ubuntu)
        if: ${{ matrix.os == 'ubuntu-latest' }}
        run: |
          sudo apt-get update
          sudo apt-get install -y gnome-keyring libsecret-tools libudev-dev
      - name: Setup gnome-keyring (Ubuntu)
        if: ${{ matrix.os == 'ubuntu-latest' }}
        run: |
          # Start D-Bus session daemon
          mkdir -p ~/.dbus-session
          dbus-daemon --session --fork --print-address=1 > ~/.dbus-session/bus-address
          DBUS_SESSION_BUS_ADDRESS=$(cat ~/.dbus-session/bus-address)
          export DBUS_SESSION_BUS_ADDRESS
          echo "DBUS_SESSION_BUS_ADDRESS=$DBUS_SESSION_BUS_ADDRESS" >> "$GITHUB_ENV"
          # Start gnome-keyring daemon for keychain tests
          echo "foobar" | gnome-keyring-daemon --unlock --components=secrets --daemonize
      - name: Install Rust components
        run: rustup component add rustfmt clippy
      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          name: fnox-${{ matrix.os }}
          path: target/debug
      - run: chmod +x target/debug/fnox
      - name: Setup age key for fnox
        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
        run: mkdir -p ~/.config/fnox && echo "${{ secrets.AGE_SECRET }}" > ~/.config/fnox/age.txt
      - name: Redact secrets in CI output
        if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
        run: |
          fnox ci-redact
          # shellcheck disable=SC2016
          fnox run -- sh -c 'echo MY_UNIMPORTANT_SECRET: $MY_UNIMPORTANT_SECRET'
      - name: Run tests
        run: cargo test --workspace
      - name: mise run render
        run: mise run render
      - name: assert render produces no diff
        run: |
          if [ -n "$(git status --porcelain)" ]; then
            echo "::error::'mise run render' produced changes. Run it locally and commit."
            git status
            git diff HEAD
            exit 1
          fi
      - name: Check formatting
        uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4
        with:
          timeout_minutes: 5
          max_attempts: 3
          command: mise run lint
      - name: Run clippy
        run: cargo clippy --workspace --all-targets -- -D warnings

  msrv:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          submodules: recursive
          persist-credentials: false
      # save-if: only saves on main pushes, so PRs cannot poison the cache.
      - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 # zizmor: ignore[cache-poisoning]
        with:
          shared-key: msrv
          save-if: ${{ github.ref == 'refs/heads/main' }}
      - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
        with:
          cache: false
      - name: Install system dependencies
        run: sudo apt-get update && sudo apt-get install -y libudev-dev
      # cargo-msrv doesn't accept --workspace; verifying the binary's MSRV
      # transitively covers fnox-core because the binary depends on it.
      - run: cargo msrv verify

  final:
    permissions: {}
    needs:
      - ci-bats
      - ci-other
      - msrv
    runs-on: ubuntu-latest
    timeout-minutes: 1
    # Run on success or upstream failure but skip when the workflow is cancelled
    # — `always()` would override `cancel-in-progress` and waste a runner.
    if: ${{ !cancelled() }}
    steps:
      - name: Check job results
        if: |
          needs.ci-bats.result != 'success' ||
          needs.ci-other.result != 'success' ||
          needs.msrv.result != 'success'
        run: exit 1
      - run: echo "All CI jobs completed successfully"