Fix Bun patch compatibility and annotations (#245) #10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Bun patch compatibility | |
| # Native Bun installer matrix: builds the CLI once per OS, downloads each | |
| # pinned Bun release straight from its GitHub release (retried, SHA-256 | |
| # verified against the release's SHASUMS256.txt) and runs | |
| # `scripts/backtest-bun.py` — hosted, vendored and vendored-detached mode | |
| # against the public minimist free patch, verifying the INSTALLED bytes, | |
| # lock stability, digest rejection and rollback on Linux, macOS and Windows. | |
| # No Socket API token is needed. See docs/testing/bun-compatibility.md. | |
| # | |
| # The hermetic (wiremock) real-bun suites run on every PR in ci.yml's `e2e` | |
| # matrix; this workflow is the production-service, many-release twin. | |
| on: | |
| pull_request: | |
| paths: | |
| - '.github/actions/upload-artifact/**' | |
| - '.github/workflows/bun-compatibility.yml' | |
| - 'scripts/backtest-bun.py' | |
| - 'docs/testing/bun-compatibility.md' | |
| - 'Cargo.lock' | |
| - 'crates/socket-patch-core/src/vendor/**' | |
| - 'crates/socket-patch-core/src/patch/redirect/**' | |
| - 'crates/socket-patch-core/src/vendor/bun_lock_text.rs' | |
| - 'crates/socket-patch-core/src/crawlers/npm_crawler.rs' | |
| - 'crates/socket-patch-core/src/crawlers/pkg_managers.rs' | |
| - 'crates/socket-patch-core/src/constants.rs' | |
| - 'crates/socket-patch-core/src/utils/process.rs' | |
| - 'crates/socket-patch-core/tests/fixtures/redirect/npm/bun/**' | |
| - 'crates/socket-patch-cli/src/commands/get.rs' | |
| - 'crates/socket-patch-cli/src/commands/scan/**' | |
| - 'crates/socket-patch-cli/src/commands/rollback.rs' | |
| - 'crates/socket-patch-cli/src/commands/vendor.rs' | |
| - 'crates/socket-patch-cli/src/commands/repair_vendor.rs' | |
| - 'crates/socket-patch-cli/src/commands/remove.rs' | |
| # Main runs are the only rust-cache writers (save-if below), so a | |
| # path-filtered push trigger is what seeds the cache the PR builds restore | |
| # (rust-cache keys on Cargo.lock, so Cargo.lock belongs here) and re-runs | |
| # the matrix post-merge on the code paths it exercises: the vendored engine | |
| # (`vendor/**` — bun_lock.rs, bun_lock_text.rs's shared version gate, | |
| # npm_flavor.rs, lock_inventory.rs), the hosted rewriter + unwinds, and the | |
| # CLI drivers (`scan/**` — hosted.rs, vendor_flow.rs, mod.rs — plus the | |
| # vendor / repair / remove commands the matrix runs). | |
| push: | |
| branches: [main] | |
| paths: | |
| - '.github/workflows/bun-compatibility.yml' | |
| - 'scripts/backtest-bun.py' | |
| - 'Cargo.lock' | |
| - 'crates/socket-patch-core/src/vendor/**' | |
| - 'crates/socket-patch-core/src/patch/redirect/mod.rs' | |
| - 'crates/socket-patch-core/src/patch/redirect/replay.rs' | |
| - 'crates/socket-patch-core/src/patch/redirect/takeover.rs' | |
| - 'crates/socket-patch-cli/src/commands/get.rs' | |
| - 'crates/socket-patch-cli/src/commands/scan/**' | |
| - 'crates/socket-patch-cli/src/commands/rollback.rs' | |
| - 'crates/socket-patch-cli/src/commands/vendor.rs' | |
| - 'crates/socket-patch-cli/src/commands/repair_vendor.rs' | |
| - 'crates/socket-patch-cli/src/commands/remove.rs' | |
| workflow_dispatch: | |
| inputs: | |
| versions: | |
| description: 'Space-separated Bun versions (empty = the pinned matrix below; every cell runs the override; releases before 1.1.0 are skipped on Windows, which has no build of them)' | |
| required: false | |
| default: '' | |
| shapes: | |
| description: 'Space-separated shapes (empty = every shape; a cell where no requested shape applies to its release reports noCells and passes)' | |
| required: false | |
| default: '' | |
| modes: | |
| description: 'Space-separated modes from hosted / vendored / vendored-detached (empty = all three)' | |
| required: false | |
| default: '' | |
| permissions: | |
| contents: read | |
| # Supersede stale PR runs. 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: bun-patch-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| env: | |
| CARGO_PROFILE_DEV_DEBUG: '0' | |
| CARGO_INCREMENTAL: '0' | |
| jobs: | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Cache cargo | |
| # save-if keeps writes on main so open PRs do not churn the repo's | |
| # 10 GiB cache budget; rust-cache's automatic key already includes | |
| # the runner OS, so one logical key serves all three builds. | |
| uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 | |
| with: | |
| key: bun-native | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Build CLI | |
| run: cargo build --locked -p socket-patch-cli | |
| - name: Upload CLI | |
| uses: ./.github/actions/upload-artifact | |
| with: | |
| name: bun-cli-${{ matrix.os }} | |
| path: | | |
| target/debug/socket-patch | |
| target/debug/socket-patch.exe | |
| if-no-files-found: error | |
| retention-days: 7 | |
| native: | |
| needs: build | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| # Every lock-format and behaviour boundary the CLI has to survive: | |
| # 0.8.1 / 1.0.0 / 1.0.36 / 1.1.0 / 1.1.38 binary bun.lockb only | |
| # 1.1.39 first text lock (lockfileVersion 0, opt-in) | |
| # 1.1.43 first `--lockfile-only` (the lockb migration recipe) | |
| # 1.1.45 last v0 writer | |
| # 1.2.0 / 1.2.23 / 1.3.0 text default, lockfileVersion 1 | |
| # 1.3.9 / 1.3.10 URL/local tarball sha512 enforcement boundary | |
| # (1.3.9 installs a tampered tarball, 1.3.10 refuses) | |
| # 1.3.14 last pre-v2 default | |
| # 1.4.0 / 1.4.2 lockfileVersion 2 | |
| bun: ['0.8.1', '1.0.0', '1.0.36', '1.1.0', '1.1.38', '1.1.39', '1.1.43', '1.1.45', '1.2.0', '1.2.23', '1.3.0', '1.3.9', '1.3.10', '1.3.14', '1.4.0', '1.4.2'] | |
| exclude: | |
| # No Windows binary before Bun 1.1.0 (every later release above | |
| # ships bun-windows-x64.zip). | |
| - {os: windows-latest, bun: '0.8.1'} | |
| - {os: windows-latest, bun: '1.0.0'} | |
| - {os: windows-latest, bun: '1.0.36'} | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Download CLI | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 | |
| with: | |
| pattern: bun-cli-${{ matrix.os }}* | |
| merge-multiple: true | |
| path: native-cli | |
| - name: Setup Python | |
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Download Bun ${{ matrix.bun }} | |
| id: bun | |
| # Pre-populate the exact directory layout the script's install_tool() | |
| # looks up (`tools/<version>/<asset>/bun[.exe]`, SHASUMS256.txt beside | |
| # it) for EVERY release the run needs: the matrix release (or the | |
| # dispatch override) plus Bun 1.1.38, the baseline the `legacy-lockb` | |
| # shape installs with on every other release (LEGACY_BUN in | |
| # scripts/backtest-bun.py) — de-duplicated, and only when that shape | |
| # is in play. install_tool() would fetch a missing release itself, | |
| # retried and SHASUMS-verified (never fail-open), but a GitHub outage | |
| # during that in-job fetch kills the whole cell before any case runs; | |
| # here the backoff loop rides it out, the SHASUMS listing is checked | |
| # BEFORE the zip is fetched (a release with no asset for this OS fails | |
| # in one request instead of five 404 rounds — or, for the pre-1.1.0 | |
| # releases that never shipped a Windows build, is skipped with a | |
| # notice), and the archive is verified against the release's own | |
| # SHASUMS256.txt before extraction (fail closed). The releases that | |
| # were actually staged are exported for the run step. | |
| shell: bash | |
| env: | |
| MATRIX_BUN: ${{ matrix.bun }} | |
| VERSIONS_OVERRIDE: ${{ github.event.inputs.versions }} | |
| SHAPES_OVERRIDE: ${{ github.event.inputs.shapes }} | |
| run: | | |
| set -euo pipefail | |
| case "$RUNNER_OS" in | |
| Linux) system=linux ;; | |
| macOS) system=darwin ;; | |
| Windows) system=windows ;; | |
| *) echo "::error::unsupported runner OS: $RUNNER_OS"; exit 1 ;; | |
| esac | |
| # Same asset choice as install_tool(): aarch64 only for arm64 | |
| # Linux/macOS; Windows is always x64. | |
| arch=x64 | |
| if [ "$RUNNER_ARCH" = "ARM64" ] && [ "$system" != windows ]; then arch=aarch64; fi | |
| asset="bun-${system}-${arch}" | |
| fetch() { | |
| # curl --retry only covers timeouts and 408/429/5xx; the outer | |
| # loop also rides out connection resets and truncated bodies. | |
| for attempt in 1 2 3 4 5; do | |
| if curl -fsSL --retry 5 --retry-delay 5 -o "$2" "$1"; then return 0; fi | |
| rm -f "$2" | |
| if [ "$attempt" = 5 ]; then | |
| echo "::error::download of $1 failed on all 5 attempts" | |
| return 1 | |
| fi | |
| echo "::warning::download of $1 failed (attempt $attempt); retrying" | |
| sleep $((10 * attempt)) | |
| done | |
| } | |
| before_windows_builds() { | |
| # True when $1 predates Bun 1.1.0, the first release with a | |
| # Windows binary (the matrix `exclude` list covers the pinned | |
| # releases; a dispatch override can name any release). | |
| IFS=. read -r major minor _ <<<"$1" | |
| [ "${major:-0}" -lt 1 ] || { [ "${major:-0}" -eq 1 ] && [ "${minor:-0}" -lt 1 ]; } | |
| } | |
| requested="$MATRIX_BUN" | |
| if [ -n "$VERSIONS_OVERRIDE" ]; then requested="$VERSIONS_OVERRIDE"; fi | |
| run_versions="" | |
| for version in $requested; do | |
| if [ "$system" = windows ] && before_windows_builds "$version"; then | |
| echo "::notice::skipping Bun ${version} on Windows: no bun-windows-x64.zip before 1.1.0" | |
| else | |
| run_versions="${run_versions} ${version}" | |
| fi | |
| done | |
| run_versions="${run_versions# }" | |
| # The run step's --versions; empty only when every requested | |
| # release was skipped above (the run step then reports noCells). | |
| echo "versions=${run_versions}" >> "$GITHUB_OUTPUT" | |
| stage="$run_versions" | |
| if [ -n "$run_versions" ] && { [ -z "$SHAPES_OVERRIDE" ] || [[ " $SHAPES_OVERRIDE " == *" legacy-lockb "* ]]; }; then | |
| stage="${stage} 1.1.38" | |
| fi | |
| for version in $(printf '%s\n' $stage | awk 'NF && !seen[$0]++'); do | |
| base="https://github.com/oven-sh/bun/releases/download/bun-v${version}" | |
| dir="native-bun/tools/${version}" | |
| mkdir -p "$dir" | |
| fetch "${base}/SHASUMS256.txt" "${dir}/SHASUMS256.txt" | |
| expected="$(awk -v name="${asset}.zip" '{ sub(/\r$/, "") } $2 == name { print $1 }' "${dir}/SHASUMS256.txt")" | |
| if [ -z "$expected" ]; then | |
| echo "::error::${asset}.zip is not listed in SHASUMS256.txt for bun-v${version}" | |
| exit 1 | |
| fi | |
| fetch "${base}/${asset}.zip" "${dir}/${asset}.zip" | |
| python3 - "${dir}/${asset}.zip" "$expected" "$dir" <<'PY' | |
| import hashlib, pathlib, sys, zipfile | |
| archive, expected, dest = pathlib.Path(sys.argv[1]), sys.argv[2], pathlib.Path(sys.argv[3]) | |
| actual = hashlib.sha256(archive.read_bytes()).hexdigest() | |
| if actual != expected: | |
| sys.exit(f'::error::SHA-256 mismatch for {archive}: expected {expected}, got {actual}') | |
| with zipfile.ZipFile(archive) as zipped: | |
| zipped.extractall(dest) | |
| archive.unlink() | |
| print(f'{archive.name} sha256 {actual} verified') | |
| PY | |
| binary="${dir}/${asset}/bun" | |
| if [ "$system" = windows ]; then binary="${binary}.exe"; fi | |
| chmod +x "$binary" || true | |
| actual="$("$binary" --version)" | |
| if [ "$actual" != "$version" ]; then | |
| echo "::error::expected bun ${version} at ${binary}, got ${actual}" | |
| exit 1 | |
| fi | |
| done | |
| - name: Install, verify patched bytes, reject corruption, and roll back | |
| shell: bash | |
| env: | |
| # The releases the download step staged: the matrix release or the | |
| # dispatch override, minus any pre-1.1.0 release skipped on Windows. | |
| BUN_VERSIONS: ${{ steps.bun.outputs.versions }} | |
| # Provenance for the captures the depscan SBOM fixtures import: | |
| # `cliRevision` is the branch-resolvable head commit (PR head, or the | |
| # pushed commit on main); `cliBuildSha` is the commit actions/checkout | |
| # actually built (refs/pull/N/merge on PRs) — they diverge once main | |
| # advances past the PR's merge-base, so the script records both. | |
| CLI_REVISION: ${{ github.event.pull_request.head.sha || github.sha }} | |
| CLI_BUILD_SHA: ${{ github.sha }} | |
| SHAPES_OVERRIDE: ${{ github.event.inputs.shapes }} | |
| MODES_OVERRIDE: ${{ github.event.inputs.modes }} | |
| run: | | |
| if [ -z "$BUN_VERSIONS" ]; then | |
| # Every requested release was skipped by the download step (a | |
| # pre-1.1.0 override on Windows): nothing to run, not a failure — | |
| # but the artifact must say so rather than go missing. | |
| echo "::notice::no Bun release to run on ${RUNNER_OS}: every requested release predates the first Windows build (1.1.0)" | |
| mkdir -p native-bun | |
| printf '[{"noCells": true, "passed": false, "error": "every requested Bun release was skipped on %s: no Windows build before 1.1.0"}]\n' "$RUNNER_OS" > native-bun/summary.json | |
| exit 0 | |
| fi | |
| chmod +x native-cli/socket-patch* || true | |
| cli="native-cli/socket-patch" | |
| if [ "$RUNNER_OS" = "Windows" ]; then cli="native-cli/socket-patch.exe"; fi | |
| modes="hosted vendored vendored-detached" | |
| if [ -n "$MODES_OVERRIDE" ]; then modes="$MODES_OVERRIDE"; fi | |
| shapes_arg=() | |
| if [ -n "$SHAPES_OVERRIDE" ]; then shapes_arg=(--shapes $SHAPES_OVERRIDE); fi | |
| python3 scripts/backtest-bun.py \ | |
| --cli "$cli" \ | |
| --cli-revision "$CLI_REVISION" \ | |
| --cli-build-sha "$CLI_BUILD_SHA" \ | |
| --output native-bun \ | |
| --tools native-bun/tools \ | |
| --versions $BUN_VERSIONS \ | |
| --modes $modes \ | |
| "${shapes_arg[@]}" \ | |
| --jobs 3 | |
| - name: Upload results | |
| uses: ./.github/actions/upload-artifact | |
| if: always() | |
| with: | |
| name: bun-results-${{ matrix.os }}-${{ matrix.bun }} | |
| include-hidden-files: true | |
| path: | | |
| native-bun/summary.json | |
| native-bun/captures/**/result.json | |
| native-bun/captures/**/cli-output.json | |
| native-bun/captures/**/tree/** | |
| native-bun/captures/**/*.log | |
| retention-days: 14 |