Skip to content

Commit 1642534

Browse files
committed
Make release:npm script more robust
1 parent 022efd4 commit 1642534

File tree

1 file changed

+82
-21
lines changed

1 file changed

+82
-21
lines changed

scripts/release-npm-packages.js

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
'use strict'
22

3+
const crypto = require('node:crypto')
4+
const fs = require('node:fs/promises')
35
const path = require('node:path')
46

57
const semver = require('semver')
6-
const ssri = require('ssri')
78

89
const constants = require('@socketregistry/scripts/constants')
910
const { execScript } = require('@socketsecurity/registry/lib/agent')
1011
const {
12+
extractPackage,
1113
fetchPackageManifest,
1214
getReleaseTag,
13-
packPackage,
1415
readPackageJson
1516
} = require('@socketsecurity/registry/lib/packages')
1617
const { readPackageJsonSync } = require('@socketsecurity/registry/lib/packages')
18+
const { readFileUtf8 } = require('@socketsecurity/registry/lib/fs')
1719
const { pEach } = require('@socketsecurity/registry/lib/promises')
20+
const { toSortedObject } = require('@socketsecurity/registry/lib/objects')
1821

1922
const {
2023
LATEST,
24+
PACKAGE_JSON,
2125
SOCKET_REGISTRY_PACKAGE_NAME,
2226
SOCKET_REGISTRY_SCOPE,
2327
abortSignal,
@@ -32,6 +36,49 @@ const registryPkg = packageData({
3236
path: registryPkgPath
3337
})
3438

39+
const EXTRACT_PACKAGE_TMP_PREFIX = 'release-npm-'
40+
41+
async function getPackageFileHashes(spec) {
42+
const fileHashes = {}
43+
44+
// Extract package to a temp directory and compute hashes.
45+
await extractPackage(
46+
spec,
47+
{
48+
tmpPrefix: EXTRACT_PACKAGE_TMP_PREFIX
49+
},
50+
async tmpDir => {
51+
// Walk the directory and compute hashes for all files.
52+
async function walkDir(dir, baseDir = tmpDir) {
53+
const entries = await fs.readdir(dir, { withFileTypes: true })
54+
for (const entry of entries) {
55+
const fullPath = path.join(dir, entry.name)
56+
const relativePath = path.relative(baseDir, fullPath)
57+
58+
if (entry.isDirectory()) {
59+
// Recurse into subdirectories.
60+
// eslint-disable-next-line no-await-in-loop
61+
await walkDir(fullPath, baseDir)
62+
} else if (entry.isFile() && entry.name !== PACKAGE_JSON) {
63+
// Skip package.json files as they contain version info.
64+
// eslint-disable-next-line no-await-in-loop
65+
const content = await readFileUtf8(fullPath)
66+
const hash = crypto
67+
.createHash('sha256')
68+
.update(content, 'utf8')
69+
.digest('hex')
70+
fileHashes[relativePath] = hash
71+
}
72+
}
73+
}
74+
75+
await walkDir(tmpDir)
76+
}
77+
)
78+
79+
return toSortedObject(fileHashes)
80+
}
81+
3582
async function hasPackageChanged(pkg, manifest_) {
3683
const manifest =
3784
manifest_ ?? (await fetchPackageManifest(`${pkg.name}@${pkg.tag}`))
@@ -46,22 +93,11 @@ async function hasPackageChanged(pkg, manifest_) {
4693
const localPkgJson = readPackageJsonSync(pkg.path)
4794

4895
// Check if dependencies have changed.
49-
const localDeps = localPkgJson.dependencies ?? {}
50-
const remoteDeps = manifest.dependencies ?? {}
51-
52-
// Sort keys for consistent comparison.
53-
const sortedLocalDeps = Object.keys(localDeps).sort().reduce((acc, key) => {
54-
acc[key] = localDeps[key]
55-
return acc
56-
}, {})
96+
const localDeps = toSortedObject(localPkgJson.dependencies ?? {})
97+
const remoteDeps = toSortedObject(manifest.dependencies ?? {})
5798

58-
const sortedRemoteDeps = Object.keys(remoteDeps).sort().reduce((acc, key) => {
59-
acc[key] = remoteDeps[key]
60-
return acc
61-
}, {})
62-
63-
const localDepsStr = JSON.stringify(sortedLocalDeps)
64-
const remoteDepsStr = JSON.stringify(sortedRemoteDeps)
99+
const localDepsStr = JSON.stringify(localDeps)
100+
const remoteDepsStr = JSON.stringify(remoteDeps)
65101

66102
// If dependencies changed, we need to bump.
67103
if (localDepsStr !== remoteDepsStr) {
@@ -78,10 +114,35 @@ async function hasPackageChanged(pkg, manifest_) {
78114
}
79115
}
80116

81-
// Skip tarball comparison entirely - it's too prone to false positives.
82-
// If dependencies and key fields haven't changed, assume no bump is needed.
83-
// The build process and manifest update will handle any actual code changes.
84-
return false
117+
// Compare actual file contents by extracting packages and comparing SHA hashes.
118+
try {
119+
const { 0: remoteHashes, 1: localHashes } = await Promise.all([
120+
getPackageFileHashes(`${pkg.name}@${manifest.version}`),
121+
getPackageFileHashes(pkg.path, true)
122+
])
123+
124+
// Compare the file hashes.
125+
const remoteFiles = Object.keys(remoteHashes)
126+
const localFiles = Object.keys(localHashes)
127+
128+
// Check if file lists are different.
129+
if (JSON.stringify(remoteFiles) !== JSON.stringify(localFiles)) {
130+
return true
131+
}
132+
133+
// Check if any file content is different.
134+
for (const file of remoteFiles) {
135+
if (remoteHashes[file] !== localHashes[file]) {
136+
return true
137+
}
138+
}
139+
140+
return false
141+
} catch (e) {
142+
// If comparison fails, be conservative and assume changes.
143+
console.error(`Error comparing packages for ${pkg.name}:`, e?.message)
144+
return true
145+
}
85146
}
86147

87148
async function maybeBumpPackage(pkg, options = {}) {

0 commit comments

Comments
 (0)