Skip to content

feat(apply): safety hardening — atomicity, locking, pnpm CoW, sidecars, Maven gate #164

feat(apply): safety hardening — atomicity, locking, pnpm CoW, sidecars, Maven gate

feat(apply): safety hardening — atomicity, locking, pnpm CoW, sidecars, Maven gate #164

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
clippy:
runs-on: ubuntu-latest
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
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ubuntu-latest-cargo-clippy-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ubuntu-latest-cargo-clippy-
- name: Run clippy
run: cargo clippy --workspace --all-features -- -D warnings
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
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
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ matrix.os }}-cargo-
- name: Build
run: cargo build --workspace --all-features
- name: Run tests
run: cargo test --workspace --all-features
test-release:
runs-on: ubuntu-latest
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
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ubuntu-latest-cargo-release-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ubuntu-latest-cargo-release-
- name: Run tests (release)
run: cargo test --workspace --all-features --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
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
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ubuntu-latest-cargo-coverage-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ubuntu-latest-cargo-coverage-
- 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.
#
# Explicit feature list (instead of --all-features) excludes 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 \
--features cargo,golang,maven,composer,nuget \
--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: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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 above).
#
# 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
permissions:
contents: read
strategy:
fail-fast: false
matrix:
ecosystem: [npm, pypi, gem, cargo, golang, maven, composer, nuget]
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 --features cargo,golang,maven,composer,nuget
- 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: |
cargo llvm-cov \
--features docker-e2e,cargo,golang,maven,composer,nuget \
--no-report \
--test docker_e2e_${{ matrix.ecosystem }}
- 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: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
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
permissions:
contents: read
steps:
- 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-*
- 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: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: coverage-lcov
path: coverage.lcov
if-no-files-found: error
retention-days: 30
dispatch-tests:
runs-on: ubuntu-latest
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_npm
- os: ubuntu-latest
suite: e2e_pypi
- os: ubuntu-latest
suite: e2e_cargo
- os: ubuntu-latest
suite: e2e_golang
- os: ubuntu-latest
suite: e2e_maven
- os: ubuntu-latest
suite: e2e_gem
- os: ubuntu-latest
suite: e2e_composer
- os: ubuntu-latest
suite: e2e_nuget
- os: macos-latest
suite: e2e_npm
- os: macos-latest
suite: e2e_pypi
- os: ubuntu-latest
suite: e2e_scan
- os: macos-latest
suite: e2e_scan
# 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
runs-on: ${{ matrix.os }}
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
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ matrix.os }}-cargo-e2e-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ matrix.os }}-cargo-e2e-
- 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 Python
if: matrix.suite == 'e2e_pypi'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.12.x'
- name: Setup Ruby
if: matrix.suite == 'e2e_gem'
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.
ruby-version: '3.2.10'
bundler-cache: false
- name: Run e2e tests
run: cargo test -p socket-patch-cli --all-features --test ${{ matrix.suite }} -- --ignored
# ----------------------------------------------------------------------
# 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 existing `e2e` job above stays for
# `--ignored` real-API smoke runs (manual / scheduled).
# ----------------------------------------------------------------------
e2e-docker:
runs-on: ubuntu-latest
permissions:
contents: read
strategy:
fail-fast: false
matrix:
ecosystem: [npm, pypi, gem, cargo, golang, maven, composer, nuget]
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
run: cargo test -p socket-patch-cli --features docker-e2e --test docker_e2e_${{ matrix.ecosystem }}