How to Route GitHub Actions Builds Through Chainsaw

Intermediate 20 minutes DevOps / Platform Engineers Advanced Configuration

Configure GitHub Actions to install packages through Chainsaw using a Service Token, repository secrets, and job-local package-manager configuration, with a PR-gating example.

Overview

GitHub Actions runners are shared, reused infrastructure, so the safe pattern is to generate package-manager configuration inside each job, read credentials from GitHub Actions secrets, and avoid any persistent global runner state. This page is the GitHub-Actions-specific slice of How to Integrate Chainsaw with CI/CD Pipelines; see that guide for GitLab CI and Jenkins and for the cross-cutting cache-invalidation and policy guidance.

Prerequisites

  • A running Chainsaw instance reachable from your GitHub Actions runners
  • A Chainsaw client credential with Client Type set to Service Token
  • Repository scope set to only the ecosystems the pipeline needs
  • CHAINSAW_CLIENT_ID and CHAINSAW_CLIENT_SECRET stored as GitHub Actions repository (or environment) secrets
Create separate service tokens for production, staging, and pull-request pipelines. This gives each pipeline isolated audit trails, policy scope, and rotation windows.
Invalidate CI dependency caches when switching to Chainsaw. If your workflow uses actions/cache, packages cached before Chainsaw was configured are restored from cache and never pass through the firewall. Delete or rotate the cache key once during initial onboarding, and clear local vendor/lock files in the first run.

npm

name: node-build
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Clear local caches (first-time Chainsaw setup)
        run: rm -rf node_modules package-lock.json

      - name: Configure npm for Chainsaw
        run: |
          CHAINSAW_NPM_AUTH="$(printf '%s' "$CHAINSAW_CLIENT_ID:$CHAINSAW_CLIENT_SECRET" | base64)"
          cat > .npmrc <<EOF
          registry=https://chain305.com/chainproxy/repository/@default/npmjs/
          //chain305.com/chainproxy/repository/@default/npmjs/:_auth=${CHAINSAW_NPM_AUTH}
          //chain305.com/chainproxy/repository/@default/npmjs/:always-auth=true
          EOF
        env:
          CHAINSAW_CLIENT_ID: ${{ secrets.CHAINSAW_CLIENT_ID }}
          CHAINSAW_CLIENT_SECRET: ${{ secrets.CHAINSAW_CLIENT_SECRET }}

      - name: Install dependencies
        run: npm ci

The _auth field expects the base64-encoded CLIENT_ID:CLIENT_SECRET form. Passing the raw literal makes npm ship installs with no Authorization header and the proxy returns HTTP 401. always-auth=true keeps the credential on tarball fetches, not just metadata lookups.

pip

name: python-test
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install dependencies through Chainsaw
        env:
          PIP_INDEX_URL: https://chain305.com/chainproxy/repository/@default/pypi/simple/
          PIP_RETRIES: "5"
          PIP_TIMEOUT: "60"
          CHAINSAW_CLIENT_ID: ${{ secrets.CHAINSAW_CLIENT_ID }}
          CHAINSAW_CLIENT_SECRET: ${{ secrets.CHAINSAW_CLIENT_SECRET }}
        run: |
          cat > "$HOME/.netrc" <<EOF
          machine chain305.com
            login ${CHAINSAW_CLIENT_ID}
            password ${CHAINSAW_CLIENT_SECRET}
          EOF
          chmod 600 "$HOME/.netrc"
          pip cache purge
          python -m pip install -U pip
          pip install -r requirements.txt

Maven

- name: Clear Maven local cache (first-time Chainsaw setup)
  run: rm -rf ~/.m2/repository

- name: Write Maven settings.xml
  env:
    CHAINSAW_CLIENT_ID: ${{ secrets.CHAINSAW_CLIENT_ID }}
    CHAINSAW_CLIENT_SECRET: ${{ secrets.CHAINSAW_CLIENT_SECRET }}
  run: |
    mkdir -p ~/.m2
    cat > ~/.m2/settings.xml <<'XML'
    <settings>
      <mirrors>
        <mirror>
          <id>chainsaw</id>
          <url>https://chain305.com/chainproxy/repository/@default/maven-central/</url>
          <mirrorOf>*</mirrorOf>
        </mirror>
      </mirrors>
      <servers>
        <server>
          <id>chainsaw</id>
          <username>${env.CHAINSAW_CLIENT_ID}</username>
          <password>${env.CHAINSAW_CLIENT_SECRET}</password>
        </server>
      </servers>
    </settings>
    XML

- name: Build
  run: mvn -B verify

Docker / OCI

- name: Login to Chainsaw registry
  env:
    CHAINSAW_CLIENT_ID: ${{ secrets.CHAINSAW_CLIENT_ID }}
    CHAINSAW_CLIENT_SECRET: ${{ secrets.CHAINSAW_CLIENT_SECRET }}
  run: |
    echo "$CHAINSAW_CLIENT_SECRET" | docker login chain305.com --username "$CHAINSAW_CLIENT_ID" --password-stdin
    docker pull chain305.com/library/alpine:3.21

Avoid passing Chainsaw secrets through Docker build args — they can persist in image metadata and layer history. Prefer installing dependencies outside the image build, or use BuildKit secrets for job-time package-manager config.

Gating Pull Requests

Run the install/build job on pull_request and make it a required status check on the protected branch. When a policy blocks a package, the install step exits non-zero and the PR check fails — the dependency never enters the merge.

name: dependency-firewall
on:
  pull_request:
    branches: [main]

jobs:
  install:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Configure npm for Chainsaw
        run: |
          CHAINSAW_NPM_AUTH="$(printf '%s' "$CHAINSAW_CLIENT_ID:$CHAINSAW_CLIENT_SECRET" | base64)"
          cat > .npmrc <<EOF
          registry=https://chain305.com/chainproxy/repository/@default/npmjs/
          //chain305.com/chainproxy/repository/@default/npmjs/:_auth=${CHAINSAW_NPM_AUTH}
          //chain305.com/chainproxy/repository/@default/npmjs/:always-auth=true
          EOF
        env:
          CHAINSAW_CLIENT_ID: ${{ secrets.CHAINSAW_CLIENT_ID }}
          CHAINSAW_CLIENT_SECRET: ${{ secrets.CHAINSAW_CLIENT_SECRET }}

      # A policy block (e.g. CHW-2002 vulnerability, CHW-2004 typosquat)
      # returns HTTP 403, npm ci exits non-zero, and this required check fails.
      - name: Install dependencies (gates the PR)
        run: npm ci

Then enable Require status checks to pass before merging for this job under Settings → Branches → Branch protection rules. Scope a stricter policy to the pull-request service token so PRs are gated more aggressively than scheduled builds.

Use a dedicated PR-pipeline service token so failing-check noise and audit trails stay separate from production builds. See How to Create and Manage Client Credentials.

Report Findings to GitHub Code Scanning (SARIF)

Beyond gating installs, Chainsaw emits its scan results as a SARIF 2.1.0 log that GitHub ingests into the Security → Code scanning tab, so supply-chain findings surface as annotations on the pull request.

name: chainsaw-code-scanning
on: [pull_request]

permissions:
  contents: read
  security-events: write   # required to upload SARIF

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Audit the workflow files
        env:
          CHAINSAW_SERVER: ${{ secrets.CHAINSAW_SERVER }}
          CHAINSAW_TOKEN: ${{ secrets.CHAINSAW_TOKEN }}
        run: chainsaw scan-actions . --format sarif --output chainsaw.sarif
        continue-on-error: true   # still upload the SARIF when a finding is flagged

      - name: Upload to code scanning
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: chainsaw.sarif

chainsaw scan --path . --format sarif --output deps.sarif produces the same SARIF shape for the dependency tree. Results go to the --output file while logs stay on stderr, so the upload step always finds the file — even when the scan exits non-zero on a high-severity finding (scan-actions exits 1).

--format also accepts json (the --json flag is sugar for --format=json); the JSON envelope carries a top-level schemaVersion so a consumer can pin on a known shape. Add --quiet to suppress progress chatter in CI logs — it never hides a block verdict or changes the exit code. To scan a newline-delimited list of package specs instead of a tree, pipe it in (strictly opt-in): cat specs.txt | chainsaw scan -.

Verify Pipeline Activity

After the first run:

  1. Open Traffic in the Chainsaw dashboard.
  2. Filter by the service token’s Client ID.
  3. Confirm requests use the expected repository and outcome.
  4. Check the cache hit ratio after a warm run.

Next Steps