ci(publish): pin next-channel @mosaicstack deps to exact same-pipeline builds (#1389) (#1400)
ci/woodpecker/push/publish Pipeline was canceled
ci/woodpecker/push/publish Pipeline was canceled
Co-authored-by: code-be-01 <[email protected]>
This commit was merged in pull request #1400.
This commit is contained in:
+114
-7
@@ -209,23 +209,38 @@ steps:
|
|||||||
const pipelineNumber = process.env.CI_PIPELINE_NUMBER;
|
const pipelineNumber = process.env.CI_PIPELINE_NUMBER;
|
||||||
const roots = ['apps', 'packages', 'plugins'];
|
const roots = ['apps', 'packages', 'plugins'];
|
||||||
const updated = [];
|
const updated = [];
|
||||||
|
const exactVersions = new Map(); // name -> bumped next version
|
||||||
|
|
||||||
function walk(dir) {
|
function walk(dir, visit) {
|
||||||
if (!fs.existsSync(dir)) return;
|
if (!fs.existsSync(dir)) return;
|
||||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.turbo') continue;
|
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.turbo') continue;
|
||||||
const fullPath = path.join(dir, entry.name);
|
const fullPath = path.join(dir, entry.name);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
const packagePath = path.join(fullPath, 'package.json');
|
const packagePath = path.join(fullPath, 'package.json');
|
||||||
if (fs.existsSync(packagePath)) updatePackage(packagePath);
|
if (fs.existsSync(packagePath)) {
|
||||||
walk(fullPath);
|
const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||||
|
if (manifest.name?.startsWith('@mosaicstack/') && !manifest.private) {
|
||||||
|
visit(manifest, packagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(fullPath, visit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePackage(packagePath) {
|
// #1389: two passes. Pass 1 bumps every publishable manifest to
|
||||||
const manifest = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
// <stable+1>-next.<pipeline> exactly as before, recording name ->
|
||||||
if (!manifest.name?.startsWith('@mosaicstack/') || manifest.private) return;
|
// bumped version. Pass 2 rewrites every published manifest's
|
||||||
|
// @mosaicstack/* dependency entries (dependencies, devDependencies,
|
||||||
|
// peerDependencies, optionalDependencies) to the EXACT same-pipeline
|
||||||
|
// build. A caret range like ^0.0.3-next.2636 leaves the resolver free
|
||||||
|
// to pick any later build — and on a host with a stale cache, an
|
||||||
|
// installer-side scaffold pinned at stable, or a registry hiccup, that
|
||||||
|
// freedom is how a "next" install ends up executing stable-era code
|
||||||
|
// (web1 evidence: old tier validator, missing migrations). Exact pins
|
||||||
|
// make the defect class unrepresentable regardless of resolver path.
|
||||||
|
function bump(manifest, packagePath) {
|
||||||
const stableMatch = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(manifest.version);
|
const stableMatch = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(manifest.version);
|
||||||
if (!stableMatch) {
|
if (!stableMatch) {
|
||||||
throw new Error(manifest.name + " has unsupported semver version '" + manifest.version + "'");
|
throw new Error(manifest.name + " has unsupported semver version '" + manifest.version + "'");
|
||||||
@@ -234,13 +249,40 @@ steps:
|
|||||||
const oldVersion = manifest.version;
|
const oldVersion = manifest.version;
|
||||||
manifest.version = major + '.' + minor + '.' + (Number(patch) + 1) + '-next.' + pipelineNumber;
|
manifest.version = major + '.' + minor + '.' + (Number(patch) + 1) + '-next.' + pipelineNumber;
|
||||||
fs.writeFileSync(packagePath, JSON.stringify(manifest, null, 2) + '\n');
|
fs.writeFileSync(packagePath, JSON.stringify(manifest, null, 2) + '\n');
|
||||||
|
exactVersions.set(manifest.name, manifest.version);
|
||||||
updated.push(manifest.name + ' ' + oldVersion + ' -> ' + manifest.version);
|
updated.push(manifest.name + ' ' + oldVersion + ' -> ' + manifest.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const root of roots) walk(root);
|
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
||||||
|
let pinnedEntries = 0;
|
||||||
|
function pin(manifest, packagePath) {
|
||||||
|
let changed = false;
|
||||||
|
for (const field of DEP_FIELDS) {
|
||||||
|
const deps = manifest[field];
|
||||||
|
if (!deps || typeof deps !== 'object') continue;
|
||||||
|
for (const [name, range] of Object.entries(deps)) {
|
||||||
|
if (!name.startsWith('@mosaicstack/')) continue;
|
||||||
|
const exact = exactVersions.get(name);
|
||||||
|
if (!exact) {
|
||||||
|
throw new Error(
|
||||||
|
manifest.name + ' depends on ' + name +
|
||||||
|
' which has no bumped version in this publish set — cannot pin');
|
||||||
|
}
|
||||||
|
if (range === exact) continue;
|
||||||
|
deps[name] = exact;
|
||||||
|
pinnedEntries++;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) fs.writeFileSync(packagePath, JSON.stringify(manifest, null, 2) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const root of roots) walk(root, bump);
|
||||||
|
for (const root of roots) walk(root, pin);
|
||||||
if (updated.length === 0) throw new Error('No publishable @mosaicstack/* packages found');
|
if (updated.length === 0) throw new Error('No publishable @mosaicstack/* packages found');
|
||||||
console.log('[publish-next] computed prerelease versions for ' + updated.length + ' packages:');
|
console.log('[publish-next] computed prerelease versions for ' + updated.length + ' packages:');
|
||||||
for (const line of updated) console.log('[publish-next] ' + line);
|
for (const line of updated) console.log('[publish-next] ' + line);
|
||||||
|
console.log('[publish-next] pinned ' + pinnedEntries + ' @mosaicstack/* dep entries to exact same-pipeline versions across ' + updated.length + ' manifests');
|
||||||
NODE
|
NODE
|
||||||
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" --filter "!@mosaicstack/mosaic-as" publish --no-git-checks --access public --tag next
|
pnpm --filter "@mosaicstack/*" --filter "!@mosaicstack/web" --filter "!@mosaicstack/mosaic-as" publish --no-git-checks --access public --tag next
|
||||||
EXPECTED_VERSION="$(node -p "require('./packages/mosaic/package.json').version")"
|
EXPECTED_VERSION="$(node -p "require('./packages/mosaic/package.json').version")"
|
||||||
@@ -250,6 +292,71 @@ steps:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "[publish-next] @mosaicstack/mosaic@next resolves to $RESOLVED_VERSION"
|
echo "[publish-next] @mosaicstack/mosaic@next resolves to $RESOLVED_VERSION"
|
||||||
|
# #1389 post-publish guard: every freshly published manifest must carry
|
||||||
|
# EXACT same-pipeline @mosaicstack/* dep pins (no ranges, no stable
|
||||||
|
# fallback). A leak here fails the pipeline instead of shipping.
|
||||||
|
node <<'GUARD'
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const pipelineNumber = process.env.CI_PIPELINE_NUMBER;
|
||||||
|
const registry = 'https://git.mosaicstack.dev/api/packages/mosaicstack/npm/';
|
||||||
|
const roots = ['apps', 'packages', 'plugins'];
|
||||||
|
const published = [];
|
||||||
|
function walk(dir) {
|
||||||
|
if (!fs.existsSync(dir)) return;
|
||||||
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.turbo') continue;
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const packagePath = path.join(fullPath, 'package.json');
|
||||||
|
if (fs.existsSync(packagePath)) {
|
||||||
|
const m = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||||
|
if (m.name?.startsWith('@mosaicstack/') && !m.private) published.push(m.name);
|
||||||
|
}
|
||||||
|
walk(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const root of roots) walk(root);
|
||||||
|
let failures = 0;
|
||||||
|
for (const name of published) {
|
||||||
|
let manifest;
|
||||||
|
try {
|
||||||
|
const out = execFileSync('npm', ['view', name + '@next', '--json', '--registry', registry],
|
||||||
|
{ encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
||||||
|
const arr = JSON.parse(out);
|
||||||
|
manifest = Array.isArray(arr) ? arr[arr.length - 1] : arr;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[publish-next-guard] FAIL ' + name + ': npm view failed: ' + e.message);
|
||||||
|
failures++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
|
||||||
|
for (const field of fields) {
|
||||||
|
const deps = manifest[field];
|
||||||
|
if (!deps || typeof deps !== 'object') continue;
|
||||||
|
for (const [dep, range] of Object.entries(deps)) {
|
||||||
|
if (!dep.startsWith('@mosaicstack/')) continue;
|
||||||
|
const expected = dep === name ? manifest.version : null;
|
||||||
|
const isExactPin = /^\d+\.\d+\.\d+-next\./.test(range);
|
||||||
|
const samePipeline = range.endsWith('-next.' + pipelineNumber);
|
||||||
|
if (!isExactPin) {
|
||||||
|
console.error('[publish-next-guard] FAIL ' + name + ' -> ' + dep + ' range "' + range + '" is not an exact -next pin (stable-leak class, #1389)');
|
||||||
|
failures++;
|
||||||
|
} else if (!samePipeline) {
|
||||||
|
console.error('[publish-next-guard] FAIL ' + name + ' -> ' + dep + ' pinned "' + range + '" but this pipeline published -next.' + pipelineNumber + ' (cross-pipeline pin)');
|
||||||
|
failures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failures > 0) {
|
||||||
|
console.error('[publish-next-guard] FATAL: ' + failures + ' dep-pin violation(s) — stable-dep leak into next publish (#1389)');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('[publish-next-guard] OK: all ' + published.length + ' published manifests carry exact same-pipeline @mosaicstack/* dep pins');
|
||||||
|
GUARD
|
||||||
depends_on:
|
depends_on:
|
||||||
- build
|
- build
|
||||||
- verify
|
- verify
|
||||||
|
|||||||
Reference in New Issue
Block a user