How to Integrate Chainsaw with CI/CD Pipelines

Intermediate 25 minutes DevOps / Platform Engineers Advanced Configuration

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_ID and CHAINSAW_CLIENT_SECRET stored in the CI platform secret store
Create separate service tokens for production, staging, and pull-request pipelines. This gives each pipeline isolated audit trails, policy scope, and rotation windows.

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.

SurfaceVariable(s)Value shapeWhere it is used
CI/CD — this guide, the dashboard’s CI/CD tab, and the CI snippets on chain305.com/integrationsCHAINSAW_CLIENT_ID and CHAINSAW_CLIENT_SECRETTwo 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 dialogCHAINSAW_TOKENOne 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 credentialCHAINSAW_CLIENT_SECRET as a placeholder literal, plus CHAINSAW_TOKEN in the npm formThe 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 FaceHF_TOKENThe 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: inputCHAINSAW_TOKEN again, but a different credentialA 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 with E401 client credentials required on the first request.
  • HTTP Basic works too, and is what _auth, Maven <password> and docker login use. Both Authorization: Basic and Authorization: Bearer id:secret are accepted; pick whichever your tool offers natively rather than converting between them.
PracticeCI/CD default
CredentialsStore in CI secrets, variables, or Jenkins Credentials
Package-manager configGenerate inside each job
Registry routingRoute through one Chainsaw endpoint per ecosystem
CachingUse Chainsaw’s proxy cache first, then CI dependency caches
Cache invalidationClear local caches on first Chainsaw setup so all packages flow through the firewall
FallbackPrefer strict routing through Chainsaw over direct public-registry fallback
RotationReset service-token secrets on a schedule
Invalidate CI dependency caches when switching to Chainsaw. If your pipeline uses a dependency cache (e.g., 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
The 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
Your 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:

  1. Open Traffic.
  2. Filter by the service token’s Client ID.
  3. Confirm requests use the expected repository and outcome.
  4. 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