stout
Guide · DevOps

Using stout in CI/CD Pipelines

Speed up GitHub Actions, GitLab CI, and Jenkins pipelines with stout — lock files, caching, and parallel installs.

Neul Labs ·
#ci-cd#github-actions#devops#automation

Why stout in CI/CD?

Package installation is often one of the slowest steps in a CI/CD pipeline. A typical brew install sequence in GitHub Actions can consume two to five minutes of every workflow run, and that cost multiplies across every push, every PR, and every matrix job. stout cuts that time by 10-100x with parallel installs, a compact binary index, and excellent caching behavior.

Quick start with GitHub Actions

The fastest way to use stout in GitHub Actions is with the official action:

name: Build and Test
on: [push, pull_request]

jobs:
  test:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - name: Install stout
        uses: neullabs/setup-stout@v1
        with:
          version: "latest"

      - name: Install dependencies
        run: stout install ripgrep fd bat jq yq

      - name: Run tests
        run: make test

The neullabs/setup-stout action downloads the stout binary, adds it to PATH, and optionally restores the package cache from a previous run.

Caching packages between runs

stout produces deterministic installs, which makes caching straightforward. Use the built-in cache support in the setup action:

- name: Install stout
  uses: neullabs/setup-stout@v1
  with:
    version: "latest"
    cache: true
    cache-key: stout-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('stout.lock') }}

When cache: true is set, the action saves and restores the stout prefix directory (/usr/local/stout or /opt/stout) between runs. The cache-key should include the lock file hash so the cache invalidates when dependencies change.

If you prefer to manage caching yourself:

- name: Cache stout packages
  uses: actions/cache@v4
  with:
    path: |
      /usr/local/stout
      ~/.stout/cache
    key: stout-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('stout.lock') }}
    restore-keys: |
      stout-${{ runner.os }}-${{ runner.arch }}-

- name: Install dependencies
  run: stout bundle install --frozen

The --frozen flag tells stout to install exactly the versions specified in the lock file, failing if the lock file is missing or outdated. This ensures reproducible builds.

Using lock files for deterministic builds

Lock files are essential for CI/CD. They guarantee that every pipeline run uses exactly the same package versions, regardless of when it runs or what upstream has published since.

Generate a lock file locally:

stout bundle lock --file Brewfile --output stout.lock

Commit stout.lock to your repository. In CI, install from the lock file:

stout bundle install --frozen --file stout.lock

The lock file format records the exact version, SHA-256 hash, and download URL for every package and dependency:

# stout.lock (auto-generated, do not edit manually)
[metadata]
generated_at = "2026-03-15T10:30:00Z"
stout_version = "1.4.0"
platform = "arm64_sonoma"

[[packages]]
name = "ripgrep"
version = "14.1.1"
sha256 = "a1b2c3d4..."
url = "https://index.stout.dev/bottles/ripgrep-14.1.1.arm64_sonoma.tar.gz"
dependencies = ["pcre2"]

[[packages]]
name = "pcre2"
version = "10.44"
sha256 = "e5f6a7b8..."
url = "https://index.stout.dev/bottles/pcre2-10.44.arm64_sonoma.tar.gz"
dependencies = []

GitLab CI configuration

stout works equally well in GitLab CI. Here is a complete .gitlab-ci.yml example:

stages:
  - setup
  - test
  - deploy

variables:
  STOUT_CACHE_DIR: "$CI_PROJECT_DIR/.stout-cache"

.stout-setup: &stout-setup
  before_script:
    - curl -fsSL https://get.stout.dev | sh
    - eval "$(stout shellenv bash)"
    - stout bundle install --frozen --file stout.lock

cache:
  key: "stout-${CI_RUNNER_OS}-${CI_RUNNER_ARCH}"
  paths:
    - .stout-cache/

test:
  stage: test
  <<: *stout-setup
  script:
    - make test

lint:
  stage: test
  <<: *stout-setup
  script:
    - make lint

Jenkins pipeline

For Jenkins, install stout in a pipeline step and cache the prefix directory:

pipeline {
    agent any

    stages {
        stage('Setup') {
            steps {
                sh '''
                    if ! command -v stout &> /dev/null; then
                        curl -fsSL https://get.stout.dev | sh
                    fi
                    eval "$(stout shellenv bash)"
                    stout bundle install --frozen --file stout.lock
                '''
            }
        }

        stage('Test') {
            steps {
                sh 'make test'
            }
        }
    }
}

For persistent Jenkins agents, install stout once on the agent and use lock files to manage per-project dependencies.

Parallel installs in matrix builds

stout’s parallel installation is especially valuable in matrix builds where you need the same set of packages on multiple runners:

jobs:
  test:
    strategy:
      matrix:
        os: [macos-14, macos-13, ubuntu-22.04]
        node: [18, 20, 22]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: neullabs/setup-stout@v1
        with:
          cache: true

      - name: Install system dependencies
        run: |
          stout install --parallel \
            cmake ninja pkg-config \
            openssl@3 libpq imagemagick

      - name: Run tests
        run: make test

The --parallel flag is the default behavior, but including it explicitly documents your intent. stout installs all six packages and their dependencies concurrently, saturating available bandwidth and CPU cores.

Environment variables for CI

stout respects several environment variables useful for CI environments:

# Disable interactive prompts (auto-yes to all confirmations)
export STOUT_NONINTERACTIVE=1

# Disable colored output (for cleaner CI logs)
export STOUT_NO_COLOR=1

# Set a custom prefix (useful for containerized builds)
export STOUT_PREFIX=/opt/stout

# Set a custom cache directory
export STOUT_CACHE_DIR=/tmp/stout-cache

# Enable verbose output for debugging
export STOUT_VERBOSE=1

Docker integration

For Docker-based CI, add stout to your base image:

FROM ubuntu:22.04

# Install stout
RUN curl -fsSL https://get.stout.dev | sh \
    && eval "$(stout shellenv bash)" \
    && echo 'eval "$(stout shellenv bash)"' >> /etc/bash.bashrc

# Pre-install common dependencies
COPY stout.lock /tmp/stout.lock
RUN stout bundle install --frozen --file /tmp/stout.lock \
    && rm /tmp/stout.lock

# Your application
WORKDIR /app
COPY . .

Because stout installs are deterministic from lock files, Docker layer caching works perfectly. The stout bundle install layer only rebuilds when stout.lock changes.

Performance benchmarks

Measured on GitHub Actions macos-14 runners (M1, 3 vCPU, 7 GB RAM), installing a typical web development toolset (node, python, go, rust, postgresql, redis, imagemagick):

ToolCold installCached install
Homebrew4m 12s2m 05s
stout18s3s

The improvement comes from three factors: parallel downloads and installs, no Ruby interpreter overhead, and a compact binary index that updates in milliseconds instead of fetching a full Git repository.

Troubleshooting CI issues

Problem: “Lock file is out of date” Your stout.lock does not match your Brewfile. Regenerate it locally with stout bundle lock and commit the updated lock file.

Problem: Packages fail to install on Linux runners Ensure your lock file was generated for the correct platform. Use platform-specific lock files if you target both macOS and Linux:

stout bundle lock --file Brewfile --platform arm64_sonoma --output stout-macos.lock
stout bundle lock --file Brewfile --platform x86_64_linux --output stout-linux.lock

Problem: Cache is not being restored Check that your cache key includes the correct lock file hash and that the runner OS and architecture match. Stale caches from a different stout version may also cause issues; include the stout version in your cache key if upgrades are frequent.

Need Rust performance engineering or AI agent expertise?

Neul Labs — the team behind stout — consults on Rust development, performance optimization, CLI tool design, and AI agent infrastructure. We build fast, reliable systems that ship.