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:
parent
a5f31b0d95
commit
e874dec285
2 changed files with 171 additions and 17 deletions
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
138
publishing/transform-workspace-deps.sh
Executable file
138
publishing/transform-workspace-deps.sh
Executable 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));
|
||||
")"
|
||||
Loading…
Add table
Reference in a new issue