@shipfox/api-definitions 13.1.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.
- package/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +35 -0
- package/dist/core/entities/index.d.ts +1 -1
- package/dist/core/entities/index.d.ts.map +1 -1
- package/dist/core/entities/index.js +1 -1
- package/dist/core/entities/index.js.map +1 -1
- package/dist/core/entities/integration-context.d.ts +4 -0
- package/dist/core/entities/integration-context.d.ts.map +1 -1
- package/dist/core/entities/integration-context.js.map +1 -1
- package/dist/core/entities/sync-state.d.ts +10 -4
- package/dist/core/entities/sync-state.d.ts.map +1 -1
- package/dist/core/entities/sync-state.js +26 -7
- package/dist/core/entities/sync-state.js.map +1 -1
- package/dist/core/entities/validation-diagnostic.d.ts +8 -0
- package/dist/core/entities/validation-diagnostic.d.ts.map +1 -0
- package/dist/core/entities/validation-diagnostic.js +3 -0
- package/dist/core/entities/validation-diagnostic.js.map +1 -0
- package/dist/core/entities/workflow-definition.d.ts +2 -0
- package/dist/core/entities/workflow-definition.d.ts.map +1 -1
- package/dist/core/entities/workflow-definition.js.map +1 -1
- package/dist/core/errors.d.ts +10 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +9 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/integrations.d.ts +3 -1
- package/dist/core/integrations.d.ts.map +1 -1
- package/dist/core/integrations.js +10 -2
- package/dist/core/integrations.js.map +1 -1
- package/dist/core/parse-definition.d.ts +4 -4
- package/dist/core/parse-definition.d.ts.map +1 -1
- package/dist/core/parse-definition.js +5 -5
- package/dist/core/parse-definition.js.map +1 -1
- package/dist/core/resolve-definition-at-ref.d.ts +66 -0
- package/dist/core/resolve-definition-at-ref.d.ts.map +1 -0
- package/dist/core/resolve-definition-at-ref.js +374 -0
- package/dist/core/resolve-definition-at-ref.js.map +1 -0
- package/dist/core/sync-definitions.d.ts +2 -2
- package/dist/core/sync-definitions.d.ts.map +1 -1
- package/dist/core/sync-definitions.js +5 -5
- package/dist/core/sync-definitions.js.map +1 -1
- package/dist/core/validate-definition.d.ts +3 -3
- package/dist/core/validate-definition.d.ts.map +1 -1
- package/dist/core/validate-definition.js +17 -12
- package/dist/core/validate-definition.js.map +1 -1
- package/dist/core/workflow-model/cron-trigger.d.ts +1 -1
- package/dist/core/workflow-model/cron-trigger.d.ts.map +1 -1
- package/dist/core/workflow-model/cron-trigger.js +9 -5
- package/dist/core/workflow-model/cron-trigger.js.map +1 -1
- package/dist/core/workflow-model/index.d.ts +1 -1
- package/dist/core/workflow-model/index.d.ts.map +1 -1
- package/dist/core/workflow-model/index.js.map +1 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.d.ts +4 -2
- package/dist/core/workflow-model/invalid-workflow-model-error.d.ts.map +1 -1
- package/dist/core/workflow-model/invalid-workflow-model-error.js.map +1 -1
- package/dist/core/workflow-model/normalize-job-listening.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-job-listening.js +51 -30
- package/dist/core/workflow-model/normalize-job-listening.js.map +1 -1
- package/dist/core/workflow-model/normalize-triggers.d.ts +1 -1
- package/dist/core/workflow-model/normalize-triggers.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-triggers.js +48 -18
- package/dist/core/workflow-model/normalize-triggers.js.map +1 -1
- package/dist/core/workflow-model/normalize-workflow-document.d.ts +1 -1
- package/dist/core/workflow-model/normalize-workflow-document.d.ts.map +1 -1
- package/dist/core/workflow-model/normalize-workflow-document.js +3 -3
- package/dist/core/workflow-model/normalize-workflow-document.js.map +1 -1
- package/dist/core/workflow-model/parse-interpolation-field.d.ts.map +1 -1
- package/dist/core/workflow-model/parse-interpolation-field.js +1 -15
- package/dist/core/workflow-model/parse-interpolation-field.js.map +1 -1
- package/dist/core/workflow-model/validate-predicate-expression.d.ts +3 -1
- package/dist/core/workflow-model/validate-predicate-expression.d.ts.map +1 -1
- package/dist/core/workflow-model/validate-predicate-expression.js +3 -0
- package/dist/core/workflow-model/validate-predicate-expression.js.map +1 -1
- package/dist/core/workflow-model/validation-issue.d.ts +2 -1
- package/dist/core/workflow-model/validation-issue.d.ts.map +1 -1
- package/dist/core/workflow-model/validation-issue.js +12 -15
- package/dist/core/workflow-model/validation-issue.js.map +1 -1
- package/dist/db/db.d.ts +196 -4
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +3 -1
- package/dist/db/db.js.map +1 -1
- package/dist/db/definition-triggers.js +3 -1
- package/dist/db/definition-triggers.js.map +1 -1
- package/dist/db/definitions.d.ts +17 -0
- package/dist/db/definitions.d.ts.map +1 -1
- package/dist/db/definitions.js +190 -23
- package/dist/db/definitions.js.map +1 -1
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +2 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema/definitions.d.ts +17 -0
- package/dist/db/schema/definitions.d.ts.map +1 -1
- package/dist/db/schema/definitions.js +8 -0
- package/dist/db/schema/definitions.js.map +1 -1
- package/dist/db/schema/sync-states.d.ts +7 -3
- package/dist/db/schema/sync-states.d.ts.map +1 -1
- package/dist/db/schema/sync-states.js +5 -1
- package/dist/db/schema/sync-states.js.map +1 -1
- package/dist/db/schema/workflows.d.ts +78 -0
- package/dist/db/schema/workflows.d.ts.map +1 -0
- package/dist/db/schema/workflows.js +17 -0
- package/dist/db/schema/workflows.js.map +1 -0
- package/dist/db/sync-states.d.ts +2 -2
- package/dist/db/sync-states.d.ts.map +1 -1
- package/dist/db/sync-states.js +4 -4
- package/dist/db/sync-states.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +4 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +19 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/presentation/dto/definition.d.ts.map +1 -1
- package/dist/presentation/dto/definition.js +11 -4
- package/dist/presentation/dto/definition.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +9 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +66 -2
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/create-definition.d.ts.map +1 -1
- package/dist/presentation/routes/create-definition.js +16 -3
- package/dist/presentation/routes/create-definition.js.map +1 -1
- package/dist/presentation/routes/get-definition.d.ts.map +1 -1
- package/dist/presentation/routes/get-definition.js +18 -4
- package/dist/presentation/routes/get-definition.js.map +1 -1
- package/dist/presentation/routes/project-access.d.ts +1 -0
- package/dist/presentation/routes/project-access.d.ts.map +1 -1
- package/dist/presentation/routes/validate-definition.js +3 -3
- package/dist/presentation/routes/validate-definition.js.map +1 -1
- package/dist/temporal/activities/sync-activities.d.ts +3 -3
- package/dist/temporal/activities/sync-activities.d.ts.map +1 -1
- package/dist/temporal/activities/sync-activities.js +7 -4
- package/dist/temporal/activities/sync-activities.js.map +1 -1
- package/dist/temporal/workflows/definition-sync.js +1 -1
- package/dist/temporal/workflows/definition-sync.js.map +1 -1
- package/dist/temporal/workflows/index.bundle.js +50 -22
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0003_nebulous_nemesis.sql +11 -0
- package/drizzle/0004_workflow_ref_lookup.sql +1 -0
- package/drizzle/meta/0003_snapshot.json +627 -0
- package/drizzle/meta/0004_snapshot.json +649 -0
- package/drizzle/meta/_journal.json +14 -0
- package/drizzle.config.ts +1 -0
- package/package.json +7 -7
- package/src/core/entities/index.ts +1 -1
- package/src/core/entities/integration-context.ts +4 -0
- package/src/core/entities/sync-state.test.ts +49 -0
- package/src/core/entities/sync-state.ts +35 -12
- package/src/core/entities/validation-diagnostic.ts +8 -0
- package/src/core/entities/workflow-definition.ts +2 -0
- package/src/core/errors.ts +26 -0
- package/src/core/index.ts +12 -0
- package/src/core/integrations.test.ts +38 -0
- package/src/core/integrations.ts +10 -1
- package/src/core/parse-definition-diagnostics.test.ts +47 -0
- package/src/core/parse-definition.test.ts +29 -5
- package/src/core/parse-definition.ts +7 -7
- package/src/core/resolve-definition-at-ref.test.ts +792 -0
- package/src/core/resolve-definition-at-ref.ts +551 -0
- package/src/core/sync-definitions.test.ts +61 -2
- package/src/core/sync-definitions.ts +7 -7
- package/src/core/validate-definition.test.ts +31 -22
- package/src/core/validate-definition.ts +21 -13
- package/src/core/workflow-model/cron-trigger.ts +6 -2
- package/src/core/workflow-model/index.ts +1 -0
- package/src/core/workflow-model/invalid-workflow-model-error.ts +5 -1
- package/src/core/workflow-model/normalize-job-listening.ts +55 -23
- package/src/core/workflow-model/normalize-triggers.ts +44 -15
- package/src/core/workflow-model/normalize-workflow-document.test.ts +444 -36
- package/src/core/workflow-model/normalize-workflow-document.ts +8 -4
- package/src/core/workflow-model/parse-interpolation-field.ts +1 -23
- package/src/core/workflow-model/validate-predicate-expression.ts +6 -0
- package/src/core/workflow-model/validation-issue.ts +8 -13
- package/src/db/db.ts +7 -1
- package/src/db/definition-triggers.test.ts +44 -0
- package/src/db/definition-triggers.ts +1 -1
- package/src/db/definitions.test.ts +300 -1
- package/src/db/definitions.ts +282 -28
- package/src/db/index.ts +2 -0
- package/src/db/schema/definitions.test.ts +1 -0
- package/src/db/schema/definitions.ts +11 -0
- package/src/db/schema/sync-states.ts +12 -3
- package/src/db/schema/workflows.ts +23 -0
- package/src/db/sync-states.test.ts +87 -18
- package/src/db/sync-states.ts +6 -6
- package/src/db/workflow-lineage-migration.test.ts +158 -0
- package/src/index.ts +3 -1
- package/src/metrics/index.ts +1 -0
- package/src/metrics/instance.test.ts +50 -0
- package/src/metrics/instance.ts +23 -0
- package/src/presentation/dto/definition.test.ts +89 -1
- package/src/presentation/dto/definition.ts +8 -5
- package/src/presentation/inter-module.test.ts +177 -0
- package/src/presentation/inter-module.ts +77 -4
- package/src/presentation/routes/create-definition.test.ts +38 -0
- package/src/presentation/routes/create-definition.ts +20 -3
- package/src/presentation/routes/get-definition.test.ts +125 -11
- package/src/presentation/routes/get-definition.ts +21 -4
- package/src/presentation/routes/list-definitions.test.ts +1 -1
- package/src/presentation/routes/validate-definition.test.ts +1 -1
- package/src/presentation/routes/validate-definition.ts +3 -3
- package/src/presentation/subscribers/on-integration-connection-available.test.ts +3 -0
- package/src/temporal/activities/sync-activities.test.ts +46 -2
- package/src/temporal/activities/sync-activities.ts +9 -7
- package/src/temporal/workflows/definition-sync.ts +1 -1
- package/test/factories/definition.ts +1 -0
- package/test/globalSetup.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/core/entities/validation-warning.d.ts +0 -6
- package/dist/core/entities/validation-warning.d.ts.map +0 -1
- package/dist/core/entities/validation-warning.js +0 -3
- package/dist/core/entities/validation-warning.js.map +0 -1
- package/src/core/entities/validation-warning.ts +0 -5
- package/src/core/parse-definition-warnings.test.ts +0 -33
package/src/db/definitions.ts
CHANGED
|
@@ -5,7 +5,19 @@ import {
|
|
|
5
5
|
} from '@shipfox/api-definitions-dto';
|
|
6
6
|
import {writeOutboxEvent} from '@shipfox/node-outbox';
|
|
7
7
|
import type {WorkflowDocument} from '@shipfox/workflow-document';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
and,
|
|
10
|
+
asc,
|
|
11
|
+
eq,
|
|
12
|
+
gt,
|
|
13
|
+
inArray,
|
|
14
|
+
isNotNull,
|
|
15
|
+
isNull,
|
|
16
|
+
notInArray,
|
|
17
|
+
or,
|
|
18
|
+
type SQL,
|
|
19
|
+
sql,
|
|
20
|
+
} from 'drizzle-orm';
|
|
9
21
|
import type {
|
|
10
22
|
WorkflowDefinition,
|
|
11
23
|
WorkflowDefinitionPayload,
|
|
@@ -14,8 +26,9 @@ import type {
|
|
|
14
26
|
import type {WorkflowModel} from '#core/entities/workflow-model.js';
|
|
15
27
|
import {db} from './db.js';
|
|
16
28
|
import {definitionTriggersFor} from './definition-triggers.js';
|
|
17
|
-
import {toDefinition, workflowDefinitions} from './schema/definitions.js';
|
|
29
|
+
import {type DefinitionDb, toDefinition, workflowDefinitions} from './schema/definitions.js';
|
|
18
30
|
import {definitionsOutbox} from './schema/outbox.js';
|
|
31
|
+
import {workflowWorkflows} from './schema/workflows.js';
|
|
19
32
|
|
|
20
33
|
type Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];
|
|
21
34
|
|
|
@@ -33,7 +46,155 @@ export interface UpsertDefinitionParams {
|
|
|
33
46
|
ref?: string | undefined;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Finds or creates the workflow lineage for (projectId, configPath) and returns
|
|
51
|
+
* its id. Pathless manual definitions share a project-scoped lineage because
|
|
52
|
+
* their request has no more specific stable key.
|
|
53
|
+
*/
|
|
54
|
+
async function findOrCreateWorkflow(
|
|
55
|
+
tx: Tx,
|
|
56
|
+
params: {projectId: string; configPath: string | null},
|
|
57
|
+
): Promise<string> {
|
|
58
|
+
if (params.configPath === null) {
|
|
59
|
+
// Serialize the first pathless definition for a project so concurrent saves
|
|
60
|
+
// cannot create multiple project-scoped lineages.
|
|
61
|
+
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${params.projectId}, 0))`);
|
|
62
|
+
const existing = await tx
|
|
63
|
+
.select({workflowId: workflowDefinitions.workflowId})
|
|
64
|
+
.from(workflowDefinitions)
|
|
65
|
+
.where(
|
|
66
|
+
and(
|
|
67
|
+
eq(workflowDefinitions.projectId, params.projectId),
|
|
68
|
+
isNull(workflowDefinitions.configPath),
|
|
69
|
+
eq(workflowDefinitions.source, 'manual'),
|
|
70
|
+
isNotNull(workflowDefinitions.workflowId),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
.orderBy(asc(workflowDefinitions.createdAt), asc(workflowDefinitions.id))
|
|
74
|
+
.limit(1);
|
|
75
|
+
const existingRow = existing[0];
|
|
76
|
+
if (existingRow?.workflowId) return existingRow.workflowId;
|
|
77
|
+
|
|
78
|
+
const inserted = await tx
|
|
79
|
+
.insert(workflowWorkflows)
|
|
80
|
+
.values({projectId: params.projectId, configPath: null})
|
|
81
|
+
.onConflictDoNothing()
|
|
82
|
+
.returning({id: workflowWorkflows.id});
|
|
83
|
+
const insertedRow = inserted[0];
|
|
84
|
+
if (insertedRow) return insertedRow.id;
|
|
85
|
+
|
|
86
|
+
const existingLineage = await tx
|
|
87
|
+
.select({id: workflowWorkflows.id})
|
|
88
|
+
.from(workflowWorkflows)
|
|
89
|
+
.where(
|
|
90
|
+
and(
|
|
91
|
+
eq(workflowWorkflows.projectId, params.projectId),
|
|
92
|
+
isNull(workflowWorkflows.configPath),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
.limit(1);
|
|
96
|
+
const existingLineageRow = existingLineage[0];
|
|
97
|
+
if (!existingLineageRow) {
|
|
98
|
+
throw new Error(`Pathless workflow lineage missing for project ${params.projectId}`);
|
|
99
|
+
}
|
|
100
|
+
return existingLineageRow.id;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const inserted = await tx
|
|
104
|
+
.insert(workflowWorkflows)
|
|
105
|
+
.values({projectId: params.projectId, configPath: params.configPath})
|
|
106
|
+
.onConflictDoNothing()
|
|
107
|
+
.returning({id: workflowWorkflows.id});
|
|
108
|
+
const insertedRow = inserted[0];
|
|
109
|
+
if (insertedRow) return insertedRow.id;
|
|
110
|
+
|
|
111
|
+
const existing = await tx
|
|
112
|
+
.select({id: workflowWorkflows.id})
|
|
113
|
+
.from(workflowWorkflows)
|
|
114
|
+
.where(
|
|
115
|
+
and(
|
|
116
|
+
eq(workflowWorkflows.projectId, params.projectId),
|
|
117
|
+
eq(workflowWorkflows.configPath, params.configPath),
|
|
118
|
+
),
|
|
119
|
+
)
|
|
120
|
+
.limit(1);
|
|
121
|
+
const existingRow = existing[0];
|
|
122
|
+
if (!existingRow) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Workflow lineage upsert returned no row for project ${params.projectId} and config path ${params.configPath}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return existingRow.id;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function findOrCreateWorkflows(
|
|
131
|
+
tx: Tx,
|
|
132
|
+
params: {projectId: string; configPaths: string[]},
|
|
133
|
+
): Promise<Map<string, string>> {
|
|
134
|
+
const configPaths = [...new Set(params.configPaths)].sort((left, right) =>
|
|
135
|
+
left < right ? -1 : left > right ? 1 : 0,
|
|
136
|
+
);
|
|
137
|
+
if (configPaths.length === 0) return new Map();
|
|
138
|
+
|
|
139
|
+
await tx
|
|
140
|
+
.insert(workflowWorkflows)
|
|
141
|
+
.values(configPaths.map((configPath) => ({projectId: params.projectId, configPath})))
|
|
142
|
+
.onConflictDoNothing();
|
|
143
|
+
|
|
144
|
+
const rows = await tx
|
|
145
|
+
.select({id: workflowWorkflows.id, configPath: workflowWorkflows.configPath})
|
|
146
|
+
.from(workflowWorkflows)
|
|
147
|
+
.where(
|
|
148
|
+
and(
|
|
149
|
+
eq(workflowWorkflows.projectId, params.projectId),
|
|
150
|
+
inArray(workflowWorkflows.configPath, configPaths),
|
|
151
|
+
),
|
|
152
|
+
);
|
|
153
|
+
const workflowIds = new Map<string, string>();
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
if (row.configPath !== null) workflowIds.set(row.configPath, row.id);
|
|
156
|
+
}
|
|
157
|
+
for (const configPath of configPaths) {
|
|
158
|
+
if (!workflowIds.has(configPath)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Workflow lineage upsert returned no row for project ${params.projectId} and config path ${configPath}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return workflowIds;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function ensureWorkflowId(
|
|
168
|
+
tx: Tx,
|
|
169
|
+
row: Pick<DefinitionDb, 'id' | 'projectId' | 'configPath' | 'workflowId'>,
|
|
170
|
+
): Promise<string> {
|
|
171
|
+
if (row.workflowId !== null) return row.workflowId;
|
|
172
|
+
|
|
173
|
+
const workflowId = await findOrCreateWorkflow(tx, {
|
|
174
|
+
projectId: row.projectId,
|
|
175
|
+
configPath: row.configPath,
|
|
176
|
+
});
|
|
177
|
+
const updated = await tx
|
|
178
|
+
.update(workflowDefinitions)
|
|
179
|
+
.set({workflowId})
|
|
180
|
+
.where(and(eq(workflowDefinitions.id, row.id), isNull(workflowDefinitions.workflowId)))
|
|
181
|
+
.returning({workflowId: workflowDefinitions.workflowId});
|
|
182
|
+
const updatedRow = updated[0];
|
|
183
|
+
if (updatedRow?.workflowId) return updatedRow.workflowId;
|
|
184
|
+
|
|
185
|
+
const current = await tx
|
|
186
|
+
.select({workflowId: workflowDefinitions.workflowId})
|
|
187
|
+
.from(workflowDefinitions)
|
|
188
|
+
.where(eq(workflowDefinitions.id, row.id))
|
|
189
|
+
.limit(1);
|
|
190
|
+
const currentRow = current[0];
|
|
191
|
+
if (!currentRow?.workflowId) {
|
|
192
|
+
throw new Error(`Definition ${row.id} still has no workflow lineage after reconciliation`);
|
|
193
|
+
}
|
|
194
|
+
return currentRow.workflowId;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildUpsertQuery(tx: Tx, params: UpsertDefinitionParams & {workflowId: string}) {
|
|
37
198
|
const source = params.source ?? 'manual';
|
|
38
199
|
if (source === 'vcs' && !params.configPath) {
|
|
39
200
|
throw new Error('configPath is required for VCS definitions');
|
|
@@ -54,6 +215,7 @@ function buildUpsertQuery(tx: Tx, params: UpsertDefinitionParams) {
|
|
|
54
215
|
};
|
|
55
216
|
|
|
56
217
|
const set = {
|
|
218
|
+
workflowId: params.workflowId,
|
|
57
219
|
name: params.name,
|
|
58
220
|
source,
|
|
59
221
|
definition,
|
|
@@ -65,6 +227,7 @@ function buildUpsertQuery(tx: Tx, params: UpsertDefinitionParams) {
|
|
|
65
227
|
|
|
66
228
|
const values = {
|
|
67
229
|
projectId: params.projectId,
|
|
230
|
+
workflowId: params.workflowId,
|
|
68
231
|
configPath: params.configPath ?? null,
|
|
69
232
|
source,
|
|
70
233
|
sha: params.sha ?? null,
|
|
@@ -121,7 +284,11 @@ export async function upsertDefinition(
|
|
|
121
284
|
params: UpsertDefinitionParams,
|
|
122
285
|
): Promise<WorkflowDefinition> {
|
|
123
286
|
return await db().transaction(async (tx) => {
|
|
124
|
-
const
|
|
287
|
+
const workflowId = await findOrCreateWorkflow(tx, {
|
|
288
|
+
projectId: params.projectId,
|
|
289
|
+
configPath: params.configPath ?? null,
|
|
290
|
+
});
|
|
291
|
+
const rows = await buildUpsertQuery(tx, {...params, workflowId});
|
|
125
292
|
const row = rows[0];
|
|
126
293
|
if (!row) throw new Error('Upsert returned no rows');
|
|
127
294
|
|
|
@@ -140,16 +307,68 @@ export async function upsertDefinition(
|
|
|
140
307
|
});
|
|
141
308
|
}
|
|
142
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Finds or creates the workflow lineage for (projectId, configPath) outside a
|
|
312
|
+
* sync. A dev definition uses the lineage for run numbering without ever
|
|
313
|
+
* persisting a definition row or publishing a resolution event.
|
|
314
|
+
*/
|
|
315
|
+
export async function findOrCreateWorkflowLineage(params: {
|
|
316
|
+
projectId: string;
|
|
317
|
+
configPath: string;
|
|
318
|
+
}): Promise<string> {
|
|
319
|
+
return await db().transaction(async (tx) => {
|
|
320
|
+
return await findOrCreateWorkflow(tx, params);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
143
324
|
export async function getDefinitionById(id: string): Promise<WorkflowDefinition | undefined> {
|
|
325
|
+
return await db().transaction(async (tx) => {
|
|
326
|
+
const rows = await tx
|
|
327
|
+
.select()
|
|
328
|
+
.from(workflowDefinitions)
|
|
329
|
+
.where(and(eq(workflowDefinitions.id, id), isNull(workflowDefinitions.deletedAt)))
|
|
330
|
+
.limit(1);
|
|
331
|
+
const row = rows[0];
|
|
332
|
+
|
|
333
|
+
if (row) {
|
|
334
|
+
const workflowId = await ensureWorkflowId(tx, row);
|
|
335
|
+
return toDefinition({...row, workflowId});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return undefined;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export async function getWorkflowLineageById(
|
|
343
|
+
id: string,
|
|
344
|
+
): Promise<{id: string; projectId: string} | undefined> {
|
|
345
|
+
const rows = await db()
|
|
346
|
+
.select({id: workflowWorkflows.id, projectId: workflowWorkflows.projectId})
|
|
347
|
+
.from(workflowWorkflows)
|
|
348
|
+
.where(eq(workflowWorkflows.id, id))
|
|
349
|
+
.limit(1);
|
|
350
|
+
|
|
351
|
+
return rows[0];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export async function getDefinitionByWorkflowId(params: {
|
|
355
|
+
workflowId: string;
|
|
356
|
+
ref: string;
|
|
357
|
+
}): Promise<WorkflowDefinition | undefined> {
|
|
144
358
|
const rows = await db()
|
|
145
359
|
.select()
|
|
146
360
|
.from(workflowDefinitions)
|
|
147
|
-
.where(
|
|
361
|
+
.where(
|
|
362
|
+
and(
|
|
363
|
+
eq(workflowDefinitions.workflowId, params.workflowId),
|
|
364
|
+
eq(workflowDefinitions.ref, params.ref),
|
|
365
|
+
isNull(workflowDefinitions.deletedAt),
|
|
366
|
+
),
|
|
367
|
+
)
|
|
148
368
|
.limit(1);
|
|
149
369
|
const row = rows[0];
|
|
150
370
|
|
|
151
|
-
|
|
152
|
-
return toDefinition(row);
|
|
371
|
+
return row ? toDefinition({...row, workflowId: params.workflowId}) : undefined;
|
|
153
372
|
}
|
|
154
373
|
|
|
155
374
|
export interface DefinitionCursor {
|
|
@@ -179,28 +398,35 @@ function cursorWhere(cursor: DefinitionCursor | undefined): SQL | undefined {
|
|
|
179
398
|
export async function listDefinitions(
|
|
180
399
|
params: ListDefinitionsParams,
|
|
181
400
|
): Promise<ListDefinitionsResult> {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
401
|
+
return await db().transaction(async (tx) => {
|
|
402
|
+
const conditions = [
|
|
403
|
+
eq(workflowDefinitions.projectId, params.projectId),
|
|
404
|
+
isNull(workflowDefinitions.deletedAt),
|
|
405
|
+
];
|
|
406
|
+
const cursorCondition = cursorWhere(params.cursor);
|
|
407
|
+
if (cursorCondition) conditions.push(cursorCondition);
|
|
408
|
+
|
|
409
|
+
const rows = await tx
|
|
410
|
+
.select()
|
|
411
|
+
.from(workflowDefinitions)
|
|
412
|
+
.where(and(...conditions))
|
|
413
|
+
.orderBy(asc(workflowDefinitions.name), asc(workflowDefinitions.id))
|
|
414
|
+
.limit(params.limit + 1);
|
|
415
|
+
|
|
416
|
+
const hasMore = rows.length > params.limit;
|
|
417
|
+
const pageRows = hasMore ? rows.slice(0, params.limit) : rows;
|
|
418
|
+
const last = pageRows.at(-1);
|
|
419
|
+
const definitions: WorkflowDefinition[] = [];
|
|
420
|
+
for (const row of pageRows) {
|
|
421
|
+
const workflowId = await ensureWorkflowId(tx, row);
|
|
422
|
+
definitions.push(toDefinition({...row, workflowId}));
|
|
423
|
+
}
|
|
199
424
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
425
|
+
return {
|
|
426
|
+
definitions,
|
|
427
|
+
nextCursor: hasMore && last ? {value: last.name, id: last.id} : null,
|
|
428
|
+
};
|
|
429
|
+
});
|
|
204
430
|
}
|
|
205
431
|
|
|
206
432
|
export async function listDefinitionsByProject(projectId: string): Promise<WorkflowDefinition[]> {
|
|
@@ -281,11 +507,17 @@ export async function applyVcsDefinitionsBatch(
|
|
|
281
507
|
): Promise<ApplyVcsDefinitionsBatchResult> {
|
|
282
508
|
return await db().transaction(async (tx) => {
|
|
283
509
|
let appliedCount = 0;
|
|
510
|
+
const prepared: Array<{
|
|
511
|
+
item: (typeof params.upserts)[number];
|
|
512
|
+
unchanged: boolean;
|
|
513
|
+
workflowId: string | null | undefined;
|
|
514
|
+
}> = [];
|
|
284
515
|
for (const item of params.upserts) {
|
|
285
516
|
const existing = await tx
|
|
286
517
|
.select({
|
|
287
518
|
contentHash: workflowDefinitions.contentHash,
|
|
288
519
|
deletedAt: workflowDefinitions.deletedAt,
|
|
520
|
+
workflowId: workflowDefinitions.workflowId,
|
|
289
521
|
})
|
|
290
522
|
.from(workflowDefinitions)
|
|
291
523
|
.where(
|
|
@@ -303,9 +535,31 @@ export async function applyVcsDefinitionsBatch(
|
|
|
303
535
|
previous.deletedAt === null &&
|
|
304
536
|
previous.contentHash === item.contentHash;
|
|
305
537
|
|
|
538
|
+
prepared.push({item, unchanged, workflowId: previous?.workflowId});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const workflowIds = await findOrCreateWorkflows(tx, {
|
|
542
|
+
projectId: params.projectId,
|
|
543
|
+
configPaths: prepared.filter(({unchanged}) => !unchanged).map(({item}) => item.configPath),
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
for (const {item, unchanged, workflowId: previousWorkflowId} of prepared) {
|
|
547
|
+
const workflowId = unchanged
|
|
548
|
+
? (previousWorkflowId ??
|
|
549
|
+
(await findOrCreateWorkflow(tx, {
|
|
550
|
+
projectId: params.projectId,
|
|
551
|
+
configPath: item.configPath,
|
|
552
|
+
})))
|
|
553
|
+
: workflowIds.get(item.configPath);
|
|
554
|
+
if (!workflowId) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`Workflow lineage missing for project ${params.projectId} and config path ${item.configPath}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
306
559
|
const rows = await buildUpsertQuery(tx, {
|
|
307
560
|
projectId: params.projectId,
|
|
308
561
|
workspaceId: params.workspaceId,
|
|
562
|
+
workflowId,
|
|
309
563
|
configPath: item.configPath,
|
|
310
564
|
source: 'vcs',
|
|
311
565
|
ref: params.ref,
|
package/src/db/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export type {
|
|
|
12
12
|
} from './definitions.js';
|
|
13
13
|
export {
|
|
14
14
|
applyVcsDefinitionsBatch,
|
|
15
|
+
findOrCreateWorkflowLineage,
|
|
15
16
|
getDefinitionById,
|
|
16
17
|
invalidateCache,
|
|
17
18
|
listDefinitions,
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
} from './definitions.js';
|
|
22
23
|
export {definitionsOutbox} from './schema/outbox.js';
|
|
23
24
|
export {definitionSyncStates} from './schema/sync-states.js';
|
|
25
|
+
export {type WorkflowCreateDb, type WorkflowDb, workflowWorkflows} from './schema/workflows.js';
|
|
24
26
|
export {
|
|
25
27
|
type DefinitionSyncStateKey,
|
|
26
28
|
getLatestDefinitionSyncState,
|
|
@@ -16,6 +16,7 @@ describe('toDefinition', () => {
|
|
|
16
16
|
};
|
|
17
17
|
const row: DefinitionDb = {
|
|
18
18
|
id: '019e98ab-6656-7ca1-b9ad-1ca4442c479d',
|
|
19
|
+
workflowId: '019e98ab-6656-7ca1-b9ad-1ca4442c479e',
|
|
19
20
|
projectId: '019e98ab-b90f-7265-b13c-8b441c991381',
|
|
20
21
|
configPath: '.shipfox/workflows/manual.yml',
|
|
21
22
|
source: 'manual',
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
WorkflowDefinitionPayload,
|
|
7
7
|
} from '#core/entities/workflow-definition.js';
|
|
8
8
|
import {pgTable} from './common.js';
|
|
9
|
+
import {workflowWorkflows} from './workflows.js';
|
|
9
10
|
|
|
10
11
|
export const definitionSourceEnum = pgEnum('definitions_source', ['manual', 'vcs']);
|
|
11
12
|
|
|
@@ -13,6 +14,8 @@ export const workflowDefinitions = pgTable(
|
|
|
13
14
|
'workflow_definitions',
|
|
14
15
|
{
|
|
15
16
|
id: uuidv7PrimaryKey(),
|
|
17
|
+
// Schema-only upgrades leave historical rows null until they are touched.
|
|
18
|
+
workflowId: uuid('workflow_id').references(() => workflowWorkflows.id),
|
|
16
19
|
projectId: uuid('project_id').notNull(),
|
|
17
20
|
configPath: text('config_path'),
|
|
18
21
|
source: definitionSourceEnum('source').notNull().default('manual'),
|
|
@@ -41,6 +44,9 @@ export const workflowDefinitions = pgTable(
|
|
|
41
44
|
index('definitions_wd_project_name_id_idx')
|
|
42
45
|
.on(table.projectId, table.name, table.id)
|
|
43
46
|
.where(sql`"deleted_at" IS NULL`),
|
|
47
|
+
index('definitions_wd_workflow_ref_lookup')
|
|
48
|
+
.on(table.workflowId, table.ref)
|
|
49
|
+
.where(sql`"ref" IS NOT NULL AND "deleted_at" IS NULL`),
|
|
44
50
|
// Binds source to its git coordinates so the partial indexes above stay
|
|
45
51
|
// unambiguous: vcs rows carry a ref or sha; manual rows carry neither.
|
|
46
52
|
check(
|
|
@@ -55,8 +61,13 @@ export type DefinitionCreateDb = typeof workflowDefinitions.$inferInsert;
|
|
|
55
61
|
export type DefinitionUpdateDb = Partial<DefinitionCreateDb>;
|
|
56
62
|
|
|
57
63
|
export function toDefinition(row: DefinitionDb): WorkflowDefinition {
|
|
64
|
+
if (row.workflowId === null) {
|
|
65
|
+
throw new Error(`Definition ${row.id} has no workflow lineage`);
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
return {
|
|
59
69
|
id: row.id,
|
|
70
|
+
workflowId: row.workflowId,
|
|
60
71
|
projectId: row.projectId,
|
|
61
72
|
configPath: row.configPath,
|
|
62
73
|
source: row.source,
|
|
@@ -2,13 +2,17 @@ import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
|
2
2
|
import {sql} from 'drizzle-orm';
|
|
3
3
|
import {index, jsonb, pgEnum, text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
|
|
4
4
|
import type {
|
|
5
|
+
DefinitionSyncDiagnostic,
|
|
5
6
|
DefinitionSyncErrorCode,
|
|
6
7
|
DefinitionSyncState,
|
|
7
8
|
DefinitionSyncStatus,
|
|
8
|
-
DefinitionSyncWarning,
|
|
9
9
|
} from '#core/entities/sync-state.js';
|
|
10
10
|
import {pgTable} from './common.js';
|
|
11
11
|
|
|
12
|
+
type StoredDefinitionSyncDiagnostic = Omit<DefinitionSyncDiagnostic, 'severity'> & {
|
|
13
|
+
severity?: DefinitionSyncDiagnostic['severity'];
|
|
14
|
+
};
|
|
15
|
+
|
|
12
16
|
export const definitionSyncStatusEnum = pgEnum('definitions_sync_status', [
|
|
13
17
|
'pending',
|
|
14
18
|
'syncing',
|
|
@@ -43,7 +47,7 @@ export const definitionSyncStates = pgTable(
|
|
|
43
47
|
status: definitionSyncStatusEnum('status').notNull().default('pending'),
|
|
44
48
|
lastErrorCode: definitionSyncErrorCodeEnum('last_error_code'),
|
|
45
49
|
lastErrorMessage: text('last_error_message'),
|
|
46
|
-
warnings: jsonb('warnings').notNull().default([]).$type<
|
|
50
|
+
warnings: jsonb('warnings').notNull().default([]).$type<StoredDefinitionSyncDiagnostic[]>(),
|
|
47
51
|
startedAt: timestamp('started_at', {withTimezone: true}),
|
|
48
52
|
finishedAt: timestamp('finished_at', {withTimezone: true}),
|
|
49
53
|
createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
|
|
@@ -64,6 +68,11 @@ export type DefinitionSyncStateDb = typeof definitionSyncStates.$inferSelect;
|
|
|
64
68
|
export type DefinitionSyncStateCreateDb = typeof definitionSyncStates.$inferInsert;
|
|
65
69
|
|
|
66
70
|
export function toDefinitionSyncState(row: DefinitionSyncStateDb): DefinitionSyncState {
|
|
71
|
+
const diagnostics = (row.warnings ?? []).map(({severity, ...diagnostic}) => ({
|
|
72
|
+
...diagnostic,
|
|
73
|
+
severity: severity ?? 'warning',
|
|
74
|
+
}));
|
|
75
|
+
|
|
67
76
|
return {
|
|
68
77
|
id: row.id,
|
|
69
78
|
projectId: row.projectId,
|
|
@@ -73,7 +82,7 @@ export function toDefinitionSyncState(row: DefinitionSyncStateDb): DefinitionSyn
|
|
|
73
82
|
status: row.status as DefinitionSyncStatus,
|
|
74
83
|
lastErrorCode: row.lastErrorCode as DefinitionSyncErrorCode | null,
|
|
75
84
|
lastErrorMessage: row.lastErrorMessage,
|
|
76
|
-
|
|
85
|
+
diagnostics,
|
|
77
86
|
startedAt: row.startedAt,
|
|
78
87
|
finishedAt: row.finishedAt,
|
|
79
88
|
createdAt: row.createdAt,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
2
|
+
import {sql} from 'drizzle-orm';
|
|
3
|
+
import {text, timestamp, uniqueIndex, uuid} from 'drizzle-orm/pg-core';
|
|
4
|
+
import {pgTable} from './common.js';
|
|
5
|
+
|
|
6
|
+
export const workflowWorkflows = pgTable(
|
|
7
|
+
'workflows',
|
|
8
|
+
{
|
|
9
|
+
id: uuidv7PrimaryKey(),
|
|
10
|
+
projectId: uuid('project_id').notNull(),
|
|
11
|
+
configPath: text('config_path'),
|
|
12
|
+
createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
|
|
13
|
+
},
|
|
14
|
+
(table) => [
|
|
15
|
+
uniqueIndex('definitions_workflows_project_path_unique').on(table.projectId, table.configPath),
|
|
16
|
+
uniqueIndex('definitions_workflows_project_pathless_unique')
|
|
17
|
+
.on(table.projectId)
|
|
18
|
+
.where(sql`"config_path" IS NULL`),
|
|
19
|
+
],
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export type WorkflowDb = typeof workflowWorkflows.$inferSelect;
|
|
23
|
+
export type WorkflowCreateDb = typeof workflowWorkflows.$inferInsert;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFINITION_SYNC_DIAGNOSTICS_MAX_COUNT,
|
|
2
3
|
DEFINITION_SYNC_WARNING_CODE_MAX_LENGTH,
|
|
3
4
|
DEFINITION_SYNC_WARNING_MESSAGE_MAX_LENGTH,
|
|
4
5
|
DEFINITION_SYNC_WARNING_PATH_MAX_LENGTH,
|
|
5
|
-
DEFINITION_SYNC_WARNINGS_MAX_COUNT,
|
|
6
6
|
} from '@shipfox/api-definitions-dto';
|
|
7
7
|
import {eq} from 'drizzle-orm';
|
|
8
8
|
import {db} from './db.js';
|
|
9
9
|
import {definitionSyncStates} from './schema/sync-states.js';
|
|
10
|
-
import {markDefinitionSyncState} from './sync-states.js';
|
|
10
|
+
import {getLatestDefinitionSyncState, markDefinitionSyncState} from './sync-states.js';
|
|
11
11
|
|
|
12
12
|
describe('definition sync state queries', () => {
|
|
13
13
|
let projectId: string;
|
|
@@ -65,14 +65,21 @@ describe('definition sync state queries', () => {
|
|
|
65
65
|
expect(second.startedAt?.getTime()).toBe(first.startedAt?.getTime());
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
it('persists
|
|
69
|
-
const
|
|
68
|
+
it('persists diagnostics and clears them on a subsequent sync', async () => {
|
|
69
|
+
const diagnostics = [
|
|
70
|
+
{
|
|
71
|
+
code: 'invalid-trigger-event',
|
|
72
|
+
message: 'Trigger event is never delivered.',
|
|
73
|
+
path: 'triggers.on_demand',
|
|
74
|
+
severity: 'error',
|
|
75
|
+
},
|
|
70
76
|
{
|
|
71
77
|
code: 're-evaluating-command',
|
|
72
78
|
message: 'Workflow data is re-executed as shell code.',
|
|
73
79
|
path: 'jobs.build.steps.0.run',
|
|
80
|
+
severity: 'warning',
|
|
74
81
|
},
|
|
75
|
-
];
|
|
82
|
+
] as const;
|
|
76
83
|
|
|
77
84
|
const succeeded = await markDefinitionSyncState({
|
|
78
85
|
projectId,
|
|
@@ -80,10 +87,10 @@ describe('definition sync state queries', () => {
|
|
|
80
87
|
sourceExternalRepositoryId: 'gitea-owner/platform',
|
|
81
88
|
ref: 'main',
|
|
82
89
|
status: 'succeeded',
|
|
83
|
-
|
|
90
|
+
diagnostics,
|
|
84
91
|
});
|
|
85
92
|
|
|
86
|
-
expect(succeeded.
|
|
93
|
+
expect(succeeded.diagnostics).toEqual(diagnostics);
|
|
87
94
|
|
|
88
95
|
const syncing = await markDefinitionSyncState({
|
|
89
96
|
projectId,
|
|
@@ -93,27 +100,89 @@ describe('definition sync state queries', () => {
|
|
|
93
100
|
status: 'syncing',
|
|
94
101
|
});
|
|
95
102
|
|
|
96
|
-
expect(syncing.
|
|
103
|
+
expect(syncing.diagnostics).toEqual([]);
|
|
97
104
|
});
|
|
98
105
|
|
|
99
|
-
it('bounds
|
|
106
|
+
it('orders errors before warnings and bounds diagnostic payloads before persisting them', async () => {
|
|
100
107
|
const state = await markDefinitionSyncState({
|
|
101
108
|
projectId,
|
|
102
109
|
sourceConnectionId,
|
|
103
110
|
sourceExternalRepositoryId: 'gitea-owner/platform',
|
|
104
111
|
ref: 'main',
|
|
105
112
|
status: 'succeeded',
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
diagnostics: [
|
|
114
|
+
{
|
|
115
|
+
code: 'warning-last',
|
|
116
|
+
message: 'warning last',
|
|
117
|
+
severity: 'warning',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
code: 'error-first',
|
|
121
|
+
message: 'error first',
|
|
122
|
+
filePath: '.shipfox/workflows/deploy.yml',
|
|
123
|
+
severity: 'error',
|
|
124
|
+
},
|
|
125
|
+
...Array.from({length: DEFINITION_SYNC_DIAGNOSTICS_MAX_COUNT}, (_, index) => ({
|
|
126
|
+
code: `${index}-${'c'.repeat(DEFINITION_SYNC_WARNING_CODE_MAX_LENGTH)}`,
|
|
127
|
+
message: `${index}-${'m'.repeat(DEFINITION_SYNC_WARNING_MESSAGE_MAX_LENGTH)}`,
|
|
128
|
+
path: `${index}-${'p'.repeat(DEFINITION_SYNC_WARNING_PATH_MAX_LENGTH)}`,
|
|
129
|
+
severity: 'warning' as const,
|
|
130
|
+
})),
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(state.diagnostics).toHaveLength(DEFINITION_SYNC_DIAGNOSTICS_MAX_COUNT);
|
|
135
|
+
expect(state.diagnostics[0]).toEqual({
|
|
136
|
+
code: 'error-first',
|
|
137
|
+
message: 'error first',
|
|
138
|
+
filePath: '.shipfox/workflows/deploy.yml',
|
|
139
|
+
severity: 'error',
|
|
140
|
+
});
|
|
141
|
+
expect(state.diagnostics[1]).toEqual({
|
|
142
|
+
code: 'warning-last',
|
|
143
|
+
message: 'warning last',
|
|
144
|
+
severity: 'warning',
|
|
145
|
+
});
|
|
146
|
+
expect(state.diagnostics[2]?.code).toHaveLength(DEFINITION_SYNC_WARNING_CODE_MAX_LENGTH);
|
|
147
|
+
expect(state.diagnostics[2]?.message).toHaveLength(DEFINITION_SYNC_WARNING_MESSAGE_MAX_LENGTH);
|
|
148
|
+
expect(state.diagnostics[2]?.path).toHaveLength(DEFINITION_SYNC_WARNING_PATH_MAX_LENGTH);
|
|
149
|
+
// The two sentinel diagnostics survive truncation; overflow warnings drop first.
|
|
150
|
+
expect(state.diagnostics[0]?.severity).toBe('error');
|
|
151
|
+
expect(state.diagnostics[1]?.severity).toBe('warning');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('normalizes legacy stored warnings without a severity', async () => {
|
|
155
|
+
await db()
|
|
156
|
+
.insert(definitionSyncStates)
|
|
157
|
+
.values({
|
|
158
|
+
projectId,
|
|
159
|
+
sourceConnectionId,
|
|
160
|
+
sourceExternalRepositoryId: 'gitea-owner/platform',
|
|
161
|
+
ref: 'main',
|
|
162
|
+
status: 'succeeded',
|
|
163
|
+
warnings: [
|
|
164
|
+
{
|
|
165
|
+
code: 'legacy-warning',
|
|
166
|
+
message: 'Written before diagnostics had severity.',
|
|
167
|
+
path: 'jobs.build.steps.0.run',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const state = await getLatestDefinitionSyncState({
|
|
173
|
+
projectId,
|
|
174
|
+
sourceConnectionId,
|
|
175
|
+
sourceExternalRepositoryId: 'gitea-owner/platform',
|
|
111
176
|
});
|
|
112
177
|
|
|
113
|
-
expect(state
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
178
|
+
expect(state?.diagnostics).toEqual([
|
|
179
|
+
{
|
|
180
|
+
code: 'legacy-warning',
|
|
181
|
+
message: 'Written before diagnostics had severity.',
|
|
182
|
+
path: 'jobs.build.steps.0.run',
|
|
183
|
+
severity: 'warning',
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
117
186
|
});
|
|
118
187
|
|
|
119
188
|
it('clears stale finish data when a sync starts again', async () => {
|