session-orchestrator 4.0.0 → 4.1.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +3 -2
- package/.codex-plugin/skills/architecture/SKILL.md +20 -0
- package/.codex-plugin/skills/autopilot/SKILL.md +21 -0
- package/.codex-plugin/skills/autopilot/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/bootstrap/SKILL.md +22 -0
- package/.codex-plugin/skills/bootstrap/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/brainstorm/SKILL.md +22 -0
- package/.codex-plugin/skills/brainstorm/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/claude-md-drift-check/SKILL.md +17 -0
- package/.codex-plugin/skills/close/SKILL.md +21 -0
- package/.codex-plugin/skills/close/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +24 -0
- package/.codex-plugin/skills/debug/SKILL.md +21 -0
- package/.codex-plugin/skills/debug/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/discovery/SKILL.md +21 -0
- package/.codex-plugin/skills/discovery/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/dispatcher/SKILL.md +21 -0
- package/.codex-plugin/skills/dispatcher/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/docs-orchestrator/SKILL.md +20 -0
- package/.codex-plugin/skills/ecosystem-health/SKILL.md +22 -0
- package/.codex-plugin/skills/eli5/SKILL.md +21 -0
- package/.codex-plugin/skills/eli5/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/eval/SKILL.md +21 -0
- package/.codex-plugin/skills/eval/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/evolve/SKILL.md +21 -0
- package/.codex-plugin/skills/evolve/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/frontmatter-guard/SKILL.md +17 -0
- package/.codex-plugin/skills/gitlab-ops/SKILL.md +22 -0
- package/.codex-plugin/skills/gitlab-portfolio/SKILL.md +17 -0
- package/.codex-plugin/skills/go/SKILL.md +22 -0
- package/.codex-plugin/skills/go/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/grill/SKILL.md +21 -0
- package/.codex-plugin/skills/grill/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/harness-audit/SKILL.md +19 -0
- package/.codex-plugin/skills/harness-audit/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/hook-development/SKILL.md +17 -0
- package/.codex-plugin/skills/mcp-builder/SKILL.md +17 -0
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +21 -0
- package/.codex-plugin/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/mode-selector/SKILL.md +19 -0
- package/.codex-plugin/skills/npm-publish/SKILL.md +18 -0
- package/.codex-plugin/skills/peekaboo-driver/SKILL.md +20 -0
- package/.codex-plugin/skills/persona-panel/SKILL.md +22 -0
- package/.codex-plugin/skills/persona-panel/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/plan/SKILL.md +22 -0
- package/.codex-plugin/skills/plan/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/playwright-driver/SKILL.md +22 -0
- package/.codex-plugin/skills/portfolio/SKILL.md +21 -0
- package/.codex-plugin/skills/portfolio/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/quality-gates/SKILL.md +22 -0
- package/.codex-plugin/skills/reconcile/SKILL.md +21 -0
- package/.codex-plugin/skills/reconcile/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/release/SKILL.md +22 -0
- package/.codex-plugin/skills/release/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/remote-offload/SKILL.md +22 -0
- package/.codex-plugin/skills/repo-audit/SKILL.md +19 -0
- package/.codex-plugin/skills/repo-audit/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/session/SKILL.md +21 -0
- package/.codex-plugin/skills/session/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/session-end/SKILL.md +22 -0
- package/.codex-plugin/skills/session-plan/SKILL.md +22 -0
- package/.codex-plugin/skills/session-start/SKILL.md +22 -0
- package/.codex-plugin/skills/spinout/SKILL.md +21 -0
- package/.codex-plugin/skills/spinout/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/sunset-review/SKILL.md +21 -0
- package/.codex-plugin/skills/sunset-review/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/templates-ack/SKILL.md +21 -0
- package/.codex-plugin/skills/templates-ack/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test/SKILL.md +21 -0
- package/.codex-plugin/skills/test/agents/openai.yaml +5 -0
- package/.codex-plugin/skills/test-runner/SKILL.md +22 -0
- package/.codex-plugin/skills/tmux-layout/SKILL.md +23 -0
- package/.codex-plugin/skills/using-orchestrator/SKILL.md +19 -0
- package/.codex-plugin/skills/vault-mirror/SKILL.md +17 -0
- package/.codex-plugin/skills/vault-sync/SKILL.md +17 -0
- package/.codex-plugin/skills/wave-executor/SKILL.md +22 -0
- package/.codex-plugin/skills/write-executable-plan/SKILL.md +24 -0
- package/{plugin.json → .cursor-plugin/plugin.json} +5 -2
- package/CHANGELOG.md +213 -1
- package/README.md +70 -58
- package/commands/release.md +4 -4
- package/docs/codex-setup.md +43 -9
- package/docs/components.md +3 -2
- package/docs/instruction-delivery.md +12 -5
- package/docs/migration-v4.md +33 -9
- package/hooks/_lib/hook-import-set.json +4 -3
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/on-stop.mjs +25 -4
- package/package.json +2 -2
- package/scripts/ci/assert-coverage-green.mjs +100 -0
- package/scripts/generate-codex-skills.mjs +246 -0
- package/scripts/generate-hook-import-set.mjs +51 -8
- package/scripts/lib/codex/plugin-contract.mjs +6 -0
- package/scripts/lib/config/host-paths.mjs +20 -4
- package/scripts/lib/events.mjs +3 -3
- package/scripts/lib/gates/gate-full.mjs +7 -3
- package/scripts/lib/owner-config-banner.mjs +7 -9
- package/scripts/lib/owner-yaml.mjs +8 -1
- package/scripts/lib/plugin-update-banner.mjs +10 -2
- package/scripts/lib/project-hygiene.mjs +182 -6
- package/scripts/lib/reconcile/engine.mjs +38 -7
- package/scripts/lib/session-identity/own-session.mjs +24 -13
- package/scripts/lib/session-schema/constants.mjs +38 -11
- package/scripts/lib/session-start-probes.mjs +12 -0
- package/scripts/lib/telemetry/schema.mjs +39 -18
- package/scripts/lib/telemetry-flush-health-banner.mjs +211 -0
- package/scripts/lib/validate/check-codex-skills.mjs +191 -0
- package/scripts/lib/validate/check-owner-leakage.mjs +107 -62
- package/scripts/lib/validate/check-skill-links.mjs +37 -7
- package/scripts/lib/validate/check-test-git-config-target.mjs +192 -12
- package/scripts/lib/validate/check-unwired-features.mjs +163 -13
- package/scripts/lib/validate/confidential-names.mjs +95 -30
- package/scripts/lib/validate/repo-files.mjs +48 -14
- package/scripts/lib/vault-mirror/render-sessions.mjs +8 -1
- package/scripts/release.mjs +141 -29
- package/scripts/site-numbers.mjs +344 -8
- package/scripts/validate-plugin.mjs +3 -0
- package/skills/session-start/SKILL.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +5 -0
- package/skills/vault-sync/SKILL.md +10 -0
package/scripts/release.mjs
CHANGED
|
@@ -121,12 +121,10 @@ export const SURFACES = [
|
|
|
121
121
|
patterns: [/"version":\s*"(\d+\.\d+\.\d+)"/],
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
// class the table exists for, caught one gate later than it should have been.
|
|
129
|
-
file: 'plugin.json',
|
|
124
|
+
// Cursor's native manifest replaces the root Agent Plugins manifest so
|
|
125
|
+
// Codex can load its own component paths and cache version independently.
|
|
126
|
+
// It remains a required version surface for every release.
|
|
127
|
+
file: '.cursor-plugin/plugin.json',
|
|
130
128
|
patterns: [/"version":\s*"(\d+\.\d+\.\d+)"/],
|
|
131
129
|
},
|
|
132
130
|
{
|
|
@@ -191,6 +189,14 @@ export const SURFACES = [
|
|
|
191
189
|
patterns: [/data-metric="version"[^>]*>(\d+\.\d+\.\d+)</g],
|
|
192
190
|
checkOnly: true,
|
|
193
191
|
},
|
|
192
|
+
{
|
|
193
|
+
// The German landing page (2026-09-07 redesign) mirrors the EN metric cells
|
|
194
|
+
// one-to-one; same single writer (scripts/site-numbers.mjs --write), so it is
|
|
195
|
+
// checkOnly for the same reason as site/index.html above.
|
|
196
|
+
file: 'site/de/index.html',
|
|
197
|
+
patterns: [/data-metric="version"[^>]*>(\d+\.\d+\.\d+)</g],
|
|
198
|
+
checkOnly: true,
|
|
199
|
+
},
|
|
194
200
|
{
|
|
195
201
|
file: 'site/llms.txt',
|
|
196
202
|
patterns: [/Version:\s*(\d+\.\d+\.\d+)/],
|
|
@@ -362,7 +368,18 @@ const INTENTIONAL_TEST_ASSET_PATHS = new Set([
|
|
|
362
368
|
// tests/scripts/site-numbers.test.mjs, which forbids ANY vX.Y.Z and the current
|
|
363
369
|
// package version outside a `data-metric` cell on EVERY shipped page, and
|
|
364
370
|
// exempts exactly the lines marked `site-numbers:historical`.
|
|
365
|
-
|
|
371
|
+
// 4.0.1 (measured 2026-09-07): the 4.0.0 sweep would have flagged five files whose only literal
|
|
372
|
+
// is PROSE HISTORY of the major ("moved out of agents/ in 4.0.0", "removed in 4.0.0",
|
|
373
|
+
// "releases 4.0.0 would have blocked on itself") — CLAUDE.md, its generated twin AGENTS.md,
|
|
374
|
+
// CONTRIBUTING.md, NOTICE and .husky/pre-push. None of them is a version SURFACE (no
|
|
375
|
+
// `"version":`, badge or `vX.Y.Z` form), so they join the history allowlist rather than being
|
|
376
|
+
// reworded to dodge the sweep.
|
|
377
|
+
// Four more prose-history files surfaced once the detail line stopped truncating at five hits
|
|
378
|
+
// (same 2026-09-07 sweep): site/llms-full.txt ("The v4.0.0 release REMOVES public surfaces" — its
|
|
379
|
+
// version SURFACE is checked separately by the SURFACES row, so the sweep on it is redundant),
|
|
380
|
+
// skills/architecture/references/domain-model.md ("Merged here in v4.0.0"), skills/autopilot/SKILL.md
|
|
381
|
+
// ("4.0.0 — see docs/migration-v4.md") and templates/_shared/journey-manifest.md ("Retired … in 4.0.0").
|
|
382
|
+
export const HISTORY_ALLOWLIST = /^(CHANGELOG\.md|README\.md|CLAUDE\.md|AGENTS\.md|CONTRIBUTING\.md|NOTICE|\.husky\/pre-push|docs\/|tests\/|skills\/npm-publish\/|skills\/architecture\/references\/domain-model\.md|skills\/autopilot\/SKILL\.md|templates\/_shared\/journey-manifest\.md|scripts\/release\.mjs|\.orchestrator\/|site\/leaderboard\.json|site\/guide\/index\.html|site\/llms-full\.txt|commands\/release\.md)/;
|
|
366
383
|
|
|
367
384
|
/** Pure check over packed-entry lines. Returns violations: {name, line}[]. */
|
|
368
385
|
export function checkLeakage(lines) {
|
|
@@ -443,28 +460,95 @@ export const MIN_PACKED_ENTRIES = 400;
|
|
|
443
460
|
// ---------------------------------------------------------------------------
|
|
444
461
|
|
|
445
462
|
/**
|
|
446
|
-
*
|
|
463
|
+
* A regex matching `literal` as a VERSION TOKEN, not as a substring.
|
|
464
|
+
*
|
|
465
|
+
* THE BUG (measured 2026-09-07, mid-release): the sweep matched the previous tag `4.0.0`
|
|
466
|
+
* inside `>=24.0.0` — `package.json`'s own engines field and a `scripts/lib/` string that
|
|
467
|
+
* quotes it — so a release could not be cut without either rewording an engines constraint or
|
|
468
|
+
* widening the allowlist over two files that carry no version surface at all. The boundary is
|
|
469
|
+
* therefore part of WHAT IS SWEPT FOR, not an allowlist row: `24.0.0`, `14.0.0` and `4.0.0.1`
|
|
470
|
+
* are different literals, at every path, forever.
|
|
471
|
+
*
|
|
472
|
+
* @param {string} literal — the previous release version
|
|
473
|
+
* @returns {RegExp} global regex; `4.0.0` matches only when not preceded by `[0-9.]` and not
|
|
474
|
+
* continued by a further numeric component (`(?!\.?[0-9])`).
|
|
475
|
+
*/
|
|
476
|
+
function versionTokenRegex(literal) {
|
|
477
|
+
return new RegExp(`(?<![0-9.])${literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?!\\.?[0-9])`, 'g');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Is EVERY token occurrence of `literal` on this line a dependency range (`^X.Y.Z` / `~X.Y.Z`)?
|
|
447
482
|
*
|
|
448
483
|
* One bare occurrence anywhere on the line is enough to call the whole line drift — a comment
|
|
449
|
-
* that also happens to mention a ranged dep must not be excused by that mention.
|
|
484
|
+
* that also happens to mention a ranged dep must not be excused by that mention. Occurrences are
|
|
485
|
+
* counted with {@link versionTokenRegex}, so `>=24.0.0` is not an occurrence of `4.0.0` here
|
|
486
|
+
* either — otherwise a line pinning `^4.0.0` beside an engines constraint would read as drift.
|
|
450
487
|
*
|
|
451
488
|
* @param {string} content — the matching line's text
|
|
452
|
-
* @param {string} literal — the previous release version, matched
|
|
489
|
+
* @param {string} literal — the previous release version, matched as a token
|
|
453
490
|
* @returns {boolean}
|
|
454
491
|
*/
|
|
455
492
|
export function isDependencyRangeOnly(content, literal) {
|
|
456
|
-
|
|
493
|
+
const re = versionTokenRegex(literal);
|
|
457
494
|
let seen = 0;
|
|
458
|
-
for (
|
|
459
|
-
const at = content.indexOf(literal, from);
|
|
460
|
-
if (at === -1) break;
|
|
495
|
+
for (let m = re.exec(content); m; m = re.exec(content)) {
|
|
461
496
|
seen += 1;
|
|
462
|
-
|
|
463
|
-
|
|
497
|
+
const before = m.index > 0 ? content[m.index - 1] : '';
|
|
498
|
+
if (before !== '^' && before !== '~') return false;
|
|
464
499
|
}
|
|
465
500
|
return seen > 0;
|
|
466
501
|
}
|
|
467
502
|
|
|
503
|
+
/** Lockfiles whose dependency entries are third-party history, never our surface. */
|
|
504
|
+
const LOCKFILE_BASENAMES = new Set(['package-lock.json', 'npm-shrinkwrap.json']);
|
|
505
|
+
|
|
506
|
+
/** Code files in which a `//`, `*`, `/*` or `#` line is comment prose, never a version surface. */
|
|
507
|
+
const CODE_COMMENT_EXTENSIONS = new Set(['.mjs', '.js', '.cjs', '.ts', '.sh']);
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Is this `path:line:content` row version HISTORY rather than a stale surface?
|
|
511
|
+
*
|
|
512
|
+
* Three classes, all measured on the 4.0.1 cut (2026-09-07) as FALSE POSITIVES of the raw
|
|
513
|
+
* substring sweep, and all expressed as PREDICATES for the same reason the range carve-out
|
|
514
|
+
* above is one: a per-path allowlist row fixes the instance and leaves the class open.
|
|
515
|
+
*
|
|
516
|
+
* 1. **Dependency range** — `^X.Y.Z` / `~X.Y.Z` (see {@link isDependencyRangeOnly}).
|
|
517
|
+
* 2. **Lockfile dependency entry** — `package-lock.json` carried 72 hits for `4.0.0`, every one
|
|
518
|
+
* a third-party package version or an engines range. Our OWN entry there is still swept: the
|
|
519
|
+
* root package's `"version"` line, which npm writes in the `packages[""]` record at the top
|
|
520
|
+
* of the file. NAMED CEILING (BV-004): "at the top" is read as `line <= 20`, which covers
|
|
521
|
+
* every lockfileVersion-3 file npm writes today (the root record starts at line 5). Revisit
|
|
522
|
+
* trigger: a lockfile whose root `"version"` sits below line 20 — then key on the enclosing
|
|
523
|
+
* JSON path instead of the line number.
|
|
524
|
+
* 3. **Comment prose in a code file** — `// (pre-4.0.0 checkouts, …)` and a `* since 4.0.0`
|
|
525
|
+
* docblock line. No SURFACES pattern is ever a comment (every one is `"version": "X.Y.Z"`,
|
|
526
|
+
* `vX.Y.Z` or a badge), so excusing comment lines cannot mask a stale surface.
|
|
527
|
+
*
|
|
528
|
+
* @param {string} file — repo-relative path
|
|
529
|
+
* @param {number} line — 1-based line number
|
|
530
|
+
* @param {string} content — the matching line's text
|
|
531
|
+
* @param {string} prevTag — the previous release literal being swept for
|
|
532
|
+
* @returns {boolean} true = history, skip the row
|
|
533
|
+
*/
|
|
534
|
+
function isHistoryRow(file, line, content, prevTag) {
|
|
535
|
+
if (!versionTokenRegex(prevTag).test(content)) return true;
|
|
536
|
+
if (isDependencyRangeOnly(content, prevTag)) return true;
|
|
537
|
+
const base = file.split('/').pop();
|
|
538
|
+
if (LOCKFILE_BASENAMES.has(base)) {
|
|
539
|
+
const isRootVersionLine = line <= 20 && new RegExp(`"version":\\s*"${prevTag.replace(/\./g, '\\.')}"`).test(content);
|
|
540
|
+
return !isRootVersionLine;
|
|
541
|
+
}
|
|
542
|
+
const dot = base.lastIndexOf('.');
|
|
543
|
+
const ext = dot === -1 ? '' : base.slice(dot);
|
|
544
|
+
if (CODE_COMMENT_EXTENSIONS.has(ext)) {
|
|
545
|
+
const trimmed = content.trim();
|
|
546
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) return true;
|
|
547
|
+
if (ext === '.sh' && trimmed.startsWith('#')) return true;
|
|
548
|
+
}
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
468
552
|
/**
|
|
469
553
|
* Drift sweep verdict over a grep-shaped result (`-l` file list or `-n` line hits).
|
|
470
554
|
*
|
|
@@ -488,6 +572,13 @@ export function isDependencyRangeOnly(content, literal) {
|
|
|
488
572
|
* written as one (see SURFACES above: every pattern is an exact `"version": "X.Y.Z"`, `vX.Y.Z`
|
|
489
573
|
* or badge form), so the predicate cannot mask a stale surface.
|
|
490
574
|
*
|
|
575
|
+
* THREE FURTHER CLASSES are history for the same reason, all measured 2026-09-07 mid-release
|
|
576
|
+
* and all decided by {@link isHistoryRow}, never by an allowlist row: a literal that is only a
|
|
577
|
+
* SUBSTRING of a longer version (`4.0.0` inside `>=24.0.0`), a `package-lock.json` /
|
|
578
|
+
* `npm-shrinkwrap.json` row that is not the root package's own `"version"` line (72 of the 72
|
|
579
|
+
* lockfile hits on that cut were third-party), and COMMENT PROSE in a code file
|
|
580
|
+
* (`// (pre-4.0.0 checkouts, …)`). See that function for each one's ceiling.
|
|
581
|
+
*
|
|
491
582
|
* Accepts BOTH `git grep` output shapes. A bare `path` (from `-l`) carries no content and is
|
|
492
583
|
* therefore always a hit — the fail-closed reading, unchanged, and the shape {@link collectDriftHits}
|
|
493
584
|
* emits for a file it could not READ. `path:line:content` (from `-n`)
|
|
@@ -510,13 +601,13 @@ export function evaluateDriftSweep(grep, prevTag, allowlist) {
|
|
|
510
601
|
const withContent = row.match(/^(.+?):(\d+):(.*)$/);
|
|
511
602
|
const file = withContent ? withContent[1] : row;
|
|
512
603
|
if (allowlist.test(file)) continue;
|
|
513
|
-
if (withContent &&
|
|
604
|
+
if (withContent && isHistoryRow(file, Number(withContent[2]), withContent[3], prevTag)) continue;
|
|
514
605
|
if (!hits.includes(file)) hits.push(file);
|
|
515
606
|
}
|
|
516
607
|
return {
|
|
517
608
|
ok: hits.length === 0,
|
|
518
609
|
detail: hits.length
|
|
519
|
-
? `still carry ${prevTag}: ${hits.slice(0, 5).join(', ')}`
|
|
610
|
+
? `still carry ${prevTag} (${hits.length} file(s)): ${hits.slice(0, 5).join(', ')}${hits.length > 5 ? ', …' : ''}`
|
|
520
611
|
: `no file outside the allowlist still carries ${prevTag} (tracked + untracked-not-ignored)`,
|
|
521
612
|
};
|
|
522
613
|
}
|
|
@@ -1429,10 +1520,15 @@ export function printPublishOutcome(outcome, target, io = {}) {
|
|
|
1429
1520
|
log(` ${outcome.release.detail}.`);
|
|
1430
1521
|
} else {
|
|
1431
1522
|
error(`\nRECONCILIATION: ${outcome.release.detail}`);
|
|
1432
|
-
|
|
1433
|
-
|
|
1523
|
+
const recovery = outcome.release.recovery;
|
|
1524
|
+
if (recovery?.inspect) {
|
|
1525
|
+
error(` Inspect with: ${renderRecoveryCommand(recovery.inspect)}`);
|
|
1526
|
+
if (outcome.release.state === 'create-failed' && recovery.create) {
|
|
1527
|
+
error(` Recover with: ${renderRecoveryCommand(recovery.create)}`);
|
|
1528
|
+
error(' The notes file is retained for recovery; remove it after the release is reconciled.');
|
|
1529
|
+
}
|
|
1434
1530
|
} else {
|
|
1435
|
-
error('
|
|
1531
|
+
error(' Resolve the GitHub repository identity before inspecting or creating the release.');
|
|
1436
1532
|
}
|
|
1437
1533
|
}
|
|
1438
1534
|
|
|
@@ -1462,6 +1558,11 @@ export function printPublishOutcome(outcome, target, io = {}) {
|
|
|
1462
1558
|
return 0;
|
|
1463
1559
|
}
|
|
1464
1560
|
|
|
1561
|
+
/** Render argv for a POSIX shell without interpreting paths as shell code. */
|
|
1562
|
+
function renderRecoveryCommand(argv) {
|
|
1563
|
+
return argv.map((arg) => /^[\w./:@=+-]+$/.test(arg) ? arg : `'${arg.replaceAll("'", "'\\''")}'`).join(' ');
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1465
1566
|
/**
|
|
1466
1567
|
* Create the GitHub release for `v<target>`, or confirm the existing one.
|
|
1467
1568
|
*
|
|
@@ -1491,18 +1592,23 @@ export function printPublishOutcome(outcome, target, io = {}) {
|
|
|
1491
1592
|
* @param {string} repoRoot
|
|
1492
1593
|
* @param {string} target
|
|
1493
1594
|
* @param {{runImpl?: Function, repoSpec?: string}} [deps] — injection seam for tests
|
|
1494
|
-
* @returns {{ok: boolean, created: boolean, tag: string, state: 'exists'|'created'|'unknown'|'create-failed', detail: string, argv?: string[]}}
|
|
1595
|
+
* @returns {{ok: boolean, created: boolean, tag: string, state: 'exists'|'created'|'unknown'|'create-failed', detail: string, argv?: string[], recovery?: {inspect: string[], create?: string[]}}}
|
|
1495
1596
|
*/
|
|
1496
1597
|
export function ensureGithubRelease(repoRoot, target, deps = {}) {
|
|
1497
1598
|
const runImpl = deps.runImpl ?? run;
|
|
1498
1599
|
const tag = `v${target}`;
|
|
1499
|
-
|
|
1500
|
-
// resolveRepoSpec returns undefined when it cannot auto-detect; its contract
|
|
1501
|
-
// is that callers OMIT the flag rather than pass `-R undefined`.
|
|
1502
|
-
const repoFlag = spec ? ['--repo', spec] : [];
|
|
1600
|
+
let recovery;
|
|
1503
1601
|
|
|
1504
1602
|
try {
|
|
1505
|
-
const
|
|
1603
|
+
const spec = deps.repoSpec ?? resolveRepoSpec({ repoRoot, vcs: 'github' });
|
|
1604
|
+
if (typeof spec !== 'string' || !spec.trim()) {
|
|
1605
|
+
return { ok: false, created: false, tag, state: 'unknown', detail: 'GitHub repository identity could not be resolved' };
|
|
1606
|
+
}
|
|
1607
|
+
// Recovery must carry the same resolved identity as the real invocation.
|
|
1608
|
+
// An absent identity cannot safely fall back to the caller's ambient repo.
|
|
1609
|
+
const repoFlag = ['--repo', spec];
|
|
1610
|
+
recovery = { inspect: ['gh', 'release', 'view', tag, ...repoFlag] };
|
|
1611
|
+
const existing = runImpl(recovery.inspect[0], recovery.inspect.slice(1), { cwd: repoRoot });
|
|
1506
1612
|
const viewOutput = `${existing.stdout || ''}\n${existing.stderr || ''}`.trim();
|
|
1507
1613
|
if (existing.status === 0 && viewOutput) {
|
|
1508
1614
|
return { ok: true, created: false, tag, state: 'exists', detail: `GitHub release ${tag} already exists — no-op` };
|
|
@@ -1516,6 +1622,7 @@ export function ensureGithubRelease(repoRoot, target, deps = {}) {
|
|
|
1516
1622
|
created: false,
|
|
1517
1623
|
tag,
|
|
1518
1624
|
state: 'unknown',
|
|
1625
|
+
recovery,
|
|
1519
1626
|
detail: `could not determine whether GitHub release ${tag} exists (gh release view exited ${existing.status}: ${viewOutput.slice(0, 300) || 'empty output'})`,
|
|
1520
1627
|
};
|
|
1521
1628
|
}
|
|
@@ -1523,26 +1630,31 @@ export function ensureGithubRelease(repoRoot, target, deps = {}) {
|
|
|
1523
1630
|
const notesDir = mkdtempSync(join(tmpdir(), 'release-ghnotes-'));
|
|
1524
1631
|
const notesFile = join(notesDir, 'notes.md');
|
|
1525
1632
|
let argv;
|
|
1633
|
+
let retainNotes = false;
|
|
1526
1634
|
try {
|
|
1527
1635
|
writeFileSync(notesFile, `${changelogExcerpt(repoRoot, target)}\n`);
|
|
1528
1636
|
argv = ['release', 'create', tag, ...repoFlag, '--verify-tag', '--title', tag, '--notes-file', notesFile];
|
|
1529
1637
|
const created = runImpl('gh', argv, { cwd: repoRoot });
|
|
1530
1638
|
if (created.status !== 0) {
|
|
1639
|
+
// A recovery argv pointing to a file deleted by finally is unusable.
|
|
1640
|
+
// Preserve only this failed-create excerpt; successful runs still clean up.
|
|
1641
|
+
retainNotes = true;
|
|
1531
1642
|
return {
|
|
1532
1643
|
ok: false,
|
|
1533
1644
|
created: false,
|
|
1534
1645
|
tag,
|
|
1535
1646
|
state: 'create-failed',
|
|
1536
1647
|
argv,
|
|
1648
|
+
recovery: { ...recovery, create: ['gh', ...argv] },
|
|
1537
1649
|
detail: `gh release create exited ${created.status}: ${(created.stderr || created.stdout || '').trim().slice(0, 300)}`,
|
|
1538
1650
|
};
|
|
1539
1651
|
}
|
|
1540
1652
|
return { ok: true, created: true, tag, state: 'created', argv, detail: `GitHub release ${tag} created (--verify-tag)` };
|
|
1541
1653
|
} finally {
|
|
1542
|
-
rmSync(notesDir, { recursive: true, force: true });
|
|
1654
|
+
if (!retainNotes) rmSync(notesDir, { recursive: true, force: true });
|
|
1543
1655
|
}
|
|
1544
1656
|
} catch (err) {
|
|
1545
|
-
return { ok: false, created: false, tag, state: 'unknown', detail: `gh could not be run: ${err.message}` };
|
|
1657
|
+
return { ok: false, created: false, tag, state: 'unknown', ...(recovery ? { recovery } : {}), detail: `gh could not be run: ${err.message}` };
|
|
1546
1658
|
}
|
|
1547
1659
|
}
|
|
1548
1660
|
|