@shipfox/client-workflows 22.0.3 → 23.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 +1 -1
- package/CHANGELOG.md +25 -0
- package/dist/core/entities/job.d.ts +2 -2
- package/dist/core/entities/job.d.ts.map +1 -1
- package/dist/core/entities/job.js.map +1 -1
- package/dist/pages/project-workflows-page.d.ts.map +1 -1
- package/dist/pages/project-workflows-page.js +74 -24
- package/dist/pages/project-workflows-page.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/core/entities/job.ts +2 -2
- package/src/pages/project-workflows-page.test.tsx +85 -5
- package/src/pages/project-workflows-page.tsx +71 -17
- package/test/fixtures/workflow-run.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-workflows",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "23.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -28,13 +28,13 @@
|
|
|
28
28
|
"@swc/helpers": "^0.5.17",
|
|
29
29
|
"date-fns": "^4.1.0",
|
|
30
30
|
"@shipfox/annotations-dto": "12.3.0",
|
|
31
|
-
"@shipfox/api-definitions-dto": "
|
|
32
|
-
"@shipfox/api-workflows-dto": "
|
|
31
|
+
"@shipfox/api-definitions-dto": "14.0.0",
|
|
32
|
+
"@shipfox/api-workflows-dto": "14.0.0",
|
|
33
33
|
"@shipfox/client-api": "6.0.1",
|
|
34
34
|
"@shipfox/client-logs": "22.0.3",
|
|
35
|
-
"@shipfox/client-projects": "
|
|
36
|
-
"@shipfox/client-shell": "
|
|
37
|
-
"@shipfox/client-triggers": "
|
|
35
|
+
"@shipfox/client-projects": "23.0.0",
|
|
36
|
+
"@shipfox/client-shell": "23.0.0",
|
|
37
|
+
"@shipfox/client-triggers": "23.0.0",
|
|
38
38
|
"@shipfox/client-ui": "22.0.3",
|
|
39
39
|
"@shipfox/react-ui": "2.2.0"
|
|
40
40
|
},
|
package/src/core/entities/job.ts
CHANGED
|
@@ -28,13 +28,13 @@ export type JobStatusReason =
|
|
|
28
28
|
export interface JobListening {
|
|
29
29
|
on: Array<{
|
|
30
30
|
source: string;
|
|
31
|
-
event
|
|
31
|
+
event?: string | undefined;
|
|
32
32
|
inputs?: Record<string, unknown> | undefined;
|
|
33
33
|
filter?: string | undefined;
|
|
34
34
|
}>;
|
|
35
35
|
until: Array<{
|
|
36
36
|
source: string;
|
|
37
|
-
event
|
|
37
|
+
event?: string | undefined;
|
|
38
38
|
inputs?: Record<string, unknown> | undefined;
|
|
39
39
|
filter?: string | undefined;
|
|
40
40
|
}> | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {configureApiClient} from '@shipfox/client-api';
|
|
2
|
-
import {fireEvent, screen, waitFor} from '@testing-library/react';
|
|
2
|
+
import {fireEvent, screen, waitFor, within} from '@testing-library/react';
|
|
3
3
|
import {
|
|
4
4
|
jsonResponse,
|
|
5
5
|
PROJECT_TEST_WID,
|
|
@@ -69,11 +69,12 @@ describe('ProjectWorkflowsPage', () => {
|
|
|
69
69
|
finished_at: null,
|
|
70
70
|
last_error_code: 'no-workflow-files',
|
|
71
71
|
last_error_message: 'No workflow files found',
|
|
72
|
-
|
|
72
|
+
diagnostics: [
|
|
73
73
|
{
|
|
74
74
|
code: 're-evaluating-command',
|
|
75
75
|
message: 'Workflow data is re-executed as shell code.',
|
|
76
76
|
path: 'jobs.build.steps.0.run',
|
|
77
|
+
severity: 'warning',
|
|
77
78
|
},
|
|
78
79
|
],
|
|
79
80
|
},
|
|
@@ -104,16 +105,20 @@ describe('ProjectWorkflowsPage', () => {
|
|
|
104
105
|
finished_at: '2026-05-07T01:00:00.000Z',
|
|
105
106
|
last_error_code: null,
|
|
106
107
|
last_error_message: null,
|
|
107
|
-
|
|
108
|
+
diagnostics: [
|
|
108
109
|
{
|
|
109
110
|
code: 're-evaluating-command',
|
|
110
111
|
message: 'Workflow data is re-executed as shell code.',
|
|
111
112
|
path: 'jobs.build.steps.0.run',
|
|
113
|
+
file_path: '.shipfox/workflows/warning.yml',
|
|
114
|
+
severity: 'warning',
|
|
112
115
|
},
|
|
113
116
|
{
|
|
114
117
|
code: 're-evaluating-command',
|
|
115
118
|
message: 'Workflow data is re-executed as shell code.',
|
|
116
119
|
path: 'jobs.build.steps.0.run',
|
|
120
|
+
file_path: '.shipfox/workflows/warning.yml',
|
|
121
|
+
severity: 'warning',
|
|
117
122
|
},
|
|
118
123
|
],
|
|
119
124
|
},
|
|
@@ -125,12 +130,87 @@ describe('ProjectWorkflowsPage', () => {
|
|
|
125
130
|
renderWorkflowsPage();
|
|
126
131
|
|
|
127
132
|
expect(await screen.findByText('Workflow definition warnings')).toBeInTheDocument();
|
|
133
|
+
// Both warnings group under the shared workflow file; the file label renders once.
|
|
128
134
|
expect(screen.getAllByText('Workflow data is re-executed as shell code.')).toHaveLength(2);
|
|
129
|
-
expect(screen.
|
|
135
|
+
expect(screen.getAllByText('.shipfox/workflows/warning.yml')).toHaveLength(1);
|
|
130
136
|
expect(screen.getAllByText('jobs.build.steps.0.run')).toHaveLength(2);
|
|
137
|
+
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
131
138
|
expect(screen.queryByText('Workflow sync failed')).not.toBeInTheDocument();
|
|
132
139
|
});
|
|
133
140
|
|
|
141
|
+
test('shows errors and warnings together with severity labels, grouped by file path', async () => {
|
|
142
|
+
configureApiClient({
|
|
143
|
+
fetchImpl: createProjectDetailFetch({
|
|
144
|
+
definitions: jsonResponse(
|
|
145
|
+
definitionsDto({
|
|
146
|
+
sync: {
|
|
147
|
+
ref: 'main',
|
|
148
|
+
status: 'succeeded',
|
|
149
|
+
last_sync_at: '2026-05-07T01:00:00.000Z',
|
|
150
|
+
started_at: '2026-05-07T00:59:55.000Z',
|
|
151
|
+
finished_at: '2026-05-07T01:00:00.000Z',
|
|
152
|
+
last_error_code: null,
|
|
153
|
+
last_error_message: null,
|
|
154
|
+
diagnostics: [
|
|
155
|
+
{
|
|
156
|
+
code: 'invalid-trigger-event',
|
|
157
|
+
message: 'Trigger event is never delivered by this source.',
|
|
158
|
+
path: 'triggers.on_deploy',
|
|
159
|
+
file_path: '.shipfox/workflows/deploy.yml',
|
|
160
|
+
severity: 'error',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
code: 'unknown-trigger-source',
|
|
164
|
+
message: 'No connection matches this source slug.',
|
|
165
|
+
path: 'triggers.on_deploy',
|
|
166
|
+
file_path: '.shipfox/workflows/deploy.yml',
|
|
167
|
+
severity: 'warning',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
code: 're-evaluating-command',
|
|
171
|
+
message: 'Workflow data is re-executed as shell code.',
|
|
172
|
+
path: 'jobs.build.steps.0.run',
|
|
173
|
+
file_path: '.shipfox/workflows/build.yml',
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
}),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
renderWorkflowsPage();
|
|
184
|
+
|
|
185
|
+
expect(await screen.findByText('Workflow definition diagnostics')).toBeInTheDocument();
|
|
186
|
+
const diagnosticsCallout = screen.getByRole('status');
|
|
187
|
+
expect(diagnosticsCallout).toBeInTheDocument();
|
|
188
|
+
// Two groups: the deploy workflow file and the build workflow file.
|
|
189
|
+
expect(within(diagnosticsCallout).getAllByText('.shipfox/workflows/deploy.yml')).toHaveLength(
|
|
190
|
+
1,
|
|
191
|
+
);
|
|
192
|
+
expect(within(diagnosticsCallout).getAllByText('.shipfox/workflows/build.yml')).toHaveLength(1);
|
|
193
|
+
expect(within(diagnosticsCallout).getAllByText('triggers.on_deploy')).toHaveLength(2);
|
|
194
|
+
expect(within(diagnosticsCallout).getAllByText('jobs.build.steps.0.run')).toHaveLength(1);
|
|
195
|
+
const errorRow = within(diagnosticsCallout).getByText(
|
|
196
|
+
'Trigger event is never delivered by this source.',
|
|
197
|
+
);
|
|
198
|
+
const warningRow = within(diagnosticsCallout).getByText(
|
|
199
|
+
'No connection matches this source slug.',
|
|
200
|
+
);
|
|
201
|
+
expect(errorRow).toHaveClass('text-tag-error-text');
|
|
202
|
+
expect(warningRow).not.toHaveClass('text-tag-error-text');
|
|
203
|
+
expect(
|
|
204
|
+
within(diagnosticsCallout).getByText('Error:', {selector: 'span.font-medium'}),
|
|
205
|
+
).toBeInTheDocument();
|
|
206
|
+
expect(
|
|
207
|
+
within(diagnosticsCallout).getAllByText('Warning:', {selector: 'span.font-medium'}),
|
|
208
|
+
).toHaveLength(2);
|
|
209
|
+
expect(
|
|
210
|
+
within(diagnosticsCallout).getByText('Workflow data is re-executed as shell code.'),
|
|
211
|
+
).toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
|
|
134
214
|
test('opens and closes the definition drawer by clicking the row', async () => {
|
|
135
215
|
configureApiClient({fetchImpl: createProjectDetailFetch()});
|
|
136
216
|
|
|
@@ -301,7 +381,7 @@ function baseDefinitionsDto() {
|
|
|
301
381
|
finished_at: '2026-05-07T01:00:00.000Z',
|
|
302
382
|
last_error_code: null,
|
|
303
383
|
last_error_message: null,
|
|
304
|
-
|
|
384
|
+
diagnostics: [],
|
|
305
385
|
},
|
|
306
386
|
};
|
|
307
387
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {ApiError} from '@shipfox/client-api';
|
|
2
2
|
import {
|
|
3
3
|
type Definition,
|
|
4
|
+
type DefinitionSyncDiagnostic,
|
|
4
5
|
type DefinitionSyncSummary,
|
|
5
6
|
SourceStrip,
|
|
6
7
|
useDefinitionsInfiniteQuery,
|
|
@@ -101,7 +102,7 @@ function ProjectWorkflowsPageInner({projectId}: {projectId: string}) {
|
|
|
101
102
|
/>
|
|
102
103
|
|
|
103
104
|
<WorkflowSyncAlert sync={sync} />
|
|
104
|
-
<
|
|
105
|
+
<WorkflowSyncDiagnostics sync={sync} />
|
|
105
106
|
|
|
106
107
|
<WorkflowDefinitionsList
|
|
107
108
|
definitions={definitions}
|
|
@@ -382,30 +383,54 @@ function WorkflowSyncAlert({sync}: {sync: DefinitionSyncSummary | null | undefin
|
|
|
382
383
|
);
|
|
383
384
|
}
|
|
384
385
|
|
|
385
|
-
function
|
|
386
|
-
if (sync?.status !== 'succeeded' || sync.
|
|
386
|
+
function WorkflowSyncDiagnostics({sync}: {sync: DefinitionSyncSummary | null | undefined}) {
|
|
387
|
+
if (sync?.status !== 'succeeded' || sync.diagnostics.length === 0) return null;
|
|
387
388
|
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
389
|
+
const hasErrors = sync.diagnostics.some((diagnostic) => diagnostic.severity === 'error');
|
|
390
|
+
const hasWarnings = sync.diagnostics.some((diagnostic) => diagnostic.severity === 'warning');
|
|
391
|
+
const groups = groupDiagnosticsByFilePath(sync.diagnostics);
|
|
392
|
+
const title =
|
|
393
|
+
hasErrors && hasWarnings
|
|
394
|
+
? 'Workflow definition diagnostics'
|
|
395
|
+
: hasErrors
|
|
396
|
+
? 'Workflow definition errors'
|
|
397
|
+
: 'Workflow definition warnings';
|
|
395
398
|
|
|
396
399
|
return (
|
|
397
|
-
<Callout role="status" type=
|
|
400
|
+
<Callout role="status" type={hasErrors ? 'error' : 'warning'}>
|
|
398
401
|
<div className="flex flex-col gap-inline">
|
|
399
402
|
<Text size="sm" bold>
|
|
400
|
-
|
|
403
|
+
{title}
|
|
401
404
|
</Text>
|
|
402
405
|
<ul className="flex flex-col gap-tight">
|
|
403
|
-
{
|
|
404
|
-
<li key={key}>
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
<Code className="text-foreground-neutral-muted">{warning.path}</Code>
|
|
406
|
+
{groups.map((group) => (
|
|
407
|
+
<li key={group.key} className="flex flex-col gap-tight">
|
|
408
|
+
{group.filePath ? (
|
|
409
|
+
<Code className="text-foreground-neutral-muted">{group.filePath}</Code>
|
|
408
410
|
) : null}
|
|
411
|
+
<ul className="flex flex-col gap-tight">
|
|
412
|
+
{group.items.map(({key, diagnostic}) => {
|
|
413
|
+
const severityLabel = diagnostic.severity === 'error' ? 'Error' : 'Warning';
|
|
414
|
+
|
|
415
|
+
return (
|
|
416
|
+
<li key={key}>
|
|
417
|
+
{diagnostic.path ? (
|
|
418
|
+
<Code className="text-foreground-neutral-muted">{diagnostic.path}</Code>
|
|
419
|
+
) : null}
|
|
420
|
+
<Text size="sm">
|
|
421
|
+
<span className="font-medium">{severityLabel}:</span>{' '}
|
|
422
|
+
<span
|
|
423
|
+
className={
|
|
424
|
+
diagnostic.severity === 'error' ? 'text-tag-error-text' : undefined
|
|
425
|
+
}
|
|
426
|
+
>
|
|
427
|
+
{diagnostic.message}
|
|
428
|
+
</span>
|
|
429
|
+
</Text>
|
|
430
|
+
</li>
|
|
431
|
+
);
|
|
432
|
+
})}
|
|
433
|
+
</ul>
|
|
409
434
|
</li>
|
|
410
435
|
))}
|
|
411
436
|
</ul>
|
|
@@ -414,6 +439,35 @@ function WorkflowSyncWarnings({sync}: {sync: DefinitionSyncSummary | null | unde
|
|
|
414
439
|
);
|
|
415
440
|
}
|
|
416
441
|
|
|
442
|
+
interface DiagnosticGroup {
|
|
443
|
+
key: string;
|
|
444
|
+
filePath: string | undefined;
|
|
445
|
+
items: {key: string; diagnostic: DefinitionSyncDiagnostic}[];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function groupDiagnosticsByFilePath(
|
|
449
|
+
diagnostics: readonly DefinitionSyncDiagnostic[],
|
|
450
|
+
): DiagnosticGroup[] {
|
|
451
|
+
const groups: DiagnosticGroup[] = [];
|
|
452
|
+
const indexByFilePath = new Map<string, number>();
|
|
453
|
+
for (const diagnostic of diagnostics) {
|
|
454
|
+
const filePathKey = diagnostic.filePath ?? '';
|
|
455
|
+
const groupIndex = indexByFilePath.get(filePathKey);
|
|
456
|
+
if (groupIndex === undefined) {
|
|
457
|
+
indexByFilePath.set(filePathKey, groups.length);
|
|
458
|
+
groups.push({
|
|
459
|
+
key: `${filePathKey}-${groups.length}`,
|
|
460
|
+
filePath: diagnostic.filePath,
|
|
461
|
+
items: [{key: `${filePathKey}-0`, diagnostic}],
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
const group = groups[groupIndex];
|
|
465
|
+
if (group) group.items.push({key: `${filePathKey}-${group.items.length}`, diagnostic});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return groups;
|
|
469
|
+
}
|
|
470
|
+
|
|
417
471
|
function DefinitionSheet({
|
|
418
472
|
definition,
|
|
419
473
|
onOpenChange,
|