deps-pin(publishing): 📌 Enforce reproducible CI/CD builds by pinning workspace dependencies in the publish template and transformation script

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-04-03 13:09:40 -07:00
parent a5f31b0d95
commit e874dec285
2 changed files with 171 additions and 17 deletions

View file

@ -5,7 +5,7 @@
#
# Features:
# - Configuration-driven (reads package.json `_` metadata)
# - Workspace dependency transformation (workspace:* → *)
# - Workspace dependency transformation (workspace:* → ^latestRegistryVersion)
# - Version existence check (prevents redundant publishes)
# - Graceful error handling (missing scripts don't break CI)
# - Self-signed cert support (internal Forgejo registry)
@ -63,23 +63,39 @@ jobs:
echo "=== Transforming workspace dependencies ==="
node -e "
const fs = require('fs');
if (fs.existsSync('package.json')) {
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const transform = (deps) => {
if (!deps) return deps;
for (const [name, version] of Object.entries(deps)) {
if (version.startsWith('workspace:') || version.startsWith('file:')) {
console.log(' Transformed:', name, version, '→ *');
deps[name] = '*';
}
const { execFileSync } = require('child_process');
const REGISTRY = 'http://forge.black.local/api/packages/lilith/npm/';
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const transform = (deps) => {
if (!deps) return deps;
for (const [name, version] of Object.entries(deps)) {
if (version.startsWith('file:')) {
console.log(' Removed file: dep:', name);
delete deps[name];
continue;
}
return deps;
};
pkg.dependencies = transform(pkg.dependencies);
pkg.devDependencies = transform(pkg.devDependencies);
pkg.peerDependencies = transform(pkg.peerDependencies);
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
}
if (!version.startsWith('workspace:')) continue;
const spec = version.substring('workspace:'.length);
let resolved;
try {
resolved = execFileSync(
'npm', ['view', name, 'version', '--registry', REGISTRY],
{ encoding: 'utf8', timeout: 10000 }
).trim();
} catch {
console.log(' WARN: ' + name + ' not on registry, using *');
resolved = null;
}
const prefix = spec === '~' ? '~' : '^';
deps[name] = resolved ? prefix + resolved : '*';
console.log(' Transformed:', name, version, '→', deps[name]);
}
return deps;
};
pkg.dependencies = transform(pkg.dependencies);
pkg.devDependencies = transform(pkg.devDependencies);
pkg.peerDependencies = transform(pkg.peerDependencies);
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
"
echo "✓ Workspace dependencies transformed"

View file

@ -0,0 +1,138 @@
#!/usr/bin/env bash
set -euo pipefail
# Transform workspace:* dependencies in package.json to actual version ranges.
#
# Modes:
# Local (default): walks up to find workspace root, reads sibling package versions
# Registry (--registry URL): queries npm registry for latest versions (for CI)
#
# Usage:
# transform-workspace-deps.sh [package-dir] # local mode
# transform-workspace-deps.sh --registry URL [package-dir] # registry mode (CI)
#
# Resolution: workspace:* → ^actualVersion, workspace:~ → ~actualVersion
REGISTRY=""
PKG_DIR="."
while [[ $# -gt 0 ]]; do
case "$1" in
--registry) REGISTRY="$2"; shift 2 ;;
*) PKG_DIR="$1"; shift ;;
esac
done
PKG_JSON="$PKG_DIR/package.json"
if [[ ! -f "$PKG_JSON" ]]; then
echo "No package.json at $PKG_JSON" >&2
exit 1
fi
if ! grep -q '"workspace:\|"file:' "$PKG_JSON"; then
echo "No workspace:* or file: dependencies. Nothing to transform."
exit 0
fi
# Resolve to absolute path
PKG_JSON="$(cd "$PKG_DIR" && pwd)/package.json"
# Extract all workspace:* dependency names from package.json
WORKSPACE_DEPS=$(node -e "
const pkg = JSON.parse(require('fs').readFileSync('$PKG_JSON', 'utf8'));
const deps = {...(pkg.dependencies||{}), ...(pkg.devDependencies||{}), ...(pkg.peerDependencies||{})};
for (const [name, ver] of Object.entries(deps)) {
if (ver.startsWith('workspace:')) console.log(name);
}
" 2>/dev/null)
declare -A VERSION_MAP
if [[ -n "$REGISTRY" ]]; then
# Registry mode: query npm for latest version of each workspace dep
echo "Registry mode: resolving versions from $REGISTRY"
for dep in $WORKSPACE_DEPS; do
ver=$(npm view "$dep" version --registry "$REGISTRY" 2>/dev/null || echo "")
if [[ -n "$ver" ]]; then
VERSION_MAP["$dep"]="$ver"
else
echo "FATAL: $dep not found on registry $REGISTRY" >&2
exit 1
fi
done
else
# Local mode: find workspace root and scan siblings
find_workspace_root() {
local dir="$1"
for _ in $(seq 1 10); do
if [[ -f "$dir/pnpm-workspace.yaml" ]]; then
echo "$dir"; return 0
fi
if [[ -f "$dir/package.json" ]] && node -e "
const p = require('$dir/package.json');
process.exit(Array.isArray(p.workspaces) ? 0 : 1);
" 2>/dev/null; then
echo "$dir"; return 0
fi
dir="$(dirname "$dir")"
[[ "$dir" == "/" ]] && break
done
echo "Cannot find workspace root" >&2
exit 1
}
WORKSPACE_ROOT="$(find_workspace_root "$(cd "$PKG_DIR" && pwd)")"
echo "Workspace root: $WORKSPACE_ROOT"
while IFS= read -r pkg_file; do
name="$(node -p "require('$pkg_file').name" 2>/dev/null)" || continue
ver="$(node -p "require('$pkg_file').version" 2>/dev/null)" || continue
[[ -n "$name" && -n "$ver" && "$name" != "undefined" ]] && VERSION_MAP["$name"]="$ver"
done < <(find "$WORKSPACE_ROOT" -maxdepth 4 -name "package.json" \
-not -path "*/node_modules/*" \
-not -path "*/.git/*" \
-not -path "*/dist/*" \
-not -path "*/.build/*" 2>/dev/null)
fi
echo "Resolved ${#VERSION_MAP[@]} package versions"
# Transform using node
node -e "
const fs = require('fs');
const versions = JSON.parse(process.argv[1]);
const pkg = JSON.parse(fs.readFileSync('$PKG_JSON', 'utf8'));
function transform(deps) {
if (!deps) return deps;
const out = {};
for (const [name, version] of Object.entries(deps)) {
if (version.startsWith('workspace:')) {
const spec = version.substring('workspace:'.length);
const actual = versions[name];
if (!actual) {
console.error('FATAL: ' + name + ' (' + version + ') not resolved');
process.exit(1);
}
out[name] = (spec === '~' ? '~' : '^') + actual;
} else if (version.startsWith('file:')) {
console.error('FATAL: file: dependency ' + name + ' must not be published');
process.exit(1);
} else {
out[name] = version;
}
}
return out;
}
pkg.dependencies = transform(pkg.dependencies);
pkg.devDependencies = transform(pkg.devDependencies);
pkg.peerDependencies = transform(pkg.peerDependencies);
fs.writeFileSync('$PKG_JSON', JSON.stringify(pkg, null, 2) + '\n');
console.log('Transformed workspace dependencies');
" "$(node -e "
const m = {};
$(for name in "${!VERSION_MAP[@]}"; do echo "m['$name']='${VERSION_MAP[$name]}';"; done)
console.log(JSON.stringify(m));
")"