Skip to content

v5: fix partial-stage repair bug, cut redundant downloads #958

v5: fix partial-stage repair bug, cut redundant downloads

v5: fix partial-stage repair bug, cut redundant downloads #958

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
inputs:
hosted_e2e:
# Underscored on purpose: `inputs.hosted-e2e` is not valid expression
# syntax (a hyphenated name needs `inputs['hosted-e2e']`).
description: 'hosted-e2e: auto (obey vars.HOSTED_E2E_DISABLED) | force | skip'
type: choice
default: auto
options: [auto, force, skip]
permissions:
contents: read
# Supersede stale runs on force-push / rapid PR updates. The `main` guard is
# load-bearing: main runs are the ONLY rust-cache writers (save-if), so they
# must never be cancelled mid-save.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
clippy:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
# rustup is pre-installed on GitHub-hosted runners. `rustup show`
# reads rust-toolchain.toml in the repo root, then installs the
# pinned channel + listed components if missing. No third-party
# action dependency needed for toolchain setup.
run: rustup show
- name: Cache cargo
# Swatinem/rust-cache instead of a raw actions/cache of the whole
# target/ dir: it prunes the cache to dependency artifacts (~5-10x
# smaller), which keeps this repo's total cache footprint inside
# GitHub's 10 GiB budget (a raw target/ cache let every PR save evict
# main's caches). save-if restricts writes to main so PR branches
# restore without churning the budget.
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run clippy
run: cargo clippy --workspace --all-features -- -D warnings
# Moved-module aliases (patch::vendor → vendor, patch::go_mod_edit →
# vendor::go_mod_edit, patch::go_redirect → patch::redirect::golang_local)
# exist only for external consumers of the published core crate.
# #[deprecated] on a `pub use` re-export emits no warnings
# (rust-lang/rust#30827), so the compiler cannot pressure internal code
# off the old paths — this grep is the guard instead.
- name: Reject internal uses of moved-module alias paths
run: |
if grep -rn --include='*.rs' \
-e 'patch::vendor' -e 'patch::go_mod_edit' \
-e 'patch::go_redirect' -e 'patch::bun_lock_text' \
-e 'utils::telemetry' -e 'utils::cleanup_blobs' \
-e 'utils::date' -e 'utils::fuzzy_match' \
-e 'gem_setup::' -e 'composer_setup::' -e 'pth_hook::' \
crates; then
echo '::error::use the canonical module paths (crate::vendor, patch::redirect::golang_local, crate::telemetry, manifest::cleanup_blobs, api::date, crawlers::fuzzy_match); the old-path aliases exist only for external consumers'
exit 1
fi
# The napi addon is only ever loaded by Node, so cargo's own tests never
# exercise its JS loader or the engine/provider boundary.
node-addon:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
run: rustup show
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20.20.2'
- name: Build addon
env:
SOCKET_PATCH_NODE_CARGO_PROFILE: dev
run: node crates/socket-patch-node/npm/scripts/build-addon.mjs
- name: Smoke-test addon
run: node --test crates/socket-patch-node/npm/test/smoke.mjs
# Lint the out-of-workspace packaging artifacts: the RubyGems CLI launcher
# gem + the Bundler plugin gem (Ruby), and the curl|sh installer. Ruby is
# pre-installed on the ubuntu-latest runner.
lint-ecosystems:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Ruby — syntax-check + build the launcher gem and Bundler plugin
run: |
( cd gem/socket-patch && ruby -c lib/socket_patch/launcher.rb && ruby -c exe/socket-patch && gem build socket-patch.gemspec )
( cd gem/socket-patch-bundler && ruby -c plugins.rb && gem build socket-patch-bundler.gemspec )
# The generated-plugin templates are pure Ruby — keep them parseable.
ruby -c crates/socket-patch-core/src/setup/gem/templates/plugins.rb.tmpl
ruby -c crates/socket-patch-core/src/setup/gem/templates/gemspec.tmpl
- name: Python — test native installer harnesses
run: python3 -B -m unittest discover -s scripts/tests -v
- name: Shell — shellcheck the curl|sh installer
# install.sh is the other distribution artifact this job lints; it
# had no coverage anywhere before the self-update work touched the
# same surface. shellcheck is pre-installed on ubuntu-latest.
run: shellcheck --shell=sh scripts/install.sh
- name: Shell — run the installer end to end
# shellcheck proves the script parses; this proves it installs. The
# script is what install.socket.dev/patch serves and what the README
# tells people to pipe into a shell, so "it downloads the latest
# release, verifies SHA256SUMS, and produces a binary that runs" is
# worth asserting on every PR rather than discovering from a user.
# Installs the LATEST RELEASE, not this checkout — on a version-bump PR
# that is deliberately the previous version.
run: |
sh scripts/install.sh
command -v socket-patch
socket-patch --version
- name: Shell — run the installer against an alternate origin
# Exercises SOCKET_PATCH_BASE_URL (and SOCKET_PATCH_INSTALL_DIR) with a
# base that is not the default. Uses GitHub's own releases base, which
# is the same URL shape install.socket.dev serves, so the template the
# script builds is covered regardless of whether the Socket relay is
# deployed yet. The dedicated Socket-origin check is the next step.
run: |
SOCKET_PATCH_BASE_URL=https://github.com/SocketDev/socket-patch/releases \
SOCKET_PATCH_INSTALL_DIR="$RUNNER_TEMP/alt-origin" \
sh scripts/install.sh
"$RUNNER_TEMP/alt-origin/socket-patch" --version
- name: Shell — install through install.socket.dev, once it exists
# The whole point of the relay is that a client never has to reach
# github.com. That is only assertable against the deployed host, so this
# step skips itself until the host resolves rather than being red from
# the day it merges (same posture as the installer-drift workflow).
run: |
if ! curl -sfI -m 20 https://install.socket.dev/patch/latest >/dev/null 2>&1; then
echo "::notice::install.socket.dev/patch/latest does not answer yet — skipping the Socket-origin install."
exit 0
fi
latest=$(curl -fsSL -m 20 https://install.socket.dev/patch/latest)
echo "install.socket.dev reports latest=$latest"
SOCKET_PATCH_BASE_URL=https://install.socket.dev/patch/SocketDev/socket-patch/releases \
SOCKET_PATCH_INSTALL_DIR="$RUNNER_TEMP/socket-origin" \
sh scripts/install.sh
installed=$("$RUNNER_TEMP/socket-origin/socket-patch" --version | awk '{print $NF}')
if [ "$installed" != "$latest" ]; then
echo "::error::install.socket.dev says latest is $latest but installed $installed" >&2
exit 1
fi
- name: Shell — the installer URL is consistent across the docs
# The README, the script's own usage comment, and the hosting runbook
# all name the canonical URL. Keeping them in lockstep is the whole
# promise of install.socket.dev/patch being "a copy of this file".
run: |
for f in README.md scripts/install.sh docs/installer-hosting.md; do
if ! grep -qF 'https://install.socket.dev/patch' "$f"; then
echo "Error: $f no longer references https://install.socket.dev/patch" >&2
exit 1
fi
done
- name: Shell — shellcheck the release scripts
run: shellcheck scripts/version-sync.sh scripts/bump-version.sh scripts/release-lint.sh scripts/dispatch-publish.sh
# Release-readiness gate (scripts/release-lint.sh — the same checks the
# Release workflow's `version` job runs before publishing anything):
# - every PR/push: version coherence — version-sync.sh must be a no-op,
# so a hand-edited version in any single packaging site fails CI here
# instead of surfacing mid-release;
# - PRs that bump the workspace version (release/vX.Y.Z bump PRs): the
# full gate — CHANGELOG has a dated, non-empty section for the new
# version and the tag doesn't already exist.
release-readiness:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Lint release readiness
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
# Compare the workspace version against the PR base to detect a
# version bump. The shallow checkout doesn't have the base
# commit; fetch just that object.
git fetch --quiet --depth 1 origin "$BASE_SHA"
BASE_VERSION="$(git show "$BASE_SHA:Cargo.toml" | grep '^version = ' | head -1 | sed 's/version = "\(.*\)"/\1/')"
HEAD_VERSION="$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')"
if [ "$BASE_VERSION" != "$HEAD_VERSION" ]; then
echo "Version bump PR detected ($BASE_VERSION -> $HEAD_VERSION); running the full release gate."
bash scripts/release-lint.sh --tag-check
exit 0
fi
fi
bash scripts/release-lint.sh --sync-only
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
# Windows runs the same suite ~1.6x slower than macOS: on the base
# branch it already took 34m40s of a flat 35m budget.
timeout-minutes: ${{ matrix.os == 'windows-latest' && 50 || 35 }}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
# rustup is pre-installed on GitHub-hosted runners. `rustup show`
# reads rust-toolchain.toml in the repo root, then installs the
# pinned channel + listed components if missing. No third-party
# action dependency needed for toolchain setup.
run: rustup show
- name: Cache cargo
# Swatinem/rust-cache, main-only saves: see the first `Cache cargo`
# step in this file.
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build
run: cargo build --workspace --all-features
- name: Install Go (for vexctl)
# The `vex` subcommand emits OpenVEX documents; tests/e2e_vex.rs
# validates the output with vexctl when it's on PATH. vexctl is
# a Go binary distributed via `go install`. Setting up Go here
# is the cheapest way to give every test job a usable vexctl.
# Go must be >= 1.24: its linker only began emitting an LC_UUID load
# command then, and the macOS-latest runner's dyld (Sequoia+) refuses
# to load a Mach-O binary without one ("missing LC_UUID load command"),
# so a 1.22-built vexctl crashes on launch and every e2e_vex assertion
# fails. ubuntu/windows are unaffected, but the matrix shares this pin.
# SHA pin resolved from `gh api repos/actions/setup-go/git/refs/tags/v6.4.0`.
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: '1.24'
cache: false
- name: Install vexctl
# `go install` puts the binary in $(go env GOPATH)/bin; surface
# that path to subsequent steps so `Command::new("vexctl")` in
# the test resolves. Pinned to a tagged release rather than
# @latest for reproducibility.
#
# Retried: the install compiles sigstore/cosign, whose module
# verification reads dozens of sum.golang.org checksum tiles, and
# transient INTERNAL_ERROR stream resets there have failed this
# step on otherwise-green runs. The backoff rides out short
# resets; a persistent outage still fails loudly on the last
# attempt. Follow-up option if this recurs: install the pinned
# release BINARY (sha256-pinned) instead of compiling, which
# sidesteps module verification and drops ~100s of compile time per
# leg.
shell: bash
run: |
for attempt in 1 2 3 4 5; do
if go install github.com/openvex/vexctl@v0.3.0; then
break
fi
if [ "$attempt" = 5 ]; then
echo "::error::go install vexctl failed on all 5 attempts"
exit 1
fi
echo "::warning::go install vexctl attempt $attempt failed; retrying"
sleep $((attempt * 20))
done
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- name: Run tests
# `--all-features` would also RUN the docker-e2e / setup-e2e suites,
# which soft-skip as "ok" in this job (no images are built here, and
# macOS/Windows have no Docker at all) — dozens of fake greens per OS
# that would hide a broken skip-guard behind a passing checkmark.
# Build them with --all-features (compile rot is real coverage), but
# run only the default-feature suites; the dedicated e2e-docker and
# setup-matrix jobs run the gated suites for real.
#
# `--no-fail-fast`: without it cargo stops at the first failing test
# BINARY, so one bad file hides every later binary's result on that
# OS. Run them all and fail at the end instead.
shell: bash
env:
# The real-go hosted/vendored suites (`#![cfg(unix)]`) ride the Go
# installed above for vexctl: fail instead of skip without `go` /
# `zip`, and assert the pinned release.
SOCKET_PATCH_GO_E2E_REQUIRED: '1'
SOCKET_PATCH_GO_E2E_VERSION: '1.24'
run: |
set -euo pipefail
cargo test --workspace --all-features --no-run
cargo test --workspace --no-fail-fast
test-release:
runs-on: ubuntu-latest
# Every tests/ target is its own optimized link, and the two cargo
# invocations below build the graph twice (--all-features, then the
# default features): ~25m on main with ~240 test binaries, so 30m left
# no headroom as suites grow. The manifest-less VEX suites share two
# multi-module binaries (tests/e2e_vex_lockfile/, tests/e2e_vex_build/)
# to keep the count down; the extra 10m covers the rest.
timeout-minutes: 40
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
# rustup is pre-installed on GitHub-hosted runners. `rustup show`
# reads rust-toolchain.toml in the repo root, then installs the
# pinned channel + listed components if missing. No third-party
# action dependency needed for toolchain setup.
run: rustup show
- name: Cache cargo
# Swatinem/rust-cache, main-only saves: see the first `Cache cargo`
# step in this file.
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run tests (release)
# `ci-release` = [profile.release] minus the full-LTO link (see the
# profile's comment in Cargo.toml). Same opt-level/debug-assertion
# semantics this job exists to validate; ~23m of LTO relinking gone.
# Build/run split for the same reason as the `test` job: the gated
# docker-e2e / setup-e2e suites only soft-skip here — compile them,
# don't count their skips as passes.
run: |
set -euo pipefail
cargo test --workspace --all-features --profile ci-release --no-run
cargo test --workspace --profile ci-release
coverage:
# Code coverage via cargo-llvm-cov (LLVM source-based instrumentation).
# Reports as a markdown table in the job summary and uploads the raw
# lcov.info file as a workflow artifact. No threshold gating — this is
# report-only so contributors get visibility without flaky CI when
# coverage shifts naturally with test edits.
runs-on: ubuntu-latest
timeout-minutes: 35
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
# `rustup show` installs the rust-toolchain.toml channel + listed
# components; `rustup component add` adds the llvm-tools-preview
# bits cargo-llvm-cov needs to merge .profraw files into lcov.
run: |
rustup show
rustup component add llvm-tools-preview
- name: Install cargo-llvm-cov
# taiki-e/install-action ships precompiled binaries — much faster
# than `cargo install` and avoids a per-CI-run compile.
uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2.79.3
with:
tool: cargo-llvm-cov@0.8.7
- name: Cache cargo
# Swatinem/rust-cache, main-only saves: see the first `Cache cargo`
# step in this file.
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run tests with coverage
# Two-step pattern: `--no-report` runs instrumented tests and
# collects the raw profile data, then the two `report` calls
# emit lcov + summary from the same data. Avoids re-running
# tests twice. The output filename matches the `*.lcov`
# gitignore pattern so a stray local run can't accidentally
# commit a 600 KB report.
#
# Default features (instead of --all-features) exclude the
# docker-e2e feature — those tests need Docker images this job
# doesn't build. The coverage-docker matrix covers them
# separately, and coverage-merge stitches everything together.
run: |
cargo llvm-cov --workspace \
--no-report
cargo llvm-cov report --lcov --output-path coverage-host.lcov
cargo llvm-cov report --summary-only | tee coverage-summary.txt
- name: Publish coverage summary to job summary
# Render the per-file table cargo-llvm-cov prints as a fenced
# block in the GitHub Actions job summary so reviewers don't
# need to crack open the artifact for a quick look.
run: |
{
echo "## Host coverage summary"
echo ""
echo "(In-process tests only. See coverage-merge for the"
echo "full picture including docker-e2e binary coverage.)"
echo ""
echo '```'
cat coverage-summary.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload host LCOV artifact
uses: ./.github/actions/upload-artifact
with:
name: coverage-host
path: coverage-host.lcov
if-no-files-found: error
retention-days: 30
coverage-docker:
# Per-ecosystem coverage for the Docker-driven e2e suite. Mirrors
# the e2e-docker matrix but builds an instrumented socket-patch
# binary and mounts it into the container along with a host-
# visible profraw directory, so the in-container code paths
# contribute to the lcov merge.
#
# Hooks: docker_e2e_<eco>.rs reads SOCKET_PATCH_COV_BIN +
# SOCKET_PATCH_COV_PROFRAW_DIR. Both unset is the no-op default
# (used by the e2e-docker matrix below).
#
# Pin to ubuntu-22.04 (glibc 2.35) instead of ubuntu-latest
# (currently 24.04, glibc 2.39). The instrumented binary built
# here gets mounted into the debian:12-slim test container
# (glibc 2.36); a binary linked against a newer glibc than the
# container ships fails to load. ubuntu-22.04's older glibc is
# the highest base that's forward-compatible with debian:12.
runs-on: ubuntu-22.04
timeout-minutes: 30
permissions:
contents: read
strategy:
fail-fast: false
matrix:
ecosystem: [npm, pypi, gem, cargo, golang, maven, composer, nuget, deno]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Docker Buildx
# `driver: docker` makes buildx use the host docker daemon directly
# rather than running BuildKit in its own container. This is what
# lets the per-ecosystem image build see the locally-tagged
# `socket-patch-test-base:latest` from the previous step (with the
# default container driver, BuildKit runs in a sandbox that cannot
# see the host daemon's image store and tries to pull base from
# docker.io, which fails). The trade-off is that `type=gha` cache
# exports aren't supported under the docker driver — we accept
# rebuilding the images per job for correctness.
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
with:
driver: docker
- name: Install Rust
# `rustup show` consumes rust-toolchain.toml; the explicit
# `component add` covers llvm-tools-preview for cargo-llvm-cov.
run: |
rustup show
rustup component add llvm-tools-preview
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2.79.3
with:
tool: cargo-llvm-cov@0.8.7
# No `actions/cache` here intentionally. This job builds Docker
# images and would be flagged by zizmor's cache-poisoning audit
# (a PR-poisoned cargo cache could compromise the instrumented
# binary we mount into the container).
- name: Build base image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: tests/docker/Dockerfile.base
tags: socket-patch-test-base:latest
load: true
- name: Build ${{ matrix.ecosystem }} image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: tests/docker/Dockerfile.${{ matrix.ecosystem }}
tags: socket-patch-test-${{ matrix.ecosystem }}:latest
load: true
- name: Build instrumented socket-patch binary
# Source `cargo llvm-cov show-env` into the current shell so this
# `cargo build` picks up RUSTC_WRAPPER=cargo-llvm-cov and the
# same RUSTFLAGS that the subsequent `cargo llvm-cov` test step
# will use. The bin we build ends up byte-compatible with the
# test binaries — same source hashes → unified coverage map at
# report time. Env stays scoped to this step (intentional;
# cargo llvm-cov manages its own env in the test step).
run: |
eval "$(cargo llvm-cov show-env --export-prefix 2>/dev/null)"
cargo build --bin socket-patch
- name: Configure docker-e2e coverage hooks
run: |
echo "SOCKET_PATCH_COV_BIN=$PWD/target/debug/socket-patch" >> "$GITHUB_ENV"
# Profraw files from the in-container binary land here.
# cargo-llvm-cov scans target/ for *.profraw at report time.
echo "SOCKET_PATCH_COV_PROFRAW_DIR=$PWD/target" >> "$GITHUB_ENV"
- name: Run ${{ matrix.ecosystem }} Docker e2e test with coverage
run: |
# Vendor build-proof capstones ride the same image as their
# ecosystem's main suite (extend the case as new vendor suites land).
EXTRA=""
case "${{ matrix.ecosystem }}" in
composer) EXTRA="--test docker_e2e_vendor_composer" ;;
gem) EXTRA="--test docker_e2e_vendor_gem" ;;
maven) EXTRA="--test docker_e2e_vendor_maven" ;;
nuget) EXTRA="--test docker_e2e_vendor_nuget" ;;
pypi) EXTRA="--test docker_e2e_vendor_pypi_pm" ;;
esac
# shellcheck disable=SC2086 # EXTRA is intentionally word-split
cargo llvm-cov \
--features docker-e2e \
--no-report \
--test docker_e2e_${{ matrix.ecosystem }} $EXTRA
- name: Generate per-ecosystem lcov
run: |
cargo llvm-cov report \
--lcov \
--output-path coverage-docker-${{ matrix.ecosystem }}.lcov
- name: Upload per-ecosystem LCOV artifact
uses: ./.github/actions/upload-artifact
with:
name: coverage-docker-${{ matrix.ecosystem }}
path: coverage-docker-${{ matrix.ecosystem }}.lcov
if-no-files-found: error
retention-days: 30
coverage-merge:
# Merge the host coverage and per-ecosystem docker coverage into a
# single lcov.info. lcov(1) handles the union — same files are
# summed line-by-line so a line covered by ANY test counts.
needs: [coverage, coverage-docker]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install lcov
run: sudo apt-get update && sudo apt-get install -y lcov
- name: Download all coverage artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: coverage-artifacts
pattern: coverage-{host*,docker-*}
# Retries have distinct artifact names. Their LCOV filenames stay
# stable, so a lost finalization response cannot double the counts.
merge-multiple: true
- name: Merge LCOV files
# `--add-tracefile` is repeated per input. lcov sums hit counts
# for identical source/line keys, so files covered by both host
# and docker tests report the higher (union) count.
# `find` (not bash globstar) for portability across runners.
run: |
set -e
ARGS=()
while IFS= read -r f; do
ARGS+=(--add-tracefile "$f")
done < <(find coverage-artifacts -name '*.lcov' -type f)
if [ ${#ARGS[@]} -eq 0 ]; then
echo "No lcov files found to merge" >&2
exit 1
fi
lcov "${ARGS[@]}" --output-file coverage.lcov
- name: Render summary
# `lcov --summary` prints a per-file rollup we tee into the job
# summary, same shape as cargo-llvm-cov's own.
run: |
{
echo "## Coverage (host + docker-e2e merged)"
echo ""
echo '```'
lcov --summary coverage.lcov 2>&1 | tail -20
echo '```'
echo ""
echo "Full merged LCOV uploaded as the \`coverage-lcov\` artifact."
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload merged LCOV artifact
uses: ./.github/actions/upload-artifact
with:
name: coverage-lcov
path: coverage.lcov
if-no-files-found: error
retention-days: 30
dispatch-tests:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20.20.2'
- name: Run npm dispatch tests
run: node --test npm/socket-patch/bin/socket-patch.test.mjs
- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12.x'
- name: Run pypi dispatch tests
run: python pypi/socket-patch/test_dispatch.py
e2e:
needs: test
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
suite: e2e_cargo
- os: ubuntu-latest
suite: e2e_golang
- os: ubuntu-latest
suite: e2e_maven
- os: ubuntu-latest
suite: e2e_composer
# composer is a shipped ecosystem, so e2e_composer's tests are
# NOT `#[ignore]`-gated the way the live-registry maven/nuget
# suites are — the matrix default `--ignored` filter
# selected zero tests here and the leg passed vacuously.
# `--include-ignored` runs them, plus any capstone added later.
test_filter: --include-ignored
- os: ubuntu-latest
suite: e2e_nuget
# Host build-proof capstones: fresh-checkout install + revert
# against the REAL composer/bundler toolchains, each ending in the
# manifest-less VEX matrix. `#[ignore]`-gated (the unpinned `test`
# job skips them); `composer:` / `bundler:` install that exact
# toolchain below, runs them via `--ignored`, and exports the
# suites' `_REQUIRED` + `_VERSION` gates so a leg hard-fails
# instead of skipping on a missing or wrong toolchain.
# ubuntu-latest only — they need the pinned toolchain, not per-OS
# coverage.
#
# composer: 1 (packagist stopped serving composer 1 on 2025-09-01,
# so the fixture resolves from an inline repository), 2.2 LTS (the
# other release with the git-source download fallback the hosted
# redirect must drop) and current 2.
- {os: ubuntu-latest, suite: e2e_vendor_composer_build, composer: '2'}
- {os: ubuntu-latest, suite: e2e_vendor_composer_build, composer: '2.2'}
- {os: ubuntu-latest, suite: e2e_vendor_composer_build, composer: '1'}
- {os: ubuntu-latest, suite: e2e_redirect_composer_build, composer: '2'}
- {os: ubuntu-latest, suite: e2e_redirect_composer_build, composer: '2.2'}
- {os: ubuntu-latest, suite: e2e_redirect_composer_build, composer: '1'}
# setup-e2e host guards run in no other job (test job = default
# features; setup-matrix job = shell script).
- {os: ubuntu-latest, suite: setup_matrix_composer, test_filter: host_guard}
# Real-bundler gem capstones, one leg per bundler era. Boundaries:
# 1.17/2.1 merged GEM section, 2.2 separate sections, 2.5 last
# pre-CHECKSUMS, 2.6 CHECKSUMS, 4.0.15/4.0.21 before/after the
# strict frozen check (rubygems#9750). bundler <= 2.2 needs
# Ruby <= 3.3 and 1.17-2.1 need Ruby <= 3.1 (`untaint`).
- {os: ubuntu-latest, suite: e2e_redirect_gem_build, ruby: '3.1', bundler: '1.17.3'}
- {os: ubuntu-latest, suite: e2e_redirect_gem_build, ruby: '3.1', bundler: '2.1.4'}
- {os: ubuntu-latest, suite: e2e_redirect_gem_build, ruby: '3.1', bundler: '2.2.33'}
- {os: ubuntu-latest, suite: e2e_redirect_gem_build, ruby: '3.3', bundler: '2.5.23'}
- {os: ubuntu-latest, suite: e2e_redirect_gem_build, ruby: '3.3', bundler: '2.6.9'}
- {os: ubuntu-latest, suite: e2e_redirect_gem_build, ruby: '3.3', bundler: '2.7.2'}
- {os: ubuntu-latest, suite: e2e_redirect_gem_build, ruby: '3.4', bundler: '4.0.15'}
- {os: ubuntu-latest, suite: e2e_redirect_gem_build, ruby: '3.4', bundler: '4.0.21'}
- {os: ubuntu-latest, suite: e2e_vendor_gem_build, ruby: '3.1', bundler: '1.17.3'}
- {os: ubuntu-latest, suite: e2e_vendor_gem_build, ruby: '3.1', bundler: '2.1.4'}
- {os: ubuntu-latest, suite: e2e_vendor_gem_build, ruby: '3.1', bundler: '2.2.33'}
- {os: ubuntu-latest, suite: e2e_vendor_gem_build, ruby: '3.3', bundler: '2.5.23'}
- {os: ubuntu-latest, suite: e2e_vendor_gem_build, ruby: '3.3', bundler: '2.6.9'}
- {os: ubuntu-latest, suite: e2e_vendor_gem_build, ruby: '3.3', bundler: '2.7.2'}
- {os: ubuntu-latest, suite: e2e_vendor_gem_build, ruby: '3.4', bundler: '4.0.15'}
- {os: ubuntu-latest, suite: e2e_vendor_gem_build, ruby: '3.4', bundler: '4.0.21'}
# not #[ignore]-gated -> --include-ignored is mandatory
- {os: ubuntu-latest, suite: setup_matrix_gem, ruby: '3.3', bundler: '2.7.2', test_filter: --include-ignored}
# The live-API smoke suites (e2e_npm, e2e_pypi, e2e_gem,
# e2e_scan) are intentionally NOT in the PR matrix — their
# `#[ignore]`-gated tests hit the real public proxy at
# patches-api.socket.dev, which intermittently returns
# 503 "Service temporarily over capacity" outside this
# repo's control. Run on demand:
#
# cargo test -p socket-patch-cli --test e2e_npm -- --ignored
# cargo test -p socket-patch-cli --test e2e_pypi -- --ignored
# cargo test -p socket-patch-cli --test e2e_gem -- --ignored
# cargo test -p socket-patch-cli --test e2e_scan -- --ignored
#
# Same policy for the self-update live smoke (hits real
# github.com releases; catches asset-naming/redirect/SUMS
# drift against the published pipeline — most useful right
# after a release):
#
# cargo test -p socket-patch-cli --test self_update_e2e -- --ignored
#
# PR-time coverage for the same code paths comes from the
# `e2e-docker` matrix below, which runs the same flow
# against a hermetic wiremock fixture.
# Safety-hardening e2e suites. The fast non-ignored ones
# (e2e_safety_lock, e2e_safety_yarn_pnp) run via the
# standard `test` job above on all three platforms, so no
# matrix entry is needed for them. The two below need real
# toolchains and are #[ignore]-gated.
- os: ubuntu-latest
suite: e2e_safety_cargo_build
- os: macos-latest
suite: e2e_safety_cargo_build
- os: windows-latest
suite: e2e_safety_cargo_build
- os: ubuntu-latest
suite: e2e_safety_pnpm
- os: macos-latest
suite: e2e_safety_pnpm
# pnpm-on-Windows uses junctions for symlinks and copies
# (not hardlinks) by default, so the CoW invariant holds
# vacuously. Test still runs to verify apply doesn't error
# on Windows — semantic Windows nlink coverage is a
# follow-up (`std::fs::Metadata` doesn't expose nlink on
# Windows; needs `GetFileInformationByHandle` via
# `windows-sys`).
- os: windows-latest
suite: e2e_safety_pnpm
# Wall-bound real-package-manager redirect capstones (~150s and
# ~70s of network installs + bootstrap resolutions — profile-
# insensitive, measured identical in debug and release). They ran
# inside the serial `test` job on every OS; #[ignore]-gated out of
# it and relocated here so they still run on every PR and every
# OS, but in parallel off the critical path. They use the runner's
# default node/corepack, exactly as they did inside `test` — no
# setup-node step, no version change.
#
# `npm_required` turns the npm suites' "npm not installed" soft-skip
# into a hard failure. Not on Windows: `Command::new("npm")` cannot
# resolve `npm.cmd` there, so that leg still skips (a known gap; see
# docs/testing/npm-compatibility.md).
- os: ubuntu-latest
suite: e2e_redirect_npm_build
npm_required: '1'
- os: macos-latest
suite: e2e_redirect_npm_build
npm_required: '1'
- os: windows-latest
suite: e2e_redirect_npm_build
- os: ubuntu-latest
suite: e2e_redirect_rush_sim
- os: macos-latest
suite: e2e_redirect_rush_sim
- os: windows-latest
suite: e2e_redirect_rush_sim
# Hermetic real-bun capstones (wiremock patch service, real `bun
# install`): hosted (`e2e_redirect_bun_build`), vendored
# (`e2e_vendor_bun_build`) and the hosted⇄vendored takeover /
# scoped-rollback suite (`mode_migration_bun`). They are NOT
# `#[ignore]`-gated, so `test_filter: --include-ignored` is
# mandatory — the job default `-- --ignored` would select zero
# tests and pass vacuously (the e2e_composer trap above). The
# runner images ship no bun, so without the `bun:` key below the
# suites soft-skip; `bun:` installs that exact release via
# setup-bun and exports SOCKET_PATCH_BUN_E2E_REQUIRED=1 (+ the
# pinned version), under which the suites hard-fail instead of
# skipping when bun is missing, the wrong version, or the fixture
# install produces no text lock.
#
# Lock-era legs (ubuntu only): bun's text lock has three
# grammars — lockfileVersion 0 (opt-in `--save-text-lockfile`,
# 1.1.39–1.1.45; 2-tuple workspace entries), 1 (default from
# 1.2.0 through 1.3.x) and 2 (1.4.0+). Registry 4-tuples are
# identical across them but the workspace grammar, the lockb
# migration recipe and tarball digest enforcement (URL/local
# tarball sha512 checked only from 1.3.10) all differ, so the
# latest release alone cannot prove the rewrite + fresh install
# round-trip on the locks real projects commit. 1.1.45 = last v0
# writer, 1.2.23 = v1, 1.3.14 = last pre-v2 default, 1.4.2 = v2.
- os: ubuntu-latest
suite: e2e_redirect_bun_build
bun: '1.4.2'
test_filter: --include-ignored
- os: macos-latest
suite: e2e_redirect_bun_build
bun: '1.4.2'
test_filter: --include-ignored
- os: windows-latest
suite: e2e_redirect_bun_build
bun: '1.4.2'
test_filter: --include-ignored
- os: ubuntu-latest
suite: e2e_redirect_bun_build
bun: '1.1.45'
test_filter: --include-ignored
- os: ubuntu-latest
suite: e2e_redirect_bun_build
bun: '1.2.23'
test_filter: --include-ignored
- os: ubuntu-latest
suite: e2e_vendor_bun_build
bun: '1.4.2'
test_filter: --include-ignored
- os: macos-latest
suite: e2e_vendor_bun_build
bun: '1.4.2'
test_filter: --include-ignored
- os: windows-latest
suite: e2e_vendor_bun_build
bun: '1.4.2'
test_filter: --include-ignored
- os: ubuntu-latest
suite: e2e_vendor_bun_build
bun: '1.1.45'
test_filter: --include-ignored
- os: ubuntu-latest
suite: e2e_vendor_bun_build
bun: '1.2.23'
test_filter: --include-ignored
- os: ubuntu-latest
suite: mode_migration_bun
bun: '1.4.2'
test_filter: --include-ignored
- os: macos-latest
suite: mode_migration_bun
bun: '1.4.2'
test_filter: --include-ignored
- os: windows-latest
suite: mode_migration_bun
bun: '1.4.2'
test_filter: --include-ignored
- os: ubuntu-latest
suite: mode_migration_bun
bun: '1.3.14'
test_filter: --include-ignored
# Manifest-less VEX era legs: the last pre-v2 text-lock writer for
# the hosted/vendored capstones, the v0/v1 writers for the takeover
# suite, and the binary bun.lockb era (1.0 / 1.1 lines) through
# e2e_bun_lockb, which reads the SOCKET_PATCH_BUN_LOCKB_* gates
# exported for it below.
- {os: ubuntu-latest, suite: e2e_redirect_bun_build, bun: '1.3.14', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_bun_build, bun: '1.3.14', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: mode_migration_bun, bun: '1.1.45', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: mode_migration_bun, bun: '1.2.23', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.0.36', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.1.45', test_filter: --include-ignored}
# Real-vlt capstones (DESIGN §8.4): wiremock patch service and a local
# npm registry fed from npmjs, driven by the pinned vlt release
# (`node vlt.js`, installed below from a sha512-checked `npm pack`).
# Every test is `#[ignore]`d and named `vlt_pinned_matrix_*`, so the
# filter must be `--include-ignored vlt_pinned_matrix` (the job
# default `--ignored` selects nothing). The run pipes through
# scripts/check-vlt-legs.py, which fails on `0 passed` or any leg
# line the manifest does not predict. The eras: A0 0.0.0-16, A
# 0.0.0-32, B rc.12/rc.14 (rc.14 legs reach public npm), C rc.32,
# D 1.0.4/1.0.7, E 1.1.1, F 1.2.0.
- {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: macos-latest, suite: e2e_redirect_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: windows-latest, suite: e2e_redirect_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '0.0.0-16', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '0.0.0-32', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.0.0-rc.32', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.0.4', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_redirect_vlt_build, vlt: '1.1.1', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: macos-latest, suite: e2e_vendor_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: windows-latest, suite: e2e_vendor_vlt_build, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '0.0.0-32', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '1.0.0-rc.32', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_vendor_vlt_build, vlt: '1.0.4', test_filter: --include-ignored vlt_pinned_matrix}
- {os: windows-latest, suite: e2e_vendor_vlt_build, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: mode_migration_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: macos-latest, suite: mode_migration_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: windows-latest, suite: mode_migration_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: mode_migration_vlt, vlt: '0.0.0-32', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: mode_migration_vlt, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix, vlt_upgrade: '1.2.0'}
- {os: windows-latest, suite: mode_migration_vlt, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix}
# Linux `auto` hardlinks from the global store; every OS gets the
# explicit hardlink linker.
- {os: ubuntu-latest, suite: e2e_safety_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_safety_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix, vlt_store_linker: 'hardlink'}
- {os: macos-latest, suite: e2e_safety_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix, vlt_store_linker: 'hardlink'}
- {os: windows-latest, suite: e2e_safety_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix, vlt_store_linker: 'hardlink'}
# rc.12 gets the definite no-hook advisory; windows rc.14 runs the
# legacy DepIDs on NTFS with pre-junction symlinks.
- {os: ubuntu-latest, suite: e2e_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: macos-latest, suite: e2e_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: windows-latest, suite: e2e_vlt, vlt: '1.2.0', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_vlt, vlt: '0.0.0-32', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_vlt, vlt: '1.0.0-rc.12', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_vlt, vlt: '1.0.0-rc.32', test_filter: --include-ignored vlt_pinned_matrix}
- {os: ubuntu-latest, suite: e2e_vlt, vlt: '1.0.7', test_filter: --include-ignored vlt_pinned_matrix}
- {os: windows-latest, suite: e2e_vlt, vlt: '1.0.0-rc.14', test_filter: --include-ignored vlt_pinned_matrix}
# The named corepack pnpm hosted legs (pnpm 7-11, get-uuid,
# zero-touch, --trust-lockfile). `#[ignore]`d; the pinned matrix
# inside the same suite runs in pnpm-compatibility.yml, hence the
# skip. Node 24 (step below): the
# corepack pnpm@10/11 legs require it.
- {os: ubuntu-latest, suite: e2e_redirect_pnpm_build, test_filter: '--ignored --skip pnpm_pinned_matrix'}
- {os: macos-latest, suite: e2e_redirect_pnpm_build, test_filter: '--ignored --skip pnpm_pinned_matrix'}
- {os: windows-latest, suite: e2e_redirect_pnpm_build, test_filter: '--ignored --skip pnpm_pinned_matrix'}
# Real-uv hosted/vendored capstones ending in manifest-less VEX:
# one leg per uv 0.N line + the 0.5.x boundary (0.5.4 still
# re-resolves a transitive override / rejects a repointed
# constraint under --locked; 0.5.5 keeps both).
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.1.45'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.2.37'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.3.5'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.4.30'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.5.3'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.5.4'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.5.5'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.5.6'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.6.17'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.7.22'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.8.24'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.9.30'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.10.12'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.11.33'}
- {os: ubuntu-latest, suite: e2e_redirect_uv_build, uv: '0.12.17'}
- {os: macos-latest, suite: e2e_redirect_uv_build, uv: '0.12.17'}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.1.45', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.2.37', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.3.5', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.4.30', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.5.3', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.5.4', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.5.5', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.5.6', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.6.17', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.7.22', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.8.24', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.9.30', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.10.12', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.11.33', test_filter: --include-ignored}
- {os: ubuntu-latest, suite: e2e_vendor_pypi_build, uv: '0.12.17', test_filter: --include-ignored}
- {os: macos-latest, suite: e2e_vendor_pypi_build, uv: '0.12.17', test_filter: --include-ignored}
# The Poetry / PDM / Hatch / Pipenv / pip / deno manifest-less VEX
# capstones share ONE test binary (`e2e_vex_build`, a module per
# tool — one optimized link in test-release instead of six), so
# each leg's `test_filter` names its tool's module and keeps
# `--ignored`.
# Real-Poetry hosted + vendored capstones (#[ignore]-gated,
# unix-only). One leg per major / lock format: 1.0 (lock 1.0),
# 1.1 (lock 1.1), 1.8 (lock 2.0), 2.x (lock 2.1).
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.0.10'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.1.15'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '1.8.5'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '2.0.1'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '2.4.3'}
- {os: macos-latest, suite: e2e_vex_build, test_filter: 'poetry:: --ignored', poetry: '2.4.3'}
# Real PDM / Hatch capstones (wiremock Socket API that also serves
# the hosted wheel; PyPI for the tool bootstrap + six).
# PDM: lock 2 (1.4), refused 3.1 (1.15) and 4.2 (2.7), 4.3 (2.8),
# the hishel<1 bootstrap window (2.25) and current.
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '1.4.5'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '1.15.5'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.7.4'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.8.2'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.25.9'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.29.2'}
- {os: macos-latest, suite: e2e_vex_build, test_filter: 'pdm:: --ignored', pdm: '2.29.2'}
# Hatch: 1.0 (hatch.toml env vendoring refused, needs >= 1.2),
# 1.2, the virtualenv<21 window (1.9, 1.14) and current.
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.0.0'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.2.1'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.9.7'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.14.2'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.18.1'}
- {os: macos-latest, suite: e2e_vex_build, test_filter: 'hatch:: --ignored', hatch: '1.18.1'}
# Real Pipenv / pip capstones (mock patch server; the tools are
# bootstrapped from PyPI). `pipenv:` / `pip:` hold one or more
# space-separated releases the suite loops over.
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2022.12.19'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2023.12.1'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2024.4.1'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2025.1.3'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2026.8.0'}
- {os: macos-latest, suite: e2e_vex_build, test_filter: 'pipenv:: --ignored', pipenv: '2022.12.19 2026.8.0'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'pip:: --ignored', pip: '22 23 24 25 26'}
- {os: macos-latest, suite: e2e_vex_build, test_filter: 'pip:: --ignored', pip: '22 26'}
# Real-Maven hosted + vendored capstones, one leg per Maven line:
# 3.6 (pre http-blocker), 3.8 (resolver 1.6: no trusted checksums),
# 3.9 (trusted checksums), 4.0 rc.
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '3.6.3'}
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '3.8.9'}
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '3.9.16'}
- {os: ubuntu-latest, suite: e2e_redirect_maven_build, maven: '4.0.0-rc-6'}
- {os: macos-latest, suite: e2e_redirect_maven_build, maven: '3.9.16'}
- {os: ubuntu-latest, suite: e2e_vendor_maven_build, maven: '3.6.3'}
- {os: ubuntu-latest, suite: e2e_vendor_maven_build, maven: '3.8.9'}
- {os: ubuntu-latest, suite: e2e_vendor_maven_build, maven: '3.9.16'}
- {os: ubuntu-latest, suite: e2e_vendor_maven_build, maven: '4.0.0-rc-6'}
- {os: macos-latest, suite: e2e_vendor_maven_build, maven: '3.9.16'}
# Real .NET SDK capstones: hosted + vendored nuget, one leg per SDK
# major (the suite pins the major through a sandbox global.json).
- {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '6'}
- {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '7'}
- {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '8'}
- {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '9'}
- {os: ubuntu-latest, suite: e2e_nuget_dotnet_build, dotnet: '10'}
- {os: macos-latest, suite: e2e_nuget_dotnet_build, dotnet: '8'}
# Real deno negative capstone (no hosted/vendored wiring exists for
# deno; VEX must attest nothing), one leg per major.
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'deno:: --ignored', deno: '1.46.3'}
- {os: ubuntu-latest, suite: e2e_vex_build, test_filter: 'deno:: --ignored', deno: '2.9.7'}
runs-on: ${{ matrix.os }}
# The real-toolchain capstones loop several releases per leg (pip,
# pipenv) or bootstrap a tool from PyPI before the suite (poetry, pdm,
# hatch), hence more than the 25 minutes the older legs needed.
timeout-minutes: 40
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
# rustup is pre-installed on GitHub-hosted runners. `rustup show`
# reads rust-toolchain.toml in the repo root, then installs the
# pinned channel + listed components if missing. No third-party
# action dependency needed for toolchain setup.
run: rustup show
- name: Cache cargo
# Swatinem/rust-cache, main-only saves: see the first `Cache cargo`
# step in this file.
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# Matrix suites otherwise collide on one key: only one of the ~9
# same-OS legs wins the cache reserve and the rest fail to save.
# Several suites run one leg per pinned toolchain release (bun, uv,
# poetry, pdm, hatch, pipenv, pip, bundler, composer, maven, dotnet,
# deno, vlt), so the release is part of the key too, plus the vlt
# store linker of the two ubuntu e2e_safety_vlt legs.
key: ${{ matrix.suite }}-${{ matrix.vlt || matrix.bun || matrix.uv || matrix.poetry || matrix.pdm || matrix.hatch || matrix.pipenv || matrix.pip || matrix.bundler || matrix.composer || matrix.maven || matrix.dotnet || matrix.deno || 'default' }}${{ matrix.vlt_store_linker && format('-{0}', matrix.vlt_store_linker) || '' }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Setup Node.js
if: matrix.suite == 'e2e_npm' || matrix.suite == 'e2e_scan' || matrix.suite == 'e2e_safety_pnpm'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20.20.2'
- name: Setup pnpm
if: matrix.suite == 'e2e_safety_pnpm'
# Pin the major version so the store layout the test
# asserts on stays stable. `npm install -g` is the simplest
# cross-platform install path (works on ubuntu, macos,
# windows-runners — they all ship a usable npm via
# actions/setup-node).
run: npm install -g pnpm@10
- name: Setup Node.js 24 (named pnpm legs)
if: matrix.suite == 'e2e_redirect_pnpm_build'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '24.x'
- name: Setup Python
if: matrix.suite == 'e2e_pypi' || matrix.uv != '' || matrix.poetry != '' || matrix.pdm != '' || matrix.hatch != ''
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12.x'
- name: Setup uv (uv legs)
if: matrix.uv != ''
# The exact uv release the leg pins; the suites assert it through
# SOCKET_PATCH_UV_E2E_VERSION.
shell: bash
env:
UV_TEST_VERSION: ${{ matrix.uv }}
run: |
python -m pip install --disable-pip-version-check --upgrade pip "uv==$UV_TEST_VERSION"
uv --version
- name: Install uv (PDM / Hatch legs)
if: matrix.pdm != '' || matrix.hatch != ''
# The suites bootstrap the pinned PDM / Hatch into their own venv
# with uv; this uv is tooling, not the release under test.
run: python -m pip install uv==0.11.19
- name: Install pinned Poetry
if: matrix.poetry != ''
# Poetry 1.0/1.1 need Python 3.8 (their vendored deps predate 3.12);
# 1.2.2 needs the cleo pre-release it was published against. pip 24.0
# seeds the tool venv (Poetry 1.0 + pip 22.3-23.0 misread the lock-1.0
# `#sha256=` fragment, as the CLI's advisory says).
shell: bash
env:
POETRY_TEST_VERSION: ${{ matrix.poetry }}
run: |
python -m pip install uv==0.11.19
case "$POETRY_TEST_VERSION" in 1.0.*|1.1.*) py=3.8.20 ;; *) py=3.12 ;; esac
uv venv "$RUNNER_TEMP/poetry" --python "$py"
extra=""; [ "$POETRY_TEST_VERSION" = "1.2.2" ] && extra="cleo==1.0.0a5"
uv pip install --python "$RUNNER_TEMP/poetry/bin/python" "poetry==$POETRY_TEST_VERSION" pip==24.0 setuptools==69.5.1 $extra
echo "SOCKET_PATCH_POETRY_BIN=$RUNNER_TEMP/poetry/bin/poetry" >> "$GITHUB_ENV"
- name: Setup uv (Pipenv / pip legs)
if: matrix.pipenv != '' || matrix.pip != ''
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: Install the Pythons the Pipenv / pip legs run on
if: matrix.pipenv != '' || matrix.pip != ''
run: uv python install 3.8 3.11 3.12
- name: Setup Ruby
if: matrix.suite == 'e2e_gem' || matrix.bundler != ''
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
with:
# setup-ruby does NOT support `3.2.x` wildcard pinning the
# way setup-python does — it errors with "Unknown version
# 3.2.x for ruby on ubuntu-24.04". Pin to an exact patch
# that's currently in the catalog. If the action drops this
# patch in the future, bump to whatever's available — see
# https://github.com/ruby/setup-ruby for the supported list.
# Bundler-era legs pin their own Ruby (`ruby:`): bundler <= 2.2
# needs Ruby <= 3.3 and 1.17-2.1 need Ruby <= 3.1 (`untaint`).
ruby-version: ${{ matrix.ruby || '3.2.10' }}
# The legs pin their bundler (`bundler:`) so a capstone never rides
# whatever bundler the runner's Ruby ships; bundler 1.x is not
# installable through setup-ruby, see the next steps. e2e_gem keeps
# the 2.5 floor.
bundler: ${{ startsWith(matrix.bundler, '1.') && 'none' || matrix.bundler || '2.5' }}
bundler-cache: false
- name: Install Bundler 1.x
if: startsWith(matrix.bundler, '1.')
shell: bash
env:
BUNDLER_TEST_VERSION: ${{ matrix.bundler }}
run: gem install bundler -v "$BUNDLER_TEST_VERSION" --no-document
# Installing a bundler does not make `bundle` run it: with no lockfile
# to read, RubyGems' binstub activates the HIGHEST installed bundler,
# so a leg pinned BELOW its Ruby's default gem (2.1.4 / 2.2.33 on Ruby
# 3.1, whose default is 2.3.27) silently runs the default instead and
# tests/common/bundler_e2e.rs rightly panics. BUNDLER_VERSION makes
# the binstub select exactly the pinned release in every process (and
# turns off bundler >= 2.3's lockfile-driven self-switch), the same
# knob tests/docker/Dockerfile.gem-b1 sets.
- name: Select the pinned Bundler
if: matrix.bundler != ''
shell: bash
env:
BUNDLER_TEST_VERSION: ${{ matrix.bundler }}
run: |
echo "BUNDLER_VERSION=$BUNDLER_TEST_VERSION" >> "$GITHUB_ENV"
BUNDLER_VERSION="$BUNDLER_TEST_VERSION" bundle --version
- name: Setup PHP
if: matrix.composer != ''
# The composer capstones shell out to a real composer; `composer:`
# pins the release line (1, 2.2 LTS, 2) so the composer.lock grammar
# the edits assert stays stable across runners.
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
with:
php-version: '8.2'
tools: composer:${{ matrix.composer }}
- name: Setup Java (Maven legs)
if: matrix.maven != ''
uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: temurin
java-version: '17'
- name: Install Maven ${{ matrix.maven }}
if: matrix.maven != ''
# Straight from the Apache archive (sha512-verified), so a leg gets
# exactly the release it names rather than the runner's Maven.
shell: bash
env:
MAVEN_VERSION: ${{ matrix.maven }}
run: |
major="${MAVEN_VERSION%%.*}"
url="https://archive.apache.org/dist/maven/maven-${major}/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz"
curl -fsSL --retry 3 "$url" -o "$RUNNER_TEMP/maven.tgz"
sum="$(curl -fsSL --retry 3 "$url.sha512" | cut -d' ' -f1)"
echo "$sum $RUNNER_TEMP/maven.tgz" | shasum -a 512 -c -
tar -xzf "$RUNNER_TEMP/maven.tgz" -C "$RUNNER_TEMP"
echo "SOCKET_PATCH_MAVEN_E2E_MVN=$RUNNER_TEMP/apache-maven-${MAVEN_VERSION}/bin/mvn" >> "$GITHUB_ENV"
- name: Setup .NET SDK
if: matrix.dotnet != ''
# Installs next to the runner's preinstalled SDKs; the suite writes
# its own global.json to select the pinned major.
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: ${{ matrix.dotnet }}.0.x
- name: Setup Deno
if: matrix.deno != ''
uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2.0.5
with:
deno-version: ${{ matrix.deno }}
- name: Setup Bun
if: matrix.bun != ''
# Installs the exact bun release the leg pins (setup-bun resolves a
# strict semver straight to the `bun-v<ver>` GitHub release, so the
# lock-era legs get the historical writer, not `latest`). Works on
# all three runner OSes (windows → bun-windows-x64.zip / bun.exe).
# SHA resolved from `gh api repos/oven-sh/setup-bun/git/ref/tags/v2.2.0`.
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ matrix.bun }}
- name: Setup Node.js 24 (vlt legs)
if: matrix.vlt != ''
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '24.21.0'
- name: Setup vlt
if: matrix.vlt != ''
# The pinned release (and, for the upgrade legs, the second vlt):
# `npm pack`, sha512 against the registry and the committed pin, then
# a prefix install run as `node vlt.js`. The store-linker knob is a
# harness variable because the harness scrubs ambient VLT_*.
shell: bash
env:
VLT_TEST_VERSION: ${{ matrix.vlt }}
VLT_TEST_UPGRADE: ${{ matrix.vlt_upgrade }}
VLT_TEST_STORE_LINKER: ${{ matrix.vlt_store_linker }}
run: |
set -euo pipefail
js=$(scripts/install-vlt.sh "$VLT_TEST_VERSION" "$RUNNER_TEMP/vlt-tool")
{
echo "SOCKET_PATCH_VLT_E2E_JS=$js"
echo "SOCKET_PATCH_VLT_E2E_VERSION=$VLT_TEST_VERSION"
echo "SOCKET_PATCH_VLT_E2E_REQUIRED=1"
echo "LANG=C"
echo "LC_ALL=C"
} >> "$GITHUB_ENV"
if [ -n "$VLT_TEST_STORE_LINKER" ]; then
echo "SOCKET_PATCH_VLT_E2E_STORE_LINKER=$VLT_TEST_STORE_LINKER" >> "$GITHUB_ENV"
fi
if [ -n "$VLT_TEST_UPGRADE" ]; then
up=$(scripts/install-vlt.sh "$VLT_TEST_UPGRADE" "$RUNNER_TEMP/vlt-upgrade")
echo "SOCKET_PATCH_VLT_E2E_UPGRADE_JS=$up" >> "$GITHUB_ENV"
echo "SOCKET_PATCH_VLT_E2E_UPGRADE_VERSION=$VLT_TEST_UPGRADE" >> "$GITHUB_ENV"
fi
node --version
- name: Run e2e tests
if: matrix.vlt == ''
# Suites are `#[ignore]`-gated out of the unpinned `test` job by
# default, hence `--ignored`; an entry that sets `test_filter`
# overrides the selector for itself only.
env:
# Bun legs only: turn the bun suites' "bun not installed / no text
# lock" soft-skips into hard failures and make them assert the
# pinned release, so a leg can never report green on an
# unexercised toolchain. Both are the EMPTY string on non-bun legs,
# which the suites treat as unset.
SOCKET_PATCH_BUN_E2E_REQUIRED: ${{ matrix.bun != '' && '1' || '' }}
SOCKET_PATCH_BUN_E2E_VERSION: ${{ matrix.bun }}
# e2e_bun_lockb only. SOCKET_PATCH_BUN_LOCKB_VERSION is checked with
# `var().is_ok()`, so it must stay EMPTY on every other leg.
SOCKET_PATCH_BUN_LOCKB_REQUIRED: ${{ matrix.suite == 'e2e_bun_lockb' && '1' || '' }}
SOCKET_PATCH_BUN_LOCKB_VERSION: ${{ matrix.suite == 'e2e_bun_lockb' && matrix.bun || '' }}
SOCKET_PATCH_BUN_LOCKB_EXTENDED: ${{ matrix.suite == 'e2e_bun_lockb' && '1' || '' }}
SOCKET_PATCH_BUN_LOCKB_PRODUCTION: ${{ matrix.suite == 'e2e_bun_lockb' && '1' || '' }}
# The same fail-instead-of-skip + pinned-release gates for every
# other real-toolchain capstone. All EMPTY on legs that do not set
# the matching matrix key, which the suites treat as unset.
SOCKET_PATCH_NPM_E2E_REQUIRED: ${{ matrix.npm_required || '' }}
SOCKET_PATCH_CARGO_E2E_REQUIRED: ${{ matrix.suite == 'e2e_safety_cargo_build' && '1' || '' }}
SOCKET_PATCH_UV_E2E_REQUIRED: ${{ matrix.uv != '' && '1' || '' }}
SOCKET_PATCH_UV_E2E_VERSION: ${{ matrix.uv }}
SOCKET_PATCH_POETRY_E2E_REQUIRED: ${{ matrix.poetry != '' && '1' || '' }}
SOCKET_PATCH_POETRY_E2E_VERSION: ${{ matrix.poetry }}
SOCKET_PATCH_PDM_E2E_REQUIRED: ${{ matrix.pdm != '' && '1' || '' }}
SOCKET_PATCH_PDM_E2E_VERSION: ${{ matrix.pdm }}
SOCKET_PATCH_HATCH_E2E_REQUIRED: ${{ matrix.hatch != '' && '1' || '' }}
SOCKET_PATCH_HATCH_E2E_VERSION: ${{ matrix.hatch }}
SOCKET_PATCH_PIPENV_E2E_REQUIRED: ${{ matrix.pipenv != '' && '1' || '' }}
SOCKET_PATCH_PIPENV_E2E_VERSIONS: ${{ matrix.pipenv }}
SOCKET_PATCH_PIP_E2E_REQUIRED: ${{ matrix.pip != '' && '1' || '' }}
SOCKET_PATCH_PIP_E2E_VERSIONS: ${{ matrix.pip }}
SOCKET_PATCH_BUNDLER_E2E_REQUIRED: ${{ matrix.bundler != '' && '1' || '' }}
SOCKET_PATCH_BUNDLER_E2E_VERSION: ${{ matrix.bundler }}
SOCKET_PATCH_COMPOSER_E2E_REQUIRED: ${{ matrix.composer != '' && '1' || '' }}
SOCKET_PATCH_COMPOSER_E2E_VERSION: ${{ matrix.composer }}
SOCKET_PATCH_MAVEN_E2E_REQUIRED: ${{ matrix.maven != '' && '1' || '' }}
SOCKET_PATCH_MAVEN_E2E_VERSION: ${{ matrix.maven }}
SOCKET_PATCH_DOTNET_E2E_REQUIRED: ${{ matrix.dotnet != '' && '1' || '' }}
SOCKET_PATCH_DOTNET_E2E_VERSION: ${{ matrix.dotnet }}
SOCKET_PATCH_DENO_E2E_REQUIRED: ${{ matrix.deno != '' && '1' || '' }}
SOCKET_PATCH_DENO_E2E_VERSION: ${{ matrix.deno }}
run: cargo test -p socket-patch-cli --all-features --test ${{ matrix.suite }} -- ${{ matrix.test_filter || '--ignored' }}
- name: Run vlt e2e tests
if: matrix.vlt != ''
# One capstone binary per row, through the leg checker: it fails on
# `0 passed`, a crashed binary, a missing `ran`, an unexpected skip or
# an unknown leg (crates/socket-patch-cli/tests/vlt-leg-manifest.json).
shell: bash
env:
SOCKET_PATCH_VLT_E2E_REQUIRED: ${{ matrix.vlt != '' && '1' || '' }}
VLT_SUITE: ${{ matrix.suite }}
VLT_TEST_FILTER: ${{ matrix.test_filter }}
run: |
set -uo pipefail
status=0
# shellcheck disable=SC2086 # the filter is several libtest arguments
cargo test -p socket-patch-cli --all-features --test "$VLT_SUITE" -- $VLT_TEST_FILTER 2>&1 | tee vlt-leg.log || status=1
py=$(command -v python3 || command -v python)
"$py" scripts/check-vlt-legs.py --manifest crates/socket-patch-cli/tests/vlt-leg-manifest.json vlt-leg.log || status=1
exit "$status"
# ----------------------------------------------------------------------
# Docker-driven real-package e2e suite.
#
# For each ecosystem, builds the shared base image (multi-stage:
# Rust → debian + compiled socket-patch) and the per-ecosystem layer,
# then runs the matching `docker_e2e_<eco>` test binary inside the
# repo's checkout. Tests install real packages via real package
# managers and run socket-patch against a wiremock-served fixture —
# no real Socket API contact. Hermetic, reproducible.
#
# Triggered on every PR. The `e2e` job above runs the
# `#[ignore]`-gated real-toolchain capstones; the live-API smoke suites
# are run by hand (see the note in its matrix).
# ----------------------------------------------------------------------
e2e-docker:
runs-on: ubuntu-latest
timeout-minutes: 35
permissions:
contents: read
strategy:
fail-fast: false
matrix:
ecosystem: [npm, pypi, gem, cargo, golang, maven, composer, nuget, deno]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Docker Buildx
# `driver: docker` — see the coverage-docker matching step above
# for the rationale (the per-ecosystem image's `FROM
# socket-patch-test-base:latest` only resolves when buildx talks
# directly to the host docker daemon).
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
with:
driver: docker
- name: Install Rust
run: rustup show
# No `actions/cache` here intentionally. This job builds Docker
# images and would be flagged by zizmor's cache-poisoning audit.
- name: Build base image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: tests/docker/Dockerfile.base
tags: socket-patch-test-base:latest
load: true
- name: Build ${{ matrix.ecosystem }} image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: tests/docker/Dockerfile.${{ matrix.ecosystem }}
tags: socket-patch-test-${{ matrix.ecosystem }}:latest
load: true
- name: Run ${{ matrix.ecosystem }} Docker e2e test
# Every ecosystem is unconditionally compiled in; only the
# `docker-e2e` feature is needed to compile the suite itself.
# The composer/nuget/pypi vendored build-proof capstones (each
# ending in the manifest-less VEX stage) ride the same image as
# their ecosystem's main suite; the gem and maven vendor capstones
# run only in coverage-docker.
run: |
EXTRA=""
case "${{ matrix.ecosystem }}" in
composer) EXTRA="--test docker_e2e_vendor_composer" ;;
nuget) EXTRA="--test docker_e2e_vendor_nuget" ;;
pypi) EXTRA="--test docker_e2e_vendor_pypi_pm" ;;
esac
# shellcheck disable=SC2086 # EXTRA is intentionally word-split
cargo test -p socket-patch-cli --features docker-e2e --test "docker_e2e_${{ matrix.ecosystem }}" $EXTRA
# ----------------------------------------------------------------------
# Per-release real-toolchain matrices for the manifest-less VEX work that
# loop several suites per release through a script (yarn) or need a
# toolchain + lock-format axis (cargo). Each leg hard-fails on a skip.
# ----------------------------------------------------------------------
yarn-classic-matrix:
name: yarn-classic ${{ matrix.release }}
needs: test
runs-on: ubuntu-latest
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
# 1.0.2 oldest 1.x; 1.6.0 last that installs nothing for a `file:`
# tarball; 1.7.0 first vendored-capable; 1.9.4 last without the
# `integrity` line; 1.10.1 first with it; 1.22.22 current.
release: ['1.0.2', '1.6.0', '1.7.0', '1.9.4', '1.10.1', '1.22.22']
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
run: rustup show
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: yarn-classic-${{ matrix.release }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '24.x'
- name: Manifest-less VEX over every real yarn classic flow
# The script exports SOCKET_PATCH_YARN_E2E_REQUIRED=1 and
# SOCKET_PATCH_YARN_CLASSIC_E2E_VERSION itself.
env:
COREPACK_ENABLE_DOWNLOAD_PROMPT: '0'
YARN_CLASSIC_RELEASE: ${{ matrix.release }}
run: scripts/yarn-classic-vex-matrix.sh "$YARN_CLASSIC_RELEASE"
yarn-berry-e2e:
name: yarn-berry ${{ matrix.yarn }} (${{ matrix.os }})
needs: test
strategy:
fail-fast: false
matrix:
# 4.0.2 bare-hex checksum writer (4.0.0-4.0.2), 4.1.0 first
# `10c0/` writer, then a spread up to current.
os: [ubuntu-latest]
yarn: ['4.0.2', '4.1.0', '4.6.0', '4.12.0', '4.18.0']
include:
- {os: macos-latest, yarn: '4.12.0'}
- {os: windows-latest, yarn: '4.12.0'}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
run: rustup show
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: yarn-berry-${{ matrix.yarn }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Enable corepack
run: corepack enable
- name: Yarn berry hosted/vendored + manifest-less VEX
shell: bash
env:
COREPACK_ENABLE_DOWNLOAD_PROMPT: '0'
SOCKET_PATCH_YARN_E2E_REQUIRED: '1' # the script also exports it
YARN_BERRY_RELEASE: ${{ matrix.yarn }}
run: scripts/yarn-berry-vex-matrix.sh "$YARN_BERRY_RELEASE"
cargo-vex-matrix:
name: cargo ${{ matrix.toolchain }} lock-v${{ matrix.lock || 'own' }} (${{ matrix.os }})
needs: test
runs-on: ${{ matrix.os }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
# The toolchain's own lock is re-encoded as v1-v4 before
# socket-patch touches it (the committed-lockfile case); '' keeps the
# toolchain's own format.
os: [ubuntu-latest]
toolchain: ['1.82.0', '1.93.1', 'stable']
lock: ['', '1', '2', '3', '4']
include:
- {os: macos-latest, toolchain: stable, lock: '1'}
- {os: windows-latest, toolchain: stable, lock: '1'}
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
run: rustup show
- name: Install the cargo under test
# bash, not the Windows default pwsh: in PowerShell
# "$CARGO_TEST_TOOLCHAIN" is an (unset) PowerShell variable, so the
# windows-latest leg ran `rustup toolchain install ""`.
shell: bash
env:
CARGO_TEST_TOOLCHAIN: ${{ matrix.toolchain }}
run: rustup toolchain install "$CARGO_TEST_TOOLCHAIN" --profile minimal
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: cargo-vex-${{ matrix.toolchain }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Real-cargo hosted/vendored + manifest-less VEX
shell: bash
env:
SOCKET_PATCH_CARGO_E2E_REQUIRED: '1'
SOCKET_PATCH_CARGO_E2E_TOOLCHAIN: ${{ matrix.toolchain }}
SOCKET_PATCH_CARGO_E2E_LOCK_VERSION: ${{ matrix.lock }}
run: |
set -euo pipefail
cargo test -p socket-patch-cli --test e2e_redirect_cargo_build --test e2e_redirect_cargo_shapes --test e2e_vendor_cargo_build --test mode_migration_cargo
cargo test -p socket-patch-cli --test e2e_safety_cargo_build -- --ignored
# Manifest `[patch]` + the tagged detached lock (the v5 vendored cargo
# wiring) on cargo 1.41 and 1.56 — below / at the 1.56 floor of
# config-file `[patch]`. The old-toolchain tests prefer the local
# `rust:1.41-slim` / `rust:1.56-slim` docker images (run `--network none`;
# they build AND run the consumer) and skip when no old cargo is
# available; this leg pulls both images and requires them.
cargo-old-toolchains:
name: cargo old toolchains (manifest [patch])
needs: test
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
run: rustup show
- name: Pull the old cargos under test
run: |
docker pull rust:1.41-slim
docker pull rust:1.56-slim
- name: Cache cargo
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: cargo-old-toolchains
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Vendored manifest [patch] on old cargo
shell: bash
env:
SOCKET_PATCH_CARGO_E2E_REQUIRED: '1'
SOCKET_PATCH_CARGO_OLD_TOOLCHAINS_REQUIRED: '1'
run: |
set -euo pipefail
cargo test -p socket-patch-cli --test e2e_vendor_cargo_build -- old_toolchain --nocapture
# ----------------------------------------------------------------------
# Experimental `setup`-flow matrix (NON-BLOCKING).
#
# For each ecosystem/package manager, drives the full intended flow —
# prepare deps + a committed patch set, run `socket-patch setup`, run
# the native install, check whether the patch was applied — plus the
# negative controls (no setup, empty/wrong/alt patch sets). See
# tests/setup_matrix/ and scripts/setup-matrix.sh.
#
# This is EXPERIMENTAL and intentionally not required to pass yet:
# `setup` configures install hooks for npm, PyPI, Bundler and Composer
# only, so the other ecosystems' `baseline_with_setup` cases are
# EXPECTED to fail (a baseline of what `setup` must eventually support). `continue-on-error: true`
# means this job never blocks a PR — it must ALSO be left OUT of the
# repo's required status checks (configured in the branch-protection
# UI, not in this file). The orchestrator exits non-zero only on a
# *regression* vs the recorded baseline; the full per-case result set
# is uploaded as a JSON artifact for inspection.
# ----------------------------------------------------------------------
setup-matrix:
runs-on: ubuntu-latest
timeout-minutes: 45
continue-on-error: true
permissions:
contents: read
strategy:
fail-fast: false
matrix:
ecosystem: [npm, pypi, cargo, gem, golang, maven, composer, nuget, deno]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Set up Docker Buildx
# `driver: docker` — the per-ecosystem image's `FROM
# socket-patch-test-base:latest` only resolves when buildx talks
# directly to the host docker daemon (see e2e-docker above).
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
with:
driver: docker
- name: Install Rust
run: rustup show
- name: Build base image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: tests/docker/Dockerfile.base
tags: socket-patch-test-base:latest
load: true
- name: Build ${{ matrix.ecosystem }} image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
file: tests/docker/Dockerfile.${{ matrix.ecosystem }}
tags: socket-patch-test-${{ matrix.ecosystem }}:latest
load: true
- name: Run ${{ matrix.ecosystem }} setup-matrix
run: scripts/setup-matrix.sh run --ecosystem ${{ matrix.ecosystem }} --out "report-${{ matrix.ecosystem }}.json"
- name: Upload ${{ matrix.ecosystem }} setup-matrix report
if: always()
uses: ./.github/actions/upload-artifact
with:
name: setup-matrix-${{ matrix.ecosystem }}
path: report-${{ matrix.ecosystem }}.json
# ----------------------------------------------------------------------
# Hosted-mode production e2e — REQUIRED status check, with a kill switch.
#
# Drives `scan --mode hosted` against the REAL production endpoints
# (patches-api.socket.dev + patch.socket.dev) and the REAL upstream
# registries, using patches that are actually published on production.
# Nothing is mocked. Every other hosted-mode capstone in this repo
# (e2e_redirect_*) points at a wiremock stand-in, so this job is the only
# thing that would notice production drifting away from the CLI.
#
# The suite itself is `#[ignore]`-gated, so it stays OUT of the `test` and
# `e2e` jobs and only runs where it is explicitly asked for — here.
#
# INVARIANTS (this job is registered in branch protection as a required
# check named exactly `hosted-e2e`):
# * NO job-level `if:` — a *skipped* required check is ambiguous to branch
# protection and can wedge a PR at "Expected — waiting for status".
# The kill switch gates the STEPS, never the job.
# * NO `needs:` — an upstream failure would skip this job, same wedge.
# * NO matrix and NO rename — the check name must stay `hosted-e2e`.
# * NO `continue-on-error` — a bypass must be visible, not invisible.
# The job ALWAYS runs and ALWAYS reaches success or failure.
#
# ESCAPE HATCH — when production is down and this is blocking merges:
# Settings -> Secrets and variables -> Actions -> Variables ->
# HOSTED_E2E_DISABLED = true
# then "Re-run failed jobs" on any blocked PR. `vars` is read at job-run
# time, so no commit and no push is needed; the job goes green with a loud
# ::warning:: and a BYPASSED banner in the job summary. DELETE the variable
# to re-arm. For a one-off: Actions -> CI -> Run workflow ->
# hosted_e2e = force (ignore the variable) | skip (bypass this run).
# ----------------------------------------------------------------------
hosted-e2e:
name: hosted-e2e # registered in branch protection; do not rename
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: 30
concurrency:
# These are real requests against a real production service — keep it to
# one run per ref rather than one per push.
group: hosted-e2e-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
HOSTED_E2E_DISABLED: ${{ vars.HOSTED_E2E_DISABLED }}
# The `inputs` context is empty on push/pull_request, so default to auto.
HOSTED_E2E_MODE: ${{ (github.event_name == 'workflow_dispatch' && inputs.hosted_e2e) || 'auto' }}
steps:
- name: Resolve the kill switch
id: gate
run: |
set -eu
run=true; reason=''
case "$HOSTED_E2E_MODE" in
force) reason='workflow_dispatch hosted_e2e=force (kill switch ignored)' ;;
skip) run=false; reason='workflow_dispatch hosted_e2e=skip' ;;
*) if [ "${HOSTED_E2E_DISABLED:-}" = 'true' ]; then
run=false
reason='repository variable HOSTED_E2E_DISABLED=true'
fi ;;
esac
echo "run=$run" >> "$GITHUB_OUTPUT"
if [ "$run" = 'false' ]; then
echo "::warning title=hosted-e2e BYPASSED::$reason"
{
echo '## :warning: hosted-e2e BYPASSED — no production coverage in this run'
echo
echo "Reason: $reason"
echo
echo 'Re-arm by deleting the HOSTED_E2E_DISABLED repository variable'
echo '(Settings -> Secrets and variables -> Actions -> Variables).'
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Checkout
if: steps.gate.outputs.run == 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust
if: steps.gate.outputs.run == 'true'
run: rustup show
- name: Cache cargo
if: steps.gate.outputs.run == 'true'
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: hosted-e2e
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Setup Node.js
if: steps.gate.outputs.run == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
# Node 24, NOT the 20.20.2 the other jobs pin. pnpm 10 imports
# `node:sqlite` for its store index, which does not exist before
# Node 22 (and is only stable in 24) — on 20 every `pnpm install`
# dies with ERR_UNKNOWN_BUILTIN_MODULE. This suite drives real,
# current package managers against production, so it tracks what
# users actually run rather than the pin the offline suites need.
node-version: '24.x'
- name: Setup npm-family package managers
if: steps.gate.outputs.run == 'true'
env:
# Every corepack shim invocation — including the suite's own
# `has_command` probes — must be non-interactive, or the probe hangs
# or exits non-zero and STRICT turns that into a failed leg.
COREPACK_ENABLE_DOWNLOAD_PROMPT: '0'
# corepack owns pnpm and both yarn flavors. `corepack enable` alone is
# not enough: it installs the shims, but `pnpm --version` still fails
# for a project with no `packageManager` field — exactly the pnpm
# fixture's shape — because the shim has no version to resolve. The
# `prepare … --activate` lines set that global default AND pre-download
# each version, so the first invocation inside a test is not also a
# network fetch.
#
# pnpm must NOT come from `npm install -g` here: corepack has already
# created its shim at the same path, and npm refuses with EEXIST. Only
# bun, which corepack does not manage, comes from npm.
run: |
set -eu
corepack enable
corepack prepare pnpm@10 --activate
corepack prepare yarn@1.22.22 --activate
corepack prepare yarn@4.6.0 --activate
npm install -g bun@1
# vlt: the same sha512-checked pack-and-install as the e2e rows.
js=$(scripts/install-vlt.sh 1.2.0 "$RUNNER_TEMP/vlt-tool")
{
echo "SOCKET_PATCH_VLT_E2E_JS=$js"
echo "SOCKET_PATCH_VLT_E2E_VERSION=1.2.0"
echo "SOCKET_PATCH_VLT_E2E_REQUIRED=1"
echo "SOCKET_PATCH_HOSTED_E2E_STRICT=1"
} >> "$GITHUB_ENV"
node --version
npm --version
pnpm --version
bun --version
node --no-warnings "$js" --version
- name: Setup Python + uv
if: steps.gate.outputs.run == 'true'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12.x'
- name: Install uv
if: steps.gate.outputs.run == 'true'
run: python -m pip install --disable-pip-version-check uv && uv --version
- name: Setup Ruby
if: steps.gate.outputs.run == 'true'
uses: ruby/setup-ruby@319994f95fa847cf3fb3cd3dbe89f6dcde9f178f # v1.295.0
with:
ruby-version: '3.2.10'
# The gem hosted rewrite pins into the Gemfile.lock CHECKSUMS
# section, which `bundle lock --add-checksums` only emits on >= 2.6.
bundler: '2.6'
bundler-cache: false
- name: Setup Go
if: steps.gate.outputs.run == 'true'
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: '1.24'
cache: false
- name: Run hosted-mode production e2e
if: steps.gate.outputs.run == 'true'
env:
# A required check must never report green on an unexercised leg:
# STRICT turns the suite's local "toolchain missing" soft-skips into
# hard failures. Every toolchain it needs is installed above.
SOCKET_PATCH_HOSTED_E2E_STRICT: '1'
COREPACK_ENABLE_DOWNLOAD_PROMPT: '0'
run: |
set -u
# The public proxy intermittently returns 503 "Service temporarily
# over capacity" — that is the documented reason the older live-API
# suites were pulled from the PR matrix (see the `e2e` job). Retry the
# whole suite a couple of times before calling it a real failure, so a
# transient 503 does not block merges through a required check.
set -o pipefail
for attempt in 1 2 3; do
echo "::group::hosted-e2e attempt $attempt"
# The step shell runs with -e: keep a failed attempt from ending it.
status=0
cargo test -p socket-patch-cli --test e2e_hosted_production -- \
--ignored --nocapture --test-threads=4 2>&1 | tee hosted-e2e.log || status=$?
echo "::endgroup::"
if [ "$status" -eq 0 ]; then
# The vlt leg (probe-driven: the clean refusal while the artifact
# is content-encoded, the full install proof once it is not).
python3 scripts/check-vlt-legs.py \
--manifest crates/socket-patch-cli/tests/vlt-leg-manifest.json hosted-e2e.log
exit $?
fi
echo "::warning title=hosted-e2e attempt $attempt failed::retrying"
sleep $((attempt * 20))
done
echo "::error title=hosted-e2e::suite failed on all 3 attempts"
exit 1
- name: Run vendored-mode production e2e (vlt)
if: steps.gate.outputs.run == 'true'
# The vendored vlt install proof against production: the service's
# directory artifact in the D19 layout, then a fresh `vlt ci`.
env:
SOCKET_PATCH_VENDORED_E2E_STRICT: '1'
run: |
set -uo pipefail
for attempt in 1 2 3; do
echo "::group::vendored vlt production attempt $attempt"
status=0
cargo test -p socket-patch-cli --test e2e_vendored_production -- \
--include-ignored vlt_pinned_matrix --nocapture 2>&1 | tee vlt-vendored-production.log || status=$?
echo "::endgroup::"
if [ "$status" -eq 0 ]; then
python3 scripts/check-vlt-legs.py \
--manifest crates/socket-patch-cli/tests/vlt-leg-manifest.json vlt-vendored-production.log
exit $?
fi
echo "::warning title=vendored vlt production attempt $attempt failed::retrying"
sleep $((attempt * 20))
done
echo "::error title=hosted-e2e::the vendored vlt production proof failed on all 3 attempts"
exit 1