@celilo/cli 0.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/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { eq } from 'drizzle-orm';
|
|
5
|
+
import { type DbClient, createDbClient } from '../db/client';
|
|
6
|
+
import { moduleBuilds, modules } from '../db/schema';
|
|
7
|
+
import { buildModuleFromSource, getModuleBuildStatus, verifyArtifactsExist } from './module-build';
|
|
8
|
+
|
|
9
|
+
const TEST_DB_PATH = './test-module-build.db';
|
|
10
|
+
const TEST_MODULE_DIR = './test-module-build-dir';
|
|
11
|
+
|
|
12
|
+
describe('Module Build Service', () => {
|
|
13
|
+
let db: DbClient;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
db = createDbClient({ path: TEST_DB_PATH });
|
|
17
|
+
|
|
18
|
+
// Create tables
|
|
19
|
+
db.$client.run(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS modules (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
version TEXT NOT NULL,
|
|
24
|
+
description TEXT,
|
|
25
|
+
state TEXT NOT NULL DEFAULT 'IMPORTED',
|
|
26
|
+
manifest_data TEXT NOT NULL,
|
|
27
|
+
source_path TEXT NOT NULL,
|
|
28
|
+
imported_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
29
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
30
|
+
error_message TEXT
|
|
31
|
+
)
|
|
32
|
+
`);
|
|
33
|
+
|
|
34
|
+
db.$client.run(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS module_builds (
|
|
36
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
37
|
+
module_id TEXT NOT NULL,
|
|
38
|
+
version TEXT NOT NULL,
|
|
39
|
+
built_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
40
|
+
artifacts TEXT NOT NULL,
|
|
41
|
+
environment TEXT,
|
|
42
|
+
status TEXT NOT NULL,
|
|
43
|
+
build_log TEXT,
|
|
44
|
+
FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
|
|
45
|
+
)
|
|
46
|
+
`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
db.$client.close();
|
|
51
|
+
|
|
52
|
+
if (existsSync(TEST_DB_PATH)) {
|
|
53
|
+
await rm(TEST_DB_PATH);
|
|
54
|
+
}
|
|
55
|
+
const walPath = `${TEST_DB_PATH}-wal`;
|
|
56
|
+
const shmPath = `${TEST_DB_PATH}-shm`;
|
|
57
|
+
if (existsSync(walPath)) {
|
|
58
|
+
await rm(walPath);
|
|
59
|
+
}
|
|
60
|
+
if (existsSync(shmPath)) {
|
|
61
|
+
await rm(shmPath);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Clean up test module directory
|
|
65
|
+
if (existsSync(TEST_MODULE_DIR)) {
|
|
66
|
+
await rm(TEST_MODULE_DIR, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('buildModuleFromSource', () => {
|
|
71
|
+
test('should return error if module not found', async () => {
|
|
72
|
+
const result = await buildModuleFromSource('nonexistent', db);
|
|
73
|
+
|
|
74
|
+
expect(result.success).toBe(false);
|
|
75
|
+
expect(result.error).toContain('Module not found');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should return error if module has no build section', async () => {
|
|
79
|
+
db.insert(modules)
|
|
80
|
+
.values({
|
|
81
|
+
id: 'no-build',
|
|
82
|
+
name: 'No Build Module',
|
|
83
|
+
version: '1.0.0',
|
|
84
|
+
sourcePath: '/test/no-build',
|
|
85
|
+
manifestData: {
|
|
86
|
+
id: 'no-build',
|
|
87
|
+
name: 'No Build Module',
|
|
88
|
+
version: '1.0.0',
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
.run();
|
|
92
|
+
|
|
93
|
+
const result = await buildModuleFromSource('no-build', db);
|
|
94
|
+
|
|
95
|
+
expect(result.success).toBe(false);
|
|
96
|
+
expect(result.error).toContain('does not have a build section');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should detect Nix environment from flake.nix', async () => {
|
|
100
|
+
// Create test module directory with flake.nix
|
|
101
|
+
await mkdir(TEST_MODULE_DIR, { recursive: true });
|
|
102
|
+
await mkdir(`${TEST_MODULE_DIR}/build`, { recursive: true });
|
|
103
|
+
await writeFile(`${TEST_MODULE_DIR}/flake.nix`, '# Nix flake');
|
|
104
|
+
await writeFile(
|
|
105
|
+
`${TEST_MODULE_DIR}/build/playbook.yml`,
|
|
106
|
+
`---
|
|
107
|
+
- name: Test build
|
|
108
|
+
hosts: localhost
|
|
109
|
+
gather_facts: false
|
|
110
|
+
tasks:
|
|
111
|
+
- name: Debug message
|
|
112
|
+
ansible.builtin.debug:
|
|
113
|
+
msg: "Nix build test"
|
|
114
|
+
`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
db.insert(modules)
|
|
118
|
+
.values({
|
|
119
|
+
id: 'nix-module',
|
|
120
|
+
name: 'Nix Module',
|
|
121
|
+
version: '1.0.0',
|
|
122
|
+
sourcePath: TEST_MODULE_DIR,
|
|
123
|
+
manifestData: {
|
|
124
|
+
id: 'nix-module',
|
|
125
|
+
name: 'Nix Module',
|
|
126
|
+
version: '1.0.0',
|
|
127
|
+
build: {
|
|
128
|
+
script: 'build/playbook.yml',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
.run();
|
|
133
|
+
|
|
134
|
+
// Mock console.log to capture output
|
|
135
|
+
const consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {});
|
|
136
|
+
|
|
137
|
+
const _result = await buildModuleFromSource('nix-module', db);
|
|
138
|
+
|
|
139
|
+
// Verify Nix detection or fallback message was logged
|
|
140
|
+
// If nix is available: "ℹ Entering Nix environment"
|
|
141
|
+
// If nix not available: "⚠️ flake.nix detected but nix command not available"
|
|
142
|
+
const logCalls = consoleLogSpy.mock.calls.map((call) => call[0]);
|
|
143
|
+
const hasNixMessage = logCalls.some(
|
|
144
|
+
(msg) => msg.includes('Nix environment') || msg.includes('flake.nix detected'),
|
|
145
|
+
);
|
|
146
|
+
expect(hasNixMessage).toBe(true);
|
|
147
|
+
|
|
148
|
+
consoleLogSpy.mockRestore();
|
|
149
|
+
|
|
150
|
+
// Build result depends on whether Nix is installed
|
|
151
|
+
// We're just testing that detection happens (message logged)
|
|
152
|
+
}, 10000); // 10 second timeout
|
|
153
|
+
|
|
154
|
+
test('should record build metadata in database', async () => {
|
|
155
|
+
await mkdir(TEST_MODULE_DIR, { recursive: true });
|
|
156
|
+
await mkdir(`${TEST_MODULE_DIR}/build`, { recursive: true });
|
|
157
|
+
await writeFile(
|
|
158
|
+
`${TEST_MODULE_DIR}/build/playbook.yml`,
|
|
159
|
+
`---
|
|
160
|
+
- name: Quick test build
|
|
161
|
+
hosts: localhost
|
|
162
|
+
gather_facts: false
|
|
163
|
+
tasks:
|
|
164
|
+
- name: Echo message
|
|
165
|
+
ansible.builtin.debug:
|
|
166
|
+
msg: "Build test"
|
|
167
|
+
`,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
db.insert(modules)
|
|
171
|
+
.values({
|
|
172
|
+
id: 'record-test',
|
|
173
|
+
name: 'Record Test',
|
|
174
|
+
version: '1.0.0',
|
|
175
|
+
sourcePath: TEST_MODULE_DIR,
|
|
176
|
+
manifestData: {
|
|
177
|
+
id: 'record-test',
|
|
178
|
+
name: 'Record Test',
|
|
179
|
+
version: '1.0.0',
|
|
180
|
+
build: {
|
|
181
|
+
script: 'build/playbook.yml',
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
.run();
|
|
186
|
+
|
|
187
|
+
const result = await buildModuleFromSource('record-test', db);
|
|
188
|
+
|
|
189
|
+
// Build should succeed (simple debug task)
|
|
190
|
+
expect(result.success).toBe(true);
|
|
191
|
+
|
|
192
|
+
// Verify build metadata was recorded
|
|
193
|
+
const buildRecord = await db
|
|
194
|
+
.select()
|
|
195
|
+
.from(moduleBuilds)
|
|
196
|
+
.where(eq(moduleBuilds.moduleId, 'record-test'))
|
|
197
|
+
.get();
|
|
198
|
+
|
|
199
|
+
expect(buildRecord).toBeDefined();
|
|
200
|
+
expect(buildRecord?.moduleId).toBe('record-test');
|
|
201
|
+
expect(buildRecord?.version).toBe('1.0.0');
|
|
202
|
+
expect(buildRecord?.status).toBe('success');
|
|
203
|
+
expect(buildRecord?.environment).toBe('system'); // No flake.nix
|
|
204
|
+
}, 10000); // 10 second timeout for ansible execution
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('getModuleBuildStatus', () => {
|
|
208
|
+
test('should return null if module never built', async () => {
|
|
209
|
+
const status = await getModuleBuildStatus('never-built', db);
|
|
210
|
+
|
|
211
|
+
expect(status).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('should return latest build status', async () => {
|
|
215
|
+
// Create module first (for foreign key)
|
|
216
|
+
db.insert(modules)
|
|
217
|
+
.values({
|
|
218
|
+
id: 'test-module',
|
|
219
|
+
name: 'Test Module',
|
|
220
|
+
version: '1.0.0',
|
|
221
|
+
sourcePath: '/test/module',
|
|
222
|
+
manifestData: {
|
|
223
|
+
id: 'test-module',
|
|
224
|
+
name: 'Test Module',
|
|
225
|
+
version: '1.0.0',
|
|
226
|
+
},
|
|
227
|
+
})
|
|
228
|
+
.run();
|
|
229
|
+
|
|
230
|
+
db.insert(moduleBuilds)
|
|
231
|
+
.values({
|
|
232
|
+
moduleId: 'test-module',
|
|
233
|
+
version: '1.0.0',
|
|
234
|
+
artifacts: ['/path/to/artifact'],
|
|
235
|
+
status: 'success',
|
|
236
|
+
buildLog: 'Build completed',
|
|
237
|
+
environment: 'nix',
|
|
238
|
+
})
|
|
239
|
+
.run();
|
|
240
|
+
|
|
241
|
+
const status = await getModuleBuildStatus('test-module', db);
|
|
242
|
+
|
|
243
|
+
expect(status).toBeDefined();
|
|
244
|
+
expect(status?.status).toBe('success');
|
|
245
|
+
expect(status?.artifacts).toEqual(['/path/to/artifact']);
|
|
246
|
+
expect(status?.buildLog).toBe('Build completed');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('verifyArtifactsExist', () => {
|
|
251
|
+
test('should return true if all artifacts exist', async () => {
|
|
252
|
+
// Create test artifact
|
|
253
|
+
await mkdir(TEST_MODULE_DIR, { recursive: true });
|
|
254
|
+
const artifactPath = `${TEST_MODULE_DIR}/artifact.txt`;
|
|
255
|
+
await writeFile(artifactPath, 'test');
|
|
256
|
+
|
|
257
|
+
const result = verifyArtifactsExist([artifactPath]);
|
|
258
|
+
|
|
259
|
+
expect(result).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('should return false if any artifact missing', async () => {
|
|
263
|
+
const result = verifyArtifactsExist(['/nonexistent/artifact.txt']);
|
|
264
|
+
|
|
265
|
+
expect(result).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('should handle multiple artifacts', async () => {
|
|
269
|
+
await mkdir(TEST_MODULE_DIR, { recursive: true });
|
|
270
|
+
const artifact1 = `${TEST_MODULE_DIR}/artifact1.txt`;
|
|
271
|
+
const artifact2 = `${TEST_MODULE_DIR}/artifact2.txt`;
|
|
272
|
+
await writeFile(artifact1, 'test1');
|
|
273
|
+
await writeFile(artifact2, 'test2');
|
|
274
|
+
|
|
275
|
+
const result = verifyArtifactsExist([artifact1, artifact2]);
|
|
276
|
+
|
|
277
|
+
expect(result).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('should return false if any artifact in list is missing', async () => {
|
|
281
|
+
await mkdir(TEST_MODULE_DIR, { recursive: true });
|
|
282
|
+
const artifact1 = `${TEST_MODULE_DIR}/exists.txt`;
|
|
283
|
+
await writeFile(artifact1, 'test');
|
|
284
|
+
|
|
285
|
+
const result = verifyArtifactsExist([artifact1, '/nonexistent/missing.txt']);
|
|
286
|
+
|
|
287
|
+
expect(result).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module Build Service
|
|
3
|
+
*
|
|
4
|
+
* Executes module build scripts (Ansible playbooks) in Nix environments
|
|
5
|
+
* to compile custom software binaries before deployment.
|
|
6
|
+
*
|
|
7
|
+
* Example: Caddy with RFC2136 DNS provider
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, statSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { eq } from 'drizzle-orm';
|
|
13
|
+
import type { DbClient } from '../db/client';
|
|
14
|
+
import { type BuildStatus, moduleBuilds, modules } from '../db/schema';
|
|
15
|
+
import type { ModuleManifest } from '../manifest/schema';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build result
|
|
19
|
+
*/
|
|
20
|
+
export interface BuildResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
artifacts?: string[];
|
|
23
|
+
buildLog?: string;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build artifact from manifest (simple path string)
|
|
29
|
+
*/
|
|
30
|
+
export type BuildArtifact = string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build status record from database
|
|
34
|
+
*/
|
|
35
|
+
export interface BuildStatusRecord {
|
|
36
|
+
status: BuildStatus;
|
|
37
|
+
artifacts: string[];
|
|
38
|
+
builtAt: Date;
|
|
39
|
+
buildLog?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detect if module uses Nix environment
|
|
44
|
+
* Policy function - checks for flake.nix existence
|
|
45
|
+
*
|
|
46
|
+
* @param modulePath - Path to module directory
|
|
47
|
+
* @returns True if flake.nix exists
|
|
48
|
+
*/
|
|
49
|
+
function detectNixEnvironment(modulePath: string): boolean {
|
|
50
|
+
const flakePath = `${modulePath}/flake.nix`;
|
|
51
|
+
return existsSync(flakePath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if nix command is available
|
|
56
|
+
* Policy function - validates nix availability
|
|
57
|
+
*
|
|
58
|
+
* @returns True if nix command can be executed
|
|
59
|
+
*/
|
|
60
|
+
async function isNixAvailable(): Promise<boolean> {
|
|
61
|
+
const { spawn } = await import('node:child_process');
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const child = spawn('nix', ['--version'], {
|
|
65
|
+
stdio: 'ignore',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
child.on('close', (code) => {
|
|
69
|
+
resolve(code === 0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
child.on('error', () => {
|
|
73
|
+
resolve(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Execute build playbook with streaming progress
|
|
80
|
+
* Execution function - runs ansible-playbook with fuel-gauge progress indicator
|
|
81
|
+
*
|
|
82
|
+
* @param modulePath - Path to module directory
|
|
83
|
+
* @param playbookPath - Path to build playbook (relative to module)
|
|
84
|
+
* @param useNix - Whether to run inside Nix environment
|
|
85
|
+
* @returns Build result with output
|
|
86
|
+
*/
|
|
87
|
+
async function executeBuildCommand(
|
|
88
|
+
modulePath: string,
|
|
89
|
+
commandStr: string,
|
|
90
|
+
useNix: boolean,
|
|
91
|
+
): Promise<{ success: boolean; output: string; error?: string }> {
|
|
92
|
+
const { executeBuildWithProgress } = await import('./build-stream');
|
|
93
|
+
|
|
94
|
+
let command: string;
|
|
95
|
+
let args: string[];
|
|
96
|
+
|
|
97
|
+
if (useNix) {
|
|
98
|
+
command = 'nix';
|
|
99
|
+
args = ['develop', '--command', 'bash', '-c', commandStr];
|
|
100
|
+
} else {
|
|
101
|
+
command = 'bash';
|
|
102
|
+
args = ['-c', commandStr];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = await executeBuildWithProgress({
|
|
106
|
+
command,
|
|
107
|
+
args,
|
|
108
|
+
cwd: modulePath,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return { success: result.success, output: result.output, error: result.error };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function executeBuildScript(
|
|
115
|
+
modulePath: string,
|
|
116
|
+
scriptPath: string,
|
|
117
|
+
useNix: boolean,
|
|
118
|
+
): Promise<{ success: boolean; output: string; error?: string }> {
|
|
119
|
+
const fullScriptPath = `${modulePath}/${scriptPath}`;
|
|
120
|
+
|
|
121
|
+
if (!existsSync(fullScriptPath)) {
|
|
122
|
+
throw new Error(`Build script not found: ${scriptPath}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { executeBuildWithProgress } = await import('./build-stream');
|
|
126
|
+
|
|
127
|
+
const isShellScript = scriptPath.endsWith('.sh');
|
|
128
|
+
|
|
129
|
+
let command: string;
|
|
130
|
+
let args: string[];
|
|
131
|
+
|
|
132
|
+
if (useNix) {
|
|
133
|
+
const innerCommand = isShellScript ? 'bash' : 'ansible-playbook';
|
|
134
|
+
command = 'nix';
|
|
135
|
+
args = ['develop', '--command', innerCommand, scriptPath];
|
|
136
|
+
} else if (isShellScript) {
|
|
137
|
+
command = 'bash';
|
|
138
|
+
args = [scriptPath];
|
|
139
|
+
} else {
|
|
140
|
+
command = 'ansible-playbook';
|
|
141
|
+
args = [scriptPath];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = await executeBuildWithProgress({
|
|
145
|
+
command,
|
|
146
|
+
args,
|
|
147
|
+
cwd: modulePath,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return { success: result.success, output: result.output, error: result.error };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Verify build artifacts exist
|
|
155
|
+
* Policy function - validates file existence
|
|
156
|
+
*
|
|
157
|
+
* @param artifacts - List of artifact paths from manifest (relative to module directory)
|
|
158
|
+
* @param modulePath - Module directory path
|
|
159
|
+
* @returns Array of [artifactPath, exists, size]
|
|
160
|
+
*/
|
|
161
|
+
function verifyBuildArtifacts(
|
|
162
|
+
artifacts: BuildArtifact[],
|
|
163
|
+
modulePath: string,
|
|
164
|
+
): Array<{ path: string; exists: boolean; size?: number }> {
|
|
165
|
+
const results: Array<{ path: string; exists: boolean; size?: number }> = [];
|
|
166
|
+
|
|
167
|
+
for (const artifactPath of artifacts) {
|
|
168
|
+
// Resolve relative to module directory
|
|
169
|
+
const fullPath = join(modulePath, artifactPath);
|
|
170
|
+
const exists = existsSync(fullPath);
|
|
171
|
+
let size: number | undefined;
|
|
172
|
+
|
|
173
|
+
if (exists) {
|
|
174
|
+
const stats = statSync(fullPath);
|
|
175
|
+
size = stats.size;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
results.push({ path: fullPath, exists, size });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return results;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Format bytes to human-readable size
|
|
186
|
+
* Policy function - formatting
|
|
187
|
+
*
|
|
188
|
+
* @param bytes - Size in bytes
|
|
189
|
+
* @returns Formatted string (e.g., "48.2 MB")
|
|
190
|
+
*/
|
|
191
|
+
function formatBytes(bytes: number): string {
|
|
192
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
193
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
194
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Record build metadata in database
|
|
199
|
+
* Execution function - database write
|
|
200
|
+
*
|
|
201
|
+
* @param moduleId - Module identifier
|
|
202
|
+
* @param version - Module version
|
|
203
|
+
* @param artifacts - List of artifact paths
|
|
204
|
+
* @param status - Build status
|
|
205
|
+
* @param buildLog - Build output log
|
|
206
|
+
* @param environment - Build environment (nix or system)
|
|
207
|
+
* @param db - Database client
|
|
208
|
+
*/
|
|
209
|
+
async function recordBuildMetadata(
|
|
210
|
+
moduleId: string,
|
|
211
|
+
version: string,
|
|
212
|
+
artifacts: string[],
|
|
213
|
+
status: BuildStatus,
|
|
214
|
+
buildLog: string,
|
|
215
|
+
environment: 'nix' | 'system' | null,
|
|
216
|
+
db: DbClient,
|
|
217
|
+
): Promise<void> {
|
|
218
|
+
await db.insert(moduleBuilds).values({
|
|
219
|
+
moduleId,
|
|
220
|
+
version,
|
|
221
|
+
artifacts,
|
|
222
|
+
status,
|
|
223
|
+
buildLog,
|
|
224
|
+
environment,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build module from source
|
|
230
|
+
* Orchestrator function - coordinates build workflow
|
|
231
|
+
*
|
|
232
|
+
* @param moduleId - Module identifier
|
|
233
|
+
* @param db - Database client
|
|
234
|
+
* @returns Build result
|
|
235
|
+
*/
|
|
236
|
+
export async function buildModuleFromSource(moduleId: string, db: DbClient): Promise<BuildResult> {
|
|
237
|
+
// Fetch module from database
|
|
238
|
+
const module = await db.select().from(modules).where(eq(modules.id, moduleId)).get();
|
|
239
|
+
|
|
240
|
+
if (!module) {
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
error: `Module not found: ${moduleId}`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const manifest = module.manifestData as ModuleManifest;
|
|
248
|
+
|
|
249
|
+
// Check if module has build section
|
|
250
|
+
if (!manifest.build) {
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: `Module ${moduleId} does not have a build section in manifest`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const { command, script, artifacts } = manifest.build;
|
|
258
|
+
const modulePath = module.sourcePath;
|
|
259
|
+
|
|
260
|
+
if (!command && !script) {
|
|
261
|
+
return {
|
|
262
|
+
success: false,
|
|
263
|
+
error: `Module ${moduleId} build section must specify either "command" or "script"`,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Detect Nix environment and verify availability
|
|
268
|
+
const nixDetected = detectNixEnvironment(modulePath);
|
|
269
|
+
const nixAvailable = nixDetected ? await isNixAvailable() : false;
|
|
270
|
+
const useNix = nixDetected && nixAvailable;
|
|
271
|
+
const environment: 'nix' | 'system' = useNix ? 'nix' : 'system';
|
|
272
|
+
|
|
273
|
+
if (useNix) {
|
|
274
|
+
console.log('ℹ Entering Nix environment (flake.nix detected)...');
|
|
275
|
+
} else if (nixDetected && !nixAvailable) {
|
|
276
|
+
console.log('⚠️ flake.nix detected but nix command not available - using system Ansible');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
// Execute build script with streaming progress
|
|
281
|
+
const buildResult = command
|
|
282
|
+
? await executeBuildCommand(modulePath, command, useNix)
|
|
283
|
+
: await executeBuildScript(modulePath, script as string, useNix);
|
|
284
|
+
|
|
285
|
+
// Check if build command failed
|
|
286
|
+
if (!buildResult.success) {
|
|
287
|
+
const errorMsg = buildResult.error || 'Build command failed';
|
|
288
|
+
|
|
289
|
+
// Record failed build
|
|
290
|
+
await recordBuildMetadata(
|
|
291
|
+
moduleId,
|
|
292
|
+
manifest.version,
|
|
293
|
+
[],
|
|
294
|
+
'failed',
|
|
295
|
+
buildResult.output,
|
|
296
|
+
environment,
|
|
297
|
+
db,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
error: errorMsg,
|
|
303
|
+
buildLog: buildResult.output,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const buildLog = buildResult.output.trim();
|
|
308
|
+
|
|
309
|
+
// Verify artifacts
|
|
310
|
+
if (artifacts && artifacts.length > 0) {
|
|
311
|
+
console.log('Verifying build artifacts...');
|
|
312
|
+
const verification = verifyBuildArtifacts(artifacts, modulePath);
|
|
313
|
+
const allExist = verification.every((v) => v.exists);
|
|
314
|
+
|
|
315
|
+
if (!allExist) {
|
|
316
|
+
const missing = verification.filter((v) => !v.exists);
|
|
317
|
+
const errorMsg = `Build failed: Missing artifacts:\n${missing.map((m) => ` - ${m.path}`).join('\n')}`;
|
|
318
|
+
|
|
319
|
+
// Record failed build
|
|
320
|
+
await recordBuildMetadata(
|
|
321
|
+
moduleId,
|
|
322
|
+
manifest.version,
|
|
323
|
+
[],
|
|
324
|
+
'failed',
|
|
325
|
+
`${buildLog}\n\n${errorMsg}`,
|
|
326
|
+
environment,
|
|
327
|
+
db,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
error: errorMsg,
|
|
333
|
+
buildLog,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Display artifact info
|
|
338
|
+
for (const result of verification) {
|
|
339
|
+
const sizeStr = result.size ? ` (${formatBytes(result.size)})` : '';
|
|
340
|
+
console.log(`✓ Artifact verified: ${result.path}${sizeStr}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Record successful build
|
|
344
|
+
const artifactPaths = verification.map((v) => v.path);
|
|
345
|
+
await recordBuildMetadata(
|
|
346
|
+
moduleId,
|
|
347
|
+
manifest.version,
|
|
348
|
+
artifactPaths,
|
|
349
|
+
'success',
|
|
350
|
+
buildLog,
|
|
351
|
+
environment,
|
|
352
|
+
db,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
success: true,
|
|
357
|
+
artifacts: artifactPaths,
|
|
358
|
+
buildLog,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// No artifacts defined, just record success
|
|
363
|
+
await recordBuildMetadata(moduleId, manifest.version, [], 'success', buildLog, environment, db);
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
success: true,
|
|
367
|
+
artifacts: [],
|
|
368
|
+
buildLog,
|
|
369
|
+
};
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
372
|
+
const buildLog = `Build failed: ${errorMsg}`;
|
|
373
|
+
|
|
374
|
+
// Record failed build
|
|
375
|
+
await recordBuildMetadata(moduleId, manifest.version, [], 'failed', buildLog, environment, db);
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
success: false,
|
|
379
|
+
error: errorMsg,
|
|
380
|
+
buildLog,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get module build status
|
|
387
|
+
* Execution function - database query
|
|
388
|
+
*
|
|
389
|
+
* @param moduleId - Module identifier
|
|
390
|
+
* @param db - Database client
|
|
391
|
+
* @returns Build status record or null if never built
|
|
392
|
+
*/
|
|
393
|
+
export async function getModuleBuildStatus(
|
|
394
|
+
moduleId: string,
|
|
395
|
+
db: DbClient,
|
|
396
|
+
): Promise<BuildStatusRecord | null> {
|
|
397
|
+
const build = await db
|
|
398
|
+
.select()
|
|
399
|
+
.from(moduleBuilds)
|
|
400
|
+
.where(eq(moduleBuilds.moduleId, moduleId))
|
|
401
|
+
.orderBy(moduleBuilds.builtAt)
|
|
402
|
+
.limit(1)
|
|
403
|
+
.get();
|
|
404
|
+
|
|
405
|
+
if (!build) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
status: build.status,
|
|
411
|
+
artifacts: build.artifacts as string[],
|
|
412
|
+
builtAt: new Date(build.builtAt),
|
|
413
|
+
buildLog: build.buildLog || undefined,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Verify build artifacts still exist
|
|
419
|
+
* Policy function - validates current state of artifacts
|
|
420
|
+
*
|
|
421
|
+
* @param artifactPaths - List of full artifact paths (from database)
|
|
422
|
+
* @returns True if all artifacts exist
|
|
423
|
+
*/
|
|
424
|
+
export function verifyArtifactsExist(artifactPaths: string[]): boolean {
|
|
425
|
+
for (const path of artifactPaths) {
|
|
426
|
+
if (!existsSync(path)) {
|
|
427
|
+
return false;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
431
|
+
}
|