@dzhechkov/harness-core 0.8.43 → 0.8.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +46 -26
- package/README.md +55 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/memory-index-check.d.ts.map +1 -1
- package/dist/memory-index-check.js +17 -4
- package/dist/memory-index-check.js.map +1 -1
- package/dist/pack-artifact.d.ts +24 -0
- package/dist/pack-artifact.d.ts.map +1 -0
- package/dist/pack-artifact.js +99 -0
- package/dist/pack-artifact.js.map +1 -0
- package/dist/profile.d.ts.map +1 -1
- package/dist/profile.js +6 -0
- package/dist/profile.js.map +1 -1
- package/dist/publish-sibling-drift.d.ts +6 -2
- package/dist/publish-sibling-drift.d.ts.map +1 -1
- package/dist/publish-sibling-drift.js +10 -1
- package/dist/publish-sibling-drift.js.map +1 -1
- package/dist/publish.d.ts +1 -2
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +9 -53
- package/dist/publish.js.map +1 -1
- package/package.json +12 -12
- package/sbom.json +75 -25
- package/src/index.ts +3 -1
- package/src/memory-index-check.ts +17 -4
- package/src/pack-artifact.ts +109 -0
- package/src/profile.ts +5 -0
- package/src/publish-sibling-drift.ts +16 -3
- package/src/publish.ts +9 -50
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** One staged artifact for signing, publication and sibling drift. Linux/macOS tar. */
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { packedTarballName } from './packed-install-smoke.js';
|
|
6
|
+
|
|
7
|
+
export type ExecLike = (command: string, options: { cwd: string; stdio: 'pipe'; encoding: 'utf-8'; maxBuffer: number }) => string;
|
|
8
|
+
export interface PackArtifactResult {
|
|
9
|
+
readonly tgzPath: string;
|
|
10
|
+
readonly sha256: string;
|
|
11
|
+
readonly files: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class UnknownWorkspaceSpecError extends Error {
|
|
15
|
+
constructor(dependency: string, spec: string) {
|
|
16
|
+
super(`Unknown workspace dependency spec for ${dependency}: ${spec}; supply a version and use workspace:*, workspace:^ or workspace:~`);
|
|
17
|
+
this.name = 'UnknownWorkspaceSpecError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function rewriteWorkspaceSpecs(
|
|
22
|
+
pkgJsonText: string,
|
|
23
|
+
siblingVersions: ReadonlyMap<string, string>,
|
|
24
|
+
): string {
|
|
25
|
+
const pkg = JSON.parse(pkgJsonText) as Record<string, unknown>;
|
|
26
|
+
const fields = ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies'] as const;
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
const candidate = pkg[field];
|
|
29
|
+
if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
|
|
30
|
+
const table = candidate as Record<string, unknown>;
|
|
31
|
+
for (const [dep, spec] of Object.entries(table)) {
|
|
32
|
+
if (typeof spec !== 'string') continue;
|
|
33
|
+
const match = /^workspace:([*^~])$/.exec(spec);
|
|
34
|
+
const version = siblingVersions.get(dep);
|
|
35
|
+
if (match === null) {
|
|
36
|
+
if (spec.startsWith('workspace:') || spec.startsWith('catalog:')) {
|
|
37
|
+
throw new UnknownWorkspaceSpecError(dep, spec);
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (version === undefined) continue;
|
|
42
|
+
const marker = match[1]!;
|
|
43
|
+
table[dep] = marker === '*' ? version : `${marker}${version}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return JSON.stringify(pkg, null, 2) + '\n';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Current pins, read afresh so a later sign sees a sibling's new version. */
|
|
50
|
+
export function readWorkspaceVersions(repoRoot: string): ReadonlyMap<string, string> {
|
|
51
|
+
const scope = join(repoRoot, 'packages', '@dzhechkov');
|
|
52
|
+
const versions = new Map<string, string>();
|
|
53
|
+
if (!existsSync(scope)) return versions;
|
|
54
|
+
for (const entry of readdirSync(scope).sort()) {
|
|
55
|
+
const manifest = join(scope, entry, 'package.json');
|
|
56
|
+
if (!existsSync(manifest)) continue;
|
|
57
|
+
const pkg = JSON.parse(readFileSync(manifest, 'utf8')) as { name?: unknown; version?: unknown };
|
|
58
|
+
if (typeof pkg.name === 'string' && typeof pkg.version === 'string') versions.set(pkg.name, pkg.version);
|
|
59
|
+
}
|
|
60
|
+
return versions;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// POSIX shell argument quoting: paths may contain spaces, quotes, dollars or backticks.
|
|
64
|
+
|
|
65
|
+
export function packArtifact(opts: {
|
|
66
|
+
pkgDir: string;
|
|
67
|
+
destDir: string;
|
|
68
|
+
exec: ExecLike;
|
|
69
|
+
pinVersions: ReadonlyMap<string, string>;
|
|
70
|
+
}): PackArtifactResult {
|
|
71
|
+
const pkgDir = resolve(opts.pkgDir);
|
|
72
|
+
const destDir = resolve(opts.destDir);
|
|
73
|
+
const manifestPath = join(pkgDir, 'package.json');
|
|
74
|
+
const original = readFileSync(manifestPath);
|
|
75
|
+
try {
|
|
76
|
+
const staged = JSON.parse(rewriteWorkspaceSpecs(original.toString('utf8'), opts.pinVersions)) as Record<string, unknown>;
|
|
77
|
+
// The public rewrite helper historically leaves a known shorthand without a pin unchanged.
|
|
78
|
+
// An artifact must never ship that unresolved value, even when its caller supplied an incomplete map.
|
|
79
|
+
for (const field of ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies']) {
|
|
80
|
+
const table = staged[field];
|
|
81
|
+
if (table === null || typeof table !== 'object' || Array.isArray(table)) continue;
|
|
82
|
+
for (const [dependency, value] of Object.entries(table)) {
|
|
83
|
+
if (typeof value === 'string' && /^workspace:[*^~]$/.test(value)) {
|
|
84
|
+
throw new UnknownWorkspaceSpecError(dependency, value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const scripts = staged['scripts'];
|
|
89
|
+
if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) {
|
|
90
|
+
delete (scripts as Record<string, unknown>)['prepublishOnly'];
|
|
91
|
+
}
|
|
92
|
+
const name = staged['name'];
|
|
93
|
+
const version = staged['version'];
|
|
94
|
+
if (typeof name !== 'string' || typeof version !== 'string') throw new Error(`Missing package name/version in ${manifestPath}`);
|
|
95
|
+
mkdirSync(destDir, { recursive: true });
|
|
96
|
+
writeFileSync(manifestPath, JSON.stringify(staged, null, 2) + '\n');
|
|
97
|
+
const execOptions = { cwd: pkgDir, stdio: 'pipe' as const, encoding: 'utf-8' as const, maxBuffer: 64 * 1024 * 1024 };
|
|
98
|
+
// The command shape is a CONTRACT: publish's test fake admits exactly this template — double-quoted
|
|
99
|
+
// via JSON.stringify, verbatim from the publish.ts template it replaced (publish-confirm-seam.test.ts).
|
|
100
|
+
opts.exec(`npm pack . --pack-destination ${JSON.stringify(destDir)}`, execOptions);
|
|
101
|
+
const tgzPath = join(destDir, packedTarballName(name, version));
|
|
102
|
+
const listing = opts.exec(`tar -tzf ${JSON.stringify(tgzPath)}`, execOptions);
|
|
103
|
+
const files = listing.split(/\r?\n/).filter(p => p !== '' && !p.endsWith('/')).map(p => p.replace(/^package\//, '')).sort();
|
|
104
|
+
const sha256 = createHash('sha256').update(readFileSync(tgzPath)).digest('hex');
|
|
105
|
+
return { tgzPath, sha256, files };
|
|
106
|
+
} finally {
|
|
107
|
+
writeFileSync(manifestPath, original);
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/profile.ts
CHANGED
|
@@ -512,7 +512,12 @@ export function syncProfileBlock(profile: OperatorProfile, home?: string, now?:
|
|
|
512
512
|
let backup: string | null = null;
|
|
513
513
|
if (existing !== null) {
|
|
514
514
|
const stamp = (now ?? new Date()).toISOString().replace(/[:.]/g, '-');
|
|
515
|
+
// Two syncs inside one millisecond (a fast CI runner) share a stamp: MEASURED 2026-09-24 on
|
|
516
|
+
// package-story-gates (73ac1a32) — the second backup overwrote the first and the test that
|
|
517
|
+
// promises "backups accumulate rather than overwriting" read 1 file. A taken name gets a
|
|
518
|
+
// counter suffix; a backup is never written over an existing one.
|
|
515
519
|
backup = `${target}.dz-profile-backup-${stamp}`;
|
|
520
|
+
for (let n = 1; existsSync(backup); n += 1) backup = `${target}.dz-profile-backup-${stamp}-${n}`;
|
|
516
521
|
writeFileSync(backup, existing, { mode: 0o600 });
|
|
517
522
|
chmodSync(backup, 0o600); // writeFileSync's mode applies only at creation
|
|
518
523
|
}
|
|
@@ -44,7 +44,7 @@ export type SiblingDriftStatus = 'same' | 'drift' | 'unavailable';
|
|
|
44
44
|
* symmetric by construction (same function, same rules, both sides) but can miss a file
|
|
45
45
|
* `.npmignore` excludes or include one npm would never ship.
|
|
46
46
|
*/
|
|
47
|
-
export type InventorySource = 'npm-pack' | 'pnpm-pack' | 'readdir-approximation';
|
|
47
|
+
export type InventorySource = 'pack-artifact' | 'npm-pack' | 'pnpm-pack' | 'readdir-approximation';
|
|
48
48
|
|
|
49
49
|
export interface SiblingDriftResult {
|
|
50
50
|
readonly name: string;
|
|
@@ -98,7 +98,7 @@ export interface DetectSiblingDriftOptions {
|
|
|
98
98
|
* drift".
|
|
99
99
|
*/
|
|
100
100
|
readonly localInventory?: LocalInventory;
|
|
101
|
-
/** Label for the injected provider
|
|
101
|
+
/** Label for the injected provider (default 'npm-pack'); CLI uses 'pack-artifact'. */
|
|
102
102
|
readonly localInventorySource?: InventorySource;
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -140,6 +140,8 @@ export interface PackInventoryUnavailable {
|
|
|
140
140
|
*/
|
|
141
141
|
export interface PackedTree {
|
|
142
142
|
readonly packedDir: string;
|
|
143
|
+
/** Exact files from packArtifact().files; old providers may omit this. */
|
|
144
|
+
readonly paths?: readonly string[];
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
export type LocalInventoryResult = PackInventory | PackedTree | PackInventoryUnavailable;
|
|
@@ -453,7 +455,9 @@ export function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDrif
|
|
|
453
455
|
}
|
|
454
456
|
try {
|
|
455
457
|
workspaceHashes = 'packedDir' in localResult
|
|
456
|
-
?
|
|
458
|
+
? localResult.paths !== undefined
|
|
459
|
+
? hashTreeFromPaths(localResult.packedDir, localResult.paths, workspaceManifest)
|
|
460
|
+
: hashTreeFull(localResult.packedDir, workspaceManifest)
|
|
457
461
|
: hashTreeFromPaths(workspaceDir, localResult.paths, workspaceManifest);
|
|
458
462
|
} catch (err) {
|
|
459
463
|
if (!(err instanceof InventoryListingError)) throw err;
|
|
@@ -496,3 +500,12 @@ export function detectSiblingDrift(opts: DetectSiblingDriftOptions): SiblingDrif
|
|
|
496
500
|
}
|
|
497
501
|
return results;
|
|
498
502
|
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
/** Named drift evidence for both the publish BLOCKED line and its durable audit row. */
|
|
506
|
+
export function formatDriftFiles(changedFiles: readonly string[], source: InventorySource, max = 5): string {
|
|
507
|
+
const shown = changedFiles.slice(0, Math.max(0, max));
|
|
508
|
+
const remaining = changedFiles.length - shown.length;
|
|
509
|
+
const names = [...shown, ...(remaining > 0 ? [`… (+${remaining} more)`] : [])].join(', ');
|
|
510
|
+
return `${names || '(no changed files)'} [source: ${source}]`;
|
|
511
|
+
}
|
package/src/publish.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { createHash } from 'node:crypto';
|
|
|
23
23
|
|
|
24
24
|
import { claimCheck } from './claim-check.js';
|
|
25
25
|
import { findReleaseLine, rewriteReleaseLine, isReleaseLineToken, shortPackageName } from './release-line.js';
|
|
26
|
-
import {
|
|
26
|
+
import { packArtifact, rewriteWorkspaceSpecs } from './pack-artifact.js';
|
|
27
27
|
|
|
28
28
|
export type ProbeOutcome = {
|
|
29
29
|
readonly attempt: number;
|
|
@@ -263,27 +263,7 @@ function maxPublished(name: string, localVersion: string, exec: PublishExec = ex
|
|
|
263
263
|
* Mirror pnpm's package-time expansion of the three shorthand workspace dependency specs.
|
|
264
264
|
* Pure by construction: callers provide both the source bytes and the sibling version table.
|
|
265
265
|
*/
|
|
266
|
-
export
|
|
267
|
-
pkgJsonText: string,
|
|
268
|
-
siblingVersions: ReadonlyMap<string, string>,
|
|
269
|
-
): string {
|
|
270
|
-
const pkg = JSON.parse(pkgJsonText) as Record<string, unknown>;
|
|
271
|
-
const fields = ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies'] as const;
|
|
272
|
-
for (const field of fields) {
|
|
273
|
-
const candidate = pkg[field];
|
|
274
|
-
if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
|
|
275
|
-
const table = candidate as Record<string, unknown>;
|
|
276
|
-
for (const [dep, spec] of Object.entries(table)) {
|
|
277
|
-
if (typeof spec !== 'string') continue;
|
|
278
|
-
const match = /^workspace:([*^~])$/.exec(spec);
|
|
279
|
-
const version = siblingVersions.get(dep);
|
|
280
|
-
if (match === null || version === undefined) continue;
|
|
281
|
-
const marker = match[1]!;
|
|
282
|
-
table[dep] = marker === '*' ? version : `${marker}${version}`;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return JSON.stringify(pkg, null, 2) + '\n';
|
|
286
|
-
}
|
|
266
|
+
export { rewriteWorkspaceSpecs } from './pack-artifact.js';
|
|
287
267
|
|
|
288
268
|
/**
|
|
289
269
|
* Pure half: which `workspace:`-declared deps of a package would pack to a floor that is neither
|
|
@@ -1311,34 +1291,13 @@ export function publishPackages(
|
|
|
1311
1291
|
}
|
|
1312
1292
|
|
|
1313
1293
|
if (opts.packedTransport !== undefined) {
|
|
1314
|
-
//
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
const bumpedText = readFileSync(pkgJsonPath, 'utf-8'); // already carries newVersion
|
|
1322
|
-
const rewritten = JSON.parse(rewriteWorkspaceSpecs(bumpedText, pinVersions)) as Record<string, unknown>;
|
|
1323
|
-
const scripts = rewritten['scripts'];
|
|
1324
|
-
if (scripts !== null && typeof scripts === 'object' && !Array.isArray(scripts)) {
|
|
1325
|
-
delete (scripts as Record<string, unknown>)['prepublishOnly'];
|
|
1326
|
-
}
|
|
1327
|
-
const stagedText = JSON.stringify(rewritten, null, 2) + '\n';
|
|
1328
|
-
let tgzPath: string;
|
|
1329
|
-
let digest: string;
|
|
1330
|
-
writeFileSync(pkgJsonPath, stagedText);
|
|
1331
|
-
try {
|
|
1332
|
-
const packOptions = { cwd: pkg.dir, stdio: 'pipe' as const, encoding: 'utf-8' as const };
|
|
1333
|
-
if (opts.exec) opts.exec(`npm pack . --pack-destination ${JSON.stringify(opts.packedTransport.packDestDir)}`, packOptions);
|
|
1334
|
-
else execSync(`npm pack . --pack-destination ${JSON.stringify(opts.packedTransport.packDestDir)}`, packOptions);
|
|
1335
|
-
tgzPath = join(opts.packedTransport.packDestDir, packedTarballName(pkg.name, newVersion));
|
|
1336
|
-
digest = sha256File(tgzPath);
|
|
1337
|
-
} finally {
|
|
1338
|
-
// The COMMITTED tree keeps `workspace:` specs (only the version bump is meant to stick) —
|
|
1339
|
-
// the rewrite above is packing-only and is undone here regardless of pack's outcome.
|
|
1340
|
-
writeFileSync(pkgJsonPath, bumpedText);
|
|
1341
|
-
}
|
|
1294
|
+
// Pack, inventory and digest the exact artifact later handed to npm publish.
|
|
1295
|
+
const { tgzPath, sha256: digest } = packArtifact({
|
|
1296
|
+
pkgDir: pkg.dir,
|
|
1297
|
+
destDir: opts.packedTransport.packDestDir,
|
|
1298
|
+
exec: opts.exec ?? execSync,
|
|
1299
|
+
pinVersions,
|
|
1300
|
+
});
|
|
1342
1301
|
pendingPacked.push({
|
|
1343
1302
|
name: pkg.name,
|
|
1344
1303
|
dir: pkg.dir,
|