@shipfox/api-definitions 12.3.0 → 14.0.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.
Files changed (223) hide show
  1. package/.turbo/turbo-build.log +11 -11
  2. package/CHANGELOG.md +45 -0
  3. package/dist/core/entities/index.d.ts +1 -1
  4. package/dist/core/entities/index.d.ts.map +1 -1
  5. package/dist/core/entities/index.js +1 -1
  6. package/dist/core/entities/index.js.map +1 -1
  7. package/dist/core/entities/integration-context.d.ts +4 -0
  8. package/dist/core/entities/integration-context.d.ts.map +1 -1
  9. package/dist/core/entities/integration-context.js.map +1 -1
  10. package/dist/core/entities/sync-state.d.ts +10 -4
  11. package/dist/core/entities/sync-state.d.ts.map +1 -1
  12. package/dist/core/entities/sync-state.js +26 -7
  13. package/dist/core/entities/sync-state.js.map +1 -1
  14. package/dist/core/entities/validation-diagnostic.d.ts +8 -0
  15. package/dist/core/entities/validation-diagnostic.d.ts.map +1 -0
  16. package/dist/core/entities/validation-diagnostic.js +3 -0
  17. package/dist/core/entities/validation-diagnostic.js.map +1 -0
  18. package/dist/core/entities/workflow-definition.d.ts +2 -0
  19. package/dist/core/entities/workflow-definition.d.ts.map +1 -1
  20. package/dist/core/entities/workflow-definition.js.map +1 -1
  21. package/dist/core/errors.d.ts +10 -0
  22. package/dist/core/errors.d.ts.map +1 -1
  23. package/dist/core/errors.js +9 -0
  24. package/dist/core/errors.js.map +1 -1
  25. package/dist/core/index.d.ts +2 -1
  26. package/dist/core/index.d.ts.map +1 -1
  27. package/dist/core/index.js +2 -1
  28. package/dist/core/index.js.map +1 -1
  29. package/dist/core/integrations.d.ts +3 -1
  30. package/dist/core/integrations.d.ts.map +1 -1
  31. package/dist/core/integrations.js +10 -2
  32. package/dist/core/integrations.js.map +1 -1
  33. package/dist/core/parse-definition.d.ts +4 -4
  34. package/dist/core/parse-definition.d.ts.map +1 -1
  35. package/dist/core/parse-definition.js +5 -5
  36. package/dist/core/parse-definition.js.map +1 -1
  37. package/dist/core/resolve-definition-at-ref.d.ts +66 -0
  38. package/dist/core/resolve-definition-at-ref.d.ts.map +1 -0
  39. package/dist/core/resolve-definition-at-ref.js +374 -0
  40. package/dist/core/resolve-definition-at-ref.js.map +1 -0
  41. package/dist/core/sync-definitions.d.ts +2 -2
  42. package/dist/core/sync-definitions.d.ts.map +1 -1
  43. package/dist/core/sync-definitions.js +5 -5
  44. package/dist/core/sync-definitions.js.map +1 -1
  45. package/dist/core/validate-definition.d.ts +3 -3
  46. package/dist/core/validate-definition.d.ts.map +1 -1
  47. package/dist/core/validate-definition.js +17 -12
  48. package/dist/core/validate-definition.js.map +1 -1
  49. package/dist/core/workflow-model/cron-trigger.d.ts +1 -1
  50. package/dist/core/workflow-model/cron-trigger.d.ts.map +1 -1
  51. package/dist/core/workflow-model/cron-trigger.js +9 -5
  52. package/dist/core/workflow-model/cron-trigger.js.map +1 -1
  53. package/dist/core/workflow-model/index.d.ts +1 -1
  54. package/dist/core/workflow-model/index.d.ts.map +1 -1
  55. package/dist/core/workflow-model/index.js.map +1 -1
  56. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +4 -2
  57. package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
  58. package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
  59. package/dist/core/workflow-model/normalize-job-listening.d.ts.map +1 -1
  60. package/dist/core/workflow-model/normalize-job-listening.js +51 -30
  61. package/dist/core/workflow-model/normalize-job-listening.js.map +1 -1
  62. package/dist/core/workflow-model/normalize-triggers.d.ts +1 -1
  63. package/dist/core/workflow-model/normalize-triggers.d.ts.map +1 -1
  64. package/dist/core/workflow-model/normalize-triggers.js +48 -18
  65. package/dist/core/workflow-model/normalize-triggers.js.map +1 -1
  66. package/dist/core/workflow-model/normalize-workflow-document.d.ts +1 -1
  67. package/dist/core/workflow-model/normalize-workflow-document.d.ts.map +1 -1
  68. package/dist/core/workflow-model/normalize-workflow-document.js +3 -3
  69. package/dist/core/workflow-model/normalize-workflow-document.js.map +1 -1
  70. package/dist/core/workflow-model/parse-interpolation-field.d.ts.map +1 -1
  71. package/dist/core/workflow-model/parse-interpolation-field.js +1 -15
  72. package/dist/core/workflow-model/parse-interpolation-field.js.map +1 -1
  73. package/dist/core/workflow-model/validate-predicate-expression.d.ts +3 -1
  74. package/dist/core/workflow-model/validate-predicate-expression.d.ts.map +1 -1
  75. package/dist/core/workflow-model/validate-predicate-expression.js +3 -0
  76. package/dist/core/workflow-model/validate-predicate-expression.js.map +1 -1
  77. package/dist/core/workflow-model/validation-issue.d.ts +2 -1
  78. package/dist/core/workflow-model/validation-issue.d.ts.map +1 -1
  79. package/dist/core/workflow-model/validation-issue.js +12 -15
  80. package/dist/core/workflow-model/validation-issue.js.map +1 -1
  81. package/dist/db/db.d.ts +196 -4
  82. package/dist/db/db.d.ts.map +1 -1
  83. package/dist/db/db.js +3 -1
  84. package/dist/db/db.js.map +1 -1
  85. package/dist/db/definition-triggers.js +3 -1
  86. package/dist/db/definition-triggers.js.map +1 -1
  87. package/dist/db/definitions.d.ts +17 -0
  88. package/dist/db/definitions.d.ts.map +1 -1
  89. package/dist/db/definitions.js +190 -23
  90. package/dist/db/definitions.js.map +1 -1
  91. package/dist/db/index.d.ts +2 -1
  92. package/dist/db/index.d.ts.map +1 -1
  93. package/dist/db/index.js +2 -1
  94. package/dist/db/index.js.map +1 -1
  95. package/dist/db/schema/definitions.d.ts +17 -0
  96. package/dist/db/schema/definitions.d.ts.map +1 -1
  97. package/dist/db/schema/definitions.js +8 -0
  98. package/dist/db/schema/definitions.js.map +1 -1
  99. package/dist/db/schema/sync-states.d.ts +7 -3
  100. package/dist/db/schema/sync-states.d.ts.map +1 -1
  101. package/dist/db/schema/sync-states.js +5 -1
  102. package/dist/db/schema/sync-states.js.map +1 -1
  103. package/dist/db/schema/workflows.d.ts +78 -0
  104. package/dist/db/schema/workflows.d.ts.map +1 -0
  105. package/dist/db/schema/workflows.js +17 -0
  106. package/dist/db/schema/workflows.js.map +1 -0
  107. package/dist/db/sync-states.d.ts +2 -2
  108. package/dist/db/sync-states.d.ts.map +1 -1
  109. package/dist/db/sync-states.js +4 -4
  110. package/dist/db/sync-states.js.map +1 -1
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +5 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/metrics/index.d.ts +2 -0
  115. package/dist/metrics/index.d.ts.map +1 -0
  116. package/dist/metrics/index.js +3 -0
  117. package/dist/metrics/index.js.map +1 -0
  118. package/dist/metrics/instance.d.ts +4 -0
  119. package/dist/metrics/instance.d.ts.map +1 -0
  120. package/dist/metrics/instance.js +19 -0
  121. package/dist/metrics/instance.js.map +1 -0
  122. package/dist/presentation/dto/definition.d.ts.map +1 -1
  123. package/dist/presentation/dto/definition.js +11 -4
  124. package/dist/presentation/dto/definition.js.map +1 -1
  125. package/dist/presentation/inter-module.d.ts +9 -1
  126. package/dist/presentation/inter-module.d.ts.map +1 -1
  127. package/dist/presentation/inter-module.js +66 -2
  128. package/dist/presentation/inter-module.js.map +1 -1
  129. package/dist/presentation/routes/create-definition.d.ts.map +1 -1
  130. package/dist/presentation/routes/create-definition.js +16 -3
  131. package/dist/presentation/routes/create-definition.js.map +1 -1
  132. package/dist/presentation/routes/get-definition.d.ts.map +1 -1
  133. package/dist/presentation/routes/get-definition.js +18 -4
  134. package/dist/presentation/routes/get-definition.js.map +1 -1
  135. package/dist/presentation/routes/project-access.d.ts +1 -0
  136. package/dist/presentation/routes/project-access.d.ts.map +1 -1
  137. package/dist/presentation/routes/validate-definition.js +3 -3
  138. package/dist/presentation/routes/validate-definition.js.map +1 -1
  139. package/dist/temporal/activities/sync-activities.d.ts +3 -3
  140. package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
  141. package/dist/temporal/activities/sync-activities.js +7 -4
  142. package/dist/temporal/activities/sync-activities.js.map +1 -1
  143. package/dist/temporal/workflows/definition-sync.js +1 -1
  144. package/dist/temporal/workflows/definition-sync.js.map +1 -1
  145. package/dist/temporal/workflows/index.bundle.js +50 -22
  146. package/dist/tsconfig.test.tsbuildinfo +1 -1
  147. package/drizzle/0003_nebulous_nemesis.sql +11 -0
  148. package/drizzle/0004_workflow_ref_lookup.sql +1 -0
  149. package/drizzle/meta/0003_snapshot.json +627 -0
  150. package/drizzle/meta/0004_snapshot.json +649 -0
  151. package/drizzle/meta/_journal.json +14 -0
  152. package/drizzle.config.ts +1 -0
  153. package/package.json +7 -7
  154. package/src/core/entities/index.ts +1 -1
  155. package/src/core/entities/integration-context.ts +4 -0
  156. package/src/core/entities/sync-state.test.ts +49 -0
  157. package/src/core/entities/sync-state.ts +35 -12
  158. package/src/core/entities/validation-diagnostic.ts +8 -0
  159. package/src/core/entities/workflow-definition.ts +2 -0
  160. package/src/core/errors.ts +26 -0
  161. package/src/core/index.ts +12 -0
  162. package/src/core/integrations.test.ts +38 -0
  163. package/src/core/integrations.ts +10 -1
  164. package/src/core/parse-definition-diagnostics.test.ts +47 -0
  165. package/src/core/parse-definition.test.ts +29 -5
  166. package/src/core/parse-definition.ts +7 -7
  167. package/src/core/resolve-definition-at-ref.test.ts +792 -0
  168. package/src/core/resolve-definition-at-ref.ts +551 -0
  169. package/src/core/sync-definitions.test.ts +61 -2
  170. package/src/core/sync-definitions.ts +7 -7
  171. package/src/core/validate-definition.test.ts +31 -22
  172. package/src/core/validate-definition.ts +21 -13
  173. package/src/core/workflow-model/cron-trigger.ts +6 -2
  174. package/src/core/workflow-model/index.ts +1 -0
  175. package/src/core/workflow-model/invalid-workflow-model-error.ts +5 -1
  176. package/src/core/workflow-model/normalize-job-listening.ts +55 -23
  177. package/src/core/workflow-model/normalize-triggers.ts +44 -15
  178. package/src/core/workflow-model/normalize-workflow-document.test.ts +494 -36
  179. package/src/core/workflow-model/normalize-workflow-document.ts +8 -4
  180. package/src/core/workflow-model/parse-interpolation-field.ts +1 -23
  181. package/src/core/workflow-model/validate-predicate-expression.ts +6 -0
  182. package/src/core/workflow-model/validation-issue.ts +8 -13
  183. package/src/db/db.ts +7 -1
  184. package/src/db/definition-triggers.test.ts +44 -0
  185. package/src/db/definition-triggers.ts +1 -1
  186. package/src/db/definitions.test.ts +300 -1
  187. package/src/db/definitions.ts +282 -28
  188. package/src/db/index.ts +2 -0
  189. package/src/db/schema/definitions.test.ts +1 -0
  190. package/src/db/schema/definitions.ts +11 -0
  191. package/src/db/schema/sync-states.ts +12 -3
  192. package/src/db/schema/workflows.ts +23 -0
  193. package/src/db/sync-states.test.ts +87 -18
  194. package/src/db/sync-states.ts +6 -6
  195. package/src/db/workflow-lineage-migration.test.ts +158 -0
  196. package/src/index.ts +3 -1
  197. package/src/metrics/index.ts +1 -0
  198. package/src/metrics/instance.test.ts +50 -0
  199. package/src/metrics/instance.ts +23 -0
  200. package/src/presentation/dto/definition.test.ts +89 -1
  201. package/src/presentation/dto/definition.ts +8 -5
  202. package/src/presentation/inter-module.test.ts +177 -0
  203. package/src/presentation/inter-module.ts +77 -4
  204. package/src/presentation/routes/create-definition.test.ts +38 -0
  205. package/src/presentation/routes/create-definition.ts +20 -3
  206. package/src/presentation/routes/get-definition.test.ts +125 -11
  207. package/src/presentation/routes/get-definition.ts +21 -4
  208. package/src/presentation/routes/list-definitions.test.ts +1 -1
  209. package/src/presentation/routes/validate-definition.test.ts +1 -1
  210. package/src/presentation/routes/validate-definition.ts +3 -3
  211. package/src/presentation/subscribers/on-integration-connection-available.test.ts +3 -0
  212. package/src/temporal/activities/sync-activities.test.ts +46 -2
  213. package/src/temporal/activities/sync-activities.ts +9 -7
  214. package/src/temporal/workflows/definition-sync.ts +1 -1
  215. package/test/factories/definition.ts +1 -0
  216. package/test/globalSetup.ts +2 -0
  217. package/tsconfig.build.tsbuildinfo +1 -1
  218. package/dist/core/entities/validation-warning.d.ts +0 -6
  219. package/dist/core/entities/validation-warning.d.ts.map +0 -1
  220. package/dist/core/entities/validation-warning.js +0 -3
  221. package/dist/core/entities/validation-warning.js.map +0 -1
  222. package/src/core/entities/validation-warning.ts +0 -5
  223. package/src/core/parse-definition-warnings.test.ts +0 -33
@@ -1,11 +1,11 @@
1
1
  import {and, desc, eq} from 'drizzle-orm';
2
2
  import type {
3
+ DefinitionSyncDiagnostic,
3
4
  DefinitionSyncErrorCode,
4
5
  DefinitionSyncState,
5
6
  DefinitionSyncStatus,
6
- DefinitionSyncWarning,
7
7
  } from '#core/entities/sync-state.js';
8
- import {limitDefinitionSyncWarnings} from '#core/entities/sync-state.js';
8
+ import {limitDefinitionSyncDiagnostics} from '#core/entities/sync-state.js';
9
9
  import {db} from './db.js';
10
10
  import {definitionSyncStates, toDefinitionSyncState} from './schema/sync-states.js';
11
11
 
@@ -20,7 +20,7 @@ export interface MarkDefinitionSyncParams extends DefinitionSyncStateKey {
20
20
  status: DefinitionSyncStatus;
21
21
  lastErrorCode?: DefinitionSyncErrorCode | null | undefined;
22
22
  lastErrorMessage?: string | null | undefined;
23
- warnings?: readonly DefinitionSyncWarning[] | null | undefined;
23
+ diagnostics?: readonly DefinitionSyncDiagnostic[] | null | undefined;
24
24
  startedAt?: Date | null | undefined;
25
25
  finishedAt?: Date | null | undefined;
26
26
  }
@@ -29,7 +29,7 @@ export async function markDefinitionSyncState(
29
29
  params: MarkDefinitionSyncParams,
30
30
  ): Promise<DefinitionSyncState> {
31
31
  const now = new Date();
32
- const warnings = limitDefinitionSyncWarnings(params.warnings ?? []);
32
+ const diagnostics = limitDefinitionSyncDiagnostics(params.diagnostics ?? []);
33
33
  const [row] = await db()
34
34
  .insert(definitionSyncStates)
35
35
  .values({
@@ -40,7 +40,7 @@ export async function markDefinitionSyncState(
40
40
  status: params.status,
41
41
  lastErrorCode: params.lastErrorCode ?? null,
42
42
  lastErrorMessage: params.lastErrorMessage ?? null,
43
- warnings,
43
+ warnings: diagnostics,
44
44
  startedAt: params.startedAt ?? null,
45
45
  finishedAt: params.finishedAt ?? null,
46
46
  updatedAt: now,
@@ -56,7 +56,7 @@ export async function markDefinitionSyncState(
56
56
  status: params.status,
57
57
  lastErrorCode: params.lastErrorCode ?? null,
58
58
  lastErrorMessage: params.lastErrorMessage ?? null,
59
- warnings,
59
+ warnings: diagnostics,
60
60
  ...(params.startedAt !== undefined ? {startedAt: params.startedAt} : {}),
61
61
  ...(params.finishedAt !== undefined ? {finishedAt: params.finishedAt} : {}),
62
62
  updatedAt: now,
@@ -0,0 +1,158 @@
1
+ import {randomUUID} from 'node:crypto';
2
+ import {readFile} from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import pg from 'pg';
5
+ import {migrationsPath} from '#db/index.js';
6
+
7
+ const ADMIN_DATABASE = process.env.POSTGRES_DATABASE ?? 'api_test';
8
+
9
+ function connectTo(database: string): Promise<pg.Client> {
10
+ const client = new pg.Client({
11
+ host: process.env.POSTGRES_DIRECT_HOST ?? process.env.POSTGRES_HOST ?? 'localhost',
12
+ port: Number(process.env.POSTGRES_PORT ?? 5432),
13
+ database,
14
+ user: process.env.POSTGRES_USERNAME ?? 'shipfox',
15
+ password: process.env.POSTGRES_PASSWORD ?? 'password',
16
+ });
17
+ return client.connect().then(() => client);
18
+ }
19
+
20
+ async function applyMigration(client: pg.Client, fileName: string): Promise<void> {
21
+ const source = await readFile(path.join(migrationsPath, fileName), 'utf8');
22
+ const statements = source
23
+ .split('--> statement-breakpoint')
24
+ .map((statement) => statement.trim())
25
+ .filter((statement) => statement.length > 0);
26
+ for (const statement of statements) {
27
+ await client.query(statement);
28
+ }
29
+ }
30
+
31
+ async function insertDefinition(
32
+ client: pg.Client,
33
+ params: {
34
+ id: string;
35
+ projectId: string;
36
+ configPath?: string | null;
37
+ source?: 'manual' | 'vcs';
38
+ ref?: string | null;
39
+ deleted?: boolean;
40
+ },
41
+ ): Promise<void> {
42
+ await client.query(
43
+ `INSERT INTO "definitions_workflow_definitions"
44
+ ("id", "project_id", "config_path", "source", "ref", "name", "definition", "deleted_at")
45
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
46
+ [
47
+ params.id,
48
+ params.projectId,
49
+ params.configPath ?? null,
50
+ params.source ?? 'manual',
51
+ params.ref ?? null,
52
+ `Workflow ${params.id.slice(0, 8)}`,
53
+ JSON.stringify({document: {name: 'x'}, model: {}}),
54
+ params.deleted ? new Date().toISOString() : null,
55
+ ],
56
+ );
57
+ }
58
+
59
+ describe('workflow lineage schema migration (0003)', () => {
60
+ test('adds nullable lineage storage without backfilling existing definitions', async () => {
61
+ const scratch = `api_test_lineage_${randomUUID().replaceAll('-', '')}`;
62
+ const admin = await connectTo(ADMIN_DATABASE);
63
+ let target: pg.Client | undefined;
64
+ try {
65
+ await admin.query(`CREATE DATABASE "${scratch}"`);
66
+ target = await connectTo(scratch);
67
+
68
+ await applyMigration(target, '0000_initial.sql');
69
+ await applyMigration(target, '0001_definition_list_pagination.sql');
70
+ await applyMigration(target, '0002_lumpy_ben_parker.sql');
71
+
72
+ const projectA = randomUUID();
73
+ const projectB = randomUUID();
74
+ const rowA = randomUUID();
75
+ const rowManual = randomUUID();
76
+ const rowVcs = randomUUID();
77
+ const rowBranchMain = randomUUID();
78
+ const rowBranchDev = randomUUID();
79
+ const rowGone = randomUUID();
80
+ const rowNoPath = randomUUID();
81
+ const rowNoPathSecond = randomUUID();
82
+
83
+ await insertDefinition(target, {
84
+ id: rowA,
85
+ projectId: projectA,
86
+ configPath: 'a.yml',
87
+ source: 'vcs',
88
+ ref: 'main',
89
+ });
90
+ await insertDefinition(target, {
91
+ id: rowManual,
92
+ projectId: projectA,
93
+ configPath: 'shared.yml',
94
+ });
95
+ await insertDefinition(target, {
96
+ id: rowVcs,
97
+ projectId: projectA,
98
+ configPath: 'shared.yml',
99
+ source: 'vcs',
100
+ ref: 'main',
101
+ });
102
+ await insertDefinition(target, {
103
+ id: rowBranchMain,
104
+ projectId: projectA,
105
+ configPath: 'branches.yml',
106
+ source: 'vcs',
107
+ ref: 'main',
108
+ });
109
+ await insertDefinition(target, {
110
+ id: rowBranchDev,
111
+ projectId: projectA,
112
+ configPath: 'branches.yml',
113
+ source: 'vcs',
114
+ ref: 'dev',
115
+ });
116
+ await insertDefinition(target, {
117
+ id: rowGone,
118
+ projectId: projectA,
119
+ configPath: 'gone.yml',
120
+ source: 'vcs',
121
+ ref: 'main',
122
+ deleted: true,
123
+ });
124
+ await insertDefinition(target, {id: rowNoPath, projectId: projectB});
125
+ await insertDefinition(target, {id: rowNoPathSecond, projectId: projectB});
126
+
127
+ await applyMigration(target, '0003_nebulous_nemesis.sql');
128
+
129
+ const rows = (
130
+ await target.query<{id: string; workflow_id: string | null; config_path: string | null}>(
131
+ `SELECT "id", "workflow_id", "config_path"
132
+ FROM "definitions_workflow_definitions" ORDER BY "id"`,
133
+ )
134
+ ).rows;
135
+ const workflows = (
136
+ await target.query<{id: string; config_path: string | null}>(
137
+ `SELECT "id", "config_path" FROM "definitions_workflows" ORDER BY "id"`,
138
+ )
139
+ ).rows;
140
+
141
+ expect(rows).toHaveLength(8);
142
+ expect(rows.every((row) => row.workflow_id === null)).toBe(true);
143
+ expect(workflows).toHaveLength(0);
144
+
145
+ // The unique (project_id, config_path) constraint accepts a second project
146
+ // with the same path.
147
+ await target.query(
148
+ `INSERT INTO "definitions_workflows" ("id", "project_id", "config_path")
149
+ VALUES ($1, $2, 'a.yml')`,
150
+ [randomUUID(), projectB],
151
+ );
152
+ } finally {
153
+ await target?.end();
154
+ await admin.query(`DROP DATABASE IF EXISTS "${scratch}" WITH (FORCE)`);
155
+ await admin.end();
156
+ }
157
+ });
158
+ });
package/src/index.ts CHANGED
@@ -98,6 +98,8 @@ export function createDefinitionsModule({
98
98
  workflows: [],
99
99
  },
100
100
  ],
101
- interModulePresentations: [createDefinitionsInterModulePresentation()],
101
+ interModulePresentations: [
102
+ createDefinitionsInterModulePresentation({projects, agent, integrations}),
103
+ ],
102
104
  };
103
105
  }
@@ -0,0 +1 @@
1
+ export {recordDefinitionRefResolution} from './instance.js';
@@ -0,0 +1,50 @@
1
+ const metricMocks = vi.hoisted(() => {
2
+ const counters = new Map<string, {add: ReturnType<typeof vi.fn>}>();
3
+ const createCounter = vi.fn((name: string) => {
4
+ const counter = {add: vi.fn()};
5
+ counters.set(name, counter);
6
+ return counter;
7
+ });
8
+
9
+ return {counters, createCounter};
10
+ });
11
+
12
+ vi.mock('@shipfox/node-opentelemetry', () => ({
13
+ instanceMetrics: {
14
+ getMeter: () => ({createCounter: metricMocks.createCounter}),
15
+ },
16
+ }));
17
+
18
+ const metrics = await import('./instance.js');
19
+
20
+ function counterAdd(name: string): ReturnType<typeof vi.fn> {
21
+ const counter = metricMocks.counters.get(name);
22
+ if (!counter) throw new Error(`Missing counter: ${name}`);
23
+ return counter.add;
24
+ }
25
+
26
+ describe('definition at-ref metrics', () => {
27
+ beforeEach(() => {
28
+ counterAdd('definitions_ref_resolutions').mockReset();
29
+ });
30
+
31
+ test.each([
32
+ 'resolved',
33
+ 'ref-moved',
34
+ 'content-too-large',
35
+ 'too-many-files',
36
+ 'source-unavailable',
37
+ ] as const)('records the %s outcome', (outcome) => {
38
+ metrics.recordDefinitionRefResolution(outcome);
39
+
40
+ expect(counterAdd('definitions_ref_resolutions')).toHaveBeenCalledWith(1, {outcome});
41
+ });
42
+
43
+ test('does not let metric failures affect resolution callers', () => {
44
+ counterAdd('definitions_ref_resolutions').mockImplementationOnce(() => {
45
+ throw new Error('metrics unavailable');
46
+ });
47
+
48
+ expect(() => metrics.recordDefinitionRefResolution('resolved')).not.toThrow();
49
+ });
50
+ });
@@ -0,0 +1,23 @@
1
+ import {instanceMetrics} from '@shipfox/node-opentelemetry';
2
+ import type {DefinitionAtRefErrorCode} from '#core/errors.js';
3
+
4
+ const meter = instanceMetrics.getMeter('definitions');
5
+
6
+ export type DefinitionRefResolutionOutcome = DefinitionAtRefErrorCode | 'resolved';
7
+
8
+ const refResolutionCount = meter.createCounter<{outcome: DefinitionRefResolutionOutcome}>(
9
+ 'definitions_ref_resolutions',
10
+ {description: 'Workflow definition resolutions at a git ref by outcome'},
11
+ );
12
+
13
+ function recordMetric(record: () => void): void {
14
+ try {
15
+ record();
16
+ } catch {
17
+ // Metrics must not affect definition resolution outcomes.
18
+ }
19
+ }
20
+
21
+ export function recordDefinitionRefResolution(outcome: DefinitionRefResolutionOutcome): void {
22
+ recordMetric(() => refResolutionCount.add(1, {outcome}));
23
+ }
@@ -1,7 +1,8 @@
1
+ import type {DefinitionSyncState} from '#core/entities/sync-state.js';
1
2
  import type {WorkflowDefinition} from '#core/entities/workflow-definition.js';
2
3
  import {normalizeWorkflowDocument} from '#core/workflow-model/index.js';
3
4
  import {agentValidationCatalog} from '#test/agent-validation-catalog.js';
4
- import {toDefinitionDto} from './definition.js';
5
+ import {toDefinitionDto, toDefinitionSyncSummaryDto} from './definition.js';
5
6
 
6
7
  describe('toDefinitionDto', () => {
7
8
  it('maps the workflow definition entity to the public DTO shape', () => {
@@ -22,6 +23,7 @@ describe('toDefinitionDto', () => {
22
23
  };
23
24
  const definition: WorkflowDefinition = {
24
25
  id: '019e98ab-6656-7ca1-b9ad-1ca4442c479d',
26
+ workflowId: '019e98ab-6656-7ca1-b9ad-1ca4442c479e',
25
27
  projectId: '019e98ab-b90f-7265-b13c-8b441c991381',
26
28
  configPath: '.shipfox/workflows/manual.yml',
27
29
  source: 'manual',
@@ -57,4 +59,90 @@ describe('toDefinitionDto', () => {
57
59
  updated_at: '2026-06-09T10:00:02.000Z',
58
60
  });
59
61
  });
62
+
63
+ it('computes manual_trigger from the model so an inert manual trigger hides the Run button', () => {
64
+ const document = {
65
+ name: 'Manual workflow',
66
+ runner: 'ubuntu-latest',
67
+ triggers: {
68
+ run_now: {
69
+ source: 'manual',
70
+ event: 'fire',
71
+ filter: 'event.ref == "refs/heads/main"',
72
+ },
73
+ },
74
+ jobs: {
75
+ build: {
76
+ steps: [{run: 'pnpm build'}],
77
+ },
78
+ },
79
+ };
80
+ const definition: WorkflowDefinition = {
81
+ id: '019e98ab-6656-7ca1-b9ad-1ca4442c479d',
82
+ workflowId: '019e98ab-6656-7ca1-b9ad-1ca4442c479e',
83
+ projectId: '019e98ab-b90f-7265-b13c-8b441c991381',
84
+ configPath: '.shipfox/workflows/manual.yml',
85
+ source: 'manual',
86
+ sha: null,
87
+ ref: null,
88
+ name: document.name,
89
+ definition: document,
90
+ document,
91
+ // The manual trigger is inert: it carries a trigger-scoped error and is
92
+ // excluded from the model, while the document keeps the authored entry.
93
+ model: normalizeWorkflowDocument(document, {agentValidationCatalog}),
94
+ sourceSnapshot: null,
95
+ contentHash: null,
96
+ fetchedAt: new Date('2026-06-09T10:00:00.000Z'),
97
+ createdAt: new Date('2026-06-09T10:00:01.000Z'),
98
+ updatedAt: new Date('2026-06-09T10:00:02.000Z'),
99
+ deletedAt: null,
100
+ };
101
+
102
+ const result = toDefinitionDto(definition);
103
+
104
+ expect(result.workflow_document).toEqual(document);
105
+ expect((result.workflow_model as {triggers: readonly unknown[]}).triggers).toEqual([]);
106
+ expect(result.manual_trigger).toBeNull();
107
+ });
108
+ });
109
+
110
+ describe('toDefinitionSyncSummaryDto', () => {
111
+ it('maps the workflow file path to the public DTO field', () => {
112
+ const syncState = {
113
+ id: '019e98ab-6656-7ca1-b9ad-1ca4442c479d',
114
+ projectId: '019e98ab-b90f-7265-b13c-8b441c991381',
115
+ sourceConnectionId: '019e98ab-b90f-7265-b13c-8b441c991382',
116
+ sourceExternalRepositoryId: 'gitea:owner/platform',
117
+ ref: 'main',
118
+ status: 'succeeded',
119
+ lastErrorCode: null,
120
+ lastErrorMessage: null,
121
+ diagnostics: [
122
+ {
123
+ code: 'warning-code',
124
+ message: 'Warning',
125
+ path: 'jobs.build.steps.0.run',
126
+ filePath: '.shipfox/workflows/ci.yml',
127
+ severity: 'warning',
128
+ },
129
+ ],
130
+ startedAt: new Date('2026-06-09T10:00:00.000Z'),
131
+ finishedAt: new Date('2026-06-09T10:00:01.000Z'),
132
+ createdAt: new Date('2026-06-09T10:00:00.000Z'),
133
+ updatedAt: new Date('2026-06-09T10:00:01.000Z'),
134
+ } satisfies DefinitionSyncState;
135
+
136
+ expect(toDefinitionSyncSummaryDto(syncState)).toMatchObject({
137
+ diagnostics: [
138
+ {
139
+ code: 'warning-code',
140
+ message: 'Warning',
141
+ path: 'jobs.build.steps.0.run',
142
+ file_path: '.shipfox/workflows/ci.yml',
143
+ severity: 'warning',
144
+ },
145
+ ],
146
+ });
147
+ });
60
148
  });
@@ -4,9 +4,9 @@ import type {WorkflowDefinition} from '#core/entities/workflow-definition.js';
4
4
  import {UNRESOLVED_SYNC_REF} from '#core/sync-definitions.js';
5
5
 
6
6
  export function toDefinitionDto(definition: WorkflowDefinition): DefinitionDto {
7
- const manualEntry = Object.entries(definition.document.triggers ?? {}).find(
8
- ([, trigger]) => trigger.source === 'manual',
9
- );
7
+ // The model excludes inert triggers, so an inert manual trigger yields no Run
8
+ // button instead of a fire-route 404. The document keeps the authored entry.
9
+ const manualTrigger = definition.model.triggers.find((trigger) => trigger.source === 'manual');
10
10
  return {
11
11
  id: definition.id,
12
12
  project_id: definition.projectId,
@@ -17,7 +17,7 @@ export function toDefinitionDto(definition: WorkflowDefinition): DefinitionDto {
17
17
  name: definition.name,
18
18
  workflow_document: definition.document,
19
19
  workflow_model: definition.model,
20
- manual_trigger: manualEntry ? {name: manualEntry[0]} : null,
20
+ manual_trigger: manualTrigger ? {name: manualTrigger.key} : null,
21
21
  fetched_at: definition.fetchedAt.toISOString(),
22
22
  created_at: definition.createdAt.toISOString(),
23
23
  updated_at: definition.updatedAt.toISOString(),
@@ -37,6 +37,9 @@ export function toDefinitionSyncSummaryDto(
37
37
  finished_at: syncState.finishedAt?.toISOString() ?? null,
38
38
  last_error_code: syncState.lastErrorCode,
39
39
  last_error_message: syncState.lastErrorMessage,
40
- warnings: syncState.warnings,
40
+ diagnostics: syncState.diagnostics.map(({filePath, ...diagnostic}) => ({
41
+ ...diagnostic,
42
+ ...(filePath === undefined ? {} : {file_path: filePath}),
43
+ })),
41
44
  };
42
45
  }
@@ -0,0 +1,177 @@
1
+ import {definitionsInterModuleContract} from '@shipfox/api-definitions-dto/inter-module';
2
+ import {isInterModuleKnownError} from '@shipfox/inter-module';
3
+ import {DefinitionAtRefError} from '#core/errors.js';
4
+ import {createDefinitionsInterModulePresentation} from './inter-module.js';
5
+
6
+ const mocks = vi.hoisted(() => ({
7
+ getDefinitionById: vi.fn(),
8
+ listDefinitionsAtRef: vi.fn(),
9
+ resolveDefinitionAtRef: vi.fn(),
10
+ }));
11
+
12
+ vi.mock('#db/definitions.js', () => ({
13
+ getDefinitionById: mocks.getDefinitionById,
14
+ }));
15
+
16
+ vi.mock('#core/index.js', async (importOriginal) => {
17
+ const original = await importOriginal<typeof import('#core/index.js')>();
18
+ return {
19
+ ...original,
20
+ listDefinitionsAtRef: mocks.listDefinitionsAtRef,
21
+ resolveDefinitionAtRef: mocks.resolveDefinitionAtRef,
22
+ };
23
+ });
24
+
25
+ const PROJECT_ID = '00000000-0000-4000-8000-000000000001';
26
+ const REF = 'fix-branch';
27
+ const CONFIG_PATH = '.shipfox/workflows/ci.yml';
28
+ const COMMIT = 'a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d0';
29
+
30
+ function presentation() {
31
+ return createDefinitionsInterModulePresentation({
32
+ projects: {} as never,
33
+ agent: {} as never,
34
+ integrations: {} as never,
35
+ });
36
+ }
37
+
38
+ async function rejection(promise: Promise<unknown> | unknown): Promise<unknown> {
39
+ return await Promise.resolve(promise).catch((error: unknown) => error);
40
+ }
41
+
42
+ describe('definitions inter-module presentation', () => {
43
+ beforeEach(() => {
44
+ mocks.resolveDefinitionAtRef.mockReset();
45
+ mocks.listDefinitionsAtRef.mockReset();
46
+ mocks.getDefinitionById.mockReset();
47
+ });
48
+
49
+ it('resolves a definition at a ref through the core method', async () => {
50
+ const resolved = {
51
+ workflow: {id: '00000000-0000-4000-8000-000000000002', configPath: CONFIG_PATH},
52
+ commit: COMMIT,
53
+ model: {
54
+ version: 2,
55
+ model: {kind: 'workflow', name: 'CI', triggers: [], jobs: [], dependencies: []},
56
+ },
57
+ sourceSnapshot: {content: 'name: CI', format: 'yaml'},
58
+ triggers: {},
59
+ warnings: [],
60
+ };
61
+ mocks.resolveDefinitionAtRef.mockResolvedValue(resolved);
62
+ const controller = new AbortController();
63
+
64
+ const result = await presentation().handlers.resolveDefinitionAtRef(
65
+ {projectId: PROJECT_ID, ref: REF, configPath: CONFIG_PATH},
66
+ {signal: controller.signal},
67
+ );
68
+
69
+ expect(result).toEqual(resolved);
70
+ expect(mocks.resolveDefinitionAtRef).toHaveBeenCalledWith(
71
+ expect.objectContaining({
72
+ projectId: PROJECT_ID,
73
+ ref: REF,
74
+ configPath: CONFIG_PATH,
75
+ signal: controller.signal,
76
+ }),
77
+ );
78
+ });
79
+
80
+ it('maps every resolution domain error to its known error details', async () => {
81
+ const method = definitionsInterModuleContract.methods.resolveDefinitionAtRef;
82
+ const cases: Array<{
83
+ code: DefinitionAtRefError['code'];
84
+ details: Record<string, unknown>;
85
+ expected: Record<string, unknown>;
86
+ }> = [
87
+ {
88
+ code: 'project-not-found',
89
+ details: {projectId: PROJECT_ID},
90
+ expected: {projectId: PROJECT_ID},
91
+ },
92
+ {code: 'ref-not-found', details: {ref: REF}, expected: {ref: REF}},
93
+ {code: 'ref-invalid', details: {ref: REF}, expected: {ref: REF}},
94
+ {
95
+ code: 'ref-moved',
96
+ details: {ref: REF, expectedCommit: COMMIT},
97
+ expected: {ref: REF, expectedCommit: COMMIT},
98
+ },
99
+ {
100
+ code: 'file-not-found',
101
+ details: {ref: REF, configPath: CONFIG_PATH},
102
+ expected: {ref: REF, configPath: CONFIG_PATH},
103
+ },
104
+ {
105
+ code: 'content-too-large',
106
+ details: {configPath: CONFIG_PATH},
107
+ expected: {configPath: CONFIG_PATH},
108
+ },
109
+ {
110
+ code: 'invalid-definition',
111
+ details: {errors: [{message: 'Invalid YAML', path: 'jobs'}]},
112
+ expected: {errors: [{message: 'Invalid YAML', path: 'jobs'}]},
113
+ },
114
+ {code: 'source-unavailable', details: {}, expected: {}},
115
+ ];
116
+
117
+ for (const {code, details, expected} of cases) {
118
+ mocks.resolveDefinitionAtRef.mockRejectedValueOnce(
119
+ new DefinitionAtRefError(code, `boom: ${code}`, details),
120
+ );
121
+ const error = await rejection(
122
+ presentation().handlers.resolveDefinitionAtRef(
123
+ {projectId: PROJECT_ID, ref: REF, configPath: CONFIG_PATH},
124
+ {signal: new AbortController().signal},
125
+ ),
126
+ );
127
+ expect(isInterModuleKnownError(method, error)).toBe(true);
128
+ expect((error as {code: string}).code).toBe(code);
129
+ expect((error as {details: unknown}).details).toEqual(expected);
130
+ }
131
+ });
132
+
133
+ it('re-throws unknown errors without mapping them', async () => {
134
+ const boom = new Error('unexpected');
135
+ mocks.resolveDefinitionAtRef.mockRejectedValue(boom);
136
+ const error = await rejection(
137
+ presentation().handlers.resolveDefinitionAtRef(
138
+ {projectId: PROJECT_ID, ref: REF, configPath: CONFIG_PATH},
139
+ {signal: new AbortController().signal},
140
+ ),
141
+ );
142
+ expect(error).toBe(boom);
143
+ });
144
+
145
+ it('lists definitions at a ref with only the declared listing errors', async () => {
146
+ const method = definitionsInterModuleContract.methods.listDefinitionsAtRef;
147
+ mocks.listDefinitionsAtRef.mockRejectedValueOnce(
148
+ new DefinitionAtRefError('ref-not-found', 'boom', {ref: REF}),
149
+ );
150
+ const error = await rejection(
151
+ presentation().handlers.listDefinitionsAtRef(
152
+ {projectId: PROJECT_ID, ref: REF},
153
+ {signal: new AbortController().signal},
154
+ ),
155
+ );
156
+ expect(isInterModuleKnownError(method, error)).toBe(true);
157
+ expect((error as {code: string}).code).toBe('ref-not-found');
158
+ });
159
+
160
+ it('maps the listing file-limit failure to its known error', async () => {
161
+ const method = definitionsInterModuleContract.methods.listDefinitionsAtRef;
162
+ mocks.listDefinitionsAtRef.mockRejectedValueOnce(
163
+ new DefinitionAtRefError('too-many-files', 'too many files'),
164
+ );
165
+
166
+ const error = await rejection(
167
+ presentation().handlers.listDefinitionsAtRef(
168
+ {projectId: PROJECT_ID, ref: REF},
169
+ {signal: new AbortController().signal},
170
+ ),
171
+ );
172
+
173
+ expect(isInterModuleKnownError(method, error)).toBe(true);
174
+ expect((error as {code: string}).code).toBe('too-many-files');
175
+ expect((error as {details: unknown}).details).toEqual({});
176
+ });
177
+ });