@celilo/cli 0.3.30-alpha.0 → 0.4.0-alpha.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/drizzle/0005_module_operations.sql +12 -0
- package/drizzle/0006_base_module_aspects.sql +15 -0
- package/drizzle/0007_module_systems.sql +17 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +3 -3
- package/schemas/system_config.json +14 -28
- package/src/ansible/inventory.test.ts +46 -62
- package/src/ansible/inventory.ts +48 -25
- package/src/capabilities/registration.ts +25 -7
- package/src/capabilities/validation.test.ts +30 -0
- package/src/capabilities/validation.ts +8 -0
- package/src/cli/backup-rename.test.ts +95 -0
- package/src/cli/cli.test.ts +17 -23
- package/src/cli/command-registry.ts +199 -0
- package/src/cli/commands/backup-list.ts +1 -1
- package/src/cli/commands/events.ts +96 -0
- package/src/cli/commands/machine-add.ts +103 -59
- package/src/cli/commands/module-import.ts +153 -4
- package/src/cli/commands/module-remove.ts +86 -17
- package/src/cli/commands/module-status.ts +6 -2
- package/src/cli/commands/publish/alpha.test.ts +185 -0
- package/src/cli/commands/publish/alpha.ts +226 -0
- package/src/cli/commands/publish/changesets.test.ts +89 -0
- package/src/cli/commands/publish/changesets.ts +144 -0
- package/src/cli/commands/publish/consumer-pins.test.ts +155 -0
- package/src/cli/commands/publish/consumer-pins.ts +149 -0
- package/src/cli/commands/publish/execute.ts +131 -0
- package/src/cli/commands/publish/global-install.test.ts +154 -0
- package/src/cli/commands/publish/global-install.ts +171 -0
- package/src/cli/commands/publish/helpers.ts +227 -0
- package/src/cli/commands/publish/index.ts +365 -0
- package/src/cli/commands/publish/module-registry.test.ts +40 -0
- package/src/cli/commands/publish/module-registry.ts +64 -0
- package/src/cli/commands/publish/plan.ts +107 -0
- package/src/cli/commands/publish/preflight.ts +238 -0
- package/src/cli/commands/publish/types.ts +264 -0
- package/src/cli/commands/publish/workspace.test.ts +323 -0
- package/src/cli/commands/publish/workspace.ts +596 -0
- package/src/cli/commands/restore.ts +126 -0
- package/src/cli/commands/storage-add-local.ts +1 -1
- package/src/cli/commands/storage-add-s3.ts +1 -1
- package/src/cli/commands/subscribers-add.ts +68 -0
- package/src/cli/commands/subscribers-list.ts +48 -0
- package/src/cli/commands/subscribers-remove.ts +38 -0
- package/src/cli/commands/subscribers-serve.ts +77 -0
- package/src/cli/commands/subscribers-status.ts +33 -0
- package/src/cli/commands/subscribers-test.ts +71 -0
- package/src/cli/commands/system-apply-config-equivalence.test.ts +108 -0
- package/src/cli/commands/system-apply-config.test.ts +70 -0
- package/src/cli/commands/system-apply-config.ts +130 -0
- package/src/cli/commands/system-audit.ts +2 -1
- package/src/cli/commands/system-init-deprecation.test.ts +90 -0
- package/src/cli/commands/system-init.ts +36 -70
- package/src/cli/commands/system-update.ts +3 -2
- package/src/cli/completion.ts +22 -1
- package/src/cli/index.ts +214 -6
- package/src/cli/interactive-config.test.ts +19 -0
- package/src/cli/restore-command.test.ts +131 -0
- package/src/db/client.ts +42 -0
- package/src/db/schema.test.ts +13 -16
- package/src/db/schema.ts +161 -9
- package/src/hooks/capability-loader-firewall.test.ts +6 -15
- package/src/hooks/capability-loader.test.ts +2 -3
- package/src/hooks/capability-loader.ts +36 -2
- package/src/hooks/define-hook.test.ts +4 -0
- package/src/hooks/executor.test.ts +18 -0
- package/src/hooks/executor.ts +21 -2
- package/src/hooks/load-hook-config.test.ts +26 -24
- package/src/hooks/load-hook-config.ts +11 -2
- package/src/hooks/run-named-hook.ts +16 -0
- package/src/hooks/types.ts +9 -1
- package/src/manifest/contracts/v1.ts +70 -0
- package/src/manifest/schema.ts +262 -16
- package/src/manifest/validate-privileged.test.ts +84 -0
- package/src/manifest/validate.test.ts +156 -0
- package/src/manifest/validate.ts +69 -0
- package/src/module/import.ts +12 -0
- package/src/services/aspect-approvals.test.ts +231 -0
- package/src/services/aspect-approvals.ts +120 -0
- package/src/services/aspect-runner.test.ts +493 -0
- package/src/services/aspect-runner.ts +438 -0
- package/src/services/aspect-template-resolver.test.ts +101 -0
- package/src/services/aspect-template-resolver.ts +122 -0
- package/src/services/backup-create.ts +104 -25
- package/src/services/backup-envelope-roundtrip.test.ts +199 -0
- package/src/services/backup-in-flight-refusal.test.ts +163 -0
- package/src/services/backup-manifest.test.ts +115 -0
- package/src/services/backup-manifest.ts +163 -0
- package/src/services/backup-restore.ts +154 -19
- package/src/services/build-bus/delivery-events.ts +92 -0
- package/src/services/build-bus/event-factory.ts +54 -0
- package/src/services/build-bus/fan-out.test.ts +279 -0
- package/src/services/build-bus/fan-out.ts +161 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +157 -0
- package/src/services/build-bus/hook-dispatch.test.ts +207 -0
- package/src/services/build-bus/hook-dispatch.ts +198 -0
- package/src/services/build-bus/hook-dispatcher.ts +115 -0
- package/src/services/build-bus/index.ts +41 -0
- package/src/services/build-bus/receiver-server.test.ts +179 -0
- package/src/services/build-bus/receiver-server.ts +159 -0
- package/src/services/build-bus/status.test.ts +212 -0
- package/src/services/build-bus/status.ts +213 -0
- package/src/services/build-bus/subscriber-store.ts +113 -0
- package/src/services/celilo-events.test.ts +70 -0
- package/src/services/celilo-events.ts +92 -0
- package/src/services/celilo-mgmt-hooks.test.ts +296 -0
- package/src/services/config-interview.ts +13 -95
- package/src/services/cross-module-data-manager.ts +2 -31
- package/src/services/cross-module-read.test.ts +250 -0
- package/src/services/cross-module-read.ts +232 -0
- package/src/services/deploy-validation.ts +7 -0
- package/src/services/deployed-systems.test.ts +235 -0
- package/src/services/deployed-systems.ts +308 -0
- package/src/services/dns-provider-backfill.ts +75 -0
- package/src/services/health-runner.ts +19 -3
- package/src/services/infrastructure-variable-resolver.test.ts +6 -32
- package/src/services/infrastructure-variable-resolver.ts +3 -13
- package/src/services/machine-detector.ts +104 -48
- package/src/services/machine-pool.ts +145 -2
- package/src/services/module-config.ts +78 -120
- package/src/services/module-deploy.ts +113 -40
- package/src/services/module-operations.test.ts +154 -0
- package/src/services/module-operations.ts +154 -0
- package/src/services/module-subscriptions.test.ts +58 -0
- package/src/services/module-subscriptions.ts +24 -1
- package/src/services/module-types-generator.test.ts +3 -3
- package/src/services/module-types-generator.ts +7 -2
- package/src/services/proxmox-reconcile.test.ts +333 -0
- package/src/services/proxmox-reconcile.ts +156 -0
- package/src/services/proxmox-state-recovery.ts +3 -24
- package/src/services/restore-from-file.test.ts +177 -0
- package/src/services/restore-from-file.ts +355 -0
- package/src/services/restore-preflight.test.ts +127 -0
- package/src/services/restore-preflight.ts +118 -0
- package/src/services/storage-providers/s3.ts +10 -2
- package/src/services/system-identity.ts +30 -0
- package/src/services/system-init.test.ts +64 -21
- package/src/services/system-init.ts +28 -26
- package/src/templates/generator.test.ts +7 -16
- package/src/templates/generator.ts +28 -115
- package/src/test-utils/integration.ts +5 -2
- package/src/types/infrastructure.ts +8 -0
- package/src/variables/computed/computed-integration.test.ts +191 -0
- package/src/variables/computed/computed.test.ts +177 -0
- package/src/variables/computed/evaluate.ts +271 -0
- package/src/variables/computed/marker.ts +53 -0
- package/src/variables/computed/parse.ts +262 -0
- package/src/variables/computed/provider-lookup.ts +130 -0
- package/src/variables/context.test.ts +89 -28
- package/src/variables/context.ts +196 -191
- package/src/variables/parser.ts +3 -3
- package/src/variables/resolver.test.ts +61 -0
- package/src/variables/resolver.ts +81 -0
- package/src/variables/types.ts +23 -1
- package/src/services/dns-auto-register.ts +0 -211
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace publish — Phase 1 of `celilo publish`.
|
|
3
|
+
*
|
|
4
|
+
* Planner/executor split per v2/PUBLILO_CLI.md Phase 2:
|
|
5
|
+
*
|
|
6
|
+
* planWorkspace(opts) → WorkspaceItem[] // pure, no mutations
|
|
7
|
+
* executeWorkspace(plan, ...) // mutates package.json,
|
|
8
|
+
* // runs bun publish,
|
|
9
|
+
* // restores, verifies
|
|
10
|
+
*
|
|
11
|
+
* The planner figures out which versions each package would publish at
|
|
12
|
+
* (real, alpha-N, promoted-base), which workspace:^ deps need explicit
|
|
13
|
+
* rewriting, whether the package should be skipped (already on npm,
|
|
14
|
+
* source unchanged since prior alpha), and which pre-publish hooks
|
|
15
|
+
* fire. The executor applies the plan top-to-bottom — no decisions are
|
|
16
|
+
* made during execution, just side effects.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import {
|
|
21
|
+
cpSync,
|
|
22
|
+
existsSync,
|
|
23
|
+
mkdirSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
readdirSync,
|
|
26
|
+
rmSync,
|
|
27
|
+
statSync,
|
|
28
|
+
unlinkSync,
|
|
29
|
+
writeFileSync,
|
|
30
|
+
} from 'node:fs';
|
|
31
|
+
import { join, relative } from 'node:path';
|
|
32
|
+
import { ALPHA_TAG, alphaSkipDecision, nextAlphaNumber, stripAlphaSuffix } from './alpha';
|
|
33
|
+
import { REPO_ROOT, isPublished, readPkg } from './helpers';
|
|
34
|
+
import type {
|
|
35
|
+
PackageJson,
|
|
36
|
+
PublishMode,
|
|
37
|
+
PublishResult,
|
|
38
|
+
RewriteOptions,
|
|
39
|
+
WorkspaceItem,
|
|
40
|
+
WorkspaceRewrite,
|
|
41
|
+
WorkspaceVersionMap,
|
|
42
|
+
} from './types';
|
|
43
|
+
|
|
44
|
+
// ─── Planner ───────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export interface PlanWorkspaceInput {
|
|
47
|
+
mode: PublishMode;
|
|
48
|
+
packages: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* Starting workspace-version map (typically built from each
|
|
51
|
+
* package.json's `version` field). The planner mutates a local copy
|
|
52
|
+
* — in alpha mode it tightens the map to the alpha versions so
|
|
53
|
+
* sibling rewrites resolve correctly. Doesn't mutate the input.
|
|
54
|
+
*/
|
|
55
|
+
baseWorkspaceVersions: WorkspaceVersionMap;
|
|
56
|
+
gitHead: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface PlanWorkspaceOutput {
|
|
60
|
+
items: WorkspaceItem[];
|
|
61
|
+
/**
|
|
62
|
+
* The map of name → versionToPublish for non-skipped items. The
|
|
63
|
+
* executor uses this to rewrite workspace:^ deps consistently across
|
|
64
|
+
* sibling packages in the same publish run.
|
|
65
|
+
*/
|
|
66
|
+
workspaceVersions: WorkspaceVersionMap;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Pre-publish hooks that fire for @celilo/e2e (the only package that
|
|
71
|
+
* needs them today). Listed in the WorkspaceItem so dry-run can show
|
|
72
|
+
* "@celilo/e2e (will rebuild netapps, stage caches)" without executing
|
|
73
|
+
* anything.
|
|
74
|
+
*/
|
|
75
|
+
function workspaceHooksFor(pkg: string): WorkspaceItem['hooks'] {
|
|
76
|
+
if (pkg !== 'packages/e2e') return [];
|
|
77
|
+
return ['registryServerBundle', 'rebuildE2eNetapps', 'stageE2ePublishCaches'];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build the workspace publish plan. Pure given the world-state reads
|
|
82
|
+
* exposed by helpers (npm view, git log) — same world → same plan.
|
|
83
|
+
*
|
|
84
|
+
* In promote mode, only the named package is planned; everything else
|
|
85
|
+
* is filtered out. In alpha mode, the local workspaceVersions map gets
|
|
86
|
+
* tightened to alpha versions as we iterate, so later packages in
|
|
87
|
+
* dependency order can pin to the alpha version of earlier siblings.
|
|
88
|
+
*/
|
|
89
|
+
export function planWorkspace(opts: PlanWorkspaceInput): PlanWorkspaceOutput {
|
|
90
|
+
const { mode, packages, baseWorkspaceVersions } = opts;
|
|
91
|
+
const workspaceVersions = new Map(baseWorkspaceVersions);
|
|
92
|
+
|
|
93
|
+
const packagesToPlan: readonly string[] =
|
|
94
|
+
mode.kind === 'promote'
|
|
95
|
+
? packages.filter((p) => readPkg(p).name === mode.target.name)
|
|
96
|
+
: packages;
|
|
97
|
+
|
|
98
|
+
if (mode.kind === 'promote' && packagesToPlan.length === 0) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`--promote target "${mode.target.name}" is not a known workspace package. Known packages: ${[
|
|
101
|
+
...workspaceVersions.keys(),
|
|
102
|
+
].join(', ')}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const items: WorkspaceItem[] = [];
|
|
107
|
+
for (const pkg of packagesToPlan) {
|
|
108
|
+
const { name, version } = readPkg(pkg);
|
|
109
|
+
if (!name || !version) {
|
|
110
|
+
throw new Error(`${pkg}/package.json missing name or version`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let versionToPublish: string;
|
|
114
|
+
let skipReason: string | undefined;
|
|
115
|
+
let tag: WorkspaceItem['tag'];
|
|
116
|
+
|
|
117
|
+
if (mode.kind === 'promote') {
|
|
118
|
+
versionToPublish = stripAlphaSuffix(mode.target.version);
|
|
119
|
+
if (versionToPublish === mode.target.version) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`--promote target "${mode.target.name}@${mode.target.version}" is not an alpha (no -alpha.N suffix).`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} else if (mode.kind === 'alpha') {
|
|
125
|
+
const n = nextAlphaNumber(name, version);
|
|
126
|
+
const decision = alphaSkipDecision(pkg, name, version, n);
|
|
127
|
+
if (decision.skip) {
|
|
128
|
+
// Skipped (unchanged since its last alpha): it will NOT be
|
|
129
|
+
// republished, so consumers must pin to the alpha that already
|
|
130
|
+
// exists on npm (alpha.{n-1}), not the computed-next alpha.{n}
|
|
131
|
+
// which would never be published. (n >= 1 whenever skip is true —
|
|
132
|
+
// nextN === 0 can't skip.) Pinning to alpha.{n} made dependents
|
|
133
|
+
// unpublishable: "@celilo/cli-display@0.1.9-alpha.1 is not on npm".
|
|
134
|
+
skipReason = decision.reason;
|
|
135
|
+
versionToPublish = `${version}-alpha.${n - 1}`;
|
|
136
|
+
} else {
|
|
137
|
+
versionToPublish = `${version}-alpha.${n}`;
|
|
138
|
+
}
|
|
139
|
+
tag = ALPHA_TAG;
|
|
140
|
+
workspaceVersions.set(name, versionToPublish);
|
|
141
|
+
} else {
|
|
142
|
+
versionToPublish = version;
|
|
143
|
+
if (isPublished(name, versionToPublish)) {
|
|
144
|
+
skipReason = 'already published';
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
items.push({
|
|
149
|
+
pkg,
|
|
150
|
+
name,
|
|
151
|
+
baseVersion: version,
|
|
152
|
+
versionToPublish,
|
|
153
|
+
tag,
|
|
154
|
+
rewriteOptions: {
|
|
155
|
+
targetVersion: versionToPublish !== version ? versionToPublish : undefined,
|
|
156
|
+
exactPins: mode.kind === 'alpha',
|
|
157
|
+
gitHead: mode.kind === 'alpha' ? opts.gitHead : undefined,
|
|
158
|
+
},
|
|
159
|
+
hooks: workspaceHooksFor(pkg),
|
|
160
|
+
skipReason,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { items, workspaceVersions };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─── Write-side helpers (used by executor) ─────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Bun's publish-time rewrite of `workspace:^` is unreliable — empirically,
|
|
171
|
+
* it's been baking in `^0.1.0` regardless of the actual current version
|
|
172
|
+
* of the dep. We rewrite explicitly here:
|
|
173
|
+
* workspace:^ → ^<current-workspace-version> (or exact, in alpha mode)
|
|
174
|
+
* workspace:~ → ~<current-workspace-version> (or exact, in alpha mode)
|
|
175
|
+
* workspace:* → * (passes through; means "any version")
|
|
176
|
+
* workspace:X.Y.Z → X.Y.Z (explicit; passes through)
|
|
177
|
+
*
|
|
178
|
+
* Returns the original package.json content so the caller can restore
|
|
179
|
+
* after `bun publish` runs. We don't want to commit the rewritten form
|
|
180
|
+
* — `workspace:^` in source is more readable for dev.
|
|
181
|
+
*/
|
|
182
|
+
export function rewriteWorkspaceDeps(
|
|
183
|
+
pkg: string,
|
|
184
|
+
versions: WorkspaceVersionMap,
|
|
185
|
+
opts: RewriteOptions = {},
|
|
186
|
+
/**
|
|
187
|
+
* Optional override for the repo root. Production code uses the
|
|
188
|
+
* module-resolved REPO_ROOT (the real monorepo); tests can hand in
|
|
189
|
+
* a synthetic workspace path so the rewrite hits temp-dir files.
|
|
190
|
+
*/
|
|
191
|
+
repoRoot: string = REPO_ROOT,
|
|
192
|
+
): { original: string; rewrites: WorkspaceRewrite[] } {
|
|
193
|
+
const path = join(repoRoot, pkg, 'package.json');
|
|
194
|
+
const original = readFileSync(path, 'utf-8');
|
|
195
|
+
const parsed = JSON.parse(original) as PackageJson & { gitHead?: string };
|
|
196
|
+
const rewrites: WorkspaceRewrite[] = [];
|
|
197
|
+
|
|
198
|
+
let mutated = false;
|
|
199
|
+
if (opts.targetVersion !== undefined && opts.targetVersion !== parsed.version) {
|
|
200
|
+
parsed.version = opts.targetVersion;
|
|
201
|
+
mutated = true;
|
|
202
|
+
}
|
|
203
|
+
if (opts.gitHead !== undefined && opts.gitHead !== parsed.gitHead) {
|
|
204
|
+
parsed.gitHead = opts.gitHead;
|
|
205
|
+
mutated = true;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const buckets: Array<keyof PackageJson> = [
|
|
209
|
+
'dependencies',
|
|
210
|
+
'devDependencies',
|
|
211
|
+
'peerDependencies',
|
|
212
|
+
'optionalDependencies',
|
|
213
|
+
];
|
|
214
|
+
for (const bucket of buckets) {
|
|
215
|
+
const deps = parsed[bucket] as Record<string, string> | undefined;
|
|
216
|
+
if (!deps) continue;
|
|
217
|
+
for (const [name, oldSpec] of Object.entries(deps)) {
|
|
218
|
+
if (!oldSpec.startsWith('workspace:')) continue;
|
|
219
|
+
const wsRange = oldSpec.slice('workspace:'.length);
|
|
220
|
+
const targetVersion = versions.get(name);
|
|
221
|
+
if (!targetVersion) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
let newSpec: string;
|
|
225
|
+
if (wsRange === '*' || wsRange === '') {
|
|
226
|
+
newSpec = '*';
|
|
227
|
+
} else if (opts.exactPins) {
|
|
228
|
+
newSpec = targetVersion;
|
|
229
|
+
} else if (wsRange === '^') {
|
|
230
|
+
newSpec = `^${targetVersion}`;
|
|
231
|
+
} else if (wsRange === '~') {
|
|
232
|
+
newSpec = `~${targetVersion}`;
|
|
233
|
+
} else {
|
|
234
|
+
newSpec = wsRange;
|
|
235
|
+
}
|
|
236
|
+
deps[name] = newSpec;
|
|
237
|
+
rewrites.push({ depName: name, bucket: String(bucket), oldSpec, newSpec });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (rewrites.length > 0 || mutated) {
|
|
241
|
+
const trailingNl = original.endsWith('\n') ? '\n' : '';
|
|
242
|
+
writeFileSync(path, JSON.stringify(parsed, null, 2) + trailingNl);
|
|
243
|
+
}
|
|
244
|
+
return { original, rewrites };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function restorePackageJson(
|
|
248
|
+
pkg: string,
|
|
249
|
+
original: string,
|
|
250
|
+
repoRoot: string = REPO_ROOT,
|
|
251
|
+
): void {
|
|
252
|
+
writeFileSync(join(repoRoot, pkg, 'package.json'), original);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* After `bun publish` succeeds, ask npm what dep versions the freshly-
|
|
257
|
+
* published package.json contains. If any rewrite we made didn't make
|
|
258
|
+
* it through to the published artifact, fail loudly — don't silently
|
|
259
|
+
* leave a broken pin on npm.
|
|
260
|
+
*/
|
|
261
|
+
export async function verifyPublishedDeps(
|
|
262
|
+
name: string,
|
|
263
|
+
version: string,
|
|
264
|
+
rewrites: WorkspaceRewrite[],
|
|
265
|
+
/**
|
|
266
|
+
* Optional override pointing `npm view` at a different registry.
|
|
267
|
+
* Production omits this (uses the operator's configured npm/bun
|
|
268
|
+
* registry); tests pass their verdaccio URL.
|
|
269
|
+
*/
|
|
270
|
+
registryUrl?: string,
|
|
271
|
+
): Promise<void> {
|
|
272
|
+
// npm's registry replication is eventually-consistent. A package
|
|
273
|
+
// published 1s ago can still return empty for `npm view ... dep.X`
|
|
274
|
+
// even though the publish succeeded. Retry with backoff before
|
|
275
|
+
// declaring failure — only fail if the pin is genuinely WRONG, not
|
|
276
|
+
// just temporarily missing.
|
|
277
|
+
for (const r of rewrites) {
|
|
278
|
+
let actualPin = '';
|
|
279
|
+
let attempt = 0;
|
|
280
|
+
const maxAttempts = 6;
|
|
281
|
+
const delays = [500, 1000, 2000, 3000, 5000, 8000];
|
|
282
|
+
const baseArgs = ['view', `${name}@${version}`, `${r.bucket}.${r.depName}`];
|
|
283
|
+
const args = registryUrl ? [...baseArgs, '--registry', registryUrl] : baseArgs;
|
|
284
|
+
while (attempt < maxAttempts) {
|
|
285
|
+
const result = spawnSync('npm', args, {
|
|
286
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
287
|
+
encoding: 'utf-8',
|
|
288
|
+
});
|
|
289
|
+
actualPin = (result.stdout ?? '').trim();
|
|
290
|
+
if (actualPin) break;
|
|
291
|
+
attempt++;
|
|
292
|
+
if (attempt < maxAttempts) {
|
|
293
|
+
const delay = delays[attempt - 1];
|
|
294
|
+
process.stdout.write(
|
|
295
|
+
` (verify retry ${attempt} for ${r.depName} — npm view returned empty, waiting ${delay}ms…)\n`,
|
|
296
|
+
);
|
|
297
|
+
await new Promise((r2) => setTimeout(r2, delay));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (!actualPin) {
|
|
301
|
+
console.warn(
|
|
302
|
+
`⚠ verify could not read ${name}@${version} ${r.bucket}.${r.depName} from npm after ${maxAttempts} retries. Skipping verify for this dep.`,
|
|
303
|
+
);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (actualPin !== r.newSpec) {
|
|
307
|
+
console.error(
|
|
308
|
+
`✗ ${name}@${version} published with ${r.depName} pinned at "${actualPin}", expected "${r.newSpec}"`,
|
|
309
|
+
);
|
|
310
|
+
console.error(
|
|
311
|
+
' The pre-publish workspace:^ rewrite either failed silently or\n' +
|
|
312
|
+
' bun stripped our edit. Inspect the published artifact and the\n' +
|
|
313
|
+
' source package.json. The package is on npm with the wrong pin —\n' +
|
|
314
|
+
' consumers will get a stale dep until you republish.',
|
|
315
|
+
);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Per-package pre-publish hooks ─────────────────────────────────
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Modules excluded from the @celilo/e2e netapps shipment:
|
|
325
|
+
* - celilo-registry: bundles the bun-compiled registry server
|
|
326
|
+
* binaries (~76 MB packed). Not used by typical consumer e2e
|
|
327
|
+
* tests; including it would bloat the npm tarball past the SSL
|
|
328
|
+
* transport's reliable window.
|
|
329
|
+
* - archive: not a real module dir.
|
|
330
|
+
*/
|
|
331
|
+
const E2E_NETAPP_EXCLUDES = new Set(['archive', 'celilo-registry']);
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Pre-publish step for @celilo/e2e: package each (non-excluded)
|
|
335
|
+
* module under `<root>/modules/` into `packages/e2e/netapps/`. Replaces
|
|
336
|
+
* whatever was last left there by a local `cele2e build-infra` so the
|
|
337
|
+
* shipped tarball is always built from the current branch, not the
|
|
338
|
+
* publisher's last local development build.
|
|
339
|
+
*/
|
|
340
|
+
export function rebuildE2eNetapps(repoRoot: string): void {
|
|
341
|
+
const modulesDir = join(repoRoot, 'modules');
|
|
342
|
+
const netappsDir = join(repoRoot, 'packages/e2e/netapps');
|
|
343
|
+
const celiloWrapper = join(repoRoot, 'celilo');
|
|
344
|
+
|
|
345
|
+
if (!existsSync(modulesDir)) {
|
|
346
|
+
console.warn(
|
|
347
|
+
`⚠ ${modulesDir} not found — skipping netapp rebuild. The published tarball will reuse whatever's currently in packages/e2e/netapps/.`,
|
|
348
|
+
);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!existsSync(celiloWrapper)) {
|
|
352
|
+
console.warn(
|
|
353
|
+
`⚠ ${celiloWrapper} not found — skipping netapp rebuild. Same staleness risk as above.`,
|
|
354
|
+
);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
console.log('Rebuilding packages/e2e/netapps/ from infra/modules/...');
|
|
359
|
+
|
|
360
|
+
for (const existing of readdirSync(netappsDir).filter((f) => f.endsWith('.netapp'))) {
|
|
361
|
+
try {
|
|
362
|
+
unlinkSync(join(netappsDir, existing));
|
|
363
|
+
} catch {
|
|
364
|
+
// Best effort
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const moduleDirs = readdirSync(modulesDir).filter((name) => {
|
|
369
|
+
if (E2E_NETAPP_EXCLUDES.has(name)) return false;
|
|
370
|
+
const dir = join(modulesDir, name);
|
|
371
|
+
try {
|
|
372
|
+
return statSync(dir).isDirectory() && existsSync(join(dir, 'manifest.yml'));
|
|
373
|
+
} catch {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
let okCount = 0;
|
|
379
|
+
const failures: Array<{ name: string; stderr: string }> = [];
|
|
380
|
+
for (const name of moduleDirs) {
|
|
381
|
+
const moduleDir = join(modulesDir, name);
|
|
382
|
+
const out = join(netappsDir, `${name}.netapp`);
|
|
383
|
+
const r = spawnSync(celiloWrapper, ['package', moduleDir, '--output', out], {
|
|
384
|
+
cwd: repoRoot,
|
|
385
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
386
|
+
encoding: 'utf-8',
|
|
387
|
+
});
|
|
388
|
+
if (r.status === 0) {
|
|
389
|
+
okCount++;
|
|
390
|
+
} else {
|
|
391
|
+
failures.push({ name, stderr: (r.stderr ?? '').trim() || `exit ${r.status}` });
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (failures.length > 0) {
|
|
396
|
+
console.error(`✗ Failed to package ${failures.length} module(s):`);
|
|
397
|
+
for (const f of failures) {
|
|
398
|
+
console.error(` ${f.name}: ${f.stderr}`);
|
|
399
|
+
}
|
|
400
|
+
console.error(
|
|
401
|
+
'Aborting publish — shipping with missing netapps would silently break consumer e2e tests.',
|
|
402
|
+
);
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
console.log(
|
|
407
|
+
`Refreshed ${okCount} netapp(s) in packages/e2e/netapps/ (excluded: ${[...E2E_NETAPP_EXCLUDES].join(', ')}).\n`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Pre-publish step for @celilo/e2e: stage `.celilo-website-cache/` and
|
|
413
|
+
* `.npm-registry-cache/` inside packages/e2e/ so the tarball ships with
|
|
414
|
+
* everything Dockerfile.celilo-website-sim and Dockerfile.npm-registry-sim
|
|
415
|
+
* need to COPY at docker-build time.
|
|
416
|
+
*/
|
|
417
|
+
export function stageE2ePublishCaches(repoRoot: string): void {
|
|
418
|
+
const websiteSrc = join(repoRoot, 'modules', 'celilo-website', 'site');
|
|
419
|
+
const e2eDir = join(repoRoot, 'packages', 'e2e');
|
|
420
|
+
const websiteCache = join(e2eDir, '.celilo-website-cache');
|
|
421
|
+
const npmCache = join(e2eDir, '.npm-registry-cache');
|
|
422
|
+
const packScript = join(e2eDir, 'scripts', 'pack-celilo-packages.ts');
|
|
423
|
+
|
|
424
|
+
if (!existsSync(websiteSrc)) {
|
|
425
|
+
console.error(`✗ ${websiteSrc} not found — cannot stage .celilo-website-cache.`);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
if (!existsSync(packScript)) {
|
|
429
|
+
console.error(`✗ ${packScript} not found — cannot stage .npm-registry-cache.`);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.log('Staging .celilo-website-cache/ from modules/celilo-website/site/...');
|
|
434
|
+
const installResult = spawnSync('bun', ['install'], { cwd: websiteSrc, stdio: 'pipe' });
|
|
435
|
+
if (installResult.status !== 0) {
|
|
436
|
+
console.error('✗ bun install for celilo-website failed:');
|
|
437
|
+
console.error(installResult.stderr?.toString());
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
const buildResult = spawnSync('bun', ['run', 'build'], { cwd: websiteSrc, stdio: 'pipe' });
|
|
441
|
+
if (buildResult.status !== 0) {
|
|
442
|
+
console.error('✗ bun run build for celilo-website failed:');
|
|
443
|
+
console.error(buildResult.stderr?.toString());
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
rmSync(websiteCache, { recursive: true, force: true });
|
|
447
|
+
mkdirSync(websiteCache, { recursive: true });
|
|
448
|
+
cpSync(join(websiteSrc, 'dist'), websiteCache, { recursive: true });
|
|
449
|
+
console.log(`✓ Staged .celilo-website-cache/ (from ${relative(repoRoot, websiteSrc)}/dist/)\n`);
|
|
450
|
+
|
|
451
|
+
console.log('Staging .npm-registry-cache/ from @celilo/* workspace tarballs...');
|
|
452
|
+
const packResult = spawnSync('bun', ['run', packScript], { cwd: repoRoot, stdio: 'pipe' });
|
|
453
|
+
if (packResult.status !== 0) {
|
|
454
|
+
console.error('✗ pack-celilo-packages.ts failed:');
|
|
455
|
+
console.error(packResult.stderr?.toString());
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
const packed = existsSync(npmCache)
|
|
459
|
+
? readdirSync(npmCache).filter((f) => f.endsWith('.tgz'))
|
|
460
|
+
: [];
|
|
461
|
+
console.log(`✓ Staged .npm-registry-cache/ with ${packed.length} workspace tarball(s)\n`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ─── Executor ──────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
export interface ExecuteWorkspaceInput {
|
|
467
|
+
items: WorkspaceItem[];
|
|
468
|
+
workspaceVersions: WorkspaceVersionMap;
|
|
469
|
+
mode: PublishMode;
|
|
470
|
+
/** From the confirm() helper in index.ts — passed in to avoid coupling. */
|
|
471
|
+
confirm: (question: string) => Promise<boolean>;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export async function executeWorkspace(input: ExecuteWorkspaceInput): Promise<PublishResult> {
|
|
475
|
+
const { items, workspaceVersions, mode, confirm } = input;
|
|
476
|
+
const published: PublishResult['published'] = [];
|
|
477
|
+
const skipped: string[] = [];
|
|
478
|
+
|
|
479
|
+
for (const item of items) {
|
|
480
|
+
const { pkg, name, baseVersion, versionToPublish } = item;
|
|
481
|
+
|
|
482
|
+
if (item.skipReason) {
|
|
483
|
+
console.log(`\n→ ${name}@${versionToPublish} skipped (${item.skipReason}).`);
|
|
484
|
+
skipped.push(`${name}@${versionToPublish} (${item.skipReason})`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
console.log('\n──────────────────────────────────────────────');
|
|
489
|
+
const tag = mode.kind === 'alpha' ? ' [alpha]' : mode.kind === 'promote' ? ' [promote]' : '';
|
|
490
|
+
console.log(` ${name}@${versionToPublish} (${pkg})${tag}`);
|
|
491
|
+
console.log('──────────────────────────────────────────────\n');
|
|
492
|
+
|
|
493
|
+
// Per-package pre-publish hooks. Driven off the WorkspaceItem so the
|
|
494
|
+
// dry-run plan listed them too.
|
|
495
|
+
if (item.hooks.includes('registryServerBundle')) {
|
|
496
|
+
const { ensureRegistryServerBundle } = await import(
|
|
497
|
+
join(REPO_ROOT, 'packages/e2e/src/registry-bundle.ts')
|
|
498
|
+
);
|
|
499
|
+
ensureRegistryServerBundle(join(REPO_ROOT, 'packages/e2e'));
|
|
500
|
+
console.log(
|
|
501
|
+
'Refreshed packages/e2e/registry-server/ bundle from packages/registry-server.\n',
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
if (item.hooks.includes('rebuildE2eNetapps')) {
|
|
505
|
+
rebuildE2eNetapps(REPO_ROOT);
|
|
506
|
+
}
|
|
507
|
+
if (item.hooks.includes('stageE2ePublishCaches')) {
|
|
508
|
+
stageE2ePublishCaches(REPO_ROOT);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const { original: pkgJsonOriginal, rewrites: workspaceRewrites } = rewriteWorkspaceDeps(
|
|
512
|
+
pkg,
|
|
513
|
+
workspaceVersions,
|
|
514
|
+
item.rewriteOptions,
|
|
515
|
+
);
|
|
516
|
+
if (workspaceRewrites.length > 0) {
|
|
517
|
+
console.log('Rewrote workspace deps to explicit versions:');
|
|
518
|
+
for (const r of workspaceRewrites) {
|
|
519
|
+
console.log(` ${r.bucket}.${r.depName}: ${r.oldSpec} → ${r.newSpec}`);
|
|
520
|
+
}
|
|
521
|
+
console.log();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Belt-and-suspenders: refuse to publish if any @celilo/* dep
|
|
525
|
+
// (after workspace-rewrite) doesn't exist on npm. We publish in dep
|
|
526
|
+
// order, so this should be a no-op — but it catches operator typos
|
|
527
|
+
// and any external `bun publish` invocation that bypasses the
|
|
528
|
+
// ordered loop.
|
|
529
|
+
const checkResult = spawnSync('bun', [join(REPO_ROOT, 'scripts/check-publishable.ts'), pkg], {
|
|
530
|
+
cwd: REPO_ROOT,
|
|
531
|
+
stdio: 'inherit',
|
|
532
|
+
});
|
|
533
|
+
if (checkResult.status !== 0) {
|
|
534
|
+
restorePackageJson(pkg, pkgJsonOriginal);
|
|
535
|
+
console.error(
|
|
536
|
+
`✗ Refusing to publish ${name}@${versionToPublish} — see check-publishable output above.`,
|
|
537
|
+
);
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const publishArgs = ['publish', '--access', 'public'];
|
|
542
|
+
if (item.tag) publishArgs.push('--tag', item.tag);
|
|
543
|
+
|
|
544
|
+
let publishStatus: number | null = null;
|
|
545
|
+
let publishError: unknown = null;
|
|
546
|
+
try {
|
|
547
|
+
spawnSync('bun', [...publishArgs, '--dry-run'], {
|
|
548
|
+
cwd: join(REPO_ROOT, pkg),
|
|
549
|
+
stdio: 'inherit',
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
if (!(await confirm(`\nPublish ${name}@${versionToPublish} for real? [y/N] `))) {
|
|
553
|
+
console.log('Skipped.');
|
|
554
|
+
skipped.push(`${name}@${versionToPublish} (manual skip)`);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const r = spawnSync('bun', publishArgs, { cwd: join(REPO_ROOT, pkg), stdio: 'inherit' });
|
|
559
|
+
publishStatus = r.status;
|
|
560
|
+
} catch (err) {
|
|
561
|
+
publishError = err;
|
|
562
|
+
} finally {
|
|
563
|
+
// ALWAYS restore the source package.json so workspace:^ stays in
|
|
564
|
+
// git. Even on publish failure or operator skip.
|
|
565
|
+
restorePackageJson(pkg, pkgJsonOriginal);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (publishError) throw publishError;
|
|
569
|
+
|
|
570
|
+
if (publishStatus !== 0) {
|
|
571
|
+
// npm view can be transiently stale — recheck once before failing.
|
|
572
|
+
if (isPublished(name, versionToPublish)) {
|
|
573
|
+
console.log(
|
|
574
|
+
`\n→ ${name}@${versionToPublish} reports already-published on recheck (initial isPublished hit a stale npm view). Skipping.`,
|
|
575
|
+
);
|
|
576
|
+
skipped.push(`${name}@${versionToPublish} (already published — detected on retry)`);
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
console.error(`✗ Publish failed for ${name}@${versionToPublish}`);
|
|
580
|
+
process.exit(publishStatus ?? 1);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (workspaceRewrites.length > 0) {
|
|
584
|
+
await verifyPublishedDeps(name, versionToPublish, workspaceRewrites);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Silence the unused-var warning on baseVersion — we keep the value
|
|
588
|
+
// on the WorkspaceItem for dry-run output ("0.7.13 → 0.7.13-alpha.0").
|
|
589
|
+
void baseVersion;
|
|
590
|
+
|
|
591
|
+
console.log(`✓ Published ${name}@${versionToPublish}`);
|
|
592
|
+
published.push({ name, version: versionToPublish });
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return { published, skipped };
|
|
596
|
+
}
|