@celilo/cli 5.2.0 → 5.2.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/cli",
3
- "version": "5.2.0",
3
+ "version": "5.2.1",
4
4
  "description": "Celilo — home lab orchestration CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -345,12 +345,6 @@ export const CAPABILITY_NAME_BASELINE: readonly CapabilityNameRow[] = [
345
345
  count: 1,
346
346
  why: 'S13 — ZONE_REQUIREMENTS, a second hand-maintained copy of well-known.ts; Phase 2 (#937)',
347
347
  },
348
- {
349
- file: 'apps/celilo/src/templates/generator.ts',
350
- capability: 'dns_internal',
351
- count: 1,
352
- why: 'X4 — declaration-driven and the model for the rest; only the error string names the capability (#945)',
353
- },
354
348
  {
355
349
  file: 'apps/celilo/src/variables/context.ts',
356
350
  capability: 'dns_internal',
@@ -21,9 +21,9 @@ import { tmpdir } from 'node:os';
21
21
  import { join } from 'node:path';
22
22
  import { copyAnsibleRoleFilesDirs } from './generator';
23
23
 
24
- function moduleWithRoleFile(binary: string): string {
24
+ function moduleWithRoleFile(binary: string, layout = 'ansible'): string {
25
25
  const root = mkdtempSync(join(tmpdir(), 'celilo-rolefiles-'));
26
- const filesDir = join(root, 'ansible', 'roles', 'demo', 'files');
26
+ const filesDir = join(root, layout, 'roles', 'demo', 'files');
27
27
  mkdirSync(filesDir, { recursive: true });
28
28
  writeFileSync(join(filesDir, 'demo-linux-x86_64'), binary);
29
29
  return root;
@@ -59,6 +59,24 @@ describe('copyAnsibleRoleFilesDirs', () => {
59
59
  expect(generatedBinary(outputPath)).toBe('v2');
60
60
  });
61
61
 
62
+ /**
63
+ * celilo#1461. The template pipeline accepts BOTH `ansible/` and
64
+ * `celilo/ansible/`; this copy accepted one, so a celilo/-layout module got
65
+ * its role's tasks/ and templates/ and never its files/. `existsSync` was
66
+ * false, the function returned, and nothing anywhere said so.
67
+ *
68
+ * Generated output is the standard layout either way — the source layout is
69
+ * what varies.
70
+ */
71
+ test('finds role files/ under the celilo/ layout too', async () => {
72
+ const modulePath = moduleWithRoleFile('v1', 'celilo/ansible');
73
+ const outputPath = mkdtempSync(join(tmpdir(), 'celilo-out-'));
74
+
75
+ await copyAnsibleRoleFilesDirs(modulePath, outputPath);
76
+
77
+ expect(generatedBinary(outputPath)).toBe('v1');
78
+ });
79
+
62
80
  test('a module with no role files/ directory is not an error', async () => {
63
81
  const root = mkdtempSync(join(tmpdir(), 'celilo-norole-'));
64
82
  mkdirSync(join(root, 'ansible', 'roles', 'demo', 'tasks'), { recursive: true });
@@ -60,9 +60,18 @@ const COPY_AS_IS_EXTENSIONS = ['.yml', '.yaml', '.tf'];
60
60
  /**
61
61
  * Template directories to process
62
62
  */
63
+ // A module may keep its deployment tree at the root or under `celilo/`.
64
+ // This is THE list of supported layouts — everything that has to find a
65
+ // module's ansible/ or terraform/ derives it from here. celilo#1461 was two
66
+ // functions deciding that independently and disagreeing.
67
+ const LAYOUT_ROOTS = ['', 'celilo'];
68
+
69
+ const layoutDirs = (name: string): string[] =>
70
+ LAYOUT_ROOTS.map((root) => (root ? `${root}/${name}` : name));
71
+
63
72
  // Directories to scan for template files. Supports both root-level and
64
73
  // celilo/ subdirectory layouts (e.g., ansible/ or celilo/ansible/).
65
- const TEMPLATE_DIRS = ['terraform', 'ansible', 'celilo/terraform', 'celilo/ansible'];
74
+ const TEMPLATE_DIRS = [...layoutDirs('terraform'), ...layoutDirs('ansible')];
66
75
 
67
76
  /**
68
77
  * Check if file is a template that needs variable resolution
@@ -513,8 +522,9 @@ export async function readTemplateFiles(
513
522
  * @param files - Generated files to write
514
523
  */
515
524
  /**
516
- * Copy each `ansible/roles/<role>/files/` directory from the module source
517
- * to the generated output, verbatim (preserving binary content + mode bits).
525
+ * Copy each `<layout>/ansible/roles/<role>/files/` directory from the module
526
+ * source to the generated output, verbatim (preserving binary content + mode
527
+ * bits). Both supported layouts are scanned — see LAYOUT_ROOTS.
518
528
  *
519
529
  * These hold Ansible role-local static assets — they may be binaries and
520
530
  * don't need template variable resolution, so the standard template
@@ -524,38 +534,49 @@ export async function copyAnsibleRoleFilesDirs(
524
534
  modulePath: string,
525
535
  outputPath: string,
526
536
  ): Promise<void> {
527
- const rolesDir = join(modulePath, 'ansible', 'roles');
528
- if (!existsSync(rolesDir)) return;
529
- const roles = await readdir(rolesDir, { withFileTypes: true });
530
- for (const role of roles) {
531
- if (!role.isDirectory()) continue;
532
- const srcFilesDir = join(rolesDir, role.name, 'files');
533
- if (!existsSync(srcFilesDir)) continue;
534
- const destFilesDir = join(outputPath, 'ansible', 'roles', role.name, 'files');
535
- await mkdir(dirname(destFilesDir), { recursive: true });
536
- // `force: true` is LOAD-BEARING on bun, and its absence was celilo#925.
537
- //
538
- // Node defaults `force` to true, so this looked correct and is correct
539
- // under Node. Bun 1.3.3 does not, on this path specifically — measured,
540
- // copying "NEW" over an existing "OLD":
541
- //
542
- // recursive only -> NEW
543
- // recursive + force -> NEW
544
- // recursive + preserveTimestamps -> OLD <- what this was
545
- // recursive + force + preserveTimestamps -> NEW
546
- //
547
- // celilo runs on bun. So a module's built binary landed in `generated/`
548
- // exactly once, at first generate, and no later version ever replaced it —
549
- // silently, because `cp` reports no error, so the caller's try/catch has
550
- // nothing to catch. Ansible then copies that first binary forever and
551
- // reports `ok`, unchanged, while the module's version field advances.
552
- //
553
- // The sibling call in `storage-set-path.ts:153` already passes `force`.
554
- await cp(srcFilesDir, destFilesDir, {
555
- recursive: true,
556
- force: true,
557
- preserveTimestamps: true,
558
- });
537
+ let copied = 0;
538
+ for (const ansibleDir of layoutDirs('ansible')) {
539
+ const rolesDir = join(modulePath, ansibleDir, 'roles');
540
+ if (!existsSync(rolesDir)) continue;
541
+ const roles = await readdir(rolesDir, { withFileTypes: true });
542
+ for (const role of roles) {
543
+ if (!role.isDirectory()) continue;
544
+ const srcFilesDir = join(rolesDir, role.name, 'files');
545
+ if (!existsSync(srcFilesDir)) continue;
546
+ const destFilesDir = join(outputPath, 'ansible', 'roles', role.name, 'files');
547
+ await mkdir(dirname(destFilesDir), { recursive: true });
548
+ // `force: true` is LOAD-BEARING on bun, and its absence was celilo#925.
549
+ //
550
+ // Node defaults `force` to true, so this looked correct and is correct
551
+ // under Node. Bun 1.3.3 does not, on this path specifically — measured,
552
+ // copying "NEW" over an existing "OLD":
553
+ //
554
+ // recursive only -> NEW
555
+ // recursive + force -> NEW
556
+ // recursive + preserveTimestamps -> OLD <- what this was
557
+ // recursive + force + preserveTimestamps -> NEW
558
+ //
559
+ // celilo runs on bun. So a module's built binary landed in `generated/`
560
+ // exactly once, at first generate, and no later version ever replaced it —
561
+ // silently, because `cp` reports no error, so the caller's try/catch has
562
+ // nothing to catch. Ansible then copies that first binary forever and
563
+ // reports `ok`, unchanged, while the module's version field advances.
564
+ //
565
+ // The sibling call in `storage-set-path.ts:153` already passes `force`.
566
+ await cp(srcFilesDir, destFilesDir, {
567
+ recursive: true,
568
+ force: true,
569
+ preserveTimestamps: true,
570
+ });
571
+ copied++;
572
+ }
573
+ }
574
+
575
+ // "I looked and found nothing" must not read the same as "I copied
576
+ // everything". A module with no role files/ is legitimate; a module that has
577
+ // one somewhere this function cannot see is celilo#1461, and it was silent.
578
+ if (copied === 0) {
579
+ log.info(`No Ansible role files/ directories found under ${modulePath}`);
559
580
  }
560
581
  }
561
582