Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 362 additions & 0 deletions .github/workflows/pipboy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,362 @@
# Pip-Boy: what a change does to the packages we ship, in one PR comment.
#
# Every job runs on `pull_request` or `push`. Only `report` gets a write token,
# it builds nothing, and it comments only on same-repo PRs. Pushes to main
# store baselines that later PRs compare against. Spec and tooling:
# scripts/pipboy.
name: pipboy

on:
pull_request:
push:
branches: [main]

concurrency:
group: pipboy-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true

permissions: {}

jobs:
changes:
name: changes
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
base: ${{ steps.changes.outputs.base }}
base_history: ${{ steps.changes.outputs.base_history }}
footprint: ${{ steps.changes.outputs.footprint }}
tool: ${{ steps.changes.outputs.tool }}
rust: ${{ steps.changes.outputs.rust }}
native: ${{ steps.changes.outputs.native }}
native_matrix: ${{ steps.changes.outputs.native_matrix }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
persist-credentials: false
- id: changes
env:
EVENT: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PUSH_BEFORE: ${{ github.event.before }}
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
# The binaries the release ships, one build per target as in release.yaml.
# `linux` builds on any Rust change; the rest build on platform-specific
# changes or the `ci: native-all` label, which is read on the next push.
NATIVE_ALL: >-
[
{"target":"x86_64-unknown-linux-musl","platform":"linux-x64","runner":"depot-ubuntu-24.04-8","ext":"","tier":"linux"},
{"target":"aarch64-unknown-linux-musl","platform":"linux-arm64","runner":"depot-ubuntu-24.04-arm-8","ext":"","tier":"all"},
{"target":"x86_64-apple-darwin","platform":"darwin-x64","runner":"depot-ubuntu-24.04-8","ext":"","tier":"all"},
{"target":"aarch64-apple-darwin","platform":"darwin-arm64","runner":"depot-ubuntu-24.04-8","ext":"","tier":"all"},
{"target":"x86_64-pc-windows-gnu","platform":"win32-x64","runner":"depot-ubuntu-24.04-8","ext":".exe","tier":"all"}
]
run: |
RUST='^(sandbox-agent/(.*\.rs|(.*/)?Cargo\.(toml|lock)|deny\.toml|docker/release/.*)$|scripts/pipboy/|\.github/workflows/pipboy\.yml$)'
PLATFORM_SENSITIVE='^sandbox-agent/((.*/)?Cargo\.(toml|lock)|(.*/)?build\.rs|docker/release/.*)$'
matrix() { jq -c --arg tier "$1" 'map(select($tier == "all" or .tier == $tier))' <<< "$NATIVE_ALL"; }

if [ "$EVENT" != "pull_request" ]; then
# Pushes to main record baselines. Natives build only when Rust changed,
# and then for every platform. Hygiene stores no baseline, so it skips main.
echo "footprint=true" >> "$GITHUB_OUTPUT"
echo "tool=true" >> "$GITHUB_OUTPUT"
echo "rust=false" >> "$GITHUB_OUTPUT"
native=none
if ! files="$(git diff --name-only "$PUSH_BEFORE" HEAD 2>/dev/null)" || grep -qE "$RUST" <<< "$files"; then
native=all
fi
echo "native=$native" >> "$GITHUB_OUTPUT"
echo "native_matrix=$(matrix "$native")" >> "$GITHUB_OUTPUT"
exit 0
fi
base="$(git merge-base "$PR_BASE_SHA" HEAD)"
echo "base=$base" >> "$GITHUB_OUTPUT"
echo "base_history=$(git rev-list --first-parent -n 200 "$base" | paste -sd, -)" >> "$GITHUB_OUTPUT"
files="$(git diff --name-only "$base" HEAD)"
footprint=false
if grep -qE '^(packages/|sandbox-agent/sdks/|(sandbox-agent/)?pnpm-(lock|workspace)\.yaml|scripts/pipboy/|\.github/workflows/pipboy\.yml)' <<< "$files"; then
footprint=true
fi
echo "footprint=$footprint" >> "$GITHUB_OUTPUT"
tool=false
if grep -qE '^(scripts/pipboy/|\.github/workflows/pipboy\.yml)' <<< "$files"; then
tool=true
fi
echo "tool=$tool" >> "$GITHUB_OUTPUT"
rust=false
native=none
if grep -qE "$RUST" <<< "$files"; then
rust=true
native=linux
if grep -qE "$PLATFORM_SENSITIVE" <<< "$files" \
|| git diff -U0 "$base" HEAD -- 'sandbox-agent/*.rs' | grep -qE '^\+.*(cfg\(target_|target_os|target_arch)' \
|| jq -e 'index("ci: native-all")' <<< "$LABELS" > /dev/null; then
native=all
fi
fi
echo "rust=$rust" >> "$GITHUB_OUTPUT"
echo "native=$native" >> "$GITHUB_OUTPUT"
echo "native_matrix=$(matrix "$native")" >> "$GITHUB_OUTPUT"

self-test:
name: self-test
needs: changes
if: needs.changes.outputs.tool == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
sandbox-agent/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile --filter 'pipboy...'
- run: pnpm --filter pipboy check-types
# Replays the September 2026 registry drift against a local npm registry.
- run: pnpm --filter pipboy test

footprint:
name: footprint
needs: changes
if: needs.changes.outputs.footprint == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: read # download the base tarballs from a main run
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
sandbox-agent/pnpm-lock.yaml
# Sandbox Agent is its own pnpm workspace.
- run: pnpm install --frozen-lockfile
- run: pnpm --dir sandbox-agent install --frozen-lockfile

- name: Pack this commit
run: pnpm --silent --filter pipboy pipboy pack --out "$RUNNER_TEMP/pr"

- name: Store tarballs as the baseline for later PRs
if: github.event_name == 'push'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: footprint-tarballs
path: ${{ runner.temp }}/pr
retention-days: 90

- name: Fetch the base tarballs from main
id: baseline
if: github.event_name == 'pull_request'
env:
GITHUB_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ github.token }}
BASE_SHA: ${{ needs.changes.outputs.base }}
run: pnpm --silent --filter pipboy pipboy baseline --sha "$BASE_SHA" --artifact footprint-tarballs --out "$RUNNER_TEMP/base"

# Main has no baseline for this commit (older than this workflow, or
# expired), so pack the merge base here with this commit's tool.
- name: Pack the base
if: github.event_name == 'pull_request' && steps.baseline.outputs.found != 'true'
env:
BASE_SHA: ${{ needs.changes.outputs.base }}
run: |
git worktree add "$RUNNER_TEMP/base-src" "$BASE_SHA"
pnpm --dir "$RUNNER_TEMP/base-src" install --frozen-lockfile
pnpm --dir "$RUNNER_TEMP/base-src/sandbox-agent" install --frozen-lockfile
pnpm --silent --filter pipboy pipboy pack --root "$RUNNER_TEMP/base-src" --out "$RUNNER_TEMP/base"

- name: Measure
env:
EVENT: ${{ github.event_name }}
run: |
mkdir -p "$RUNNER_TEMP/reports"
base=()
if [ "$EVENT" = "pull_request" ]; then base=(--base "$RUNNER_TEMP/base"); fi
pnpm --silent --filter pipboy pipboy footprint --pr "$RUNNER_TEMP/pr" "${base[@]}" --out "$RUNNER_TEMP/reports/footprint.json"

- if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: report-footprint
path: ${{ runner.temp }}/reports/footprint.json
if-no-files-found: ignore
retention-days: 30

hygiene:
name: hygiene
needs: changes
if: needs.changes.outputs.rust == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
sandbox-agent/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile --filter 'pipboy...'
- uses: taiki-e/install-action@8cca0918f8471c33515202b73f56ab27f2bed283 # v2.87.29
with:
tool: cargo-deny@0.20.2,cargo-shear@1.14.0
# cargo deny reads every crate's manifest; caching the registry skips the downloads.
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
key: cargo-registry-${{ hashFiles('sandbox-agent/Cargo.lock') }}
restore-keys: cargo-registry-
- name: Check out the base
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ needs.changes.outputs.base }}
run: git worktree add "$RUNNER_TEMP/base-src" "$BASE_SHA"
- name: Diff findings
env:
EVENT: ${{ github.event_name }}
run: |
mkdir -p "$RUNNER_TEMP/reports"
base=()
if [ "$EVENT" = "pull_request" ]; then base=(--base-root "$RUNNER_TEMP/base-src"); fi
pnpm --silent --filter pipboy pipboy hygiene "${base[@]}" --out "$RUNNER_TEMP/reports/hygiene.json"
- if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: report-hygiene
path: ${{ runner.temp }}/reports/hygiene.json
if-no-files-found: ignore
retention-days: 30

native:
name: native · ${{ matrix.platform }}
needs: changes
if: needs.changes.outputs.native != 'none'
runs-on: ${{ matrix.runner }}
timeout-minutes: 90
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.changes.outputs.native_matrix) }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
# The release's own build script and Dockerfiles, so the size is what ships.
- name: Build with the release script
working-directory: sandbox-agent
env:
TARGET: ${{ matrix.target }}
run: docker/release/build.sh "$TARGET"
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
# No pnpm cache: this job runs fork code on a Depot runner and doesn't need one.
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
- run: pnpm install --frozen-lockfile --filter 'pipboy...'
- name: Measure
env:
TARGET: ${{ matrix.target }}
PLATFORM: ${{ matrix.platform }}
EXT: ${{ matrix.ext }}
run: |
mkdir -p "$RUNNER_TEMP/reports"
for binary in sandbox-agent gigacode; do
pnpm --silent --filter pipboy pipboy native --target "$binary" --platform "$PLATFORM" \
--artifact "$GITHUB_WORKSPACE/sandbox-agent/dist/$binary-$TARGET$EXT" --out "$RUNNER_TEMP/reports/native-$binary-$PLATFORM.json"
done
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: report-native-sandbox-agent-${{ matrix.platform }}
path: ${{ runner.temp }}/reports/native-sandbox-agent-${{ matrix.platform }}.json
retention-days: 90
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: report-native-gigacode-${{ matrix.platform }}
path: ${{ runner.temp }}/reports/native-gigacode-${{ matrix.platform }}.json
retention-days: 90

report:
name: report
needs: [changes, footprint, hygiene, native]
# Not `always()`: a run cancelled by a newer push must not post its partial results.
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
actions: read # job timings, report artifacts, and native baselines from main
pull-requests: write # the one Pip-Boy comment
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24
cache: pnpm
cache-dependency-path: |
pnpm-lock.yaml
sandbox-agent/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile --filter 'pipboy...'
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: report-*
path: ${{ runner.temp }}/reports
merge-multiple: true
- name: Write the comment and run summary
env:
GITHUB_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ github.token }}
# Empty for pushes and fork PRs: they get the run summary only.
PR_NUMBER: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.number || '' }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
BASE_HISTORY: ${{ needs.changes.outputs.base_history }}
# Each check job's result, so a failed job is reported as failed, not skipped.
JOB_RESULTS: ${{ toJSON(needs) }}
run: |
pnpm --silent --filter pipboy pipboy report --reports "$RUNNER_TEMP/reports" \
--base-history "$BASE_HISTORY"

zizmor:
name: zizmor
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- run: uvx zizmor@1.31.1 --offline --min-severity low .github/workflows/pipboy.yml
Loading
Loading