From 08e7809d74b7b071c3bfd156ca731dbeb77dde52 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Tue, 13 Oct 2026 09:37:20 -0400 Subject: [PATCH 1/5] ci(release): publish npm from release.yml, not publish-npm.yml Every socket-patch npm package's trusted publisher is registered against release.yml, and npm allows only one publisher per package. The 5.0.0 release dispatched publish-npm.yml, whose OIDC token named that file, so npm rejected it with E401 before staging anything. Move the npm leg back inline as release.yml's npm-publish job (same steps as publish-npm.yml) and delete publish-npm.yml, so the existing registration keeps working. Add an `npm-retry-version` input that skips the version gate, build, tag, GitHub release and crates.io jobs and re-stages npm from an existing release's assets; this retries 5.0.0 now and replaces the manual Publish npm dispatch. crates.io stays dispatched through publish-cargo.yml, since crates.io allows several publishers per crate. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yml | 2 +- .github/workflows/publish-npm.yml | 268 ------------------------- .github/workflows/release.yml | 311 ++++++++++++++++++++++++------ docs/releasing.md | 84 ++++---- scripts/dispatch-publish.sh | 12 +- 5 files changed, 298 insertions(+), 379 deletions(-) delete mode 100644 .github/workflows/publish-npm.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c01ed6dc..f743c8f60 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -972,7 +972,7 @@ jobs: - name: Run npm schema tests # The `./schema` export's zod tests. `npm install --no-save`, not - # `npm ci`, for the reason publish-npm.yml gives (a version-synced + # `npm ci`, for the reason release.yml's npm-publish job gives (a version-synced # lock can name platform packages not yet on the registry); the # platform binaries are optional and not needed here. The pack # test runs again once `npm test` has built dist/, so it also diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml deleted file mode 100644 index 64a67914a..000000000 --- a/.github/workflows/publish-npm.yml +++ /dev/null @@ -1,268 +0,0 @@ -name: Publish npm -run-name: "Publish npm ${{ inputs.version }}${{ inputs.distinct-id != '' && format(' [{0}]', inputs.distinct-id) || '' }}" - -# Stage-publishes @socketsecurity/socket-patch + the 14 platform packages to -# npm for an existing v release. Dispatched two ways, both as a -# plain workflow_dispatch run: -# - by release.yml (scripts/dispatch-publish.sh), as one leg of the -# single-dispatch release fan-out — distinct-id carries the release -# run's correlation id into this run's name; -# - manually (Actions → Publish npm → Run workflow), to retry just this -# registry after a mid-release failure: fix the cause, enter the release -# version, leave distinct-id blank. -# Every run checks out refs/tags/v and takes the prebuilt binaries -# from the GitHub release's assets (verified against SHA256SUMS), so a -# manual retry stages exactly what the release run would have — no rebuild -# needed. The GitHub release must already exist with all assets. -# -# Idempotent: already-published and already-staged versions are detected and -# skipped, so re-runs and partial-failure retries are safe. -# -# OIDC trusted publishing: the npm trusted publisher for every package is -# keyed on this repo + THIS file's name (publish-npm.yml). npm allows only -# ONE trusted publisher per package and matches the TOP-LEVEL workflow's -# filename, which is precisely why release.yml dispatches this file as its -# own run instead of calling it as a reusable workflow: dispatched, this -# file is always the top-level workflow, so the one registration covers -# both the release fan-out and manual retries. - -on: - workflow_dispatch: - inputs: - version: - description: 'Release version (X.Y.Z; the v GitHub release must exist with binaries + SHA256SUMS)' - required: true - type: string - distinct-id: - description: 'Correlation id set by release.yml to track its dispatched run — leave blank for manual runs' - required: false - default: '' - type: string - sums-digest: - description: 'Expected sha256 of the release''s SHA256SUMS asset — set by release.yml to pin the assets to what its build produced; leave blank for manual runs' - required: false - default: '' - type: string - -# Serialize same-version runs: the already-published/already-staged probes -# below are check-then-act, so two CONCURRENT runs for one version could -# race them. Serialized, a duplicate (e.g. re-dispatched by a release-run -# watcher whose `gh run watch` timed out while this run was still going) -# waits behind the live run and then no-ops. NOTE: the group holds at most -# ONE waiting run — a further same-version dispatch displaces (cancels) the -# waiting duplicate, and a watcher following the displaced run reports that -# as a failure; the publish itself is unaffected (the surviving runs no-op -# or publish). -concurrency: - group: publish-npm-${{ inputs.version }} - -permissions: {} - -jobs: - npm-publish: - runs-on: ubuntu-latest - # Bounds how long a wedged run (hung registry call) can hold this - # workflow's per-version concurrency group before retries can proceed. - timeout-minutes: 45 - permissions: - contents: read - id-token: write - steps: - - name: Validate version input - env: - VERSION: ${{ inputs.version }} - run: | - if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "::error::'${VERSION}' is not a plain X.Y.Z release version" - exit 1 - fi - - - name: Checkout release tag - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: refs/tags/v${{ inputs.version }} - persist-credentials: false - - - name: Verify tag carries the requested version - env: - VERSION: ${{ inputs.version }} - run: | - CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') - if [ "$CARGO_VERSION" != "$VERSION" ]; then - echo "::error::tag v${VERSION} carries workspace version ${CARGO_VERSION} — refusing to publish" - exit 1 - fi - - - name: Configure git for HTTPS - run: git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" - - - name: Download binaries from the GitHub release - env: - GH_TOKEN: ${{ github.token }} - VERSION: ${{ inputs.version }} - SUMS_DIGEST: ${{ inputs.sums-digest }} - run: | - mkdir artifacts - gh release download "v${VERSION}" \ - --repo "$GITHUB_REPOSITORY" \ - --dir artifacts \ - --pattern '*.tar.gz' --pattern '*.zip' --pattern 'SHA256SUMS' - cd artifacts - # Release assets are mutable (contents:write can clobber them), and - # SHA256SUMS is itself an asset of the same release — so when the - # release run dispatched us it pinned the sums file by digest, - # binding this publish to exactly what that run built (the same - # provenance the pre-split inline jobs got from same-run - # artifacts). Manual retries leave the pin blank: integrity-only. - if [ -n "$SUMS_DIGEST" ]; then - echo "${SUMS_DIGEST} SHA256SUMS" | sha256sum -c - - fi - # -c also fails on a file that SHA256SUMS lists but the download - # missed, so this doubles as a completeness check: the sums file - # was generated from the full 14-target artifact set. - sha256sum -c SHA256SUMS - - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: '22.22.1' - registry-url: 'https://registry.npmjs.org' - package-manager-cache: false - - - name: Update npm for staged publishing - run: npm install -g npm@11.15.0 - - - name: Stage binaries into platform packages - run: | - # Unix platforms: extract binary into each platform package directory - stage_unix() { - local artifact="$1" pkg_dir="$2" - tar xzf "artifacts/${artifact}.tar.gz" -C "${pkg_dir}/" - } - - # Windows platforms: extract .exe into each platform package directory - stage_win() { - local artifact="$1" pkg_dir="$2" - unzip -o "artifacts/${artifact}.zip" -d "${pkg_dir}/" - } - - stage_unix socket-patch-aarch64-apple-darwin npm/socket-patch-darwin-arm64 - stage_unix socket-patch-x86_64-apple-darwin npm/socket-patch-darwin-x64 - stage_unix socket-patch-x86_64-unknown-linux-gnu npm/socket-patch-linux-x64-gnu - stage_unix socket-patch-x86_64-unknown-linux-musl npm/socket-patch-linux-x64-musl - stage_unix socket-patch-aarch64-unknown-linux-gnu npm/socket-patch-linux-arm64-gnu - stage_unix socket-patch-aarch64-unknown-linux-musl npm/socket-patch-linux-arm64-musl - stage_unix socket-patch-arm-unknown-linux-gnueabihf npm/socket-patch-linux-arm-gnu - stage_unix socket-patch-arm-unknown-linux-musleabihf npm/socket-patch-linux-arm-musl - stage_unix socket-patch-i686-unknown-linux-gnu npm/socket-patch-linux-ia32-gnu - stage_unix socket-patch-i686-unknown-linux-musl npm/socket-patch-linux-ia32-musl - stage_unix socket-patch-aarch64-linux-android npm/socket-patch-android-arm64 - - stage_win socket-patch-x86_64-pc-windows-msvc npm/socket-patch-win32-x64 - stage_win socket-patch-i686-pc-windows-msvc npm/socket-patch-win32-ia32 - stage_win socket-patch-aarch64-pc-windows-msvc npm/socket-patch-win32-arm64 - - - name: Stage-publish platform packages - id: stage-platform - env: - VERSION: ${{ inputs.version }} - run: | - : > "${RUNNER_TEMP}/staged-packages.txt" - for pkg_dir in npm/socket-patch-*/; do - pkg_name="@socketsecurity/$(basename "$pkg_dir")" - echo "Staging ${pkg_name}..." - if npm stage publish "./${pkg_dir}" --access public; then - echo "$pkg_name" >> "${RUNNER_TEMP}/staged-packages.txt" - elif npm view "${pkg_name}@${VERSION}" version >/dev/null 2>&1; then - # Already fully published (approved) — a clean re-run. - echo "Already published, skipping." - elif npm stage list 2>/dev/null | grep -qF "${pkg_name}@${VERSION}"; then - # Staged but not yet 2FA-approved: `npm view` can't see staged - # versions, so without this check a re-run after a mid-job - # failure could never succeed. Best-effort match on the stage - # listing (format unverified against a live staged state — if it - # misses we still fail loudly below). - echo "Already staged awaiting approval, skipping." - echo "$pkg_name" >> "${RUNNER_TEMP}/staged-packages.txt" - else - exit 1 - fi - done - - - name: Copy README for npm package - run: cp README.md npm/socket-patch/README.md - - - name: Install main-package devDependencies - # `npm stage publish` (like `npm pack`) runs the package's `prepack` - # script, which compiles the `./schema` export with tsc — the - # `typescript` devDependency must be installed for that build. - # - # NOT `npm ci`: version-sync.sh (the offline stamp) drops the lock's - # platform-package entries for any other version, and the release's - # platform packages are not yet on the registry when it runs, so the - # lock has no entries for them and `npm ci` refuses it - # ("lock file's ...@ does not satisfy ...@X.Y.Z"). `npm install` - # tolerates the stubs and skips unresolvable optional platform - # packages (verified for both the published and unpublished-version - # states); --no-save keeps the runner from touching package.json. - working-directory: npm/socket-patch - run: npm install --no-save --ignore-scripts --no-audit --no-fund - - - name: Stage-publish main package - env: - VERSION: ${{ inputs.version }} - run: | - pkg_name="@socketsecurity/socket-patch" - if npm stage publish ./npm/socket-patch --access public; then - echo "$pkg_name" >> "${RUNNER_TEMP}/staged-packages.txt" - elif npm view "${pkg_name}@${VERSION}" version >/dev/null 2>&1; then - echo "Already published, skipping." - elif npm stage list 2>/dev/null | grep -qF "${pkg_name}@${VERSION}"; then - # See the platform-package step: staged-not-approved versions are - # invisible to `npm view`; tolerate them so re-runs can succeed. - echo "Already staged awaiting approval, skipping." - echo "$pkg_name" >> "${RUNNER_TEMP}/staged-packages.txt" - else - exit 1 - fi - - - name: Summarize staged versions awaiting approval - if: always() - env: - VERSION: ${{ inputs.version }} - run: | - STAGED_FILE="${RUNNER_TEMP}/staged-packages.txt" - if [ ! -s "$STAGED_FILE" ]; then - echo "No packages staged this run (all versions already published or publish step failed before staging)." >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - { - echo "## npm staged versions awaiting approval" - echo "" - echo "Version \`${VERSION}\` is staged on npm. A maintainer must approve each package with 2FA before it becomes installable." - echo "" - echo "**Approve platform packages first**, then the main \`@socketsecurity/socket-patch\` package, so install-time \`optionalDependencies\` resolution sees the platform binaries already live." - echo "" - echo "**Approve from the web** (signs in + 2FA prompts inline):" - echo "" - echo "- Org dashboard: " - echo "" - echo "Per-package review pages:" - echo "" - while IFS= read -r pkg_name; do - [ -z "$pkg_name" ] && continue - # npmjs.com renders staged versions inline on the package page; - # the access page exposes the "Staged" tab and the approve button. - echo "- \`${pkg_name}@${VERSION}\` — " - done < "$STAGED_FILE" - echo "" - echo "**Approve from the CLI** (requires \`npm@11.15.0+\` locally, signed in to the \`socketsecurity\` org with 2FA):" - echo "" - echo '```sh' - echo "npm stage list" - echo "# then, for each stage id printed above (platform packages first, main package last):" - echo "npm stage approve " - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - # Mirror to step log so it's also visible in the raw run output. - echo "::notice title=npm staged versions awaiting approval::Review and approve at https://www.npmjs.com/settings/socketsecurity/staged-packages" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f93822155..3efcc2ce8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,41 +1,40 @@ name: Release -# One-dispatch release orchestrator: version gate, build matrix, tag, and -# GitHub release live here; each registry publish lives in its own -# independently-runnable workflow (publish-cargo.yml, publish-npm.yml). -# The fan-out jobs below dispatch each at the release tag via `gh workflow run` -# (scripts/dispatch-publish.sh) and watch the dispatched run to completion, -# so this run's job graph still reflects every registry's real outcome and -# "Re-run failed jobs" re-dispatches exactly the failed legs. A failed leg -# can also be retried without this run at all: fix the cause and dispatch -# that registry's workflow manually with the release version — no rebuild -# happens either way. The npm leg takes the prebuilt binaries from -# the GitHub release's assets (verified against SHA256SUMS, pinned by digest -# on the fan-out path), which is why it waits on `github-release` below. +# One-dispatch release orchestrator: version gate, build matrix, tag, GitHub +# release, and the npm publish all live here; the crates.io publish lives in +# its own independently-runnable workflow (publish-cargo.yml). The +# cargo-publish job below dispatches it at the release tag via `gh workflow +# run` (scripts/dispatch-publish.sh) and watches the dispatched run to +# completion, so this run's job graph still reflects its real outcome and +# "Re-run failed jobs" re-dispatches it. A failed cargo leg can also be +# retried without this run: fix the cause and dispatch publish-cargo.yml +# manually with the release version — no rebuild happens either way. # -# Why dispatch instead of `uses:` (reusable workflows) — two GitHub/registry -# facts, verified 2026-08-21 against the registries' docs and source: -# 1. npm and crates.io OIDC trusted publishers match the top-level -# workflow filename (`workflow_ref` claim). npm additionally -# allows only ONE trusted publisher per package, so a leg that is -# sometimes `uses:`-called (top-level = release.yml) and sometimes -# dispatched (top-level = its own file) can never be authorized for -# both paths. Dispatching makes every leg ALWAYS the top-level workflow -# of its own run, so both claims name the leg's file and one publisher -# registration per package covers everything, on every registry. -# 2. GitHub suppresses events caused by this workflow's own GITHUB_TOKEN — -# a `release: published` (or `push: tags:`) trigger in another file -# would never fire — but workflow_dispatch (and repository_dispatch) events -# are documented exceptions, so `gh workflow run` with GITHUB_TOKEN -# works without a PAT/GitHub App token. +# Why npm is inline but crates.io is dispatched — registries' OIDC trusted +# publishers match the TOP-LEVEL workflow filename (`workflow_ref` claim): +# - npm allows only ONE trusted publisher per package, and every +# socket-patch package's publisher is registered against release.yml. +# Publishing from a job of this file keeps that registration valid. +# A failed npm job is retried with "Re-run failed jobs" on the release +# run, or — after the run is gone or when a workflow fix is needed — by +# dispatching this workflow with `npm-retry-version` set, which skips +# the version gate, build, tag, GitHub release and crates.io jobs and +# re-stages npm from the existing release's assets. Both paths run as +# release.yml, so the one registration covers them. +# - crates.io allows several publishers per crate, and publish-cargo.yml +# is registered for both crates; dispatching keeps it the top-level +# workflow of its own run, whether this run or a maintainer started it. +# GitHub suppresses events caused by this workflow's own GITHUB_TOKEN — a +# `release: published` (or `push: tags:`) trigger in another file would never +# fire — but workflow_dispatch events are a documented exception, so `gh +# workflow run` with GITHUB_TOKEN works without a PAT/GitHub App token. # # Registry credentials: -# - crates.io: OIDC trusted publishing (rust-lang/crates-io-auth-action); -# no long-lived secret, no environment. -# - npm: OIDC via `npm stage publish`; staged versions require -# manual 2FA approval (see the npm run's step summary). -# Every registry's trusted publisher is keyed on the repo + the publish -# workflow's own filename (see each publish-*.yml header), NOT release.yml. +# - crates.io: OIDC trusted publishing (rust-lang/crates-io-auth-action), +# publisher = publish-cargo.yml; no secret, no environment. +# - npm: OIDC via `npm stage publish`, publisher = release.yml; +# staged versions require manual 2FA approval (see the +# npm-publish job's step summary). on: workflow_dispatch: @@ -44,11 +43,19 @@ on: description: 'Dry run (build only, skip publish)' type: boolean default: false + npm-retry-version: + description: 'Retry ONLY the npm publish of an existing release (X.Y.Z; the v GitHub release must exist with binaries + SHA256SUMS). Skips build, tag, GitHub release and crates.io. Leave blank for a normal release.' + required: false + default: '' + type: string permissions: {} jobs: version: + # An npm-only retry skips this job, and with it (via `needs`) everything + # but npm-publish. + if: ${{ inputs.npm-retry-version == '' }} runs-on: ubuntu-latest permissions: contents: read @@ -280,18 +287,17 @@ jobs: artifacts/* fi - # ── Publish fan-out ──────────────────────────────────────────────────────── - # Each job dispatches one registry's publish workflow at the release tag - # and watches the dispatched run to completion (scripts/dispatch-publish.sh), - # failing iff that run fails — see this file's header for why the legs run - # as separate workflow_dispatch runs. The step summary links each leg's - # run. timeout-minutes bounds the watch; a timed-out watch does NOT cancel - # the leg, which finishes (and stays retryable) on its own. If a watcher - # fails while its leg is still alive (watch timeout, correlation miss), - # re-running it dispatches a second run of the same leg — safe, because - # each publish workflow serializes same-version runs with a concurrency - # group, so the duplicate queues behind the live run and then no-ops on - # the already-published probes. + # ── Publish ──────────────────────────────────────────────────────────────── + # cargo-publish dispatches publish-cargo.yml at the release tag and watches + # the dispatched run to completion (scripts/dispatch-publish.sh), failing + # iff that run fails; its step summary links the run. timeout-minutes + # bounds the watch; a timed-out watch does NOT cancel the leg, which + # finishes (and stays retryable) on its own. If the watcher fails while + # the leg is still alive (watch timeout, correlation miss), re-running it + # dispatches a second run — safe, because publish-cargo.yml serializes + # same-version runs with a concurrency group, so the duplicate queues + # behind the live run and then no-ops on the already-published probes. + # npm-publish runs inline — see this file's header for why. cargo-publish: needs: [version, build, tag] @@ -313,32 +319,219 @@ jobs: VERSION: ${{ needs.version.outputs.version }} run: bash scripts/dispatch-publish.sh publish-cargo.yml "$VERSION" - # npm takes the prebuilt binaries from the GitHub release's assets, - # so it waits on `github-release`. + # npm takes the prebuilt binaries from the GitHub release's assets, so it + # waits on `github-release`. With `npm-retry-version` set, every other job + # is skipped and this one re-stages that existing release. npm-publish: needs: [version, github-release] - if: ${{ !inputs.dry-run }} + if: >- + ${{ !cancelled() && !inputs.dry-run && ( + inputs.npm-retry-version != '' || needs.github-release.result == 'success') }} runs-on: ubuntu-latest timeout-minutes: 45 + # Serialize same-version publishes: the already-published/already-staged + # probes below are check-then-act, so two concurrent runs for one version + # (a release run and a retry) could race them. + concurrency: + group: release-npm-${{ inputs.npm-retry-version || needs.version.outputs.version }} permissions: contents: read - actions: write + id-token: write + env: + VERSION: ${{ inputs.npm-retry-version || needs.version.outputs.version }} + # Pins SHA256SUMS to what this run's build produced (github-release's + # output). Empty on an npm-only retry, where nothing was built: the + # assets are then checked for integrity only. + SUMS_DIGEST: ${{ needs.github-release.outputs.sums-digest }} + NPM_RETRY: ${{ inputs.npm-retry-version }} steps: - - name: Checkout + - name: Validate version + run: | + if ! printf '%s' "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "::error::'${VERSION}' is not a plain X.Y.Z release version" + exit 1 + fi + # Fail closed: a full release run must pin the assets it built; an + # empty digest would silently downgrade to integrity-only. + if [ -z "$NPM_RETRY" ] && [ -z "$SUMS_DIGEST" ]; then + echo "::error::github-release produced no SHA256SUMS digest; refusing an unpinned publish" + exit 1 + fi + + - name: Checkout release tag uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: refs/tags/v${{ env.VERSION }} persist-credentials: false - - name: Dispatch and watch Publish npm + - name: Verify tag carries the requested version + run: | + CARGO_VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') + if [ "$CARGO_VERSION" != "$VERSION" ]; then + echo "::error::tag v${VERSION} carries workspace version ${CARGO_VERSION} — refusing to publish" + exit 1 + fi + + - name: Configure git for HTTPS + run: git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" + + - name: Download binaries from the GitHub release env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ needs.version.outputs.version }} - SUMS_DIGEST: ${{ needs.github-release.outputs.sums-digest }} + GH_TOKEN: ${{ github.token }} + run: | + mkdir artifacts + gh release download "v${VERSION}" \ + --repo "$GITHUB_REPOSITORY" \ + --dir artifacts \ + --pattern '*.tar.gz' --pattern '*.zip' --pattern 'SHA256SUMS' + cd artifacts + # Release assets are mutable (contents:write can clobber them), and + # SHA256SUMS is itself an asset of the same release — so a full + # release run pins the sums file by digest, binding this publish to + # exactly what its build produced. + if [ -n "$SUMS_DIGEST" ]; then + echo "${SUMS_DIGEST} SHA256SUMS" | sha256sum -c - + fi + # -c also fails on a file that SHA256SUMS lists but the download + # missed, so this doubles as a completeness check: the sums file + # was generated from the full 14-target artifact set. + sha256sum -c SHA256SUMS + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '22.22.1' + registry-url: 'https://registry.npmjs.org' + package-manager-cache: false + + - name: Update npm for staged publishing + run: npm install -g npm@11.15.0 + + - name: Stage binaries into platform packages + run: | + # Unix platforms: extract binary into each platform package directory + stage_unix() { + local artifact="$1" pkg_dir="$2" + tar xzf "artifacts/${artifact}.tar.gz" -C "${pkg_dir}/" + } + + # Windows platforms: extract .exe into each platform package directory + stage_win() { + local artifact="$1" pkg_dir="$2" + unzip -o "artifacts/${artifact}.zip" -d "${pkg_dir}/" + } + + stage_unix socket-patch-aarch64-apple-darwin npm/socket-patch-darwin-arm64 + stage_unix socket-patch-x86_64-apple-darwin npm/socket-patch-darwin-x64 + stage_unix socket-patch-x86_64-unknown-linux-gnu npm/socket-patch-linux-x64-gnu + stage_unix socket-patch-x86_64-unknown-linux-musl npm/socket-patch-linux-x64-musl + stage_unix socket-patch-aarch64-unknown-linux-gnu npm/socket-patch-linux-arm64-gnu + stage_unix socket-patch-aarch64-unknown-linux-musl npm/socket-patch-linux-arm64-musl + stage_unix socket-patch-arm-unknown-linux-gnueabihf npm/socket-patch-linux-arm-gnu + stage_unix socket-patch-arm-unknown-linux-musleabihf npm/socket-patch-linux-arm-musl + stage_unix socket-patch-i686-unknown-linux-gnu npm/socket-patch-linux-ia32-gnu + stage_unix socket-patch-i686-unknown-linux-musl npm/socket-patch-linux-ia32-musl + stage_unix socket-patch-aarch64-linux-android npm/socket-patch-android-arm64 + + stage_win socket-patch-x86_64-pc-windows-msvc npm/socket-patch-win32-x64 + stage_win socket-patch-i686-pc-windows-msvc npm/socket-patch-win32-ia32 + stage_win socket-patch-aarch64-pc-windows-msvc npm/socket-patch-win32-arm64 + + - name: Stage-publish platform packages + id: stage-platform + run: | + : > "${RUNNER_TEMP}/staged-packages.txt" + for pkg_dir in npm/socket-patch-*/; do + pkg_name="@socketsecurity/$(basename "$pkg_dir")" + echo "Staging ${pkg_name}..." + if npm stage publish "./${pkg_dir}" --access public; then + echo "$pkg_name" >> "${RUNNER_TEMP}/staged-packages.txt" + elif npm view "${pkg_name}@${VERSION}" version >/dev/null 2>&1; then + # Already fully published (approved) — a clean re-run. + echo "Already published, skipping." + elif npm stage list 2>/dev/null | grep -qF "${pkg_name}@${VERSION}"; then + # Staged but not yet 2FA-approved: `npm view` can't see staged + # versions, so without this check a re-run after a mid-job + # failure could never succeed. Best-effort match on the stage + # listing (format unverified against a live staged state — if it + # misses we still fail loudly below). + echo "Already staged awaiting approval, skipping." + echo "$pkg_name" >> "${RUNNER_TEMP}/staged-packages.txt" + else + exit 1 + fi + done + + - name: Copy README for npm package + run: cp README.md npm/socket-patch/README.md + + - name: Install main-package devDependencies + # `npm stage publish` (like `npm pack`) runs the package's `prepack` + # script, which compiles the `./schema` export with tsc — the + # `typescript` devDependency must be installed for that build. + # + # NOT `npm ci`: version-sync.sh (the offline stamp) drops the lock's + # platform-package entries for any other version, and the release's + # platform packages are not yet on the registry when it runs, so the + # lock has no entries for them and `npm ci` refuses it + # ("lock file's ...@ does not satisfy ...@X.Y.Z"). `npm install` + # tolerates the stubs and skips unresolvable optional platform + # packages (verified for both the published and unpublished-version + # states); --no-save keeps the runner from touching package.json. + working-directory: npm/socket-patch + run: npm install --no-save --ignore-scripts --no-audit --no-fund + + - name: Stage-publish main package run: | - # Fail closed: an empty digest would silently downgrade the leg's - # asset check from provenance-pinned to integrity-only. - if [ -z "$SUMS_DIGEST" ]; then - echo "::error::github-release produced no SHA256SUMS digest; refusing to dispatch an unpinned publish" + pkg_name="@socketsecurity/socket-patch" + if npm stage publish ./npm/socket-patch --access public; then + echo "$pkg_name" >> "${RUNNER_TEMP}/staged-packages.txt" + elif npm view "${pkg_name}@${VERSION}" version >/dev/null 2>&1; then + echo "Already published, skipping." + elif npm stage list 2>/dev/null | grep -qF "${pkg_name}@${VERSION}"; then + # See the platform-package step: staged-not-approved versions are + # invisible to `npm view`; tolerate them so re-runs can succeed. + echo "Already staged awaiting approval, skipping." + echo "$pkg_name" >> "${RUNNER_TEMP}/staged-packages.txt" + else exit 1 fi - bash scripts/dispatch-publish.sh publish-npm.yml "$VERSION" "sums-digest=${SUMS_DIGEST}" + + - name: Summarize staged versions awaiting approval + if: always() + run: | + STAGED_FILE="${RUNNER_TEMP}/staged-packages.txt" + if [ ! -s "$STAGED_FILE" ]; then + echo "No packages staged this run (all versions already published or publish step failed before staging)." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + { + echo "## npm staged versions awaiting approval" + echo "" + echo "Version \`${VERSION}\` is staged on npm. A maintainer must approve each package with 2FA before it becomes installable." + echo "" + echo "**Approve platform packages first**, then the main \`@socketsecurity/socket-patch\` package, so install-time \`optionalDependencies\` resolution sees the platform binaries already live." + echo "" + echo "**Approve from the web** (signs in + 2FA prompts inline):" + echo "" + echo "- Org dashboard: " + echo "" + echo "Per-package review pages:" + echo "" + while IFS= read -r pkg_name; do + [ -z "$pkg_name" ] && continue + # npmjs.com renders staged versions inline on the package page; + # the access page exposes the "Staged" tab and the approve button. + echo "- \`${pkg_name}@${VERSION}\` — " + done < "$STAGED_FILE" + echo "" + echo "**Approve from the CLI** (requires \`npm@11.15.0+\` locally, signed in to the \`socketsecurity\` org with 2FA):" + echo "" + echo '```sh' + echo "npm stage list" + echo "# then, for each stage id printed above (platform packages first, main package last):" + echo "npm stage approve " + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + # Mirror to step log so it's also visible in the raw run output. + echo "::notice title=npm staged versions awaiting approval::Review and approve at https://www.npmjs.com/settings/socketsecurity/staged-packages" diff --git a/docs/releasing.md b/docs/releasing.md index cd4ac970d..0e21b75fe 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -59,24 +59,22 @@ once with `dry-run: true` — that builds all 14 targets but skips tagging and publishing. The real run: re-verifies the release gate → builds the matrix → creates and -pushes `v` → creates the GitHub release with `SHA256SUMS` → fans out -to crates.io and npm in parallel (both use OIDC trusted -publishing; no long-lived registry secrets). Each registry leg is its own -workflow (`publish-cargo.yml`, `publish-npm.yml`), dispatched at the release -tag by the release run and watched to completion, so the job graph still reflects -each registry's outcome (its step summaries link both leg runs) — and -each leg can equally be dispatched by hand (see "If a job fails -mid-release"). The `release.yml` header records why the legs are dispatched -runs rather than reusable workflows (registry trusted-publisher filename -matching; npm allows one publisher per package). +pushes `v` → creates the GitHub release with `SHA256SUMS` → publishes +to crates.io and npm in parallel (both use OIDC trusted publishing; no +long-lived registry secrets). The npm publish is the release run's own +`npm-publish` job. The crates.io leg is its own workflow (`publish-cargo.yml`), +dispatched at the release tag by the release run and watched to completion, +so the job graph still reflects its outcome (the `cargo-publish` step summary +links the leg run). The `release.yml` header records why npm runs inline and +crates.io is dispatched (registries match the trusted publisher on the +top-level workflow filename; npm allows one publisher per package). ## 4. Approve npm (the one manual step) The npm leg *stages* rather than publishes. Approve with 2FA — **platform packages first, then `@socketsecurity/socket-patch`** — so optionalDependencies resolution never sees the main package without its -binaries. Approve from the **Publish npm** run's step summary links (the -release run's `npm-publish` job summary links to that run), the +binaries. Approve from the release run's `npm-publish` job summary links, the [org staged-packages dashboard](https://www.npmjs.com/settings/socketsecurity/staged-packages), or the CLI (`npm stage list` / `npm stage approve `, npm 11.15+). @@ -106,21 +104,25 @@ partial release never requires deleting tags or re-bumping. 1. **"Re-run failed jobs"** on the release run — right when the failure was transient (network, registry hiccup) and no workflow change is needed. - Re-running a failed fan-out job dispatches a fresh run of that registry's - publish workflow *as of the tag* (a re-run never picks up workflow edits). -2. **Dispatch the failed registry's own workflow** — right when the fix - needed a change (registry-side config such as a trusted publisher, or a - workflow edit landed on the default branch): Actions → **Publish - crates.io** / **Publish npm** → - Run workflow, entering the release version (`X.Y.Z`, no `v`) and leaving - the other inputs blank. This runs the publish workflow as it exists on the - dispatched branch (default: the default branch), so workflow fixes apply. - Nothing rebuilds: each publish workflow checks out the `v` tag - and npm takes the prebuilt binaries from the GitHub release's - assets, verified against `SHA256SUMS` — the same inputs the release run - would have published. The GitHub release must exist with all assets, so - failures in `build`, `tag`, or `github-release` itself are still fixed - via the release run. + Re-running `npm-publish` re-stages from the same run; re-running + `cargo-publish` dispatches a fresh run of `publish-cargo.yml` *as of the + tag*. A re-run never picks up workflow edits. +2. **Retry just the failed registry** — right when the fix needed a change + (registry-side config such as a trusted publisher, or a workflow edit + landed on the default branch), or the release run is gone: + - **npm:** Actions → **Release** → Run workflow with `npm-retry-version` + set to the release version (`X.Y.Z`, no `v`). Every other job is + skipped; `npm-publish` re-stages from the existing release. + - **crates.io:** Actions → **Publish crates.io** → Run workflow, entering + the release version and leaving `distinct-id` blank. + + Both run the workflow as it exists on the dispatched branch (default: the + default branch), so workflow fixes apply. Nothing rebuilds: each checks + out the `v` tag, and npm takes the prebuilt binaries from the + GitHub release's assets, verified against `SHA256SUMS` — the same inputs + the release run would have published. The GitHub release must exist with + all assets, so failures in `build`, `tag`, or `github-release` itself are + still fixed via the release run. ## One-time registry setup @@ -128,24 +130,18 @@ Both registries authenticate via OIDC trusted publishing, so a missing or misconfigured trusted publisher **fails that channel's job** — configure it before dispatching a real release. Neither workflow uses a deployment environment. -Since the publish legs moved into their own workflow files, each trusted -publisher is registered against repo `SocketDev/socket-patch` + **the -publish workflow's filename** (not `release.yml`). The legs only ever run -as top-level `workflow_dispatch` runs of their own file — whether the -release run dispatched them or a maintainer did — so the OIDC token's -`workflow_ref` and `job_workflow_ref` claims both name that file, and one -registration per package satisfies both registries' top-level workflow matching. +Each trusted publisher is registered against repo `SocketDev/socket-patch` + +the filename of the **top-level** workflow that publishes, because both +registries match the OIDC token's `workflow_ref` claim: | Registry | Publisher workflow | |----------|--------------------| | crates.io (`socket-patch-core`, `socket-patch-cli`) | `publish-cargo.yml` | -| npm (main + 14 platform packages) | `publish-npm.yml` | - -**Migration from the `release.yml` publishers:** crates.io can carry the -old `release.yml` publisher alongside the new one until every -release run predating this split — whose re-run legs still authenticate as -`release.yml` — has fully landed; then delete the `release.yml` publishers. -npm allows only **one** trusted publisher per package, so its cutover is -atomic: edit each package's publisher from `release.yml` to -`publish-npm.yml` once no pre-split npm job may need re-running (approving -already-staged versions needs no OIDC, only re-staging does). +| npm (main + 14 platform packages) | `release.yml` | + +npm allows only **one** trusted publisher per package, so npm publishes from +a job of `release.yml` itself — the release run and an `npm-retry-version` +retry both run as `release.yml`. The crates.io leg always runs as a +top-level `workflow_dispatch` run of `publish-cargo.yml`, whether the release +run dispatched it or a maintainer did. crates.io allows several publishers +per crate, so an old `release.yml` registration there can stay or go. diff --git a/scripts/dispatch-publish.sh b/scripts/dispatch-publish.sh index 349e7ca5d..d7aeee2e4 100755 --- a/scripts/dispatch-publish.sh +++ b/scripts/dispatch-publish.sh @@ -1,19 +1,17 @@ #!/usr/bin/env bash # Dispatches one registry publish workflow (publish-*.yml) at the release tag # and waits for the dispatched run to finish, propagating its conclusion. -# Used by the publish fan-out jobs in release.yml — see that file's header -# for why the legs run as separate workflow_dispatch runs rather than -# reusable-workflow calls (registry trusted publishers match the TOP-LEVEL -# workflow filename on npm/crates.io, and npm allows only one publisher per -# package). +# Used by release.yml's cargo-publish job — see that file's header for why +# the crates.io leg runs as a separate workflow_dispatch run rather than a +# reusable-workflow call (registry trusted publishers match the TOP-LEVEL +# workflow filename). # # Usage: dispatch-publish.sh [key=value]... -# e.g. publish-npm.yml (must have a workflow_dispatch +# e.g. publish-cargo.yml (must have a workflow_dispatch # trigger with `version` and `distinct-id` inputs) # X.Y.Z; the tag v must exist (the dispatch runs # the workflow file as of that tag) # [key=value]... extra workflow inputs, passed through as `-f key=value` -# (e.g. sums-digest= for the npm leg) # # Requires: gh authenticated via GH_TOKEN with actions:write on # $GITHUB_REPOSITORY; GITHUB_RUN_ID/GITHUB_RUN_ATTEMPT for run correlation. From aaa9f698e1c24b93b2949ae21ef6f0006927dedf Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Mon, 12 Oct 2026 15:41:15 -0400 Subject: [PATCH 2/5] chore(release): fold 22 more PRs into the 5.0.0 notes Cover the PRs merged to main after #1432's base (17793173f..4cb3643ac): Cargo source replacement and registry = "crates-io" (#338/#455, #386), pnpm lockfile=false (#1074), berry removed descriptors (#1435), yarnrc ${VAR} reads (#1100), CRLF .npmrc append (#1196), Composer vendor-dir cascade and global home (#439/#686, #586), Go +incompatible vex (#484), gem moved BUNDLE_GEMFILE and commented Gemfile revert (#952, #988), Poetry source = "pypi" (#1282) and Hatch path characters (#547). No entries for #1411 (macOS fix to code #1393 added in this release; the #1393 entry already describes the intended behavior), #1426 (perf regression introduced and fixed within 5.0.0) or the CI/test/bench PRs. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 146c55df9..85ee0418b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -838,8 +838,16 @@ limits, and required install commands. (#393, #458, #531). - Agent `apply` and `vendor` work on Windows with Go's read-only module cache instead of failing with `Access is denied` (#346). -- **Composer.** Reinstall hints name the configured vendor directory - (`COMPOSER_VENDOR_DIR`, `config.vendor-dir`) instead of `vendor/` (#658). + - `vex` attests an applied agent-mode patch for a `+incompatible` module + instead of reporting `not_applied` (#484). +- **Composer.** + - Reinstall hints name the configured vendor directory + (`COMPOSER_VENDOR_DIR`, `config.vendor-dir`) instead of `vendor/` (#658). + - `scan -g` and agent-mode scans find installs whose `vendor-dir` is set + in the global or another inherited config, not only the project's + `composer.json` (#439, #686). + - Global discovery finds a Composer 2 home in the XDG config dir even when + an old `~/.composer` directory remains (#586). - **npm family.** - npm's `${VAR}` expansion is applied to `replace-registry-host`, so a setting that expands to `always` gets the hosted-pin warning (#1233). @@ -872,6 +880,19 @@ limits, and required install commands. be listed and unwound (#1243). - Upgrading or downgrading a vendored npm or Bun package reverts cleanly instead of being kept as drift (#1155). + - Hosted mode leaves pnpm locks alone when pnpm's `lockfile` setting is + off (`lockfile=false`, `lockfile: false` or `package-lock=false`), since + pnpm then installs from the registry; it warns + `redirect_pnpm_lockfile_disabled` and suggests vendored mode, and + lock-only `vex` no longer attests those pins (#1074). + - Yarn Berry rollback / remove after a `yarn remove` in one workspace + member restore the registry entry keyed only by the descriptors still + declared, so `yarn install --immutable` passes (#1435). + - `.yarnrc.yml` settings are read the way yarn reads them, including + `${VAR:-default}` and quoted YAML values, so a PnP project configured + through a variable is refused as PnP (#1100). + - Appending `allow-remote=all` to a CRLF `.npmrc` with no final newline + keeps its line endings intact (#1196). - Hosted scans pin npm aliases in a v2 lock's npm 6 `dependencies` mirror and in v1 locks, vendoring rewires them, and VEX withholds while the mirror still resolves from the registry (#432). @@ -927,6 +948,15 @@ limits, and required install commands. (#480). `vex` withholds, and restore handles, a hosted crate when a later dependency locks its own crates.io copy of the same version (#679, #863). + - Cargo source replacement (`[source.crates-io] replace-with` to a + directory source) is followed through every config cargo reads, so + agent mode patches, and `vex` / rollback judge, the copy cargo builds + from instead of assuming `/vendor`. A `vendor/` that no + `replace-with` wires is no longer crawled. Hosted mode skips every crate + when crates.io is replaced by an on-disk source + (`redirect_cargo_directory_source`) (#338, #455). + - Hosted mode pins a dependency declared with an explicit + `registry = "crates-io"` instead of skipping it (#386). - **RubyGems.** - Gemfile lines joining declarations with `;` are refused instead of losing the second gem; a declaration ending in a bare `;` is rewritten again @@ -955,6 +985,11 @@ limits, and required install commands. the shared gem home holds (`redirect_gem_no_lockfile`) (#1125). - Gem checks ignore system gem homes Bundler never loads for the project (`deployment`, explicit `path`) (#1098, #1109). + - With `BUNDLE_GEMFILE` pointing into another directory (Appraisal-style + `gemfiles/rails_7.gemfile`), agent mode, `vex` and the stale-install + check probe the stores Bundler actually uses (#952). + - Vendored revert no longer half-reverts when the wired Gemfile line was + commented out (#988). - **PyPI.** - A Pipenv project without a Pipenv venv no longer falls back to the system Python (agent patched system packages; vendored failed) (#504, #947). @@ -996,6 +1031,13 @@ limits, and required install commands. reinstall command for removing the patched build (#477). - Vendored `Pipfile.lock` edits change only the patched entry (keeping `\uXXXX` escapes and a leading BOM) (#1128). + - A Poetry dependency pinned to `source = "pypi"` is refused in a lock + written by Poetry 1.4–2.2, which can't install the rewired lock + (`redirect_poetry_lock_unsupported` / `pypi_poetry_source_constraint`); + other Poetry versions are rewired with a warning (#1282). + - Vendored Hatch refuses (`pypi_hatch_unsupported`) a checkout whose path + contains `%`, `#`, `;` or `?`, which Hatch's `{root:uri}` turns into a + broken file URL (#547). - **Vendoring.** Vendored mode refuses to replace a symlinked lockfile or manifest (`redirect_symlinked_file_unsupported`) instead of detaching it from its target (#627). From 5efbc173d750d6542d8ca02c17737c7acf320c06 Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Tue, 13 Oct 2026 10:13:19 -0400 Subject: [PATCH 3/5] chore(release): cover #1419, #1434, #1440, #1442, #1444 in the 5.0.0 notes Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85ee0418b..c4f699bd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,8 +41,9 @@ and `vendor` (committed patched packages), with `list` for inspection. See the selects vendored mode and an agent manifest selects agent mode, so the quick start no longer converts an existing project to hosted in place. Switching modes (in either direction) takes an explicit `--mode`; a project holding - both kinds of state is a usage error (`mode_ambiguous`, exit 2). Human runs - print a note naming the mode kept (#1088). + both kinds of state, or vendored packages beside hosted pins, is a usage + error (`mode_ambiguous`, exit 2) that names the packages. Human runs print + a note naming the mode kept (#1088, #1437). - Every command's `--json` output is one envelope: `command`, `status`, `dryRun`, `events`, `summary`, `warnings`, plus `error: {code, message}` on failure and `gc` when a sweep ran. `scan`, `get` and `rollback` drop their @@ -352,6 +353,9 @@ limits, and required install commands. - The `apply --check` drift remedy repeats the check's own `--cwd`, `--manifest-path`, `-g` / `--global-prefix` and `--ecosystems` flags, instead of a bare `socket-patch apply` that acts on another tree (#1219). +- Hosted `rollback` / `remove`, takeover and eject look up upstream registry + entries a few at a time instead of all at once, so many pins no longer + trip registry rate limits or `EMFILE` (#1220). - The OpenVEX document is written with stage + rename, so a failed write no longer leaves a truncated document behind (#1144). - Vendored Maven, Gradle and NuGet artifacts are protected from `.gitignore` @@ -880,6 +884,9 @@ limits, and required install commands. be listed and unwound (#1243). - Upgrading or downgrading a vendored npm or Bun package reverts cleanly instead of being kept as drift (#1155). + - Vendored `bun.lockb` workspaces check that git would commit the + member-relative tarball mirrors, as the root artifact already was, so a + `*.tgz` ignore rule can't break fresh frozen installs (#1116). - Hosted mode leaves pnpm locks alone when pnpm's `lockfile` setting is off (`lockfile=false`, `lockfile: false` or `package-lock=false`), since pnpm then installs from the registry; it warns @@ -1035,6 +1042,13 @@ limits, and required install commands. written by Poetry 1.4–2.2, which can't install the rewired lock (`redirect_poetry_lock_unsupported` / `pypi_poetry_source_constraint`); other Poetry versions are rewired with a warning (#1282). + - Venvs created with `--system-site-packages` (including Poetry's and + Pipenv's options for it) are crawled through to the base interpreter's + packages, so `vex` no longer attests a lock pin while an unpatched base + copy is imported, and stale-install guards warn about it (#409). + - Hosted and vendored uv refuse to wire through `[tool.uv.sources]` when + `no-sources = true` or `no-sources-package` makes uv ignore it + (`redirect_uv_project_unsupported`, `pypi_uv_no_sources`) (#564). - Vendored Hatch refuses (`pypi_hatch_unsupported`) a checkout whose path contains `%`, `#`, `;` or `?`, which Hatch's `{root:uri}` turns into a broken file URL (#547). From 656c663f4ac037b05bc1c03378b1a9d14b391e8b Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Tue, 13 Oct 2026 10:14:25 -0400 Subject: [PATCH 4/5] chore(release): 5.0.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cut [Unreleased] into [5.0.1] — 2026-10-13 and stamp 5.0.1 with scripts/version-sync.sh. The 5.0.1 notes cover main through 501c6d938 (#1449), i.e. every PR after the v5.0.0 tag (6ad6add4a): the npm publish fix (5.0.0 never reached npm or crates.io, 5.0.1 is the first v5 there), #1465, #759, #1460, #1462, #1445, #869, #1447, #1450, #1467, and #1449 under Maintenance. CI/test-only PRs (#1448, #1452, #1454, #1457, #1459) have no entries. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 49 +++++++++++++++++++ Cargo.lock | 8 +-- Cargo.toml | 4 +- npm/socket-patch-android-arm64/package.json | 2 +- npm/socket-patch-darwin-arm64/package.json | 2 +- npm/socket-patch-darwin-x64/package.json | 2 +- npm/socket-patch-linux-arm-gnu/package.json | 2 +- npm/socket-patch-linux-arm-musl/package.json | 2 +- npm/socket-patch-linux-arm64-gnu/package.json | 2 +- .../package.json | 2 +- npm/socket-patch-linux-ia32-gnu/package.json | 2 +- npm/socket-patch-linux-ia32-musl/package.json | 2 +- npm/socket-patch-linux-x64-gnu/package.json | 2 +- npm/socket-patch-linux-x64-musl/package.json | 2 +- npm/socket-patch-win32-arm64/package.json | 2 +- npm/socket-patch-win32-ia32/package.json | 2 +- npm/socket-patch-win32-x64/package.json | 2 +- npm/socket-patch/package-lock.json | 32 ++++++------ npm/socket-patch/package.json | 30 ++++++------ 19 files changed, 100 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f699bd0..a88ef4248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,55 @@ a version without a non-empty section in this file. ## [Unreleased] +## [5.0.1] — 2026-10-13 + +### Fixed + +- **5.0.1 is the first v5 release on npm and crates.io.** v5.0.0 was tagged + and its standalone binaries were published on GitHub, but its npm and + crates.io publishes failed on a trusted-publisher mismatch, so + `@socketsecurity/socket-patch` and the Cargo crates stayed on 4.0.0. npm + now publishes from the Release workflow its trusted publisher names. + Upgrading from 4.x through npm or Cargo goes straight to 5.0.1; read the + [v5 migration guide](docs/migrating-to-v5.md) and the 5.0.0 notes below + first. +- Hosted `rollback` / `remove` behind a registry mirror that passes npmjs + tarball URLs through keep Yarn Berry's `::__archiveUrl=` binding and + pnpm's `tarball:` field, so the next cold install no longer 404s (#1465). +- A Yarn Berry vendored revert rebuilds the restored `yarn.lock` entry from + the live lock instead of the one recorded at vendor time, so + `yarn install --immutable` keeps passing after later lock changes (#759). +- Hosted pnpm reads `npm_config_*` / `pnpm_config_*` environment settings + only where the project's pnpm major reads them, so an npm-era + `package-lock=false` under `npx` no longer blocks pinning a lock pnpm 11/12 + installs from (#1460). +- A yarn classic pin written by v4 for a patch that adds a dependency the + lock never locked is no longer confirmed on rescan + (`redirect_yarn_classic_dep_manifest_unlocked`), and `vex` omits hosted + npm-family copies whose new dependencies aren't installed + (`dependency_not_installed`) (#1462). +- uv: after `uv remove` or an upgrade of a hosted-pinned transitive + package's parent, `list` warns `hosted_resolution_orphaned` and `rollback` + / `remove` retire the leftover override wiring instead of failing with + `hosted_wiring_contested` (#1445). Reverting a vendored PEP 723 script lock + restores the specifier the script declares now, not the one recorded at + vendor time (#869). +- Poetry 1.3 locks are refused for a `source = "pypi"` dependency, like + 1.4–2.2 locks, since Poetry 1.3 can't solve the rewired lock (#1447). +- The stale-install remedy for a Pipenv `--site-packages` venv importing a + base-interpreter copy installs over it inside the venv, instead of an + uninstall pip refuses (#1450). +- `vendor --check` explains a missing or changed vendored artifact in plain + words, naming the file and the `repair` step, in human output and the + `--json` event `reason` (#1467). + +### Maintenance + +- Release binaries build with one codegen unit, about 16% smaller. +- npm publishing runs inline in `release.yml` (`publish-npm.yml` is removed), + and the Release workflow gains an `npm-retry-version` input that re-stages + npm from an existing GitHub release. + ## [5.0.0] — 2026-10-12 v5 centers the workflow on `scan` (hosted patches), `vex` (OpenVEX attestations), diff --git a/Cargo.lock b/Cargo.lock index 4d57d54b3..35faf6ba6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1891,7 +1891,7 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket-patch-bench" -version = "5.0.0" +version = "5.0.1" dependencies = [ "base64", "clap", @@ -1910,7 +1910,7 @@ dependencies = [ [[package]] name = "socket-patch-cli" -version = "5.0.0" +version = "5.0.1" dependencies = [ "base64", "clap", @@ -1944,7 +1944,7 @@ dependencies = [ [[package]] name = "socket-patch-core" -version = "5.0.0" +version = "5.0.1" dependencies = [ "aho-corasick", "base64", @@ -1986,7 +1986,7 @@ dependencies = [ [[package]] name = "socket-patch-node" -version = "5.0.0" +version = "5.0.1" dependencies = [ "napi", "napi-build", diff --git a/Cargo.toml b/Cargo.toml index df6c364af..ff4d145d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,13 +14,13 @@ default-members = [ resolver = "2" [workspace.package] -version = "5.0.0" +version = "5.0.1" edition = "2021" license = "MIT" repository = "https://github.com/SocketDev/socket-patch" [workspace.dependencies] -socket-patch-core = { path = "crates/socket-patch-core", version = "=5.0.0" } +socket-patch-core = { path = "crates/socket-patch-core", version = "=5.0.1" } clap = { version = "=4.5.60", features = ["derive", "env"] } serde = { version = "=1.0.228", features = ["derive"] } serde_json = { version = "=1.0.149", features = ["preserve_order", "raw_value"] } diff --git a/npm/socket-patch-android-arm64/package.json b/npm/socket-patch-android-arm64/package.json index 97cc09d24..9864dab47 100644 --- a/npm/socket-patch-android-arm64/package.json +++ b/npm/socket-patch-android-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-android-arm64", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Android ARM64", "os": [ "android" diff --git a/npm/socket-patch-darwin-arm64/package.json b/npm/socket-patch-darwin-arm64/package.json index 62052f624..6c5cef673 100644 --- a/npm/socket-patch-darwin-arm64/package.json +++ b/npm/socket-patch-darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-darwin-arm64", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for macOS ARM64", "os": [ "darwin" diff --git a/npm/socket-patch-darwin-x64/package.json b/npm/socket-patch-darwin-x64/package.json index 7bf94c87c..540f74c88 100644 --- a/npm/socket-patch-darwin-x64/package.json +++ b/npm/socket-patch-darwin-x64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-darwin-x64", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for macOS x64", "os": [ "darwin" diff --git a/npm/socket-patch-linux-arm-gnu/package.json b/npm/socket-patch-linux-arm-gnu/package.json index 6cb1a85e2..9d5156389 100644 --- a/npm/socket-patch-linux-arm-gnu/package.json +++ b/npm/socket-patch-linux-arm-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-linux-arm-gnu", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Linux ARM (glibc)", "os": [ "linux" diff --git a/npm/socket-patch-linux-arm-musl/package.json b/npm/socket-patch-linux-arm-musl/package.json index 19e611d92..98c779b1f 100644 --- a/npm/socket-patch-linux-arm-musl/package.json +++ b/npm/socket-patch-linux-arm-musl/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-linux-arm-musl", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Linux ARM (musl)", "os": [ "linux" diff --git a/npm/socket-patch-linux-arm64-gnu/package.json b/npm/socket-patch-linux-arm64-gnu/package.json index d29c560bb..752ada98c 100644 --- a/npm/socket-patch-linux-arm64-gnu/package.json +++ b/npm/socket-patch-linux-arm64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-linux-arm64-gnu", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Linux ARM64 (glibc)", "os": [ "linux" diff --git a/npm/socket-patch-linux-arm64-musl/package.json b/npm/socket-patch-linux-arm64-musl/package.json index 04b5b1f32..c2317409f 100644 --- a/npm/socket-patch-linux-arm64-musl/package.json +++ b/npm/socket-patch-linux-arm64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-linux-arm64-musl", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Linux ARM64 (musl)", "os": [ "linux" diff --git a/npm/socket-patch-linux-ia32-gnu/package.json b/npm/socket-patch-linux-ia32-gnu/package.json index 009ed71d0..e9b93747f 100644 --- a/npm/socket-patch-linux-ia32-gnu/package.json +++ b/npm/socket-patch-linux-ia32-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-linux-ia32-gnu", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Linux ia32 (glibc)", "os": [ "linux" diff --git a/npm/socket-patch-linux-ia32-musl/package.json b/npm/socket-patch-linux-ia32-musl/package.json index 0a6fb2e6a..c2357f9e1 100644 --- a/npm/socket-patch-linux-ia32-musl/package.json +++ b/npm/socket-patch-linux-ia32-musl/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-linux-ia32-musl", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Linux ia32 (musl)", "os": [ "linux" diff --git a/npm/socket-patch-linux-x64-gnu/package.json b/npm/socket-patch-linux-x64-gnu/package.json index ccf23d031..2790f998f 100644 --- a/npm/socket-patch-linux-x64-gnu/package.json +++ b/npm/socket-patch-linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-linux-x64-gnu", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Linux x64 (glibc)", "os": [ "linux" diff --git a/npm/socket-patch-linux-x64-musl/package.json b/npm/socket-patch-linux-x64-musl/package.json index 76e7a031a..58d717bc2 100644 --- a/npm/socket-patch-linux-x64-musl/package.json +++ b/npm/socket-patch-linux-x64-musl/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-linux-x64-musl", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Linux x64 (musl)", "os": [ "linux" diff --git a/npm/socket-patch-win32-arm64/package.json b/npm/socket-patch-win32-arm64/package.json index 747eb7bc5..ed1a96f5c 100644 --- a/npm/socket-patch-win32-arm64/package.json +++ b/npm/socket-patch-win32-arm64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-win32-arm64", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Windows ARM64", "os": [ "win32" diff --git a/npm/socket-patch-win32-ia32/package.json b/npm/socket-patch-win32-ia32/package.json index cdc357f6a..df3d1a3a4 100644 --- a/npm/socket-patch-win32-ia32/package.json +++ b/npm/socket-patch-win32-ia32/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-win32-ia32", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Windows ia32", "os": [ "win32" diff --git a/npm/socket-patch-win32-x64/package.json b/npm/socket-patch-win32-x64/package.json index 340df8ee6..be495250b 100644 --- a/npm/socket-patch-win32-x64/package.json +++ b/npm/socket-patch-win32-x64/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch-win32-x64", - "version": "5.0.0", + "version": "5.0.1", "description": "socket-patch binary for Windows x64", "os": [ "win32" diff --git a/npm/socket-patch/package-lock.json b/npm/socket-patch/package-lock.json index 9dfd24de9..57c89b8a1 100644 --- a/npm/socket-patch/package-lock.json +++ b/npm/socket-patch/package-lock.json @@ -1,12 +1,12 @@ { "name": "@socketsecurity/socket-patch", - "version": "5.0.0", + "version": "5.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@socketsecurity/socket-patch", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "dependencies": { "zod": "3.25.76" @@ -22,20 +22,20 @@ "node": ">=18.0.0" }, "optionalDependencies": { - "@socketsecurity/socket-patch-android-arm64": "5.0.0", - "@socketsecurity/socket-patch-darwin-arm64": "5.0.0", - "@socketsecurity/socket-patch-darwin-x64": "5.0.0", - "@socketsecurity/socket-patch-linux-arm-gnu": "5.0.0", - "@socketsecurity/socket-patch-linux-arm-musl": "5.0.0", - "@socketsecurity/socket-patch-linux-arm64-gnu": "5.0.0", - "@socketsecurity/socket-patch-linux-arm64-musl": "5.0.0", - "@socketsecurity/socket-patch-linux-ia32-gnu": "5.0.0", - "@socketsecurity/socket-patch-linux-ia32-musl": "5.0.0", - "@socketsecurity/socket-patch-linux-x64-gnu": "5.0.0", - "@socketsecurity/socket-patch-linux-x64-musl": "5.0.0", - "@socketsecurity/socket-patch-win32-arm64": "5.0.0", - "@socketsecurity/socket-patch-win32-ia32": "5.0.0", - "@socketsecurity/socket-patch-win32-x64": "5.0.0" + "@socketsecurity/socket-patch-android-arm64": "5.0.1", + "@socketsecurity/socket-patch-darwin-arm64": "5.0.1", + "@socketsecurity/socket-patch-darwin-x64": "5.0.1", + "@socketsecurity/socket-patch-linux-arm-gnu": "5.0.1", + "@socketsecurity/socket-patch-linux-arm-musl": "5.0.1", + "@socketsecurity/socket-patch-linux-arm64-gnu": "5.0.1", + "@socketsecurity/socket-patch-linux-arm64-musl": "5.0.1", + "@socketsecurity/socket-patch-linux-ia32-gnu": "5.0.1", + "@socketsecurity/socket-patch-linux-ia32-musl": "5.0.1", + "@socketsecurity/socket-patch-linux-x64-gnu": "5.0.1", + "@socketsecurity/socket-patch-linux-x64-musl": "5.0.1", + "@socketsecurity/socket-patch-win32-arm64": "5.0.1", + "@socketsecurity/socket-patch-win32-ia32": "5.0.1", + "@socketsecurity/socket-patch-win32-x64": "5.0.1" } }, "node_modules/@types/node": { diff --git a/npm/socket-patch/package.json b/npm/socket-patch/package.json index 7130a3bd3..c536f8554 100644 --- a/npm/socket-patch/package.json +++ b/npm/socket-patch/package.json @@ -1,6 +1,6 @@ { "name": "@socketsecurity/socket-patch", - "version": "5.0.0", + "version": "5.0.1", "description": "CLI tool and schema library for applying security patches to dependencies", "bin": { "socket-patch": "bin/socket-patch" @@ -49,19 +49,19 @@ "@types/node": "20.19.41" }, "optionalDependencies": { - "@socketsecurity/socket-patch-android-arm64": "5.0.0", - "@socketsecurity/socket-patch-darwin-arm64": "5.0.0", - "@socketsecurity/socket-patch-darwin-x64": "5.0.0", - "@socketsecurity/socket-patch-linux-arm-gnu": "5.0.0", - "@socketsecurity/socket-patch-linux-arm-musl": "5.0.0", - "@socketsecurity/socket-patch-linux-arm64-gnu": "5.0.0", - "@socketsecurity/socket-patch-linux-arm64-musl": "5.0.0", - "@socketsecurity/socket-patch-linux-ia32-gnu": "5.0.0", - "@socketsecurity/socket-patch-linux-ia32-musl": "5.0.0", - "@socketsecurity/socket-patch-linux-x64-gnu": "5.0.0", - "@socketsecurity/socket-patch-linux-x64-musl": "5.0.0", - "@socketsecurity/socket-patch-win32-arm64": "5.0.0", - "@socketsecurity/socket-patch-win32-ia32": "5.0.0", - "@socketsecurity/socket-patch-win32-x64": "5.0.0" + "@socketsecurity/socket-patch-android-arm64": "5.0.1", + "@socketsecurity/socket-patch-darwin-arm64": "5.0.1", + "@socketsecurity/socket-patch-darwin-x64": "5.0.1", + "@socketsecurity/socket-patch-linux-arm-gnu": "5.0.1", + "@socketsecurity/socket-patch-linux-arm-musl": "5.0.1", + "@socketsecurity/socket-patch-linux-arm64-gnu": "5.0.1", + "@socketsecurity/socket-patch-linux-arm64-musl": "5.0.1", + "@socketsecurity/socket-patch-linux-ia32-gnu": "5.0.1", + "@socketsecurity/socket-patch-linux-ia32-musl": "5.0.1", + "@socketsecurity/socket-patch-linux-x64-gnu": "5.0.1", + "@socketsecurity/socket-patch-linux-x64-musl": "5.0.1", + "@socketsecurity/socket-patch-win32-arm64": "5.0.1", + "@socketsecurity/socket-patch-win32-ia32": "5.0.1", + "@socketsecurity/socket-patch-win32-x64": "5.0.1" } } From 0b23ce5293d3aa05e8e27b40b0688b450b3b7aff Mon Sep 17 00:00:00 2001 From: Mikola Lysenko Date: Tue, 13 Oct 2026 11:11:57 -0400 Subject: [PATCH 5/5] chore(release): 5.0.1 notes for PRs merged since the cut Covers #1469, #1470, #1474, #1475, #1478, #1479, #1480 and #1482; #1484 is CI-only. The notes now cover main through 285a5e80b. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a88ef4248..3dd608933 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,32 @@ a version without a non-empty section in this file. - `vendor --check` explains a missing or changed vendored artifact in plain words, naming the file and the `repair` step, in human output and the `--json` event `reason` (#1467). +- A hosted pin beside a `file:` copy of the same release (tarball or + directory) no longer passes silently. The registry entry is still pinned, + but the in-run `--vex` no longer attests the package `not_affected`, since + npm and pnpm keep installing the `file:` copy unpatched. npm now also + warns about the directory shape (#1481), and pnpm warns + `redirect_copy_unpatched` naming the lock and entry (#935; vendored pnpm + still writes its override silently beside such a copy). +- Path-scoped `scan --mode agent ` and `rollback ` reach an + npm alias dependency (`"lp": "npm:left-pad@1.3.0"`) that pnpm's isolated + linker or npm's `install-strategy=linked` links into the member, instead + of reporting `success` with the copy unpatched (#1266). +- A hosted `scan` / `get` from a workspace member reads the root's + `workspaces` globs the way the lock's package manager does (yarn classic + ignores `!` negation, yarn Berry supports extglobs), so it refuses with + `redirect_workspace_lockfile_elsewhere` instead of reporting success with + nothing pinned (#1097). +- Bundler: a Gemfile `lockfile "custom.lock"` call is honored, so hosted + `scan` pins the lock bundler actually reads instead of a leftover + `Gemfile.lock` (#1477). Hosted `rollback` / `remove` keep the + `DEPENDENCIES` `!` for a gem in the user's own `source … do` block after + an assignment-form `if` / `case` / `begin` construct, so frozen installs + on Bundler 4.0.19+ keep passing (#1476). +- Hosted scans of Hatch, Poetry and PDM projects with many patched + dependencies are much faster: the Hatch files and the `poetry.lock` / + `pdm.lock` are rewritten in one pass instead of once per package + (#1471, #1473). ### Maintenance