foxguard 0.12.0

A security scanner as fast as a linter, written in Rust. 200+ built-in rules across 12 source languages.
Documentation
name: Release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

env:
  CARGO_TERM_COLOR: always
  NODE_VERSION: "22.12.0"

jobs:
  preflight:
    name: Release Preflight
    runs-on: ubuntu-latest
    env:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
      VSCE_PAT: ${{ secrets.VSCE_PAT }}
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
      - name: Verify release secrets
        run: |
          missing=0
          for name in NPM_TOKEN CARGO_REGISTRY_TOKEN VSCE_PAT; do
            if [ -z "${!name:-}" ]; then
              echo "::error::Missing required release secret: ${name}"
              missing=1
            fi
          done
          exit "${missing}"
      - name: Verify tag matches package versions
        run: |
          tag_version="${GITHUB_REF_NAME#v}"
          cargo_version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)
          npm_version=$(node -p "require('./packages/npm/package.json').version")
          vscode_version=$(node -p "require('./vscode-extension/package.json').version")
          vscode_lock_version=$(node -p "require('./vscode-extension/package-lock.json').version")

          for entry in \
            "Cargo.toml:${cargo_version}" \
            "packages/npm/package.json:${npm_version}" \
            "vscode-extension/package.json:${vscode_version}" \
            "vscode-extension/package-lock.json:${vscode_lock_version}"
          do
            file="${entry%%:*}"
            version="${entry#*:}"
            if [ "${version}" != "${tag_version}" ]; then
              echo "::error::${file} version ${version} does not match tag ${tag_version}"
              exit 1
            fi
          done

  build:
    name: Build ${{ matrix.target }}
    needs: preflight
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            artifact: foxguard-linux-x86_64
            use_cross: false
          - target: aarch64-unknown-linux-musl
            os: ubuntu-latest
            artifact: foxguard-linux-aarch64
            use_cross: true
          - target: x86_64-apple-darwin
            os: macos-latest
            artifact: foxguard-macos-x86_64
          - target: aarch64-apple-darwin
            os: macos-latest
            artifact: foxguard-macos-aarch64
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            artifact: foxguard-windows-x86_64.exe

    steps:
      - uses: actions/checkout@v6

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: Install musl tools (x86_64)
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: |
          sudo apt-get update
          sudo apt-get install -y musl-tools

      - name: Install cross (aarch64)
        if: matrix.use_cross == true
        run: cargo install cross --git https://github.com/cross-rs/cross

      - uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-${{ matrix.target }}-cargo-release-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: ${{ runner.os }}-${{ matrix.target }}-cargo-release-

      - name: Build
        run: |
          if [ "${{ matrix.use_cross }}" = "true" ]; then
            cross build --release --target ${{ matrix.target }}
          else
            cargo build --release --target ${{ matrix.target }}
          fi
        shell: bash

      - name: Rename binary (unix)
        if: runner.os != 'Windows'
        run: cp target/${{ matrix.target }}/release/foxguard ${{ matrix.artifact }}

      - name: Rename binary (windows)
        if: runner.os == 'Windows'
        run: cp target/${{ matrix.target }}/release/foxguard.exe ${{ matrix.artifact }}

      - uses: actions/upload-artifact@v7
        with:
          name: ${{ matrix.artifact }}
          path: ${{ matrix.artifact }}

  github-release:
    name: Create GitHub Release
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write
      id-token: write
      attestations: write
    steps:
      - uses: actions/checkout@v6

      - uses: actions/download-artifact@v8
        with:
          path: artifacts

      - name: Collect binaries
        run: |
          mkdir -p release
          find artifacts -type f -exec cp {} release/ \;
          ls -la release/

      - name: Generate checksums
        run: |
          cd release
          sha256sum * > checksums.txt
          echo "--- checksums.txt ---"
          cat checksums.txt

      - name: Attest release binaries
        uses: actions/attest-build-provenance@v2
        with:
          subject-checksums: release/checksums.txt

      - name: Attest checksum manifest
        uses: actions/attest-build-provenance@v2
        with:
          subject-path: release/checksums.txt

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          body_path: RELEASE_NOTES_${{ github.ref_name }}.md
          files: release/*
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  publish-crates:
    name: Publish crates.io
    needs: github-release
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: dtolnay/rust-toolchain@stable
      - name: Publish crate
        run: |
          set +e
          output=$(cargo publish --token "${{ secrets.CARGO_REGISTRY_TOKEN }}" 2>&1)
          status=$?
          set -e
          echo "${output}"
          if [ "${status}" -ne 0 ]; then
            crate_name=$(sed -n 's/^name = "\(.*\)"/\1/p' Cargo.toml | head -n 1)
            crate_version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n 1)
            published_version=$(curl -fsSL "https://crates.io/api/v1/crates/${crate_name}" | jq -r '.crate.max_version // ""' 2>/dev/null || echo "")
            if [ "${published_version}" = "${crate_version}" ]; then
              echo "crates.io already has ${crate_name} ${crate_version}; treating rerun as success"
              exit 0
            fi
            exit "${status}"
          fi

  publish-npm:
    name: Publish npm
    needs: github-release
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: packages/npm
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          registry-url: "https://registry.npmjs.org"
      - name: Publish npm package
        run: |
          set +e
          output=$(npm publish --access public 2>&1)
          status=$?
          set -e
          echo "${output}"
          if [ "${status}" -ne 0 ]; then
            package_name=$(node -p "require('./package.json').name")
            package_version=$(node -p "require('./package.json').version")
            published_version=$(npm view "${package_name}" version 2>/dev/null || true)
            if [ "${published_version}" = "${package_version}" ]; then
              echo "npm already has ${package_name} ${package_version}; treating rerun as success"
              exit 0
            fi
            exit "${status}"
          fi
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  publish-vscode:
    name: Publish VS Code Extension
    needs: github-release
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: vscode-extension
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: npm
          cache-dependency-path: vscode-extension/package-lock.json
      - run: npm ci
      - run: npm run compile
      - name: Publish extension
        run: |
          set +e
          output=$(npx @vscode/vsce publish -p "${VSCE_PAT}" 2>&1)
          status=$?
          echo "${output}"
          if [ "${status}" -ne 0 ] && ! echo "${output}" | grep -q "already exists"; then
            exit "${status}"
          fi
        env:
          VSCE_PAT: ${{ secrets.VSCE_PAT }}
      - name: Verify Marketplace propagation
        run: VSCODE_MARKETPLACE_VERSION="${GITHUB_REF_NAME#v}" node ../scripts/verify-vscode-marketplace.mjs


  publish-ghcr-github-app:
    name: Publish foxguard-github-app image (GHCR)
    needs: github-release
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v6

      - name: Check Dockerfile presence
        id: check
        run: |
          if [ -f Dockerfile.github-app ]; then
            echo "exists=true" >> "$GITHUB_OUTPUT"
          else
            echo "Dockerfile.github-app not present at this tag — skipping image publish."
            echo "exists=false" >> "$GITHUB_OUTPUT"
          fi

      - uses: docker/setup-qemu-action@v3
        if: steps.check.outputs.exists == 'true'

      - uses: docker/setup-buildx-action@v3
        if: steps.check.outputs.exists == 'true'

      - uses: docker/login-action@v3
        if: steps.check.outputs.exists == 'true'
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        if: steps.check.outputs.exists == 'true'
        with:
          context: .
          file: Dockerfile.github-app
          load: true
          push: false
          tags: foxguard-github-app:release-smoke
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Smoke test GitHub App container
        if: steps.check.outputs.exists == 'true'
        run: scripts/smoke-github-app-image.sh foxguard-github-app:release-smoke

      - uses: docker/build-push-action@v6
        if: steps.check.outputs.exists == 'true'
        with:
          context: .
          file: Dockerfile.github-app
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ghcr.io/0sec-labs/foxguard-github-app:${{ github.ref_name }}
            ghcr.io/0sec-labs/foxguard-github-app:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max