How to Route GitHub Actions Builds Through Chainsaw
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_IDandCHAINSAW_CLIENT_SECRETstored as GitHub Actions repository (or environment) secrets
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.
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:
- Open Traffic in the Chainsaw dashboard.
- Filter by the service token’s Client ID.
- Confirm requests use the expected repository and outcome.
- Check the cache hit ratio after a warm run.
Next Steps
- How to Integrate Chainsaw with CI/CD Pipelines — full multi-CI reference, including Gradle and CocoaPods
- Troubleshooting CI/CD Integration — common auth and policy-block failures
- How to Configure Your Package Manager to Use Chainsaw — per-ecosystem config reference