How to Integrate Chainsaw with CI/CD Pipelines
Configure GitHub Actions, GitLab CI, and Jenkins to route package installs through Chainsaw using scoped service tokens and job-local configuration.
Overview
CI/CD pipelines should use Chainsaw differently from developer workstations. A pipeline runs on shared or reused infrastructure, so the safest default is to generate package-manager configuration inside each job, read secrets from the CI platform secret store, and avoid persistent global runner state.
Use this guide for GitHub Actions, GitLab CI, and Jenkins. For local developer tools and AI agents, use the end-user and agent setup guide in each repository’s Configure Client dialog.
Prerequisites
- A running Chainsaw instance accessible from your CI/CD 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 in the CI platform secret store
Credential Variable Names Across Chainsaw Surfaces
Two different credentials, and one name — CHAINSAW_TOKEN — used for both.
The first four rows below are all the same client ID and client secret, packaged
differently because each surface inherits the name its own tooling expects. The
fifth row is not that credential at all. Nothing here is being renamed — a
rename would break every pipeline that already has the old name in its secret
store — so this table is the map. The canonical version, with the parser for
each form quoted, is configuration reference section
B30.
| Surface | Variable(s) | Value shape | Where it is used |
|---|---|---|---|
| CI/CD — this guide, the dashboard’s CI/CD tab, and the CI snippets on chain305.com/integrations | CHAINSAW_CLIENT_ID and CHAINSAW_CLIENT_SECRET | Two separate secrets. The job joins them with a colon at the point of use. | CI platform secret store; every snippet on this page |
| Repository setup guides — the “End users & AI agents” tab of the Configure Client dialog | CHAINSAW_TOKEN | One value: the plaintext pair CLIENT_ID:CLIENT_SECRET, colon-separated. | npm, Yarn, Bun, Cargo and the other per-ecosystem guides |
Config-snippet download — the generated .npmrc / pip.conf / nuget.config for an existing credential | CHAINSAW_CLIENT_SECRET as a placeholder literal, plus CHAINSAW_TOKEN in the npm form | The placeholder marks the spot to paste the secret into; CHAINSAW_TOKEN is again the joined pair. A secret is never recoverable after issue, so a revisited credential always renders placeholders. | Access → Client Credentials → Configure |
| Hugging Face | HF_TOKEN | The joined pair CLIENT_ID:CLIENT_SECRET — not a real hf_… token. | huggingface-cli, huggingface_hub |
Management API / CLI — chainsaw auth, chainsaw doctor --attest, every curl in these how-tos, the GitHub Action’s token: input | CHAINSAW_TOKEN again, but a different credential | A management-API key, c305_pa_… (or a session JWT) — not a client ID and secret, and never base64. /api/… accepts only these; the registry proxy accepts only the pair. Neither converts into the other. | Access → API keys |
Two rules hold on every surface:
- The joined form is plaintext and colon-separated. Never base64-encode it.
Chainsaw reads it from
Authorization: Bearer <token>and splits on the first colon. Base64 output contains no colon, so an encoded token fails withE401 client credentials requiredon the first request. - HTTP Basic works too, and is what
_auth, Maven<password>anddocker loginuse. BothAuthorization: BasicandAuthorization: Bearer id:secretare accepted; pick whichever your tool offers natively rather than converting between them.
Recommended Pattern
| Practice | CI/CD default |
|---|---|
| Credentials | Store in CI secrets, variables, or Jenkins Credentials |
| Package-manager config | Generate inside each job |
| Registry routing | Route through one Chainsaw endpoint per ecosystem |
| Caching | Use Chainsaw’s proxy cache first, then CI dependency caches |
| Cache invalidation | Clear local caches on first Chainsaw setup so all packages flow through the firewall |
| Fallback | Prefer strict routing through Chainsaw over direct public-registry fallback |
| Rotation | Reset service-token secrets on a schedule |
actions/cache, GitLab CI cache, persistent Jenkins workspace), packages cached before Chainsaw was configured will be restored from cache and never pass through the firewall. Delete or rotate the CI cache key once during initial Chainsaw onboarding, and clear local vendor/lock files in the first run.GitHub Actions
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
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
Gradle (Android)
- name: Clear Gradle caches (first-time Chainsaw setup)
run: rm -rf ~/.gradle/caches
- name: Configure Gradle for Chainsaw
env:
CHAINSAW_CLIENT_ID: ${{ secrets.CHAINSAW_CLIENT_ID }}
CHAINSAW_CLIENT_SECRET: ${{ secrets.CHAINSAW_CLIENT_SECRET }}
run: |
mkdir -p ~/.gradle
cat > ~/.gradle/init.gradle <<'GRADLE'
allprojects {
repositories {
clear()
maven {
url "https://chain305.com/chainproxy/repository/@default/gradle-central/"
credentials {
username = System.getenv("CHAINSAW_CLIENT_ID")
password = System.getenv("CHAINSAW_CLIENT_SECRET")
}
}
maven {
url "https://chain305.com/chainproxy/repository/@default/google-maven/"
credentials {
username = System.getenv("CHAINSAW_CLIENT_ID")
password = System.getenv("CHAINSAW_CLIENT_SECRET")
}
}
}
}
settingsEvaluated { settings ->
settings.pluginManagement {
repositories {
clear()
maven {
url "https://chain305.com/chainproxy/repository/@default/gradle-plugins/"
credentials {
username = System.getenv("CHAINSAW_CLIENT_ID")
password = System.getenv("CHAINSAW_CLIENT_SECRET")
}
}
}
}
}
GRADLE
- name: Build
env:
CHAINSAW_CLIENT_ID: ${{ secrets.CHAINSAW_CLIENT_ID }}
CHAINSAW_CLIENT_SECRET: ${{ secrets.CHAINSAW_CLIENT_SECRET }}
run: ./gradlew build
init.gradle approach overrides all repositories and plugin sources in a single file, ensuring every dependency resolves through Chainsaw. Credentials are read at build time via System.getenv().CocoaPods (iOS)
- name: Configure CocoaPods for Chainsaw
env:
CHAINSAW_CLIENT_ID: ${{ secrets.CHAINSAW_CLIENT_ID }}
CHAINSAW_CLIENT_SECRET: ${{ secrets.CHAINSAW_CLIENT_SECRET }}
run: |
cat >> ~/.netrc <<EOF
machine chain305.com
login ${CHAINSAW_CLIENT_ID}
password ${CHAINSAW_CLIENT_SECRET}
EOF
chmod 600 ~/.netrc
- name: Install pods
run: pod install
Podfile must use the Chainsaw source: source 'https://chain305.com/chainproxy/repository/@default/cocoapods-trunk/'. Replace the default CocoaPods CDN source with this line at the top of your Podfile.GitLab CI
npm
stages: [build]
variables:
NPM_CONFIG_USERCONFIG: "$CI_PROJECT_DIR/.npmrc"
build_node:
image: node:20
stage: build
before_script:
- rm -rf node_modules package-lock.json
- CHAINSAW_NPM_AUTH="$(printf '%s' "$CHAINSAW_CLIENT_ID:$CHAINSAW_CLIENT_SECRET" | base64)"
- printf "registry=https://chain305.com/chainproxy/repository/@default/npmjs/\n//chain305.com/chainproxy/repository/@default/npmjs/:_auth=%s\n//chain305.com/chainproxy/repository/@default/npmjs/:always-auth=true\n" "$CHAINSAW_NPM_AUTH" > .npmrc
script:
- npm ci
- npm test
pip
stages: [test]
variables:
PIP_INDEX_URL: "https://chain305.com/chainproxy/repository/@default/pypi/simple/"
PIP_RETRIES: "5"
PIP_TIMEOUT: "60"
test_python:
image: python:3.13
stage: test
before_script:
- printf "machine chain305.com\n login %s\n password %s\n" "$CHAINSAW_CLIENT_ID" "$CHAINSAW_CLIENT_SECRET" > "$HOME/.netrc"
- chmod 600 "$HOME/.netrc"
- pip cache purge
script:
- python -m pip install -U pip
- pip install -r requirements.txt
- pytest -q
Maven
build_maven:
image: maven:3.9-eclipse-temurin-21
stage: build
script:
- rm -rf ~/.m2/repository
- 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
- mvn -B verify
Docker / OCI
build_container:
image: docker:24-cli
services:
- docker:24-dind
before_script:
- echo "$CHAINSAW_CLIENT_SECRET" | docker login chain305.com --username "$CHAINSAW_CLIENT_ID" --password-stdin
script:
- docker pull chain305.com/library/alpine:3.21
Gradle (Android)
build_android:
image: gradle:8-jdk21
stage: build
before_script:
- rm -rf ~/.gradle/caches
- mkdir -p ~/.gradle
- |
cat > ~/.gradle/init.gradle <<GRADLE
allprojects {
repositories {
clear()
maven {
url "https://chain305.com/chainproxy/repository/@default/gradle-central/"
credentials {
username = System.getenv("CHAINSAW_CLIENT_ID")
password = System.getenv("CHAINSAW_CLIENT_SECRET")
}
}
maven {
url "https://chain305.com/chainproxy/repository/@default/google-maven/"
credentials {
username = System.getenv("CHAINSAW_CLIENT_ID")
password = System.getenv("CHAINSAW_CLIENT_SECRET")
}
}
}
}
settingsEvaluated { settings ->
settings.pluginManagement {
repositories {
clear()
maven {
url "https://chain305.com/chainproxy/repository/@default/gradle-plugins/"
credentials {
username = System.getenv("CHAINSAW_CLIENT_ID")
password = System.getenv("CHAINSAW_CLIENT_SECRET")
}
}
}
}
}
GRADLE
script:
- ./gradlew build
CocoaPods (iOS)
build_ios:
image: macos-14-xcode-16
tags: [macos]
stage: build
before_script:
- printf "machine chain305.com\n login %s\n password %s\n" "$CHAINSAW_CLIENT_ID" "$CHAINSAW_CLIENT_SECRET" >> "$HOME/.netrc"
- chmod 600 "$HOME/.netrc"
- pod cache clean --all
script:
# Podfile must use: source 'https://chain305.com/chainproxy/repository/@default/cocoapods-trunk/'
- pod install
- xcodebuild -workspace MyApp.xcworkspace -scheme MyApp build
Jenkins
Store the Chainsaw service token in Jenkins Credentials as a Username with password credential named chainsaw-service-token.
pipeline {
agent any
stages {
stage('npm install') {
steps {
withCredentials([usernamePassword(credentialsId: 'chainsaw-service-token',
usernameVariable: 'CHAINSAW_CLIENT_ID', passwordVariable: 'CHAINSAW_CLIENT_SECRET')]) {
sh '''
rm -rf node_modules package-lock.json
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
npm ci
'''
}
}
}
stage('maven build') {
steps {
withCredentials([usernamePassword(credentialsId: 'chainsaw-service-token',
usernameVariable: 'CHAINSAW_CLIENT_ID', passwordVariable: 'CHAINSAW_CLIENT_SECRET')]) {
sh '''
rm -rf ~/.m2/repository
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
mvn -B verify
'''
}
}
}
stage('docker pull') {
steps {
withCredentials([usernamePassword(credentialsId: 'chainsaw-service-token',
usernameVariable: 'CHAINSAW_CLIENT_ID', passwordVariable: 'CHAINSAW_CLIENT_SECRET')]) {
sh '''
echo "$CHAINSAW_CLIENT_SECRET" | docker login chain305.com --username "$CHAINSAW_CLIENT_ID" --password-stdin
docker pull chain305.com/library/alpine:3.21
'''
}
}
}
stage('gradle build') {
steps {
withCredentials([usernamePassword(credentialsId: 'chainsaw-service-token',
usernameVariable: 'CHAINSAW_CLIENT_ID', passwordVariable: 'CHAINSAW_CLIENT_SECRET')]) {
sh '''
rm -rf ~/.gradle/caches
mkdir -p ~/.gradle
cat > ~/.gradle/init.gradle <<'GRADLE'
allprojects {
repositories {
clear()
maven {
url "https://chain305.com/chainproxy/repository/@default/gradle-central/"
credentials {
username = System.getenv("CHAINSAW_CLIENT_ID")
password = System.getenv("CHAINSAW_CLIENT_SECRET")
}
}
maven {
url "https://chain305.com/chainproxy/repository/@default/google-maven/"
credentials {
username = System.getenv("CHAINSAW_CLIENT_ID")
password = System.getenv("CHAINSAW_CLIENT_SECRET")
}
}
}
}
GRADLE
./gradlew build
'''
}
}
}
stage('pod install') {
steps {
withCredentials([usernamePassword(credentialsId: 'chainsaw-service-token',
usernameVariable: 'CHAINSAW_CLIENT_ID', passwordVariable: 'CHAINSAW_CLIENT_SECRET')]) {
sh '''
cat >> ~/.netrc <<EOF
machine chain305.com
login ${CHAINSAW_CLIENT_ID}
password ${CHAINSAW_CLIENT_SECRET}
EOF
chmod 600 ~/.netrc
pod install
'''
}
}
}
}
}
Docker Build Guidance
Avoid passing Chainsaw secrets through Docker build args. Build args can persist in image metadata and layer history depending on how the Dockerfile is written.
Prefer one of these approaches:
- Run dependency installation outside the image build when possible.
- Use BuildKit secrets for job-time package-manager config.
- Use a multi-stage build and verify the final image does not contain package-manager credentials.
Verify Pipeline Activity
After the first CI/CD run:
- Open Traffic.
- Filter by the service token’s Client ID.
- Confirm requests use the expected repository and outcome.
- Check cache hit ratio after a warm run.
Apply CI/CD Policies
Create policies scoped to production service tokens:
- Block packages with CVSS >= 7.0.
- Block packages released within the configured freshness window.
- Require provenance where the ecosystem supports it.
- Scope by CI runner IP/CIDR if your runner network is stable.
Future Hardening: OIDC
This v1 guide uses static Chainsaw service tokens stored in each CI platform’s secret store. The next hardening step is an OIDC token-exchange flow where GitHub Actions or GitLab CI job identity is exchanged for a short-lived Chainsaw token. That requires a new Chainsaw token broker endpoint and is not part of this v1 implementation.
Next Steps
- How to Create and Manage Client Credentials — Credential lifecycle management
- How to Manage Policy Precedence and Exception Workflows — Different rules for CI/CD vs development
- How to Manage Cache and Optimize Build Performance — Cache tuning for build fleets