@dzhechkov/harness-core 0.8.27 → 0.8.29
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 +91 -31
- package/README.md +24 -5
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +10 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/cmd-usage.d.ts.map +1 -1
- package/dist/cmd-usage.js +22 -10
- package/dist/cmd-usage.js.map +1 -1
- package/dist/core-boundary.d.ts +1 -1
- package/dist/core-boundary.d.ts.map +1 -1
- package/dist/core-boundary.js +8 -1
- package/dist/core-boundary.js.map +1 -1
- package/dist/guard.d.ts +19 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +110 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/publish.d.ts +11 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +90 -4
- package/dist/publish.js.map +1 -1
- package/dist/release-line.d.ts +10 -0
- package/dist/release-line.d.ts.map +1 -0
- package/dist/release-line.js +21 -0
- package/dist/release-line.js.map +1 -0
- package/dist/repo-boundary.d.ts +13 -0
- package/dist/repo-boundary.d.ts.map +1 -0
- package/dist/repo-boundary.js +16 -0
- package/dist/repo-boundary.js.map +1 -0
- package/dist/repository-origin.d.ts +2 -0
- package/dist/repository-origin.d.ts.map +1 -0
- package/dist/repository-origin.js +2 -0
- package/dist/repository-origin.js.map +1 -0
- package/package.json +3 -3
- package/sbom.json +180 -30
- package/src/amendment-trace.ts +10 -1
- package/src/cmd-usage.ts +13 -6
- package/src/core-boundary.ts +7 -1
- package/src/guard.ts +137 -1
- package/src/index.ts +6 -1
- package/src/publish.ts +92 -3
- package/src/release-line.ts +31 -0
- package/src/repo-boundary.ts +25 -0
- package/src/repository-origin.ts +1 -0
package/src/guard.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
type VolumeShadowInput,
|
|
22
22
|
type VolumeShadowResult,
|
|
23
23
|
} from './guard-volume.js';
|
|
24
|
+
import { findReleaseLine } from './release-line.js';
|
|
24
25
|
|
|
25
26
|
export type GuardSeverity = 'hard' | 'soft';
|
|
26
27
|
export type GuardOp = 'publish' | 'teach' | 'consolidate' | 'reindex';
|
|
@@ -79,6 +80,21 @@ export interface GuardResult {
|
|
|
79
80
|
/** Facts the CLI injects; each rule reads only the fields it needs. Missing evidence ⇒ that rule is skipped. */
|
|
80
81
|
export interface GuardFacts {
|
|
81
82
|
readonly op: GuardOp;
|
|
83
|
+
/** Signature evidence gathered by the CLI. The pure evaluator never reads manifests or keys. */
|
|
84
|
+
readonly signedPacks?: readonly {
|
|
85
|
+
readonly name: string;
|
|
86
|
+
readonly dir: string;
|
|
87
|
+
readonly changed: boolean;
|
|
88
|
+
readonly ok: boolean | null;
|
|
89
|
+
readonly failures: readonly string[];
|
|
90
|
+
readonly note?: string;
|
|
91
|
+
}[];
|
|
92
|
+
/** Release-line evidence gathered by the CLI; absence means the rule was not established. */
|
|
93
|
+
readonly releaseLines?: {
|
|
94
|
+
readonly readmes: readonly { readonly path: string; readonly text: string | null }[];
|
|
95
|
+
readonly coreVersion: string | null;
|
|
96
|
+
readonly cliVersion: string | null;
|
|
97
|
+
};
|
|
82
98
|
/** Publish-only raw volume facts. Absence preserves the legacy result shape. */
|
|
83
99
|
readonly volume?: VolumeShadowInput;
|
|
84
100
|
/** for no-workspace-star: each publishable package's deps map. */
|
|
@@ -165,7 +181,7 @@ export interface GuardFacts {
|
|
|
165
181
|
/** for skills-registrable: per skill pack, dirs that would ship un-registrable (no depth-1 SKILL.md). */
|
|
166
182
|
readonly skillPacks?: readonly { readonly name: string; readonly nonRegistrable: readonly string[] }[];
|
|
167
183
|
/** for readme-first: per publishable package, is a version bump staged without a README change? */
|
|
168
|
-
readonly readmeFirst?: readonly { readonly name: string; readonly versionBumped: boolean; readonly readmeChanged: boolean }[];
|
|
184
|
+
readonly readmeFirst?: readonly { readonly name: string; readonly versionBumped: boolean; readonly readmeChanged: boolean; readonly versionUnknown?: boolean }[];
|
|
169
185
|
/**
|
|
170
186
|
* for review-round: per publishable package, does this change bump a version AND touch SOURCE, and
|
|
171
187
|
* did it bring a GRADED QE report with it? `undefined` (the whole fact absent) means the tree could
|
|
@@ -347,6 +363,99 @@ function unquoteYaml(s: string): string {
|
|
|
347
363
|
return t;
|
|
348
364
|
}
|
|
349
365
|
|
|
366
|
+
function inspectReleaseLines(
|
|
367
|
+
evidence: NonNullable<GuardFacts['releaseLines']>,
|
|
368
|
+
severity: GuardSeverity,
|
|
369
|
+
): {
|
|
370
|
+
readonly violations: Violation[];
|
|
371
|
+
readonly observations: GuardObservation[];
|
|
372
|
+
} {
|
|
373
|
+
const expected = evidence.coreVersion !== null && evidence.cliVersion !== null
|
|
374
|
+
? { core: evidence.coreVersion, cli: evidence.cliVersion }
|
|
375
|
+
: null;
|
|
376
|
+
const violations: Violation[] = [];
|
|
377
|
+
const observations: GuardObservation[] = [];
|
|
378
|
+
|
|
379
|
+
for (const readme of evidence.readmes) {
|
|
380
|
+
if (readme.text === null) {
|
|
381
|
+
observations.push({
|
|
382
|
+
schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
|
|
383
|
+
metric: 'release-line-version-sync', scope: readme.path, status: 'unknown', value: null,
|
|
384
|
+
unit: 'artifact_set', signal: false, operands: {}, method: 'release-line-regex/v1',
|
|
385
|
+
detail: 'файл не прочитан',
|
|
386
|
+
});
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const found = findReleaseLine(readme.text);
|
|
390
|
+
if (found === null) {
|
|
391
|
+
observations.push({
|
|
392
|
+
schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
|
|
393
|
+
metric: 'release-line-version-sync', scope: readme.path, status: 'unknown', value: null,
|
|
394
|
+
unit: 'artifact_set', signal: false, operands: {}, method: 'release-line-regex/v1',
|
|
395
|
+
detail: 'строка релиза не найдена',
|
|
396
|
+
});
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (expected === null) {
|
|
400
|
+
const missing = [
|
|
401
|
+
...(evidence.coreVersion === null ? ['harness-core package.json version'] : []),
|
|
402
|
+
...(evidence.cliVersion === null ? ['harness-cli package.json version'] : []),
|
|
403
|
+
];
|
|
404
|
+
observations.push({
|
|
405
|
+
schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
|
|
406
|
+
metric: 'release-line-version-sync', scope: readme.path, status: 'unknown', value: [found.core, found.cli],
|
|
407
|
+
unit: 'artifact_set', signal: false, operands: { actual: [found.core, found.cli] },
|
|
408
|
+
method: 'release-line-regex/v1', detail: `${missing.join(' и ')} не прочитана или не имеет форму N.N.N`,
|
|
409
|
+
});
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const mismatch = found.core !== expected.core || found.cli !== expected.cli;
|
|
413
|
+
const detail = mismatch
|
|
414
|
+
? `${readme.path}: строка релиза говорит core v${found.core}/cli v${found.cli}, package.json — v${expected.core}/v${expected.cli}`
|
|
415
|
+
: `${readme.path}: строка релиза совпадает с package.json (${expected.core}/${expected.cli})`;
|
|
416
|
+
observations.push({
|
|
417
|
+
schemaVersion: 'volume-shadow/v1', rule: 'release-line-in-sync' as never,
|
|
418
|
+
metric: 'release-line-version-sync', scope: readme.path,
|
|
419
|
+
status: mismatch ? 'outside-reference' : 'within-reference', value: [found.core, found.cli],
|
|
420
|
+
unit: 'artifact_set', signal: mismatch,
|
|
421
|
+
operands: { actual: [found.core, found.cli], expected: [expected.core, expected.cli] },
|
|
422
|
+
method: 'release-line-regex/v1', detail,
|
|
423
|
+
});
|
|
424
|
+
if (mismatch) violations.push({ rule: 'release-line-in-sync', severity, detail });
|
|
425
|
+
}
|
|
426
|
+
return { violations, observations };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function inspectSignatureFresh(
|
|
430
|
+
evidence: NonNullable<GuardFacts['signedPacks']>,
|
|
431
|
+
severity: GuardSeverity,
|
|
432
|
+
): {
|
|
433
|
+
readonly violations: Violation[];
|
|
434
|
+
readonly observations: GuardObservation[];
|
|
435
|
+
} {
|
|
436
|
+
const violations: Violation[] = [];
|
|
437
|
+
const observations: GuardObservation[] = [];
|
|
438
|
+
for (const pack of evidence) {
|
|
439
|
+
if (!pack.changed) continue;
|
|
440
|
+
if (pack.ok === false) {
|
|
441
|
+
const firstFailure = pack.failures[0] ?? 'verification failed without a named reason';
|
|
442
|
+
violations.push({
|
|
443
|
+
rule: 'signature-fresh',
|
|
444
|
+
severity,
|
|
445
|
+
detail: `${pack.name}: files changed but the signed manifest is stale (${firstFailure}) — re-sign: dz sign --pack ${pack.dir} --key <key>`,
|
|
446
|
+
});
|
|
447
|
+
} else if (pack.ok === true) {
|
|
448
|
+
observations.push({
|
|
449
|
+
schemaVersion: 'volume-shadow/v1', rule: 'signature-fresh' as never,
|
|
450
|
+
metric: 'signed-manifest-freshness', scope: pack.name, status: 'within-reference', value: 1,
|
|
451
|
+
unit: 'artifact_set', signal: false, operands: { changed: 1, verified: 1 },
|
|
452
|
+
method: 'verify-manifest/v1', detail: `${pack.name}: ${pack.note ?? 'changed files still match its signed manifest'}`,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { violations, observations };
|
|
457
|
+
}
|
|
458
|
+
|
|
350
459
|
/** The built-in rule set (works with no config). Ops are the mutating operations each rule guards. */
|
|
351
460
|
export const DEFAULT_RULES: readonly GuardRule[] = [
|
|
352
461
|
{ id: 'no-workspace-star', severity: 'hard', ops: ['publish'], description: 'a published package.json must carry no workspace:* dep (npm ships it verbatim → the install breaks)' },
|
|
@@ -356,6 +465,8 @@ export const DEFAULT_RULES: readonly GuardRule[] = [
|
|
|
356
465
|
{ id: 'backlog-covers-features', severity: 'soft', ops: ['publish', 'consolidate'], description: 'каталог фичи, заведённый после базовой даты, назван записью бэклога — либо несёт именованную оговорку с причиной' },
|
|
357
466
|
{ id: 'no-secrets', severity: 'hard', ops: ['teach', 'publish'], description: 'no private key or API token in lesson text or a published file' },
|
|
358
467
|
{ id: 'readme-consistency', severity: 'soft', ops: ['publish'], description: 'README counts agree (CJM header vs All Commands, etc.)' },
|
|
468
|
+
{ id: 'release-line-in-sync', severity: 'soft', ops: ['publish'], description: 'root and harness-cli README release lines agree with the harness-core and harness-cli package versions' },
|
|
469
|
+
{ id: 'signature-fresh', severity: 'soft', ops: ['publish'], description: 'a pack whose files changed in this diff still verifies against its signed .dz-manifest.json — a stale signature is named before publish, not at the gate' },
|
|
359
470
|
{ id: 'skills-registrable', severity: 'soft', ops: ['publish'], description: 'every skill directory in a skill pack has a depth-1 SKILL.md (a buried or missing one ships un-registrable — the health-advisor 1.2.0 class)' },
|
|
360
471
|
{ id: 'readme-first', severity: 'soft', ops: ['publish'], description: 'a package with a staged version bump must update its own README.md in the same change (README-first)' },
|
|
361
472
|
{ id: 'routing-store-stale', severity: 'soft', ops: ['publish'], description: 'harvested routing telemetry has been applied to the auto-cost outcome store' },
|
|
@@ -921,6 +1032,8 @@ const HAS_INPUT: Partial<Record<string, (f: GuardFacts) => boolean>> = {
|
|
|
921
1032
|
// Дерево без плагин-манифестов правилу нечего сказать: «прошло» тут значило бы «не смотрели».
|
|
922
1033
|
'plugin-manifest-audit': (f) => Array.isArray(f.pluginManifests) && f.pluginManifests.length > 0,
|
|
923
1034
|
'no-secrets': (f) => Array.isArray(f.secretTargets) && f.secretTargets.length > 0,
|
|
1035
|
+
'release-line-in-sync': (f) => typeof f.releaseLines === 'object' && f.releaseLines !== null,
|
|
1036
|
+
'signature-fresh': (f) => Array.isArray(f.signedPacks),
|
|
924
1037
|
};
|
|
925
1038
|
|
|
926
1039
|
/**
|
|
@@ -1065,6 +1178,29 @@ export function evaluateGuard(facts: GuardFacts, rules: readonly GuardRule[] = D
|
|
|
1065
1178
|
continue;
|
|
1066
1179
|
}
|
|
1067
1180
|
checked.push(r.id);
|
|
1181
|
+
if (r.id === 'readme-first') {
|
|
1182
|
+
for (const p of facts.readmeFirst ?? []) {
|
|
1183
|
+
if (p?.versionUnknown !== true) continue;
|
|
1184
|
+
observations.push({
|
|
1185
|
+
schemaVersion: 'volume-shadow/v1', rule: 'readme-first' as never,
|
|
1186
|
+
metric: 'package-version-changed-from-head', scope: p.name, status: 'unknown', value: null,
|
|
1187
|
+
unit: 'artifact_set', signal: false, operands: {}, method: 'git-show-head-package-version/v1',
|
|
1188
|
+
detail: `${p.name}: package version could not be compared with HEAD; readme-first stayed advisory and emitted no violation`,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (r.id === 'release-line-in-sync') {
|
|
1193
|
+
const inspection = inspectReleaseLines(facts.releaseLines!, r.severity);
|
|
1194
|
+
observations.push(...inspection.observations);
|
|
1195
|
+
violations.push(...inspection.violations);
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (r.id === 'signature-fresh') {
|
|
1199
|
+
const inspection = inspectSignatureFresh(facts.signedPacks!, r.severity);
|
|
1200
|
+
observations.push(...inspection.observations);
|
|
1201
|
+
violations.push(...inspection.violations);
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1068
1204
|
if ((VOLUME_SHADOW_RULE_IDS as readonly string[]).includes(r.id)) {
|
|
1069
1205
|
const emission = volume();
|
|
1070
1206
|
observations.push(...emission.observations.filter((item) => item.rule === r.id));
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,8 @@ import { createRequire } from 'node:module';
|
|
|
10
10
|
export const HARNESS_CORE_VERSION: string =
|
|
11
11
|
(createRequire(import.meta.url)('../package.json') as { version: string }).version;
|
|
12
12
|
|
|
13
|
+
export { REPOSITORY_ORIGIN } from './repository-origin.js';
|
|
14
|
+
|
|
13
15
|
export * from './skills.js';
|
|
14
16
|
export * from './apply.js';
|
|
15
17
|
export {
|
|
@@ -123,6 +125,8 @@ export { planLedgerBackfill, LEDGER_FILL_SOURCE, AMBIGUOUS, resolveLedgerRunId }
|
|
|
123
125
|
// project-skills root resolution (field report doc-25b): the ONE builder behind both the Step-0
|
|
124
126
|
// probe and the PS_GUIDANCE paragraph, so the two can never look at different roots again.
|
|
125
127
|
export { projectSkillsOneRoot, projectSkillsProbeCommand } from './project-skills-root.js';
|
|
128
|
+
export { isRepoBoundary } from './repo-boundary.js';
|
|
129
|
+
export type { RepoBoundaryIo } from './repo-boundary.js';
|
|
126
130
|
export type { LedgerBackfillPlan, LedgerBackfillRow, RunCostFacts } from './ledger-backfill.js';
|
|
127
131
|
export type { SweepResult, DriftedSkill, SyncResult, SyncCanonicalOptions } from './skill-drift.js';
|
|
128
132
|
export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
|
|
@@ -660,7 +664,8 @@ export type {
|
|
|
660
664
|
ChainDefectAges,
|
|
661
665
|
ChainedJournal,
|
|
662
666
|
} from './event-chain.js';
|
|
663
|
-
export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion } from './publish.js';
|
|
667
|
+
export { decideProvenance, environmentCanMintProvenance, publishArgv, discoverPackages, publishPackages, bumpPatch, compareVersions, findUnpackagedSkills, findUnpublishedWorkspaceFloors, rewriteWorkspaceSpecs, orderByDependencies, syncReadmeVersion, isChangelogEntryLine, changelogRegion } from './publish.js';
|
|
668
|
+
export { RELEASE_LINE_RE, findReleaseLine, rewriteReleaseLine } from './release-line.js';
|
|
664
669
|
export * from './course-staleness.js';
|
|
665
670
|
export { fetchAllDownloads } from './downloads.js';
|
|
666
671
|
export type { PackageDownloads, DownloadsReport } from './downloads.js';
|
package/src/publish.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { execSync } from 'node:child_process';
|
|
|
16
16
|
type ExecSyncOptionsWithStringEncoding = NonNullable<Parameters<typeof execSync>[1]> & { encoding: 'utf-8' };
|
|
17
17
|
|
|
18
18
|
import { claimCheck } from './claim-check.js';
|
|
19
|
+
import { rewriteReleaseLine } from './release-line.js';
|
|
19
20
|
|
|
20
21
|
export type ProbeOutcome = {
|
|
21
22
|
readonly attempt: number;
|
|
@@ -26,6 +27,10 @@ export type ProbeOutcome = {
|
|
|
26
27
|
readonly ms: number;
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
// MEASURED 2026-09-10: registry answered E404 for ~3 min (19 probes); earlier the same day > 5 min.
|
|
31
|
+
export const REGISTRY_PROBE_BUDGET = 90;
|
|
32
|
+
export const REGISTRY_PROBE_INTERVAL_MS = 10_000;
|
|
33
|
+
|
|
29
34
|
/** Result for a single package publish attempt. */
|
|
30
35
|
export interface PublishResult {
|
|
31
36
|
readonly name: string;
|
|
@@ -63,6 +68,10 @@ export interface PublishReport {
|
|
|
63
68
|
readonly skipped: number;
|
|
64
69
|
readonly errors: number;
|
|
65
70
|
readonly dryRun: boolean;
|
|
71
|
+
/** Repo-relative README paths whose first joint core/CLI release line was rewritten. */
|
|
72
|
+
readonly releaseLineSynced: readonly string[];
|
|
73
|
+
/** Post-publication sync failures are warnings: registry-confirmed packages cannot be unpublished. */
|
|
74
|
+
readonly warnings?: readonly string[] | undefined;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/** Is `p` inside `dir`? Used to refuse a signing key that lives in the repository working tree. */
|
|
@@ -126,6 +135,32 @@ function maxPublished(name: string, localVersion: string, exec: PublishExec = ex
|
|
|
126
135
|
// the publish itself succeeds, and every consumer `npm install` then fails with ETARGET. Staged is
|
|
127
136
|
// not shipped; this preflight makes the difference a refusal instead of a broken release.
|
|
128
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Mirror pnpm's package-time expansion of the three shorthand workspace dependency specs.
|
|
140
|
+
* Pure by construction: callers provide both the source bytes and the sibling version table.
|
|
141
|
+
*/
|
|
142
|
+
export function rewriteWorkspaceSpecs(
|
|
143
|
+
pkgJsonText: string,
|
|
144
|
+
siblingVersions: ReadonlyMap<string, string>,
|
|
145
|
+
): string {
|
|
146
|
+
const pkg = JSON.parse(pkgJsonText) as Record<string, unknown>;
|
|
147
|
+
const fields = ['dependencies', 'peerDependencies', 'optionalDependencies', 'devDependencies'] as const;
|
|
148
|
+
for (const field of fields) {
|
|
149
|
+
const candidate = pkg[field];
|
|
150
|
+
if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
|
|
151
|
+
const table = candidate as Record<string, unknown>;
|
|
152
|
+
for (const [dep, spec] of Object.entries(table)) {
|
|
153
|
+
if (typeof spec !== 'string') continue;
|
|
154
|
+
const match = /^workspace:([*^~])$/.exec(spec);
|
|
155
|
+
const version = siblingVersions.get(dep);
|
|
156
|
+
if (match === null || version === undefined) continue;
|
|
157
|
+
const marker = match[1]!;
|
|
158
|
+
table[dep] = marker === '*' ? version : `${marker}${version}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return JSON.stringify(pkg, null, 2) + '\n';
|
|
162
|
+
}
|
|
163
|
+
|
|
129
164
|
/**
|
|
130
165
|
* Pure half: which `workspace:`-declared deps of a package would pack to a floor that is neither
|
|
131
166
|
* being published in this batch nor already on the registry?
|
|
@@ -840,7 +875,7 @@ export function publishPackages(
|
|
|
840
875
|
|
|
841
876
|
let registryProbes = 0;
|
|
842
877
|
let confirmed = false;
|
|
843
|
-
while (registryProbes <
|
|
878
|
+
while (registryProbes < REGISTRY_PROBE_BUDGET) {
|
|
844
879
|
registryProbes++;
|
|
845
880
|
const probed = probe(pkg.name, newVersion);
|
|
846
881
|
const outcome: Omit<ProbeOutcome, 'attempt'> = typeof probed === 'boolean'
|
|
@@ -851,14 +886,15 @@ export function publishPackages(
|
|
|
851
886
|
confirmed = true;
|
|
852
887
|
break;
|
|
853
888
|
}
|
|
854
|
-
if (registryProbes <
|
|
889
|
+
if (registryProbes < REGISTRY_PROBE_BUDGET) sleep(REGISTRY_PROBE_INTERVAL_MS);
|
|
855
890
|
}
|
|
856
891
|
if (!confirmed) {
|
|
857
892
|
const last = probeLog[probeLog.length - 1]!;
|
|
858
893
|
const output = last.stderr || last.stdout;
|
|
859
894
|
const firstLine = output.split(/\r?\n/, 1)[0]?.trim() || '(empty)';
|
|
860
895
|
throw new Error(
|
|
861
|
-
`registry did not confirm ${pkg.name}@${newVersion} after ${registryProbes} probes
|
|
896
|
+
`registry did not confirm ${pkg.name}@${newVersion} after ${registryProbes} probes ` +
|
|
897
|
+
`(${Math.round(registryProbes * REGISTRY_PROBE_INTERVAL_MS / 60_000)} min); ` +
|
|
862
898
|
`last probe: code ${String(last.code)}, ${last.ms}ms, ${firstLine}`,
|
|
863
899
|
);
|
|
864
900
|
}
|
|
@@ -887,11 +923,64 @@ export function publishPackages(
|
|
|
887
923
|
}
|
|
888
924
|
}
|
|
889
925
|
|
|
926
|
+
const releaseLineSynced: string[] = [];
|
|
927
|
+
const warnings: string[] = [];
|
|
928
|
+
const releasePackageNames = new Set(['@dzhechkov/harness-core', '@dzhechkov/harness-cli']);
|
|
929
|
+
const releasePackagePublished = results.some(
|
|
930
|
+
(result) => result.status === 'published' && releasePackageNames.has(result.name),
|
|
931
|
+
);
|
|
932
|
+
if (releasePackagePublished && opts.dryRun !== true && opts.bumpOnly !== true) {
|
|
933
|
+
const currentVersion = (name: string): string | null => {
|
|
934
|
+
const landed = results.find((result) => result.name === name && result.status === 'published');
|
|
935
|
+
if (landed !== undefined) return landed.newVersion;
|
|
936
|
+
const pkg = packages.find((candidate) => candidate.name === name);
|
|
937
|
+
if (pkg === undefined) return null;
|
|
938
|
+
try {
|
|
939
|
+
const parsed = JSON.parse(readFileSync(pathJoin(pkg.dir, 'package.json'), 'utf8')) as { version?: unknown };
|
|
940
|
+
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
941
|
+
} catch {
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
const coreVersion = currentVersion('@dzhechkov/harness-core');
|
|
946
|
+
const cliVersion = currentVersion('@dzhechkov/harness-cli');
|
|
947
|
+
if (coreVersion === null || cliVersion === null) {
|
|
948
|
+
warnings.push('release-line sync skipped: could not read both harness-core and harness-cli package versions');
|
|
949
|
+
} else {
|
|
950
|
+
const readmes = [
|
|
951
|
+
{ path: 'README.md', absolute: pathJoin(monorepoRoot, 'README.md') },
|
|
952
|
+
{
|
|
953
|
+
path: 'packages/@dzhechkov/harness-cli/README.md',
|
|
954
|
+
absolute: pathJoin(monorepoRoot, 'packages', '@dzhechkov', 'harness-cli', 'README.md'),
|
|
955
|
+
},
|
|
956
|
+
] as const;
|
|
957
|
+
for (const readme of readmes) {
|
|
958
|
+
try {
|
|
959
|
+
const original = readFileSync(readme.absolute, 'utf8');
|
|
960
|
+
const updated = rewriteReleaseLine(original, coreVersion, cliVersion);
|
|
961
|
+
if (updated === null) {
|
|
962
|
+
warnings.push(`release-line sync skipped ${readme.path}: release line not found`);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
if (updated === original) continue;
|
|
966
|
+
const tmp = readme.absolute + '.sync-tmp';
|
|
967
|
+
writeFileSync(tmp, updated);
|
|
968
|
+
renameSync(tmp, readme.absolute);
|
|
969
|
+
releaseLineSynced.push(readme.path);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
warnings.push(`release-line sync failed ${readme.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
890
977
|
return {
|
|
891
978
|
packages: results,
|
|
892
979
|
published: results.filter((r) => r.status === 'published').length,
|
|
893
980
|
skipped: results.filter((r) => r.status === 'skipped').length,
|
|
894
981
|
errors: results.filter((r) => r.status === 'error').length,
|
|
895
982
|
dryRun: opts.dryRun === true,
|
|
983
|
+
releaseLineSynced,
|
|
984
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
896
985
|
};
|
|
897
986
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const RELEASE_LINE_RE = /`harness-core v(\d+\.\d+\.\d+)` · `harness-cli v(\d+\.\d+\.\d+)`/;
|
|
2
|
+
|
|
3
|
+
export interface ReleaseLineMatch {
|
|
4
|
+
readonly index: number;
|
|
5
|
+
readonly line: string;
|
|
6
|
+
readonly core: string;
|
|
7
|
+
readonly cli: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function findReleaseLine(text: string): ReleaseLineMatch | null {
|
|
11
|
+
const lines = text.split('\n');
|
|
12
|
+
for (let index = 0; index < lines.length; index++) {
|
|
13
|
+
const line = lines[index]!;
|
|
14
|
+
const match = RELEASE_LINE_RE.exec(line);
|
|
15
|
+
if (match?.[1] !== undefined && match[2] !== undefined) {
|
|
16
|
+
return { index, line, core: match[1], cli: match[2] };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function rewriteReleaseLine(text: string, core: string, cli: string): string | null {
|
|
23
|
+
const found = findReleaseLine(text);
|
|
24
|
+
if (found === null) return null;
|
|
25
|
+
const lines = text.split('\n');
|
|
26
|
+
lines[found.index] = found.line.replace(
|
|
27
|
+
RELEASE_LINE_RE,
|
|
28
|
+
`\`harness-core v${core}\` · \`harness-cli v${cli}\``,
|
|
29
|
+
);
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Filesystem facts required to recognise a real Git repository boundary. */
|
|
2
|
+
export interface RepoBoundaryIo {
|
|
3
|
+
exists(p: string): boolean;
|
|
4
|
+
isDirectory(p: string): boolean;
|
|
5
|
+
readText(p: string): string | null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A Git repository boundary is either a `.git` directory containing `HEAD`, or a worktree-style
|
|
10
|
+
* `.git` file whose first bytes are the `gitdir:` redirect. An empty or unrelated `.git` entry is
|
|
11
|
+
* not a boundary.
|
|
12
|
+
*/
|
|
13
|
+
export function isRepoBoundary(
|
|
14
|
+
dir: string,
|
|
15
|
+
io: RepoBoundaryIo,
|
|
16
|
+
join: (...p: string[]) => string,
|
|
17
|
+
): boolean {
|
|
18
|
+
const git = join(dir, '.git');
|
|
19
|
+
if (!io.exists(git)) return false;
|
|
20
|
+
if (io.isDirectory(git)) {
|
|
21
|
+
const head = join(git, 'HEAD');
|
|
22
|
+
return io.exists(head) && !io.isDirectory(head);
|
|
23
|
+
}
|
|
24
|
+
return io.readText(git)?.startsWith('gitdir:') === true;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const REPOSITORY_ORIGIN = 'git+https://github.com/djd1m/dz-harness-hub.git';
|