@celilo/cli 2.3.0 → 3.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/CELILO_CORE_MODULES.md +1 -1
- package/package.json +3 -3
- package/src/api/serve.ts +13 -1
- package/src/cli/commands/module-health.test.ts +35 -0
- package/src/cli/commands/module-health.ts +11 -3
- package/src/cli/commands/module-publish.test.ts +22 -0
- package/src/cli/commands/module-publish.ts +14 -12
- package/src/cli/commands/module-update.ts +69 -17
- package/src/cli/commands/module-upgrade-gate.test.ts +154 -0
- package/src/cli/commands/module-upgrade.ts +43 -3
- package/src/cli/commands/publish/helpers.ts +4 -3
- package/src/cli/commands/publish/index.ts +13 -1
- package/src/cli/commands/publish/plan.test.ts +64 -0
- package/src/cli/commands/publish/plan.ts +52 -19
- package/src/cli/commands/publish/types.ts +16 -3
- package/src/cli/commands/subscribers-install-daemon.test.ts +44 -0
- package/src/cli/commands/subscribers-install-daemon.ts +107 -0
- package/src/cli/commands/subscribers-serve.test.ts +22 -0
- package/src/cli/commands/subscribers-serve.ts +22 -4
- package/src/cli/commands/system-audit.ts +14 -5
- package/src/cli/commands/system-update.ts +11 -6
- package/src/cli/completion.ts +10 -1
- package/src/cli/fuel-gauge.ts +12 -4
- package/src/cli/index.ts +12 -0
- package/src/cli/json-output.test.ts +81 -0
- package/src/cli/types.ts +9 -0
- package/src/hooks/broker.test.ts +9 -3
- package/src/hooks/capability-loader.ts +49 -1
- package/src/hooks/executor.test.ts +5 -2
- package/src/hooks/hook-jail-toolchain-reach.test.ts +8 -3
- package/src/hooks/hook-jail-unreachability.test.ts +10 -2
- package/src/hooks/hook-trespass.test.ts +13 -4
- package/src/hooks/run-named-hook.ts +19 -16
- package/src/hooks/test-fixtures/artifact-writing-hook.ts +0 -1
- package/src/hooks/test-fixtures/capability-calling-hook.ts +20 -13
- package/src/hooks/test-fixtures/jail-probe-hook.ts +9 -1
- package/src/hooks/test-fixtures/jail-toolchain-hook.ts +7 -2
- package/src/hooks/test-fixtures/runaway-hook.ts +0 -1
- package/src/hooks/test-fixtures/sigterm-ignoring-hook.ts +0 -1
- package/src/hooks/test-fixtures/silent-hook.ts +0 -1
- package/src/hooks/test-fixtures/store-writing-hook.ts +20 -11
- package/src/hooks/test-fixtures/success-hook.ts +4 -4
- package/src/manifest/contracts/v1.ts +19 -14
- package/src/manifest/json-schema-roundtrip.test.ts +1 -1
- package/src/manifest/schema.ts +66 -16
- package/src/manifest/validate.test.ts +47 -0
- package/src/policy/capability-shape-baseline.ts +69 -21
- package/src/policy/capability-shape-drift.test.ts +53 -1
- package/src/policy/capability-shape.test.ts +105 -0
- package/src/policy/capability-shape.ts +283 -2
- package/src/policy/module-business-baseline.ts +17 -0
- package/src/registry/client.test.ts +65 -0
- package/src/registry/client.ts +7 -7
- package/src/secrets/storage.test.ts +69 -3
- package/src/secrets/storage.ts +71 -1
- package/src/services/audit/health.test.ts +58 -0
- package/src/services/audit/health.ts +15 -3
- package/src/services/audit/index.test.ts +1 -1
- package/src/services/audit/interface-classification.test.ts +16 -5
- package/src/services/audit/interface-classification.ts +25 -2
- package/src/services/audit/module-versions.ts +5 -1
- package/src/services/audit/public-dns.test.ts +20 -0
- package/src/services/audit/public-dns.ts +7 -2
- package/src/services/audit/recurrence-gate.test.ts +225 -0
- package/src/services/audit/trusted-sources.test.ts +17 -0
- package/src/services/audit/trusted-sources.ts +21 -0
- package/src/services/build-bus/hook-dispatch-executor.test.ts +269 -0
- package/src/services/build-bus/hook-dispatch-mgmt.test.ts +102 -150
- package/src/services/build-bus/hook-dispatch-path.test.ts +95 -0
- package/src/services/build-bus/hook-dispatch.test.ts +86 -116
- package/src/services/build-bus/hook-dispatch.ts +99 -121
- package/src/services/build-bus/hook-dispatcher.ts +106 -28
- package/src/services/build-bus/receiver-daemon.test.ts +189 -0
- package/src/services/build-bus/receiver-daemon.ts +355 -0
- package/src/services/build-bus/self-update.ts +156 -0
- package/src/services/capability-compat.test.ts +90 -0
- package/src/services/capability-compat.ts +128 -0
- package/src/services/deploy-terraform.ts +38 -1
- package/src/services/events-daemon.test.ts +57 -0
- package/src/services/events-daemon.ts +76 -0
- package/src/services/firewall-reach.ts +21 -8
- package/src/services/fleet-checks.test.ts +37 -1
- package/src/services/fleet-checks.ts +31 -1
- package/src/services/health-runner.ts +43 -5
- package/src/services/module-deploy-prune.test.ts +89 -0
- package/src/services/module-deploy.ts +72 -125
- package/src/services/module-types-drift.test.ts +1 -1
- package/src/services/module-validator/capability-versions.test.ts +13 -2
- package/src/services/provider-converge.test.ts +209 -0
- package/src/services/provider-converge.ts +241 -0
- package/src/services/static-content-converge.test.ts +39 -0
- package/src/services/static-content-converge.ts +26 -5
- package/src/services/terraform-safety.test.ts +83 -0
- package/src/services/terraform-safety.ts +53 -0
- package/src/services/update/orchestrator.test.ts +1 -1
- package/tsconfig.json +2 -13
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hook-dispatch tests. The planner is pure (no I/O); the runner
|
|
3
|
-
*
|
|
2
|
+
* Hook-dispatch tests. The planner is pure (no I/O); the runner goes
|
|
3
|
+
* through the ordinary hook executor (module-orchestrator-primitives
|
|
4
|
+
* slice 7). We test:
|
|
4
5
|
*
|
|
5
|
-
* - planHookDispatch: rule matching
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
6
|
+
* - planHookDispatch: rule matching (match logic unchanged since the
|
|
7
|
+
* bash-spawn days).
|
|
8
|
+
* - runUpstreamHook: routes a matched entry through runNamedHook with
|
|
9
|
+
* the event as the `event` input and the matched entry's definition
|
|
10
|
+
* (script + timeout). The seam under the mock is run-named-hook;
|
|
11
|
+
* the real executor hop (child spawn, jail, context) is covered
|
|
12
|
+
* end-to-end by hook-dispatch-executor.test.ts.
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
|
-
import {
|
|
11
|
-
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
-
import { tmpdir } from 'node:os';
|
|
13
|
-
import { join } from 'node:path';
|
|
15
|
+
import { describe, expect, mock, test } from 'bun:test';
|
|
14
16
|
import type { PublishEvent } from '@celilo/event-bus/build-bus';
|
|
15
|
-
import
|
|
17
|
+
import type { HookLogger } from '../../hooks/types';
|
|
18
|
+
import { type HookDispatchPlan, planHookDispatch, runUpstreamHook } from './hook-dispatch';
|
|
16
19
|
|
|
17
20
|
function buildEvent(overrides: Partial<PublishEvent> = {}): PublishEvent {
|
|
18
21
|
return {
|
|
@@ -25,24 +28,31 @@ function buildEvent(overrides: Partial<PublishEvent> = {}): PublishEvent {
|
|
|
25
28
|
};
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
const SILENT_LOGGER = {
|
|
32
|
+
info: () => {},
|
|
33
|
+
warn: () => {},
|
|
34
|
+
error: () => {},
|
|
35
|
+
success: () => {},
|
|
36
|
+
} as unknown as HookLogger;
|
|
37
|
+
|
|
28
38
|
describe('planHookDispatch', () => {
|
|
29
39
|
test('empty module list → empty plan', () => {
|
|
30
40
|
expect(planHookDispatch(buildEvent(), [])).toEqual([]);
|
|
31
41
|
});
|
|
32
42
|
|
|
33
43
|
test('module with no matching hooks → empty plan', () => {
|
|
34
|
-
const modules
|
|
44
|
+
const modules = [
|
|
35
45
|
{
|
|
36
46
|
moduleId: 'lunacycle',
|
|
37
47
|
sourcePath: '/modules/lunacycle',
|
|
38
|
-
hooks: [{ match: { registry: 'celilo-registry' }, script: './reload.
|
|
48
|
+
hooks: [{ match: { registry: 'celilo-registry' }, script: './reload.ts' }],
|
|
39
49
|
},
|
|
40
50
|
];
|
|
41
51
|
expect(planHookDispatch(buildEvent({ registry: 'npm' }), modules)).toEqual([]);
|
|
42
52
|
});
|
|
43
53
|
|
|
44
|
-
test('matching hook → plan
|
|
45
|
-
const modules
|
|
54
|
+
test('matching hook → plan for that module + entry', () => {
|
|
55
|
+
const modules = [
|
|
46
56
|
{
|
|
47
57
|
moduleId: 'lunacycle',
|
|
48
58
|
sourcePath: '/modules/lunacycle',
|
|
@@ -50,36 +60,31 @@ describe('planHookDispatch', () => {
|
|
|
50
60
|
{
|
|
51
61
|
name: 'rerun-e2e',
|
|
52
62
|
match: { registry: 'npm', package_pattern: '@celilo/*' },
|
|
53
|
-
script: './hooks/rerun.
|
|
63
|
+
script: './hooks/rerun.ts',
|
|
54
64
|
},
|
|
55
65
|
],
|
|
56
66
|
},
|
|
57
67
|
];
|
|
58
68
|
const plan = planHookDispatch(buildEvent(), modules);
|
|
59
69
|
expect(plan).toHaveLength(1);
|
|
60
|
-
expect(plan[0].
|
|
61
|
-
expect(plan[0].
|
|
62
|
-
expect(plan[0].env.CELILO_EVENT_TAG).toBe('latest');
|
|
63
|
-
expect(plan[0].env.CELILO_EVENT_PACKAGE_NAME).toBe('@celilo/cli');
|
|
64
|
-
expect(plan[0].env.CELILO_EVENT_PACKAGE_VERSION).toBe('0.4.0');
|
|
65
|
-
const payload = JSON.parse(plan[0].env.CELILO_EVENT_PAYLOAD) as PublishEvent;
|
|
66
|
-
expect(payload.package.name).toBe('@celilo/cli');
|
|
70
|
+
expect(plan[0].module.moduleId).toBe('lunacycle');
|
|
71
|
+
expect(plan[0].hook.script).toBe('./hooks/rerun.ts');
|
|
67
72
|
});
|
|
68
73
|
|
|
69
74
|
test('multiple modules + hooks → one plan entry per match', () => {
|
|
70
|
-
const modules
|
|
75
|
+
const modules = [
|
|
71
76
|
{
|
|
72
77
|
moduleId: 'lunacycle',
|
|
73
78
|
sourcePath: '/modules/lunacycle',
|
|
74
79
|
hooks: [
|
|
75
|
-
{ match: { registry: 'npm', package_pattern: '@celilo/cli' }, script: './a.
|
|
76
|
-
{ match: { registry: 'npm', package_pattern: '@celilo/e2e' }, script: './b.
|
|
80
|
+
{ match: { registry: 'npm', package_pattern: '@celilo/cli' }, script: './a.ts' },
|
|
81
|
+
{ match: { registry: 'npm', package_pattern: '@celilo/e2e' }, script: './b.ts' },
|
|
77
82
|
],
|
|
78
83
|
},
|
|
79
84
|
{
|
|
80
85
|
moduleId: 'mgmt',
|
|
81
86
|
sourcePath: '/modules/mgmt',
|
|
82
|
-
hooks: [{ match: { registry: 'celilo-registry' }, script: './c.
|
|
87
|
+
hooks: [{ match: { registry: 'celilo-registry' }, script: './c.ts' }],
|
|
83
88
|
},
|
|
84
89
|
];
|
|
85
90
|
const plan = planHookDispatch(
|
|
@@ -88,16 +93,16 @@ describe('planHookDispatch', () => {
|
|
|
88
93
|
);
|
|
89
94
|
expect(plan).toHaveLength(1);
|
|
90
95
|
expect(plan[0].module.moduleId).toBe('lunacycle');
|
|
91
|
-
expect(plan[0].hook.script).toBe('./a.
|
|
96
|
+
expect(plan[0].hook.script).toBe('./a.ts');
|
|
92
97
|
});
|
|
93
98
|
|
|
94
99
|
test('snake_case package_pattern in manifest maps to camelCase packagePattern in matcher', () => {
|
|
95
100
|
// Documents the YAML↔JS naming bridge.
|
|
96
|
-
const modules
|
|
101
|
+
const modules = [
|
|
97
102
|
{
|
|
98
103
|
moduleId: 'x',
|
|
99
104
|
sourcePath: '/m',
|
|
100
|
-
hooks: [{ match: { package_pattern: '@celilo/*' }, script: './h.
|
|
105
|
+
hooks: [{ match: { package_pattern: '@celilo/*' }, script: './h.ts' }],
|
|
101
106
|
},
|
|
102
107
|
];
|
|
103
108
|
expect(planHookDispatch(buildEvent(), modules)).toHaveLength(1);
|
|
@@ -107,101 +112,66 @@ describe('planHookDispatch', () => {
|
|
|
107
112
|
});
|
|
108
113
|
});
|
|
109
114
|
|
|
110
|
-
describe('
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
beforeEach(() => {
|
|
114
|
-
scriptDir = mkdtempSync(join(tmpdir(), 'celilo-hook-dispatch-'));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
afterEach(() => {
|
|
118
|
-
rmSync(scriptDir, { recursive: true, force: true });
|
|
119
|
-
});
|
|
115
|
+
describe('runUpstreamHook (routes through runNamedHook)', () => {
|
|
116
|
+
const calls: Array<{ moduleId: string; hookName: string; options: Record<string, unknown> }> = [];
|
|
120
117
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
118
|
+
mock.module('../../hooks/run-named-hook', () => ({
|
|
119
|
+
runNamedHook: async (
|
|
120
|
+
moduleId: string,
|
|
121
|
+
hookName: string,
|
|
122
|
+
_db: unknown,
|
|
123
|
+
_logger: unknown,
|
|
124
|
+
options: Record<string, unknown>,
|
|
125
|
+
) => {
|
|
126
|
+
calls.push({ moduleId, hookName, options });
|
|
127
|
+
return { success: true, outputs: {}, duration: 7 };
|
|
128
|
+
},
|
|
129
|
+
}));
|
|
127
130
|
|
|
128
|
-
test('
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
'
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
CELILO_EVENT_PAYLOAD: '{}',
|
|
139
|
-
CELILO_EVENT_REGISTRY: 'npm',
|
|
140
|
-
CELILO_EVENT_TAG: 'latest',
|
|
141
|
-
CELILO_EVENT_PACKAGE_NAME: '@celilo/cli',
|
|
142
|
-
CELILO_EVENT_PACKAGE_VERSION: '0.4.0',
|
|
143
|
-
},
|
|
131
|
+
test('passes the event as input and the matched entry as the hook definition', async () => {
|
|
132
|
+
calls.length = 0;
|
|
133
|
+
const plan: HookDispatchPlan = {
|
|
134
|
+
module: { moduleId: 'lunacycle', sourcePath: '/m', hooks: [] },
|
|
135
|
+
hook: { name: 'rerun-e2e', match: {}, script: './hooks/rerun.ts', timeout: 90_000 },
|
|
136
|
+
};
|
|
137
|
+
const event = buildEvent();
|
|
138
|
+
const result = await runUpstreamHook(plan, event, {
|
|
139
|
+
db: {} as Parameters<typeof runUpstreamHook>[2]['db'],
|
|
140
|
+
logger: SILENT_LOGGER,
|
|
144
141
|
});
|
|
145
|
-
expect(result.exitCode).toBe(0);
|
|
146
|
-
expect(result.stdout.trim()).toBe('hello @celilo/cli');
|
|
147
|
-
expect(result.timedOut).toBe(false);
|
|
148
|
-
});
|
|
149
142
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
scriptPath,
|
|
156
|
-
env: {
|
|
157
|
-
CELILO_EVENT_PAYLOAD: '{}',
|
|
158
|
-
CELILO_EVENT_REGISTRY: 'npm',
|
|
159
|
-
CELILO_EVENT_TAG: 'latest',
|
|
160
|
-
CELILO_EVENT_PACKAGE_NAME: '@celilo/cli',
|
|
161
|
-
CELILO_EVENT_PACKAGE_VERSION: '0.4.0',
|
|
162
|
-
},
|
|
143
|
+
expect(result).toEqual({
|
|
144
|
+
module: 'lunacycle',
|
|
145
|
+
hookName: 'rerun-e2e',
|
|
146
|
+
success: true,
|
|
147
|
+
durationMs: expect.any(Number),
|
|
163
148
|
});
|
|
164
|
-
expect(
|
|
165
|
-
expect(
|
|
149
|
+
expect(calls).toHaveLength(1);
|
|
150
|
+
expect(calls[0].moduleId).toBe('lunacycle');
|
|
151
|
+
expect(calls[0].hookName).toBe('on_upstream_publish');
|
|
152
|
+
expect(calls[0].options.inputs).toEqual({ event });
|
|
153
|
+
expect(calls[0].options.hookDefinition).toEqual({
|
|
154
|
+
script: './hooks/rerun.ts',
|
|
155
|
+
timeout: 90_000,
|
|
156
|
+
});
|
|
157
|
+
expect(calls[0].options.timeoutMs).toBe(90_000);
|
|
166
158
|
});
|
|
167
159
|
|
|
168
|
-
test('
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
hook: { match: {}, script: 'slow.sh', timeout: 1 },
|
|
173
|
-
scriptPath,
|
|
174
|
-
env: {
|
|
175
|
-
CELILO_EVENT_PAYLOAD: '{}',
|
|
176
|
-
CELILO_EVENT_REGISTRY: 'npm',
|
|
177
|
-
CELILO_EVENT_TAG: 'latest',
|
|
178
|
-
CELILO_EVENT_PACKAGE_NAME: '@celilo/cli',
|
|
179
|
-
CELILO_EVENT_PACKAGE_VERSION: '0.4.0',
|
|
160
|
+
test('a runNamedHook throw becomes an unsuccessful result, not a rejection', async () => {
|
|
161
|
+
mock.module('../../hooks/run-named-hook', () => ({
|
|
162
|
+
runNamedHook: async () => {
|
|
163
|
+
throw new Error('boom');
|
|
180
164
|
},
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
expect(result.durationMs).toBeLessThan(4000);
|
|
187
|
-
}, 10000);
|
|
188
|
-
|
|
189
|
-
test('records spawn error when the script does not exist', async () => {
|
|
190
|
-
const result = await runHookDispatch({
|
|
191
|
-
module: { moduleId: 'm', sourcePath: scriptDir, hooks: [] },
|
|
192
|
-
hook: { match: {}, script: 'does-not-exist.sh' },
|
|
193
|
-
scriptPath: join(scriptDir, 'does-not-exist.sh'),
|
|
194
|
-
env: {
|
|
195
|
-
CELILO_EVENT_PAYLOAD: '{}',
|
|
196
|
-
CELILO_EVENT_REGISTRY: 'npm',
|
|
197
|
-
CELILO_EVENT_TAG: 'latest',
|
|
198
|
-
CELILO_EVENT_PACKAGE_NAME: '@celilo/cli',
|
|
199
|
-
CELILO_EVENT_PACKAGE_VERSION: '0.4.0',
|
|
165
|
+
}));
|
|
166
|
+
const result = await runUpstreamHook(
|
|
167
|
+
{
|
|
168
|
+
module: { moduleId: 'lunacycle', sourcePath: '/m', hooks: [] },
|
|
169
|
+
hook: { match: {}, script: './hooks/rerun.ts' },
|
|
200
170
|
},
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
expect(result.
|
|
171
|
+
buildEvent(),
|
|
172
|
+
{ db: {} as never, logger: SILENT_LOGGER },
|
|
173
|
+
);
|
|
174
|
+
expect(result.success).toBe(false);
|
|
175
|
+
expect(result.error).toContain('boom');
|
|
206
176
|
});
|
|
207
177
|
});
|
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook-dispatch planning + execution for the build bus
|
|
3
|
-
* ([[openspec/changes/build-bus-poll-cd/proposal.md]] Phase 4
|
|
3
|
+
* ([[openspec/changes/build-bus-poll-cd/proposal.md]] Phase 4, reshaped by
|
|
4
|
+
* module-orchestrator-primitives slice 7).
|
|
4
5
|
*
|
|
5
6
|
* Pure: planHookDispatch(event, modules) → HookDispatchPlan[].
|
|
6
|
-
* Decides which module hooks match the event
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Decides which module hooks match the event; the executor
|
|
8
|
+
* (runUpstreamHook) takes a plan plus the event and runs the matched
|
|
9
|
+
* entry through the ORDINARY hook executor — `runNamedHook` →
|
|
10
|
+
* `invokeHook`, jailed, with the verified PublishEvent as the hook's
|
|
11
|
+
* `event` input.
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* CELILO_EVENT_PACKAGE_VERSION — convenience
|
|
13
|
+
* There is exactly one hook dispatch path in celilo (tasks.md 7.1):
|
|
14
|
+
* `apps/celilo/src/hooks/executor.ts`. This module used to carry the
|
|
15
|
+
* second one — `runHookDispatch` spawned `bash <scriptPath>` detached
|
|
16
|
+
* with `env: { ...process.env, ...plan.env }`, no jail, no broker — and
|
|
17
|
+
* slice 7 deleted it. `planHookDispatch`'s match logic is unchanged.
|
|
16
18
|
*
|
|
17
|
-
* Non-zero
|
|
18
|
-
* are operational signal for the operator, not a
|
|
19
|
+
* Non-zero hook outcomes are logged by the dispatcher but don't gate the
|
|
20
|
+
* receiver — failures are operational signal for the operator, not a
|
|
21
|
+
* publish blocker.
|
|
19
22
|
*/
|
|
20
23
|
|
|
21
|
-
import {
|
|
22
|
-
import { join } from 'node:path';
|
|
24
|
+
import type { HookDefinition } from '@celilo/capabilities';
|
|
23
25
|
import { type PublishEvent, matchesRule } from '@celilo/event-bus/build-bus';
|
|
26
|
+
import { type DbClient, getDb } from '../../db/client';
|
|
27
|
+
import { runNamedHook } from '../../hooks/run-named-hook';
|
|
28
|
+
import type { HookLogger } from '../../hooks/types';
|
|
24
29
|
|
|
25
30
|
/** Mirrors UpstreamPublishHookSchema in the manifest schema. */
|
|
26
31
|
export interface UpstreamHookEntry {
|
|
27
32
|
name?: string;
|
|
28
33
|
match: { registry?: string; tag?: string; package_pattern?: string };
|
|
34
|
+
/** Path to a TypeScript `defineHook` script, relative to the module directory. */
|
|
29
35
|
script: string;
|
|
36
|
+
/** Milliseconds before the hook is killed. */
|
|
30
37
|
timeout?: number;
|
|
31
38
|
}
|
|
32
39
|
|
|
@@ -45,16 +52,15 @@ export interface ModuleHookContext {
|
|
|
45
52
|
export interface HookDispatchPlan {
|
|
46
53
|
module: ModuleHookContext;
|
|
47
54
|
hook: UpstreamHookEntry;
|
|
48
|
-
/** Absolute path the executor will spawn. */
|
|
49
|
-
scriptPath: string;
|
|
50
|
-
/** Env vars to pass to the script. */
|
|
51
|
-
env: Record<string, string>;
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
/** Matches the old bash path's default (600s), in milliseconds. */
|
|
58
|
+
const DEFAULT_TIMEOUT_MS = 600_000;
|
|
59
|
+
|
|
54
60
|
/**
|
|
55
61
|
* Pure: walk every (module, hook) combination, keep the ones whose
|
|
56
62
|
* match rule fires for this event. Same world → same plan; injects
|
|
57
|
-
* nothing.
|
|
63
|
+
* nothing. The match logic is unchanged since the bash-spawn days.
|
|
58
64
|
*
|
|
59
65
|
* The manifest's match uses snake_case (`package_pattern`) to match
|
|
60
66
|
* YAML conventions, but the @celilo/event-bus/build-bus matcher
|
|
@@ -66,7 +72,6 @@ export function planHookDispatch(
|
|
|
66
72
|
modules: ModuleHookContext[],
|
|
67
73
|
): HookDispatchPlan[] {
|
|
68
74
|
const plans: HookDispatchPlan[] = [];
|
|
69
|
-
const env = buildHookEnv(event);
|
|
70
75
|
for (const module of modules) {
|
|
71
76
|
for (const hook of module.hooks) {
|
|
72
77
|
const rule = {
|
|
@@ -75,124 +80,97 @@ export function planHookDispatch(
|
|
|
75
80
|
packagePattern: hook.match.package_pattern,
|
|
76
81
|
};
|
|
77
82
|
if (!matchesRule(event, rule)) continue;
|
|
78
|
-
plans.push({
|
|
79
|
-
module,
|
|
80
|
-
hook,
|
|
81
|
-
scriptPath: join(module.sourcePath, hook.script),
|
|
82
|
-
env,
|
|
83
|
-
});
|
|
83
|
+
plans.push({ module, hook });
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
return plans;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
CELILO_EVENT_PAYLOAD: JSON.stringify(event),
|
|
92
|
-
CELILO_EVENT_REGISTRY: event.registry,
|
|
93
|
-
CELILO_EVENT_TAG: event.tag,
|
|
94
|
-
CELILO_EVENT_PACKAGE_NAME: event.package.name,
|
|
95
|
-
CELILO_EVENT_PACKAGE_VERSION: event.package.version,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const DEFAULT_TIMEOUT_SEC = 600;
|
|
100
|
-
|
|
101
|
-
export interface HookRunResult {
|
|
89
|
+
export interface UpstreamHookResult {
|
|
102
90
|
module: string;
|
|
103
91
|
hookName: string;
|
|
104
|
-
|
|
105
|
-
|
|
92
|
+
success: boolean;
|
|
93
|
+
error?: string;
|
|
106
94
|
durationMs: number;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** What running one plan entry needs. Injectable so tests don't need a daemon. */
|
|
98
|
+
export interface UpstreamHookDeps {
|
|
99
|
+
db: DbClient;
|
|
100
|
+
logger: HookLogger;
|
|
110
101
|
}
|
|
111
102
|
|
|
112
103
|
/**
|
|
113
|
-
* Run one plan entry
|
|
114
|
-
*
|
|
115
|
-
*
|
|
104
|
+
* Run one plan entry through the ordinary executor (module-orchestrator-
|
|
105
|
+
* primitives slice 7, tasks.md 7.4). The matched entry's script runs as a
|
|
106
|
+
* `defineHook` under `invokeHook`, jailed, with the verified PublishEvent
|
|
107
|
+
* as its `event` input — the same path `celilo module run-hook` uses for
|
|
108
|
+
* every other hook.
|
|
116
109
|
*
|
|
117
110
|
* Doesn't throw — the result carries the outcome. Caller (dispatcher)
|
|
118
111
|
* logs failures and keeps going to the next plan entry.
|
|
119
112
|
*/
|
|
120
|
-
export async function
|
|
121
|
-
|
|
113
|
+
export async function runUpstreamHook(
|
|
114
|
+
plan: HookDispatchPlan,
|
|
115
|
+
event: PublishEvent,
|
|
116
|
+
deps: UpstreamHookDeps,
|
|
117
|
+
): Promise<UpstreamHookResult> {
|
|
122
118
|
const hookName = plan.hook.name ?? plan.hook.script;
|
|
123
119
|
const startedAt = Date.now();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
child.kill(signal);
|
|
149
|
-
} catch {
|
|
150
|
-
// ignore
|
|
151
|
-
}
|
|
152
|
-
}
|
|
120
|
+
// `HookDefinition.timeout` is milliseconds; the manifest entry declares
|
|
121
|
+
// milliseconds too (schema: "Milliseconds before the hook is killed").
|
|
122
|
+
const definition: HookDefinition = {
|
|
123
|
+
script: plan.hook.script,
|
|
124
|
+
timeout: plan.hook.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
const result = await runNamedHook(
|
|
128
|
+
plan.module.moduleId,
|
|
129
|
+
'on_upstream_publish',
|
|
130
|
+
deps.db,
|
|
131
|
+
deps.logger,
|
|
132
|
+
{
|
|
133
|
+
inputs: { event },
|
|
134
|
+
timeoutMs: definition.timeout,
|
|
135
|
+
hookDefinition: definition,
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
return {
|
|
139
|
+
module: plan.module.moduleId,
|
|
140
|
+
hookName,
|
|
141
|
+
success: result.success,
|
|
142
|
+
error: result.error,
|
|
143
|
+
durationMs: Date.now() - startedAt,
|
|
153
144
|
};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
module: plan.module.moduleId,
|
|
148
|
+
hookName,
|
|
149
|
+
success: false,
|
|
150
|
+
error: error instanceof Error ? error.message : String(error),
|
|
151
|
+
durationMs: Date.now() - startedAt,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
162
155
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
child.on('close', (code) => {
|
|
184
|
-
clearTimeout(timer);
|
|
185
|
-
if (killTimer) clearTimeout(killTimer);
|
|
186
|
-
resolve({
|
|
187
|
-
module: plan.module.moduleId,
|
|
188
|
-
hookName,
|
|
189
|
-
scriptPath: plan.scriptPath,
|
|
190
|
-
exitCode: code,
|
|
191
|
-
durationMs: Date.now() - startedAt,
|
|
192
|
-
stdout,
|
|
193
|
-
stderr,
|
|
194
|
-
timedOut,
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
});
|
|
156
|
+
/**
|
|
157
|
+
* Convenience for the dispatcher: deps over the process's database. Tests
|
|
158
|
+
* inject `runUpstreamHook` instead so they never touch it.
|
|
159
|
+
*/
|
|
160
|
+
export function defaultUpstreamHookDeps(db?: DbClient): UpstreamHookDeps {
|
|
161
|
+
return {
|
|
162
|
+
get db() {
|
|
163
|
+
return db ?? getDb();
|
|
164
|
+
},
|
|
165
|
+
logger: consoleLogger(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function consoleLogger(): HookLogger {
|
|
170
|
+
return {
|
|
171
|
+
info: (message: string) => console.log(message),
|
|
172
|
+
warn: (message: string) => console.warn(message),
|
|
173
|
+
error: (message: string) => console.error(message),
|
|
174
|
+
success: (message: string) => console.log(message),
|
|
175
|
+
};
|
|
198
176
|
}
|