Skip to content

Commit 9246c28

Browse files
committed
feat(external-tools): SRI integrity migration + pnpm 11.0.6 + Node 26
Three coordinated changes that ship together because they cross the same data: 1. **Schema migration**: every `sha256` field in external-tools.json becomes `integrity` carrying a Subresource Integrity string (`<algo>-<base64>`). Same encoding as npm's package-lock.json, same encoding upstream registries publish, generalizes to sha384 and sha512 without schema churn. install-tool.mjs parses the algorithm from the SRI prefix and runs the matching createHash(). Bare 64-char hex is still accepted for backward compat (deprecated path, will be removed once all consumer call sites are migrated). 2. **pnpm 11.0.6 bump** with darwin-x64 split-path: 7 platforms get their new sha256-base64 SRIs from the GitHub release artifacts. darwin-x64 takes a different shape — the upstream pnpm SEA binary was dropped in 11.0.5 (nodejs/node#62893: LIEF/Mach-O bug Node has declined to fix), so Intel Mac instead pulls the npm-registry JS tarball and runs it through the system Node. Adds new fields `source: "npm-registry"` + `binary: "package/dist/pnpm.cjs"` and a sha512 SRI from `npm view pnpm@11.0.6 dist.integrity`. The setup action's pnpm install step branches on `source` to pick the download URL and (for npm-registry) write a `node` wrapper at $PNPM_DIR/pnpm. 3. **Node default 25.9.0 → 26.0.0** in setup + setup-and-install composite actions, matching the upstream Node 26.0.0 GA. The migration is fleet-wide; consumers reading external-tools.json need to switch from `.sha256` to `.integrity` JSON paths in the same PR window. Drift watch (template/CLAUDE.md) applies.
1 parent f3edacb commit 9246c28

5 files changed

Lines changed: 133 additions & 65 deletions

File tree

.github/actions/checkout/action.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,15 @@ runs:
8686
echo "SOCKET_TOOL_ZIZMOR_AVAILABLE=false" >> "${GITHUB_ENV:-/dev/null}"
8787
exit 0
8888
fi
89-
EXPECTED_SHA256="$(node "$JQ" "$TOOLS_FILE" zizmor checksums "$PLATFORM" sha256)"
89+
INTEGRITY="$(node "$JQ" "$TOOLS_FILE" zizmor checksums "$PLATFORM" integrity)"
9090
ZIZMOR_BIN="$ZIZMOR_DIR/zizmor"
9191
[[ "$ASSET" == *.zip ]] && ZIZMOR_BIN="$ZIZMOR_DIR/zizmor.exe"
92-
# Shared installer: fetches, verifies sha256, extracts, chmods.
92+
# Shared installer: fetches, integrity-verifies (SRI), extracts.
9393
INSTALL_TOOL="${GITHUB_ACTION_PATH}/../lib/install-tool.mjs"
9494
if [ ! -x "$ZIZMOR_BIN" ]; then
9595
node "$INSTALL_TOOL" \
9696
"https://github.com/zizmorcore/zizmor/releases/download/v${ZIZMOR_VERSION}/${ASSET}" \
97-
"$EXPECTED_SHA256" \
97+
"$INTEGRITY" \
9898
"$ZIZMOR_DIR"
9999
fi
100100
echo "$ZIZMOR_DIR" >> "${GITHUB_PATH:-/dev/null}"
@@ -106,7 +106,7 @@ runs:
106106
echo "SOCKET_TOOL_ZIZMOR_VERSION=$ZIZMOR_VERSION"
107107
echo "SOCKET_TOOL_ZIZMOR_PLATFORM=$PLATFORM"
108108
echo "SOCKET_TOOL_ZIZMOR_ASSET=$ASSET"
109-
echo "SOCKET_TOOL_ZIZMOR_SHA256=$EXPECTED_SHA256"
109+
echo "SOCKET_TOOL_ZIZMOR_INTEGRITY=$INTEGRITY"
110110
echo "SOCKET_TOOL_ZIZMOR_BIN=$ZIZMOR_BIN"
111111
echo "SOCKET_TOOL_ZIZMOR_DIR=$ZIZMOR_DIR"
112112
} >> "${GITHUB_ENV:-/dev/null}"

.github/actions/lib/install-tool.mjs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
/**
2-
* @fileoverview Downloads, sha256-verifies, and extracts a release asset.
2+
* @fileoverview Downloads, integrity-verifies, and extracts a release asset.
33
*
44
* Replaces the curl + sha256sum/shasum + tar/unzip dance repeated
55
* across pnpm/sfw/zizmor install steps. Built-in `fetch` follows
66
* redirects automatically (github.com → objects.githubusercontent.com),
7-
* `node:crypto.createHash` computes sha256 in-process, and tar/unzip
7+
* `node:crypto.createHash` computes the digest in-process, and tar/unzip
88
* shell out (already preinstalled on every supported runner image).
99
*
1010
* Usage:
11-
* node .github/actions/lib/install-tool.mjs <url> <expected-sha256> <dest-dir> [<bin-name>]
11+
* node install-tool.mjs <url> <integrity> <dest-dir> [<bin-name>]
12+
*
13+
* <integrity> is a Subresource Integrity string: `<algo>-<base64>`.
14+
* Examples: `sha256-67PM...=`, `sha512-l/kG...==`. The algorithm is
15+
* parsed from the prefix; multiple algos are supported (sha256, sha384,
16+
* sha512). Same encoding as npm package-lock.json's `integrity` field
17+
* and as `external-tools.json`'s `integrity` field.
18+
*
19+
* Backward compat: a bare 64-char hex string is also accepted and
20+
* treated as `sha256-<base64-of-hex>` for transition. Deprecated; new
21+
* call sites should pass SRI directly.
1222
*
1323
* Behavior:
1424
* - Streams the asset to <dest-dir>/<basename(url)>.
15-
* - Aborts and removes the file if sha256 mismatches.
25+
* - Aborts and removes the file if integrity mismatches.
1626
* - Extracts .tar.gz/.tgz with tar, .zip with unzip (POSIX) or
1727
* Expand-Archive (Windows). Removes the archive after extracting.
1828
* - For non-archive assets (bare binaries like sfw): the asset IS
@@ -21,21 +31,46 @@
2131
* Exit codes:
2232
* 0 success
2333
* 1 download or extraction failed
24-
* 2 sha256 mismatch (stderr names expected vs actual + the path)
34+
* 2 integrity mismatch (stderr names expected vs actual + the path)
2535
*/
2636

2737
import { spawnSync } from 'node:child_process'
2838
import { createHash } from 'node:crypto'
2939
import { chmodSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
3040
import path from 'node:path'
3141

32-
const [, , url, expectedSha256, destDir, binName] = process.argv
42+
const [, , url, integrityArg, destDir, binName] = process.argv
43+
44+
if (!url || !integrityArg || !destDir) {
45+
console.error(
46+
'× usage: install-tool.mjs <url> <integrity> <dest-dir> [<bin-name>]',
47+
)
48+
process.exit(1)
49+
}
3350

34-
if (!url || !expectedSha256 || !destDir) {
35-
console.error('× usage: install-tool.mjs <url> <expected-sha256> <dest-dir> [<bin-name>]')
51+
// Parse SRI string `<algo>-<base64>`. Bare 64-char hex is treated as
52+
// sha256 for backward compat — deprecated, will be removed once all
53+
// call sites pass SRI directly.
54+
function parseIntegrity(s) {
55+
const m = /^(sha(?:256|384|512))-(.+)$/.exec(s)
56+
if (m) {
57+
return { algo: m[1], expected: m[2] }
58+
}
59+
if (/^[0-9a-f]{64}$/i.test(s)) {
60+
// Bare sha256 hex — convert to SRI base64 for the comparison.
61+
return {
62+
algo: 'sha256',
63+
expected: Buffer.from(s, 'hex').toString('base64'),
64+
}
65+
}
66+
console.error(
67+
`× unrecognized integrity format: ${s}\n Expected SRI (e.g. sha256-base64=)`,
68+
)
3669
process.exit(1)
3770
}
3871

72+
const { algo, expected } = parseIntegrity(integrityArg)
73+
3974
mkdirSync(destDir, { recursive: true })
4075

4176
const assetName = path.basename(new URL(url).pathname)
@@ -52,17 +87,23 @@ if (process.env.GITHUB_TOKEN) {
5287

5388
const res = await fetch(url, { redirect: 'follow', headers })
5489
if (!res.ok) {
55-
console.error(`× download failed: HTTP ${res.status} ${res.statusText} for ${url}`)
90+
console.error(
91+
`× download failed: HTTP ${res.status} ${res.statusText} for ${url}`,
92+
)
5693
process.exit(1)
5794
}
5895

5996
const bytes = new Uint8Array(await res.arrayBuffer())
60-
const actualSha256 = createHash('sha256').update(bytes).digest('hex')
97+
const actual = createHash(algo).update(bytes).digest('base64')
6198

62-
if (actualSha256 !== expectedSha256) {
63-
console.error(`× sha256 mismatch for ${assetName}`)
64-
console.error(` Expected: ${expectedSha256}`)
65-
console.error(` Actual: ${actualSha256}`)
99+
// Compare base64 forms directly. Trailing `=` padding may differ
100+
// (npm strips it, our hash adds it) — strip both sides before
101+
// comparing so `sha512-...=` and `sha512-...` match.
102+
const stripPadding = b64 => b64.replace(/=+$/, '')
103+
if (stripPadding(actual) !== stripPadding(expected)) {
104+
console.error(${algo} integrity mismatch for ${assetName}`)
105+
console.error(` Expected: ${algo}-${expected}`)
106+
console.error(` Actual: ${algo}-${actual}`)
66107
console.error(` URL: ${url}`)
67108
process.exit(2)
68109
}
@@ -72,7 +113,10 @@ writeFileSync(archivePath, bytes)
72113
const lower = assetName.toLowerCase()
73114
let extractCmd
74115
let extractArgs
75-
if (lower.endsWith('.tar.gz') || lower.endsWith('.tgz')) {
116+
if (
117+
lower.endsWith('.tar.gz') ||
118+
lower.endsWith('.tgz')
119+
) {
76120
extractCmd = 'tar'
77121
extractArgs = ['xzf', archivePath, '-C', destDir]
78122
} else if (lower.endsWith('.zip')) {

.github/actions/setup-and-install/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ inputs:
2828
node-version:
2929
description: 'Node.js version to use'
3030
required: false
31-
default: '25.9.0'
31+
default: '26.0.0'
3232
scope:
3333
description: 'npm registry scope for package authentication'
3434
required: false

.github/actions/setup/action.yml

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ inputs:
1313
node-version:
1414
description: 'Node.js version'
1515
required: false
16-
default: '25.9.0'
16+
default: '26.0.0'
1717
scope:
1818
description: 'npm registry scope for package authentication'
1919
required: false
@@ -81,16 +81,37 @@ runs:
8181
PLATFORM_TOOL="${GITHUB_ACTION_PATH}/../lib/platform.mjs"
8282
PLATFORM="$(node "$PLATFORM_TOOL")"
8383
ASSET="$(node "$JQ" "$TOOLS_FILE" pnpm checksums "$PLATFORM" asset)"
84-
EXPECTED_SHA256="$(node "$JQ" "$TOOLS_FILE" pnpm checksums "$PLATFORM" sha256)"
84+
INTEGRITY="$(node "$JQ" "$TOOLS_FILE" pnpm checksums "$PLATFORM" integrity)"
85+
# Optional `source` field. When set to `npm-registry`, the asset
86+
# is a JS-only npm tarball (e.g. pnpm-11.0.6.tgz) — runs through
87+
# the system Node instead of being a SEA binary. Currently used
88+
# only for darwin-x64 because upstream pnpm dropped the SEA
89+
# binary in 11.0.5 due to nodejs/node#62893.
90+
SOURCE="$(node "$JQ" "$TOOLS_FILE" pnpm checksums "$PLATFORM" source 2>/dev/null || echo "")"
91+
BINARY_REL="$(node "$JQ" "$TOOLS_FILE" pnpm checksums "$PLATFORM" binary 2>/dev/null || echo "")"
8592
PNPM_BIN="$PNPM_DIR/pnpm"
8693
[[ "$ASSET" == *.zip ]] && PNPM_BIN="$PNPM_DIR/pnpm.exe"
87-
# Shared installer: fetches, verifies sha256, extracts, chmods.
94+
# Shared installer: fetches, integrity-verifies (SRI), extracts.
8895
INSTALL_TOOL="${GITHUB_ACTION_PATH}/../lib/install-tool.mjs"
8996
if [ ! -x "$PNPM_BIN" ]; then
90-
node "$INSTALL_TOOL" \
91-
"https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/${ASSET}" \
92-
"$EXPECTED_SHA256" \
93-
"$PNPM_DIR"
97+
if [ "$SOURCE" = "npm-registry" ]; then
98+
URL="https://registry.npmjs.org/pnpm/-/${ASSET}"
99+
else
100+
URL="https://github.com/pnpm/pnpm/releases/download/v${PNPM_VERSION}/${ASSET}"
101+
fi
102+
node "$INSTALL_TOOL" "$URL" "$INTEGRITY" "$PNPM_DIR"
103+
fi
104+
# If the platform uses the npm-registry shape, the extracted
105+
# tarball is a JS package — no native binary. Write a wrapper
106+
# that runs it through the system Node.
107+
if [ "$SOURCE" = "npm-registry" ]; then
108+
BINARY_PATH="$PNPM_DIR/$BINARY_REL"
109+
if [ ! -f "$BINARY_PATH" ]; then
110+
echo "× pnpm npm-registry tarball missing $BINARY_REL after extract" >&2
111+
exit 1
112+
fi
113+
printf '#!/bin/bash\nexec node "%s" "$@"\n' "$BINARY_PATH" > "$PNPM_BIN"
114+
chmod +x "$PNPM_BIN"
94115
fi
95116
echo "$PNPM_DIR" >> "${GITHUB_PATH:-/dev/null}"
96117
# Export canonical pnpm provenance for downstream steps and
@@ -101,7 +122,7 @@ runs:
101122
echo "SOCKET_TOOL_PNPM_VERSION=$PNPM_VERSION"
102123
echo "SOCKET_TOOL_PNPM_PLATFORM=$PLATFORM"
103124
echo "SOCKET_TOOL_PNPM_ASSET=$ASSET"
104-
echo "SOCKET_TOOL_PNPM_SHA256=$EXPECTED_SHA256"
125+
echo "SOCKET_TOOL_PNPM_INTEGRITY=$INTEGRITY"
105126
echo "SOCKET_TOOL_PNPM_BIN=$PNPM_BIN"
106127
echo "SOCKET_TOOL_PNPM_DIR=$PNPM_DIR"
107128
} >> "${GITHUB_ENV:-/dev/null}"
@@ -132,12 +153,12 @@ runs:
132153
SOCKET_API_KEY: ${{ inputs.socket-api-key }}
133154
run: | # zizmor: ignore[github-env]
134155
set -euo pipefail
135-
# SFW (Socket Firewall) version + per-platform asset/sha256
156+
# SFW (Socket Firewall) version + per-platform asset/integrity
136157
# live in external-tools.json under sfw.<flavor>.checksums.
137158
# Bumping a tool requires updating the version AND every
138-
# platform's sha256 there in the same commit. The lib/ scripts
139-
# below resolve platform → asset → URL → install at the
140-
# currently-detected runner.
159+
# platform's integrity (SRI string) there in the same commit.
160+
# The lib/ scripts below resolve platform → asset → URL →
161+
# install at the currently-detected runner.
141162
TOOLS_FILE="${GITHUB_ACTION_PATH}/../../../external-tools.json"
142163
JQ="${GITHUB_ACTION_PATH}/../lib/jq.mjs"
143164
PLATFORM_TOOL="${GITHUB_ACTION_PATH}/../lib/platform.mjs"
@@ -163,7 +184,7 @@ runs:
163184
echo " win-arm64 has no upstream binary — skip SFW-dependent steps on that runner or wait for upstream support." >&2
164185
exit 1
165186
fi
166-
EXPECTED_SHA256="$(node "$JQ" "$TOOLS_FILE" sfw "$SFW_FLAVOR" checksums "$PLATFORM" sha256)"
187+
INTEGRITY="$(node "$JQ" "$TOOLS_FILE" sfw "$SFW_FLAVOR" checksums "$PLATFORM" integrity)"
167188
SFW_BIN_NAME="$(node "$JQ" "$TOOLS_FILE" sfw "$SFW_FLAVOR" binaryName)"
168189
if [[ "$ASSET" == *.exe ]]; then
169190
SFW_BIN_NAME="${SFW_BIN_NAME}.exe"
@@ -173,7 +194,7 @@ runs:
173194
if [ ! -x "$SFW_BIN" ]; then
174195
node "$INSTALL_TOOL" \
175196
"https://github.com/${SFW_REPO}/releases/download/v${SFW_VERSION}/${ASSET}" \
176-
"$EXPECTED_SHA256" \
197+
"$INTEGRITY" \
177198
"$SFW_DIR" \
178199
"$SFW_BIN_NAME"
179200
fi
@@ -192,7 +213,7 @@ runs:
192213
echo "SOCKET_TOOL_SFW_VERSION=$SFW_VERSION"
193214
echo "SOCKET_TOOL_SFW_PLATFORM=$PLATFORM"
194215
echo "SOCKET_TOOL_SFW_ASSET=$ASSET"
195-
echo "SOCKET_TOOL_SFW_SHA256=$EXPECTED_SHA256"
216+
echo "SOCKET_TOOL_SFW_INTEGRITY=$INTEGRITY"
196217
} >> "${GITHUB_ENV:-/dev/null}"
197218
198219
- name: Create sfw shims

0 commit comments

Comments
 (0)