Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/npmjs-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ jobs:
yarn lerna publish --sign-git-tag --sign-git-commit --include-merged-tags --conventional-commits --conventional-graduate --yes
env:
NPM_CONFIG_PROVENANCE: true
# Tells bitgo's `prepack` script (scripts/generate-bitgo-shrinkwrap.ts) to
# actually generate npm-shrinkwrap.json — kept opt-in so a plain local
# `npm pack` doesn't force a network install of the full dependency tree.
BITGO_GENERATE_SHRINKWRAP: true

- name: Publish missing versions (recovery)
if: ${{ inputs.dry-run == false && inputs.recovery-mode }}
Expand All @@ -300,6 +304,7 @@ jobs:
yarn lerna publish from-package --yes
env:
NPM_CONFIG_PROVENANCE: true
BITGO_GENERATE_SHRINKWRAP: true

- name: Verify recovery published the missing versions
if: ${{ inputs.dry-run == false && inputs.recovery-mode }}
Expand All @@ -325,6 +330,26 @@ jobs:
fi
echo "✅ All public package versions on rel/latest are present on npm."

# bitgo's `prepack` script generates npm-shrinkwrap.json at pack time (see
# scripts/generate-bitgo-shrinkwrap.ts). npm only honors a shipped shrinkwrap
# for installers when the registry's own metadata says hasShrinkwrap: true —
# confirm that here so a silent registry-side omission fails the release
# instead of shipping a package whose shrinkwrap consumers won't get.
- name: Verify bitgo package has shrinkwrap metadata
if: ${{ inputs.dry-run == false }}
run: |
version=$(jq -r '.version' modules/bitgo/package.json)
# Registry metadata can lag briefly right after publish, so retry before
# failing the release on what might just be propagation delay.
if ! retry --up-to 5x --every 5s -- bash -c "
has_shrinkwrap=\$(curl -sL 'https://registry.npmjs.org/bitgo/${version}' | jq -r '.dist.hasShrinkwrap // false')
[ \"\$has_shrinkwrap\" = 'true' ]
"; then
echo "::error::Published bitgo@${version} is missing hasShrinkwrap metadata on the npm registry."
exit 1
fi
echo "✅ bitgo@${version} published with hasShrinkwrap: true."

- name: Generate version bump summary
id: version-bump-summary
if: inputs.dry-run == false
Expand Down
4 changes: 3 additions & 1 deletion modules/bitgo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"build": "yarn tsc --build --incremental --verbose .",
"prepare": "npm run build",
"prepublishOnly": "npm run compile",
"prepack": "tsx ../../scripts/generate-bitgo-shrinkwrap.ts",
"upload-artifacts": "node scripts/upload-test-reports.js",
"check-fmt": "prettier --check '**/*.{ts,js,json}'",
"unprettied": "grep -R -L --include '*.ts' --include '*.js' --include '*.json' '@prettier' src test",
Expand Down Expand Up @@ -181,6 +182,7 @@
},
"gitHead": "18e460ddf02de2dbf13c2aa243478188fb539f0c",
"files": [
"dist"
"dist",
"npm-shrinkwrap.json"
]
}
93 changes: 93 additions & 0 deletions scripts/generate-bitgo-shrinkwrap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Generates modules/bitgo/npm-shrinkwrap.json so npm consumers who install `bitgo`
* as a dependency get the same pinned transitive versions that yarn `resolutions`
* (mirrored as npm `overrides` in the root package.json) already give internal builds.
*
* Runs as bitgo's `prepack` script, so it fires after lerna has bumped the version
* on disk but before the tarball is packed — the generated shrinkwrap's top-level
* name/version always matches what actually gets published. Only runs when
* BITGO_GENERATE_SHRINKWRAP=true (set by the release workflow) — otherwise a plain
* local/offline `npm pack` would force a network install of the full dependency tree.
*
* `@bitgo/*` siblings are deliberately excluded from the resolved tree: `lerna
* publish` bumps and packs every package in the same operation, so a sibling's
* newly-bumped version is not guaranteed to be live on the registry yet when bitgo
* is packed — resolving it here would either fail the release outright or silently
* pin a stale sibling version. Excluding them means consumers resolve `@bitgo/*`
* siblings normally (by then they are published); everything beneath those siblings
* still gets pinned via `overrides` for whichever version ends up installed.
*
* `npm shrinkwrap` isn't workspace-aware and modules/bitgo/.npmrc sets
* `package-lock=false`, so generation happens in an isolated temp copy outside the
* workspace, resolving against the real npm registry.
*/

import fs from 'fs';
import os from 'os';
import path from 'path';
import execa from 'execa';

const rootDir = path.resolve(__dirname, '..');
const bitgoDir = path.join(rootDir, 'modules/bitgo');

async function main() {
if (process.env.BITGO_GENERATE_SHRINKWRAP !== 'true') {
console.log('BITGO_GENERATE_SHRINKWRAP not set to "true" — skipping npm-shrinkwrap.json generation.');
return;
}

const rootPackageJson = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
const bitgoPackageJson = JSON.parse(fs.readFileSync(path.join(bitgoDir, 'package.json'), 'utf-8'));

if (!rootPackageJson.overrides) {
throw new Error('Root package.json has no "overrides" block to propagate into the bitgo shrinkwrap.');
}

const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitgo-shrinkwrap-'));
console.log(`Generating npm-shrinkwrap.json for bitgo@${bitgoPackageJson.version} in ${tempDir}`);

try {
const siblingNames = Object.keys(bitgoPackageJson.dependencies ?? {}).filter((name) => name.startsWith('@bitgo/'));
if (siblingNames.length > 0) {
console.log(`Excluding ${siblingNames.length} @bitgo/* siblings from shrinkwrap resolution:`);
siblingNames.forEach((name) => console.log(` - ${name}`));
}

const isolatedPackageJson: Record<string, unknown> = { ...bitgoPackageJson };
delete isolatedPackageJson.devDependencies;
delete isolatedPackageJson.scripts;
isolatedPackageJson.dependencies = Object.fromEntries(
Object.entries(bitgoPackageJson.dependencies ?? {}).filter(([name]) => !name.startsWith('@bitgo/'))
);
isolatedPackageJson.overrides = rootPackageJson.overrides;

fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify(isolatedPackageJson, null, 2) + '\n');

await execa('npm', ['install', '--package-lock-only', '--ignore-scripts'], { cwd: tempDir, stdio: 'inherit' });
await execa('npm', ['shrinkwrap'], { cwd: tempDir, stdio: 'inherit' });

const shrinkwrapPath = path.join(tempDir, 'npm-shrinkwrap.json');
if (!fs.existsSync(shrinkwrapPath)) {
throw new Error(`npm shrinkwrap did not produce a file at ${shrinkwrapPath}`);
}

// The shrinkwrap was generated against a package.json with @bitgo/* siblings
// removed, so its top-level `dependencies` no longer lists them. Restore the
// real dependency list so the shipped shrinkwrap matches what's published.
const shrinkwrap = JSON.parse(fs.readFileSync(shrinkwrapPath, 'utf-8'));
shrinkwrap.dependencies = bitgoPackageJson.dependencies;
if (shrinkwrap.packages?.['']) {
shrinkwrap.packages[''].dependencies = bitgoPackageJson.dependencies;
}

fs.writeFileSync(path.join(bitgoDir, 'npm-shrinkwrap.json'), JSON.stringify(shrinkwrap, null, 2) + '\n');
console.log(`Wrote ${path.join(bitgoDir, 'npm-shrinkwrap.json')}`);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
}

main().catch((e) => {
console.error(e);
process.exit(1);
});
Loading