@celilo/cli 0.3.16 → 0.3.18
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/package.json +1 -1
- package/src/api-clients/proxmox.ts +77 -45
- package/src/cli/command-registry.ts +23 -35
- package/src/cli/commands/completion.ts +12 -11
- package/src/cli/commands/module-check.ts +158 -0
- package/src/cli/commands/module-import-routing.test.ts +52 -0
- package/src/cli/commands/module-import.ts +70 -27
- package/src/cli/commands/module-publish.test.ts +3 -90
- package/src/cli/commands/module-publish.ts +14 -118
- package/src/cli/commands/proxmox-template-selection.test.ts +150 -0
- package/src/cli/commands/proxmox-template-selection.ts +258 -0
- package/src/cli/commands/service-add-proxmox.ts +49 -127
- package/src/cli/commands/service-reconfigure.ts +36 -79
- package/src/cli/commands/service-verify.ts +20 -79
- package/src/cli/commands/{doctor.test.ts → system-doctor.test.ts} +1 -1
- package/src/cli/commands/{doctor.ts → system-doctor.ts} +93 -18
- package/src/cli/commands/system-update.ts +1 -1
- package/src/cli/completion.ts +29 -8
- package/src/cli/index.ts +25 -30
- package/src/manifest/schema.ts +9 -1
- package/src/module/import.ts +4 -2
- package/src/registry/client.ts +14 -1
- package/src/services/bus-interview.ts +13 -1
- package/src/services/bus-secret-flow.test.ts +94 -0
- package/src/services/config-interview.ts +66 -6
- package/src/services/module-deploy.ts +19 -1
- package/src/services/module-validator/capability-versions.test.ts +90 -0
- package/src/services/module-validator/capability-versions.ts +115 -0
- package/src/services/module-validator/contract-version.test.ts +24 -0
- package/src/services/module-validator/contract-version.ts +69 -0
- package/src/services/module-validator/git-hygiene.test.ts +141 -0
- package/src/services/module-validator/git-hygiene.ts +144 -0
- package/src/services/module-validator/index.test.ts +67 -0
- package/src/services/module-validator/index.ts +74 -0
- package/src/services/module-validator/manifest-schema.ts +42 -0
- package/src/services/module-validator/types.ts +43 -0
- package/src/services/module-validator/typescript-build.test.ts +58 -0
- package/src/services/module-validator/typescript-build.ts +115 -0
- package/src/services/module-validator/workspace-deps.test.ts +137 -0
- package/src/services/module-validator/workspace-deps.ts +187 -0
- package/src/services/terminal-responder.ts +75 -0
- package/src/system/prereqs.test.ts +374 -0
- package/src/system/prereqs.ts +377 -0
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module import command
|
|
3
|
+
*
|
|
4
|
+
* Single entry point for both registry and local-source imports. Routing
|
|
5
|
+
* is determined by the shape of the argument:
|
|
6
|
+
*
|
|
7
|
+
* celilo module import caddy → registry (kebab name)
|
|
8
|
+
* celilo module import ./modules/caddy → local dir (relative path)
|
|
9
|
+
* celilo module import /tmp/caddy.netapp → local file (absolute path)
|
|
10
|
+
* celilo module import ~/dev/caddy → local dir (tilde-expanded)
|
|
11
|
+
* celilo module import caddy.netapp → local file (.netapp extension)
|
|
12
|
+
*
|
|
13
|
+
* The previous CLI surface had three overlapping commands for this:
|
|
14
|
+
* `module install` (registry-only shorthand), `module import public-registry`
|
|
15
|
+
* (registry, verbose), and `module import file` (local, verbose). All have
|
|
16
|
+
* been collapsed into this one command — the routing rule below is the
|
|
17
|
+
* canonical disambiguator.
|
|
3
18
|
*/
|
|
4
19
|
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
5
21
|
import { unlink } from 'node:fs/promises';
|
|
6
22
|
import { tmpdir } from 'node:os';
|
|
7
23
|
import { join, resolve } from 'node:path';
|
|
@@ -14,47 +30,74 @@ import type { CommandResult } from '../types';
|
|
|
14
30
|
import { generateTypesForImportedModule } from './module-types';
|
|
15
31
|
|
|
16
32
|
const USAGE = `Usage:
|
|
17
|
-
celilo module import
|
|
18
|
-
celilo module import
|
|
33
|
+
celilo module import <name> Import from celilo.computer registry
|
|
34
|
+
celilo module import <path> Import from a local directory or .netapp file
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
celilo module import caddy
|
|
38
|
+
celilo module import ./modules/caddy
|
|
39
|
+
celilo module import /tmp/caddy.netapp`;
|
|
19
40
|
|
|
20
41
|
/**
|
|
21
|
-
*
|
|
42
|
+
* Decide whether the user's argument should route to local-filesystem
|
|
43
|
+
* import or registry import. Path-like arguments win — see header comment
|
|
44
|
+
* for the routing rules. Anything else is treated as a registry name
|
|
45
|
+
* (validated against the kebab-case rule before the network call).
|
|
22
46
|
*
|
|
23
|
-
*
|
|
24
|
-
* celilo module import public-registry <name>
|
|
25
|
-
* celilo module import <path> (alias for "file", kept for compatibility)
|
|
47
|
+
* Exposed for tests; not exported from the package.
|
|
26
48
|
*/
|
|
49
|
+
export function classifyImportArg(arg: string): 'path' | 'name' {
|
|
50
|
+
if (arg.startsWith('/') || arg.startsWith('./') || arg.startsWith('../')) return 'path';
|
|
51
|
+
if (arg.startsWith('~/') || arg === '~') return 'path';
|
|
52
|
+
if (arg.includes('/')) return 'path';
|
|
53
|
+
if (arg.endsWith('.netapp')) return 'path';
|
|
54
|
+
return 'name';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const KEBAB_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
58
|
+
|
|
27
59
|
export async function handleModuleImport(
|
|
28
60
|
args: string[],
|
|
29
61
|
flags: Record<string, string | boolean>,
|
|
30
62
|
): Promise<CommandResult> {
|
|
31
|
-
const
|
|
63
|
+
const arg = getArg(args, 0);
|
|
32
64
|
|
|
33
|
-
if (!
|
|
34
|
-
return { success: false, error: `
|
|
65
|
+
if (!arg) {
|
|
66
|
+
return { success: false, error: `Module name or path required.\n\n${USAGE}` };
|
|
35
67
|
}
|
|
36
68
|
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
69
|
+
if (classifyImportArg(arg) === 'name') {
|
|
70
|
+
if (!KEBAB_NAME.test(arg)) {
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
error: `Not a valid module name or path: '${arg}'.\n\n${USAGE}\n\nModule names are kebab-case (lowercase letters, digits, hyphens). For local paths, use ./, /, ~/, or include / in the argument.`,
|
|
74
|
+
};
|
|
41
75
|
}
|
|
42
|
-
return handlePublicRegistryImport(
|
|
76
|
+
return handlePublicRegistryImport(arg, flags);
|
|
43
77
|
}
|
|
44
78
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!sourcePath) {
|
|
49
|
-
return { success: false, error: `Path required.\n\n${USAGE}` };
|
|
50
|
-
}
|
|
79
|
+
return handleFileImport(arg, flags);
|
|
80
|
+
}
|
|
51
81
|
|
|
52
|
-
|
|
82
|
+
async function handleFileImport(
|
|
83
|
+
sourcePath: string,
|
|
84
|
+
flags: Record<string, string | boolean>,
|
|
85
|
+
): Promise<CommandResult> {
|
|
53
86
|
const resolvedSourcePath = resolve(sourcePath);
|
|
54
87
|
const targetBasePath = getFlag(flags, 'target', getModuleStoragePath());
|
|
55
88
|
const resolvedTargetBasePath = resolve(targetBasePath);
|
|
56
89
|
|
|
57
|
-
//
|
|
90
|
+
// Surface a more useful error than "module directory does not exist"
|
|
91
|
+
// when the user typed a registry name with a typo (e.g. `caddy/` or
|
|
92
|
+
// `./caddy`) but no such directory is present. Helps them realize
|
|
93
|
+
// they probably wanted the registry form.
|
|
94
|
+
if (!existsSync(resolvedSourcePath)) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: `Path does not exist: ${resolvedSourcePath}\n\nIf you meant the registry, use a bare module name (no leading ./ or /):\n celilo module import <name>`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
58
101
|
const db = getDb();
|
|
59
102
|
const result = await importModule({
|
|
60
103
|
sourcePath: resolvedSourcePath,
|
|
@@ -141,17 +184,17 @@ async function handlePublicRegistryImport(
|
|
|
141
184
|
return { success: false, error: result.error, details: result.details };
|
|
142
185
|
}
|
|
143
186
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
187
|
+
// Type-generation is a developer-mode aid for module AUTHORS editing
|
|
188
|
+
// hook scripts in the source tree. Registry installs are black-box
|
|
189
|
+
// dependencies — the operator isn't editing them — so skipping the
|
|
190
|
+
// type-gen pass saves the work and keeps the install output clean.
|
|
147
191
|
return {
|
|
148
192
|
success: true,
|
|
149
|
-
message: `Imported ${result.moduleId}@${latest.vers}\nFiles: ${shortenPath(result.targetPath)}
|
|
193
|
+
message: `Imported ${result.moduleId}@${latest.vers}\nFiles: ${shortenPath(result.targetPath)}`,
|
|
150
194
|
data: {
|
|
151
195
|
moduleId: result.moduleId,
|
|
152
196
|
version: latest.vers,
|
|
153
197
|
targetPath: result.targetPath,
|
|
154
|
-
typesPath: typesPath ?? undefined,
|
|
155
198
|
},
|
|
156
199
|
};
|
|
157
200
|
} finally {
|
|
@@ -15,7 +15,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
|
15
15
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import type { IndexEntry } from '../../registry/client';
|
|
18
|
-
import { handleModulePublish, resolveToken
|
|
18
|
+
import { handleModulePublish, resolveToken } from './module-publish';
|
|
19
19
|
|
|
20
20
|
const TEST_DIR = `/tmp/test-module-publish-${Date.now()}`;
|
|
21
21
|
|
|
@@ -155,95 +155,8 @@ describe('revision auto-detection algorithm', () => {
|
|
|
155
155
|
});
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
// Manifest-claimed versions for known capabilities must match the framework's
|
|
161
|
-
// runtime registry; mismatches refuse the publish.
|
|
162
|
-
|
|
163
|
-
describe('validateCapabilityVersions', () => {
|
|
164
|
-
test('no errors when no capabilities are declared', () => {
|
|
165
|
-
expect(validateCapabilityVersions({ id: 'x', version: '1.0.0' })).toEqual([]);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('no errors when provides matches the runtime registry exactly', () => {
|
|
169
|
-
expect(
|
|
170
|
-
validateCapabilityVersions({
|
|
171
|
-
id: 'caddy',
|
|
172
|
-
version: '3.0.0',
|
|
173
|
-
provides: { capabilities: [{ name: 'public_web', version: '3.0.0' }] },
|
|
174
|
-
}),
|
|
175
|
-
).toEqual([]);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test('error when provides[X].version differs from runtime', () => {
|
|
179
|
-
// The runtime is set to 3.0.0 for public_web; claiming 1.0.0 is a bug.
|
|
180
|
-
const errors = validateCapabilityVersions({
|
|
181
|
-
id: 'caddy',
|
|
182
|
-
version: '1.0.0',
|
|
183
|
-
provides: { capabilities: [{ name: 'public_web', version: '1.0.0' }] },
|
|
184
|
-
});
|
|
185
|
-
expect(errors).toHaveLength(1);
|
|
186
|
-
expect(errors[0]).toContain('provides[public_web]');
|
|
187
|
-
expect(errors[0]).toContain('1.0.0');
|
|
188
|
-
expect(errors[0]).toContain('3.0.0');
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('error when provides[X].version is newer than runtime', () => {
|
|
192
|
-
const errors = validateCapabilityVersions({
|
|
193
|
-
id: 'caddy',
|
|
194
|
-
version: '4.0.0',
|
|
195
|
-
provides: { capabilities: [{ name: 'public_web', version: '4.0.0' }] },
|
|
196
|
-
});
|
|
197
|
-
expect(errors).toHaveLength(1);
|
|
198
|
-
expect(errors[0]).toContain('provides[public_web]');
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test('skips capabilities not in the framework registry (third-party)', () => {
|
|
202
|
-
expect(
|
|
203
|
-
validateCapabilityVersions({
|
|
204
|
-
id: 'odd',
|
|
205
|
-
version: '1.0.0',
|
|
206
|
-
provides: { capabilities: [{ name: 'custom_metric', version: '99.0.0' }] },
|
|
207
|
-
}),
|
|
208
|
-
).toEqual([]);
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test('error when requires[X].version is a higher major than runtime', () => {
|
|
212
|
-
// Framework can't satisfy a requirement it doesn't know about.
|
|
213
|
-
const errors = validateCapabilityVersions({
|
|
214
|
-
id: 'lunacycle',
|
|
215
|
-
version: '1.0.0',
|
|
216
|
-
requires: { capabilities: [{ name: 'idp', version: '2.0.0' }] },
|
|
217
|
-
});
|
|
218
|
-
expect(errors).toHaveLength(1);
|
|
219
|
-
expect(errors[0]).toContain('requires[idp]');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('no error when requires[X].version is at most the runtime version', () => {
|
|
223
|
-
// dns_registrar runtime is 4.0.0; consumer requiring 4.0.0 is fine.
|
|
224
|
-
expect(
|
|
225
|
-
validateCapabilityVersions({
|
|
226
|
-
id: 'caddy',
|
|
227
|
-
version: '1.0.0',
|
|
228
|
-
requires: { capabilities: [{ name: 'dns_registrar', version: '4.0.0' }] },
|
|
229
|
-
}),
|
|
230
|
-
).toEqual([]);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test('reports multiple errors at once', () => {
|
|
234
|
-
const errors = validateCapabilityVersions({
|
|
235
|
-
id: 'broken',
|
|
236
|
-
version: '1.0.0',
|
|
237
|
-
provides: {
|
|
238
|
-
capabilities: [
|
|
239
|
-
{ name: 'public_web', version: '99.0.0' },
|
|
240
|
-
{ name: 'idp', version: '99.0.0' },
|
|
241
|
-
],
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
expect(errors).toHaveLength(2);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
158
|
+
// validateCapabilityVersions tests live with the function in
|
|
159
|
+
// services/module-validator/capability-versions.test.ts.
|
|
247
160
|
|
|
248
161
|
// ── Token resolution: --token → env → secret store ────────────────────────────
|
|
249
162
|
|
|
@@ -35,16 +35,10 @@
|
|
|
35
35
|
* timestamp / CLI version / optional --message.
|
|
36
36
|
*/
|
|
37
37
|
|
|
38
|
-
import { spawnSync } from 'node:child_process';
|
|
39
38
|
import { rm } from 'node:fs/promises';
|
|
40
39
|
import { readFile } from 'node:fs/promises';
|
|
41
40
|
import { tmpdir } from 'node:os';
|
|
42
41
|
import { join, resolve } from 'node:path';
|
|
43
|
-
import {
|
|
44
|
-
CAPABILITY_CONTRACT_VERSIONS,
|
|
45
|
-
type KnownCapabilityName,
|
|
46
|
-
compareProviderToRuntime,
|
|
47
|
-
} from '@celilo/capabilities';
|
|
48
42
|
import { parse as parseYaml } from 'yaml';
|
|
49
43
|
import { buildModule } from '../../module/packaging/build';
|
|
50
44
|
import {
|
|
@@ -55,122 +49,18 @@ import {
|
|
|
55
49
|
} from '../../module/packaging/release-metadata';
|
|
56
50
|
import { RegistryClient } from '../../registry/client';
|
|
57
51
|
import { CrossModuleDataManager } from '../../services/cross-module-data-manager';
|
|
52
|
+
import {
|
|
53
|
+
type ManifestForCapabilityCheck,
|
|
54
|
+
validateCapabilityVersions,
|
|
55
|
+
} from '../../services/module-validator/capability-versions';
|
|
56
|
+
import { checkModuleStale } from '../../services/module-validator/git-hygiene';
|
|
58
57
|
import { getFlag, hasFlag } from '../parser';
|
|
59
58
|
import type { CommandResult } from '../types';
|
|
60
59
|
|
|
61
|
-
interface
|
|
62
|
-
name: string;
|
|
63
|
-
version: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface ManifestForPublish {
|
|
60
|
+
interface ManifestForPublish extends ManifestForCapabilityCheck {
|
|
67
61
|
id: string;
|
|
68
62
|
version: string;
|
|
69
|
-
|
|
70
|
-
requires?: { capabilities?: ManifestCapabilityClaim[] };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function isKnownCapability(name: string): name is KnownCapabilityName {
|
|
74
|
-
return name in CAPABILITY_CONTRACT_VERSIONS;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Strict-publish: every `provides[X].version` must match the framework's
|
|
79
|
-
* runtime registry; every `requires[X].version` must be at most the
|
|
80
|
-
* framework's runtime version (consumers can require older minors of
|
|
81
|
-
* still-supported majors).
|
|
82
|
-
*
|
|
83
|
-
* Exported for testing.
|
|
84
|
-
*/
|
|
85
|
-
export function validateCapabilityVersions(manifest: ManifestForPublish): string[] {
|
|
86
|
-
const errors: string[] = [];
|
|
87
|
-
|
|
88
|
-
for (const p of manifest.provides?.capabilities ?? []) {
|
|
89
|
-
if (!isKnownCapability(p.name)) continue;
|
|
90
|
-
const runtime = CAPABILITY_CONTRACT_VERSIONS[p.name];
|
|
91
|
-
const r = compareProviderToRuntime(p.version, runtime);
|
|
92
|
-
if (!r.compatible) {
|
|
93
|
-
errors.push(
|
|
94
|
-
`provides[${p.name}].version is ${p.version} but framework registry is ${runtime} ` +
|
|
95
|
-
`(${r.reason}). Update the manifest, then retry.`,
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
for (const need of manifest.requires?.capabilities ?? []) {
|
|
101
|
-
if (!isKnownCapability(need.name)) continue;
|
|
102
|
-
const runtime = CAPABILITY_CONTRACT_VERSIONS[need.name];
|
|
103
|
-
// Treat the runtime as the de-facto provider for publish-time validation:
|
|
104
|
-
// if a consumer's manifest claims it requires a contract newer than the
|
|
105
|
-
// framework even has registered, no in-tree provider can satisfy it.
|
|
106
|
-
const r = compareProviderToRuntime(need.version, runtime);
|
|
107
|
-
if (!r.compatible && r.reason === 'major_mismatch_higher') {
|
|
108
|
-
errors.push(
|
|
109
|
-
`requires[${need.name}].version is ${need.version} but framework registry is ${runtime}. Bump the framework before publishing, or lower the manifest version.`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return errors;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Look up the SHA of the last commit touching the given pathspecs. Returns
|
|
119
|
-
* null on any git failure (no repo, never-committed file, etc.) — callers
|
|
120
|
-
* treat that as "can't determine, skip the check."
|
|
121
|
-
*/
|
|
122
|
-
function lastCommitTouching(pathspec: string[]): string | null {
|
|
123
|
-
const r = spawnSync('git', ['log', '-1', '--format=%H', '--', ...pathspec], {
|
|
124
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
125
|
-
encoding: 'utf-8',
|
|
126
|
-
});
|
|
127
|
-
if (r.status !== 0) return null;
|
|
128
|
-
const sha = r.stdout.trim();
|
|
129
|
-
return sha || null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function isAncestor(maybeAncestor: string, descendant: string): boolean {
|
|
133
|
-
const r = spawnSync('git', ['merge-base', '--is-ancestor', maybeAncestor, descendant], {
|
|
134
|
-
stdio: 'ignore',
|
|
135
|
-
});
|
|
136
|
-
return r.status === 0;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export interface StalenessIssue {
|
|
140
|
-
moduleDir: string;
|
|
141
|
-
lastSrcCommit: string;
|
|
142
|
-
lastManifestCommit: string;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Detect "I edited module src but forgot to bump (or touch) manifest.yml."
|
|
147
|
-
*
|
|
148
|
-
* Returns null when the manifest is the most recently-touched file in the
|
|
149
|
-
* dir (or when neither side has any commit history — e.g. brand-new module
|
|
150
|
-
* not yet committed). Returns a StalenessIssue when src has commits AFTER
|
|
151
|
-
* the last manifest.yml change — the operator must bump the manifest
|
|
152
|
-
* (semver change → reset +N to 1) or just touch it (release-only change →
|
|
153
|
-
* auto-bump +N), then re-publish.
|
|
154
|
-
*
|
|
155
|
-
* Exported for testing.
|
|
156
|
-
*/
|
|
157
|
-
export function checkModuleStale(moduleDir: string): StalenessIssue | null {
|
|
158
|
-
const manifestPath = join(moduleDir, 'manifest.yml');
|
|
159
|
-
const lastManifest = lastCommitTouching([manifestPath]);
|
|
160
|
-
// Excluding manifest.yml from the "src" pathspec is the whole point — we
|
|
161
|
-
// want to know if anything ELSE in the dir moved past it. node_modules and
|
|
162
|
-
// common build outputs are gitignored already, but be explicit defensively.
|
|
163
|
-
const lastSrc = lastCommitTouching([
|
|
164
|
-
moduleDir,
|
|
165
|
-
`:(exclude)${manifestPath}`,
|
|
166
|
-
`:(exclude)${moduleDir}/node_modules`,
|
|
167
|
-
`:(exclude)${moduleDir}/dist`,
|
|
168
|
-
]);
|
|
169
|
-
if (!lastManifest || !lastSrc) return null;
|
|
170
|
-
if (lastSrc === lastManifest) return null; // same commit changed both
|
|
171
|
-
if (!isAncestor(lastManifest, lastSrc)) return null; // manifest moved last
|
|
172
|
-
|
|
173
|
-
return { moduleDir, lastSrcCommit: lastSrc, lastManifestCommit: lastManifest };
|
|
63
|
+
description?: string;
|
|
174
64
|
}
|
|
175
65
|
|
|
176
66
|
/**
|
|
@@ -378,7 +268,13 @@ export async function publishOneModule(
|
|
|
378
268
|
const client = new RegistryClient(opts.registryUrl || undefined);
|
|
379
269
|
try {
|
|
380
270
|
console.log(`Publishing ${name}@${version} to ${client.baseUrl}...`);
|
|
381
|
-
await client.publish({
|
|
271
|
+
await client.publish({
|
|
272
|
+
name,
|
|
273
|
+
version,
|
|
274
|
+
netappPath: buildResult.packagePath,
|
|
275
|
+
token: opts.token,
|
|
276
|
+
description: manifest.description?.trim() || undefined,
|
|
277
|
+
});
|
|
382
278
|
} catch (err) {
|
|
383
279
|
return {
|
|
384
280
|
moduleDir,
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { ProxmoxAppliance } from '../../api-clients/proxmox';
|
|
3
|
+
import { buildUbuntuOptions, pickInitialTemplate } from './proxmox-template-selection';
|
|
4
|
+
|
|
5
|
+
// Snapshot of `pveam available --section system` filtered to Ubuntu, taken
|
|
6
|
+
// 2026-05-08. The point of these tests is to lock in the catalog-driven
|
|
7
|
+
// behavior that replaced the hardcoded `-1` revision URL.
|
|
8
|
+
const catalog: ProxmoxAppliance[] = [
|
|
9
|
+
{
|
|
10
|
+
template: 'ubuntu-22.04-standard_22.04-1_amd64.tar.zst',
|
|
11
|
+
package: 'ubuntu-22.04-standard',
|
|
12
|
+
version: '22.04-1',
|
|
13
|
+
section: 'system',
|
|
14
|
+
os: 'ubuntu',
|
|
15
|
+
type: 'lxc',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
template: 'ubuntu-24.04-standard_24.04-2_amd64.tar.zst',
|
|
19
|
+
package: 'ubuntu-24.04-standard',
|
|
20
|
+
version: '24.04-2',
|
|
21
|
+
section: 'system',
|
|
22
|
+
os: 'ubuntu',
|
|
23
|
+
type: 'lxc',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
template: 'ubuntu-24.10-standard_24.10-1_amd64.tar.zst',
|
|
27
|
+
package: 'ubuntu-24.10-standard',
|
|
28
|
+
version: '24.10-1',
|
|
29
|
+
section: 'system',
|
|
30
|
+
os: 'ubuntu',
|
|
31
|
+
type: 'lxc',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
template: 'ubuntu-25.04-standard_25.04-1.1_amd64.tar.zst',
|
|
35
|
+
package: 'ubuntu-25.04-standard',
|
|
36
|
+
version: '25.04-1.1',
|
|
37
|
+
section: 'system',
|
|
38
|
+
os: 'ubuntu',
|
|
39
|
+
type: 'lxc',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
template: 'ubuntu-26.04-standard_26.04-1_amd64.tar.zst',
|
|
43
|
+
package: 'ubuntu-26.04-standard',
|
|
44
|
+
version: '26.04-1',
|
|
45
|
+
section: 'system',
|
|
46
|
+
os: 'ubuntu',
|
|
47
|
+
type: 'lxc',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
describe('buildUbuntuOptions', () => {
|
|
52
|
+
test('returns Ubuntu options sorted newest-first', () => {
|
|
53
|
+
const options = buildUbuntuOptions(catalog);
|
|
54
|
+
expect(options.map((o) => o.appliance.package)).toEqual([
|
|
55
|
+
'ubuntu-26.04-standard',
|
|
56
|
+
'ubuntu-25.04-standard',
|
|
57
|
+
'ubuntu-24.10-standard',
|
|
58
|
+
'ubuntu-24.04-standard',
|
|
59
|
+
'ubuntu-22.04-standard',
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('flags LTS releases correctly', () => {
|
|
64
|
+
const options = buildUbuntuOptions(catalog);
|
|
65
|
+
const ltsByPackage = Object.fromEntries(options.map((o) => [o.appliance.package, o.isLts]));
|
|
66
|
+
expect(ltsByPackage).toEqual({
|
|
67
|
+
'ubuntu-22.04-standard': true,
|
|
68
|
+
'ubuntu-24.04-standard': true,
|
|
69
|
+
'ubuntu-24.10-standard': false,
|
|
70
|
+
'ubuntu-25.04-standard': false,
|
|
71
|
+
'ubuntu-26.04-standard': true,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('drops non-Ubuntu and non-system entries', () => {
|
|
76
|
+
const mixed: ProxmoxAppliance[] = [
|
|
77
|
+
...catalog,
|
|
78
|
+
{
|
|
79
|
+
template: 'debian-12-standard_12.7-1_amd64.tar.zst',
|
|
80
|
+
package: 'debian-12-standard',
|
|
81
|
+
version: '12.7-1',
|
|
82
|
+
section: 'system',
|
|
83
|
+
os: 'debian',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
template: 'ubuntu-24.04-special_24.04-1_amd64.tar.zst',
|
|
87
|
+
package: 'ubuntu-24.04-standard',
|
|
88
|
+
version: '24.04-1',
|
|
89
|
+
section: 'turnkeylinux',
|
|
90
|
+
os: 'ubuntu',
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
const options = buildUbuntuOptions(mixed);
|
|
94
|
+
expect(options).toHaveLength(catalog.length);
|
|
95
|
+
expect(options.every((o) => (o.appliance.section ?? 'system') === 'system')).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('returns empty array when nothing matches', () => {
|
|
99
|
+
expect(buildUbuntuOptions([])).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('pickInitialTemplate', () => {
|
|
104
|
+
const options = buildUbuntuOptions(catalog);
|
|
105
|
+
|
|
106
|
+
test('defaults to newest LTS when no current template given', () => {
|
|
107
|
+
expect(pickInitialTemplate(options)).toBe('ubuntu-26.04-standard_26.04-1_amd64.tar.zst');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('exact match wins when revision still on mirror', () => {
|
|
111
|
+
expect(pickInitialTemplate(options, 'ubuntu-22.04-standard_22.04-1_amd64.tar.zst')).toBe(
|
|
112
|
+
'ubuntu-22.04-standard_22.04-1_amd64.tar.zst',
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('falls back to family match when revision has been refreshed', () => {
|
|
117
|
+
// The case that motivated the refactor: user has stale -1 cached, mirror has -2.
|
|
118
|
+
expect(pickInitialTemplate(options, 'ubuntu-24.04-standard_24.04-1_amd64.tar.zst')).toBe(
|
|
119
|
+
'ubuntu-24.04-standard_24.04-2_amd64.tar.zst',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('falls through to newest LTS when current does not match any family', () => {
|
|
124
|
+
expect(pickInitialTemplate(options, 'ubuntu-18.04-standard_18.04-1_amd64.tar.zst')).toBe(
|
|
125
|
+
'ubuntu-26.04-standard_26.04-1_amd64.tar.zst',
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('returns undefined for empty options', () => {
|
|
130
|
+
expect(pickInitialTemplate([])).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('falls back to newest entry when no LTS in catalog', () => {
|
|
134
|
+
const noLts = buildUbuntuOptions([
|
|
135
|
+
{
|
|
136
|
+
template: 'ubuntu-25.04-standard_25.04-1.1_amd64.tar.zst',
|
|
137
|
+
package: 'ubuntu-25.04-standard',
|
|
138
|
+
version: '25.04-1.1',
|
|
139
|
+
section: 'system',
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
template: 'ubuntu-24.10-standard_24.10-1_amd64.tar.zst',
|
|
143
|
+
package: 'ubuntu-24.10-standard',
|
|
144
|
+
version: '24.10-1',
|
|
145
|
+
section: 'system',
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
expect(pickInitialTemplate(noLts)).toBe('ubuntu-25.04-standard_25.04-1.1_amd64.tar.zst');
|
|
149
|
+
});
|
|
150
|
+
});
|