@shipfox/client-workflows 15.0.0 → 17.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 (225) hide show
  1. package/.storybook/preview.css +4 -0
  2. package/.turbo/turbo-build.log +1 -1
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +41 -0
  5. package/dist/components/identifier/identifier.js +2 -2
  6. package/dist/components/identifier/identifier.js.map +1 -1
  7. package/dist/components/job-detail/agent-config-failure-callout.d.ts.map +1 -1
  8. package/dist/components/job-detail/agent-config-failure-callout.js +1 -1
  9. package/dist/components/job-detail/agent-config-failure-callout.js.map +1 -1
  10. package/dist/components/job-detail/job-context-panel.d.ts +6 -0
  11. package/dist/components/job-detail/job-context-panel.d.ts.map +1 -0
  12. package/dist/components/job-detail/job-context-panel.js +224 -0
  13. package/dist/components/job-detail/job-context-panel.js.map +1 -0
  14. package/dist/components/job-detail/job-detail-header.d.ts +10 -1
  15. package/dist/components/job-detail/job-detail-header.d.ts.map +1 -1
  16. package/dist/components/job-detail/job-detail-header.js +58 -39
  17. package/dist/components/job-detail/job-detail-header.js.map +1 -1
  18. package/dist/components/job-detail/job-detail-view.d.ts +1 -1
  19. package/dist/components/job-detail/job-detail-view.d.ts.map +1 -1
  20. package/dist/components/job-detail/job-detail-view.js +92 -41
  21. package/dist/components/job-detail/job-detail-view.js.map +1 -1
  22. package/dist/components/job-detail/job-empty-states.d.ts.map +1 -1
  23. package/dist/components/job-detail/job-empty-states.js +19 -10
  24. package/dist/components/job-detail/job-empty-states.js.map +1 -1
  25. package/dist/components/job-detail/job-execution-switcher.js +4 -4
  26. package/dist/components/job-detail/job-execution-switcher.js.map +1 -1
  27. package/dist/components/job-detail/json-code.d.ts +14 -0
  28. package/dist/components/job-detail/json-code.d.ts.map +1 -0
  29. package/dist/components/job-detail/json-code.js +93 -0
  30. package/dist/components/job-detail/json-code.js.map +1 -0
  31. package/dist/components/job-detail/step-attempt-log-panel.d.ts.map +1 -1
  32. package/dist/components/job-detail/step-attempt-log-panel.js +9 -6
  33. package/dist/components/job-detail/step-attempt-log-panel.js.map +1 -1
  34. package/dist/components/job-detail/step-troubleshooting.d.ts +18 -0
  35. package/dist/components/job-detail/step-troubleshooting.d.ts.map +1 -0
  36. package/dist/components/job-detail/step-troubleshooting.js +493 -0
  37. package/dist/components/job-detail/step-troubleshooting.js.map +1 -0
  38. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  39. package/dist/components/job-graph/job-graph-content.js +5 -4
  40. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  41. package/dist/components/job-graph/job-node.js +3 -3
  42. package/dist/components/job-graph/job-node.js.map +1 -1
  43. package/dist/components/step-list/index.d.ts +1 -0
  44. package/dist/components/step-list/index.d.ts.map +1 -1
  45. package/dist/components/step-list/index.js.map +1 -1
  46. package/dist/components/step-list/step-list-model.d.ts.map +1 -1
  47. package/dist/components/step-list/step-list-model.js +5 -0
  48. package/dist/components/step-list/step-list-model.js.map +1 -1
  49. package/dist/components/step-list/step-list.d.ts +5 -1
  50. package/dist/components/step-list/step-list.d.ts.map +1 -1
  51. package/dist/components/step-list/step-list.js +117 -82
  52. package/dist/components/step-list/step-list.js.map +1 -1
  53. package/dist/components/workflow-run-duration-label.js +1 -1
  54. package/dist/components/workflow-run-duration-label.js.map +1 -1
  55. package/dist/components/workflow-run-list/job-status-strip.js +6 -6
  56. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +1 -1
  58. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -1
  59. package/dist/components/workflow-run-list/workflow-run-filters.js +4 -4
  60. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -1
  61. package/dist/components/workflow-run-list/workflow-run-list-states.js +6 -6
  62. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list-view.js +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  65. package/dist/components/workflow-run-list/workflow-run-row.js +7 -7
  66. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  67. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +1 -1
  68. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  69. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +0 -1
  70. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  71. package/dist/components/workflow-run-summary/workflow-run-summary.js +9 -13
  72. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  73. package/dist/components/workflow-run-tabs/index.d.ts +3 -0
  74. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -1
  75. package/dist/components/workflow-run-tabs/index.js +3 -0
  76. package/dist/components/workflow-run-tabs/index.js.map +1 -1
  77. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts +16 -0
  78. package/dist/components/workflow-run-tabs/run-annotation-count-chip.d.ts.map +1 -0
  79. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js +50 -0
  80. package/dist/components/workflow-run-tabs/run-annotation-count-chip.js.map +1 -0
  81. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts +20 -0
  82. package/dist/components/workflow-run-tabs/run-annotation-item.d.ts.map +1 -0
  83. package/dist/components/workflow-run-tabs/run-annotation-item.js +102 -0
  84. package/dist/components/workflow-run-tabs/run-annotation-item.js.map +1 -0
  85. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts +39 -0
  86. package/dist/components/workflow-run-tabs/run-annotation-list.d.ts.map +1 -0
  87. package/dist/components/workflow-run-tabs/run-annotation-list.js +211 -0
  88. package/dist/components/workflow-run-tabs/run-annotation-list.js.map +1 -0
  89. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +1 -1
  90. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -1
  91. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +90 -25
  92. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -1
  93. package/dist/components/workflow-run-tabs/severity-visuals.d.ts +20 -0
  94. package/dist/components/workflow-run-tabs/severity-visuals.d.ts.map +1 -0
  95. package/dist/components/workflow-run-tabs/severity-visuals.js +31 -0
  96. package/dist/components/workflow-run-tabs/severity-visuals.js.map +1 -0
  97. package/dist/components/workflow-run-view/run-workspace-nav.d.ts +1 -1
  98. package/dist/components/workflow-run-view/run-workspace-nav.d.ts.map +1 -1
  99. package/dist/components/workflow-run-view/run-workspace-nav.js +16 -10
  100. package/dist/components/workflow-run-view/run-workspace-nav.js.map +1 -1
  101. package/dist/components/workflow-run-view/workflow-run-states.js +6 -6
  102. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  103. package/dist/components/workflow-run-view/workflow-run-view.d.ts +1 -3
  104. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  105. package/dist/components/workflow-run-view/workflow-run-view.js +217 -59
  106. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  107. package/dist/core/entities/job-execution.d.ts +7 -0
  108. package/dist/core/entities/job-execution.d.ts.map +1 -1
  109. package/dist/core/entities/job-execution.js.map +1 -1
  110. package/dist/core/entities/job.d.ts +9 -0
  111. package/dist/core/entities/job.d.ts.map +1 -1
  112. package/dist/core/entities/job.js.map +1 -1
  113. package/dist/core/entities/step-attempt.d.ts +29 -0
  114. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  115. package/dist/core/entities/step-attempt.js.map +1 -1
  116. package/dist/core/entities/step.d.ts +3 -1
  117. package/dist/core/entities/step.d.ts.map +1 -1
  118. package/dist/core/entities/step.js.map +1 -1
  119. package/dist/core/run-annotation.d.ts +79 -0
  120. package/dist/core/run-annotation.d.ts.map +1 -0
  121. package/dist/core/run-annotation.js +125 -0
  122. package/dist/core/run-annotation.js.map +1 -0
  123. package/dist/core/workflow-run.d.ts +1 -1
  124. package/dist/core/workflow-run.d.ts.map +1 -1
  125. package/dist/core/workflow-run.js.map +1 -1
  126. package/dist/hooks/api/annotations.d.ts +63 -0
  127. package/dist/hooks/api/annotations.d.ts.map +1 -0
  128. package/dist/hooks/api/annotations.js +145 -0
  129. package/dist/hooks/api/annotations.js.map +1 -0
  130. package/dist/hooks/api/run-annotation-mapper.d.ts +10 -0
  131. package/dist/hooks/api/run-annotation-mapper.d.ts.map +1 -0
  132. package/dist/hooks/api/run-annotation-mapper.js +22 -0
  133. package/dist/hooks/api/run-annotation-mapper.js.map +1 -0
  134. package/dist/hooks/api/run-annotations.d.ts +25 -0
  135. package/dist/hooks/api/run-annotations.d.ts.map +1 -0
  136. package/dist/hooks/api/run-annotations.js +78 -0
  137. package/dist/hooks/api/run-annotations.js.map +1 -0
  138. package/dist/hooks/api/step-attempt-detail.d.ts +14 -0
  139. package/dist/hooks/api/step-attempt-detail.d.ts.map +1 -0
  140. package/dist/hooks/api/step-attempt-detail.js +43 -0
  141. package/dist/hooks/api/step-attempt-detail.js.map +1 -0
  142. package/dist/hooks/api/workflow-run-mapper.d.ts +9 -2
  143. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  144. package/dist/hooks/api/workflow-run-mapper.js +47 -0
  145. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  146. package/dist/pages/project-workflows-page.js +19 -19
  147. package/dist/pages/project-workflows-page.js.map +1 -1
  148. package/dist/pages/workflow-run-list-page.js +1 -1
  149. package/dist/pages/workflow-run-list-page.js.map +1 -1
  150. package/dist/routes/inputs.d.ts +7 -2
  151. package/dist/routes/inputs.d.ts.map +1 -1
  152. package/dist/routes/inputs.js +5 -6
  153. package/dist/routes/inputs.js.map +1 -1
  154. package/dist/tsconfig.test.tsbuildinfo +1 -1
  155. package/package.json +10 -9
  156. package/src/components/identifier/identifier.tsx +2 -2
  157. package/src/components/job-detail/agent-config-failure-callout.tsx +5 -1
  158. package/src/components/job-detail/job-context-panel.test.tsx +72 -0
  159. package/src/components/job-detail/job-context-panel.tsx +237 -0
  160. package/src/components/job-detail/job-detail-header.test.tsx +3 -0
  161. package/src/components/job-detail/job-detail-header.tsx +65 -34
  162. package/src/components/job-detail/job-detail-view.stories.tsx +555 -48
  163. package/src/components/job-detail/job-detail-view.tsx +101 -42
  164. package/src/components/job-detail/job-empty-states.tsx +23 -10
  165. package/src/components/job-detail/job-execution-switcher.tsx +5 -5
  166. package/src/components/job-detail/json-code.tsx +120 -0
  167. package/src/components/job-detail/step-attempt-log-panel.test.tsx +1 -0
  168. package/src/components/job-detail/step-attempt-log-panel.tsx +17 -6
  169. package/src/components/job-detail/step-troubleshooting.stories.tsx +120 -0
  170. package/src/components/job-detail/step-troubleshooting.test.tsx +337 -0
  171. package/src/components/job-detail/step-troubleshooting.tsx +537 -0
  172. package/src/components/job-graph/job-graph-content.tsx +4 -3
  173. package/src/components/job-graph/job-node.tsx +3 -3
  174. package/src/components/step-list/index.ts +1 -0
  175. package/src/components/step-list/step-list-model.ts +5 -0
  176. package/src/components/step-list/step-list.test.tsx +75 -2
  177. package/src/components/step-list/step-list.tsx +73 -20
  178. package/src/components/workflow-run-duration-label.tsx +1 -1
  179. package/src/components/workflow-run-list/job-status-strip.tsx +6 -6
  180. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +1 -1
  181. package/src/components/workflow-run-list/workflow-run-filters.tsx +4 -4
  182. package/src/components/workflow-run-list/workflow-run-list-states.tsx +6 -6
  183. package/src/components/workflow-run-list/workflow-run-list-view.tsx +1 -1
  184. package/src/components/workflow-run-list/workflow-run-row.tsx +7 -7
  185. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +1 -1
  186. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +1 -1
  187. package/src/components/workflow-run-summary/workflow-run-summary.tsx +12 -12
  188. package/src/components/workflow-run-tabs/index.ts +7 -0
  189. package/src/components/workflow-run-tabs/run-annotation-count-chip.tsx +63 -0
  190. package/src/components/workflow-run-tabs/run-annotation-item.tsx +123 -0
  191. package/src/components/workflow-run-tabs/run-annotation-list.tsx +273 -0
  192. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +77 -3
  193. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +95 -20
  194. package/src/components/workflow-run-tabs/run-annotations.stories.tsx +293 -0
  195. package/src/components/workflow-run-tabs/severity-visuals.ts +37 -0
  196. package/src/components/workflow-run-view/run-workspace-nav.test.tsx +44 -0
  197. package/src/components/workflow-run-view/run-workspace-nav.tsx +22 -9
  198. package/src/components/workflow-run-view/workflow-run-states.tsx +6 -6
  199. package/src/components/workflow-run-view/workflow-run-view.test.tsx +246 -20
  200. package/src/components/workflow-run-view/workflow-run-view.tsx +255 -37
  201. package/src/core/entities/job-execution.ts +7 -0
  202. package/src/core/entities/job.ts +9 -0
  203. package/src/core/entities/step-attempt.ts +33 -0
  204. package/src/core/entities/step.ts +3 -1
  205. package/src/core/run-annotation.test.ts +315 -0
  206. package/src/core/run-annotation.ts +244 -0
  207. package/src/core/workflow-run.ts +4 -0
  208. package/src/hooks/api/annotations.test.ts +84 -0
  209. package/src/hooks/api/annotations.ts +240 -0
  210. package/src/hooks/api/run-annotation-mapper.ts +30 -0
  211. package/src/hooks/api/run-annotations.ts +137 -0
  212. package/src/hooks/api/step-attempt-detail.ts +67 -0
  213. package/src/hooks/api/workflow-run-mapper.ts +46 -0
  214. package/src/hooks/api/workflow-runs.test.tsx +61 -1
  215. package/src/pages/project-workflows-page.tsx +19 -19
  216. package/src/pages/workflow-job-detail-page.test.tsx +118 -1
  217. package/src/pages/workflow-run-list-page.tsx +1 -1
  218. package/src/routes/inputs.ts +7 -2
  219. package/test/fixtures/workflow-run.ts +7 -0
  220. package/tsconfig.build.tsbuildinfo +1 -1
  221. package/dist/core/workflow-run-tabs.d.ts +0 -9
  222. package/dist/core/workflow-run-tabs.d.ts.map +0 -1
  223. package/dist/core/workflow-run-tabs.js +0 -3
  224. package/dist/core/workflow-run-tabs.js.map +0 -1
  225. package/src/core/workflow-run-tabs.ts +0 -8
@@ -0,0 +1,315 @@
1
+ import {
2
+ workflowJob,
3
+ workflowJobExecutionDto,
4
+ workflowStepAttemptDto,
5
+ workflowStepDto,
6
+ } from '#test/fixtures/workflow-run.js';
7
+ import {
8
+ buildRunAnnotationList,
9
+ highestRunAnnotationSeverity,
10
+ type RunAnnotationRecord,
11
+ type RunAnnotationStyle,
12
+ summarizeJobAnnotations,
13
+ summarizeRunAnnotations,
14
+ } from './run-annotation.js';
15
+ import type {Job} from './workflow-run.js';
16
+
17
+ const BUILD_JOB_ID = '44444444-4444-4444-8444-00000000000b';
18
+ const TEST_JOB_ID = '44444444-4444-4444-8444-00000000000c';
19
+ const BUILD_EXECUTION_ID = '77777777-7777-4777-8777-00000000000b';
20
+ const TEST_EXECUTION_ONE_ID = '77777777-7777-4777-8777-00000000000c';
21
+ const TEST_EXECUTION_TWO_ID = '77777777-7777-4777-8777-00000000000d';
22
+ const BUILD_STEP_ID = '55555555-5555-4555-8555-00000000000b';
23
+ const TEST_STEP_ID = '55555555-5555-4555-8555-00000000000c';
24
+ const BUILD_ATTEMPT_ID = '66666666-6666-4666-8666-00000000000b';
25
+
26
+ describe('summarizeRunAnnotations', () => {
27
+ test('counts every annotation in the total and only severities per severity', () => {
28
+ const annotations = [
29
+ annotation({id: 'a', style: 'error'}),
30
+ annotation({id: 'b', style: 'warning'}),
31
+ annotation({id: 'c', style: 'default'}),
32
+ annotation({id: 'd', style: 'error'}),
33
+ ];
34
+
35
+ const summary = summarizeRunAnnotations(annotations);
36
+
37
+ expect(summary).toEqual({
38
+ total: 4,
39
+ error: 2,
40
+ warning: 1,
41
+ info: 0,
42
+ success: 0,
43
+ truncated: false,
44
+ });
45
+ });
46
+
47
+ test('carries truncation so counts read as a lower bound', () => {
48
+ const summary = summarizeRunAnnotations([annotation({id: 'a', style: 'info'})], {
49
+ truncated: true,
50
+ });
51
+
52
+ expect(summary.truncated).toBe(true);
53
+ });
54
+ });
55
+
56
+ describe('summarizeJobAnnotations', () => {
57
+ test('counts only the annotations owned by one job', () => {
58
+ const annotations = [
59
+ annotation({id: 'a', style: 'error', jobId: BUILD_JOB_ID}),
60
+ annotation({id: 'b', style: 'warning', jobId: TEST_JOB_ID}),
61
+ annotation({id: 'c', style: 'info', jobId: TEST_JOB_ID}),
62
+ ];
63
+
64
+ const summary = summarizeJobAnnotations(annotations, TEST_JOB_ID);
65
+
66
+ expect(summary.total).toBe(2);
67
+ expect(summary.error).toBe(0);
68
+ expect(summary.warning).toBe(1);
69
+ });
70
+ });
71
+
72
+ describe('highestRunAnnotationSeverity', () => {
73
+ test('reports the loudest severity present', () => {
74
+ const summary = summarizeRunAnnotations([
75
+ annotation({id: 'a', style: 'success'}),
76
+ annotation({id: 'b', style: 'warning'}),
77
+ ]);
78
+
79
+ expect(highestRunAnnotationSeverity(summary)).toBe('warning');
80
+ });
81
+
82
+ test('reports nothing when only unstyled annotations exist', () => {
83
+ const summary = summarizeRunAnnotations([annotation({id: 'a', style: 'default'})]);
84
+
85
+ expect(highestRunAnnotationSeverity(summary)).toBeNull();
86
+ });
87
+ });
88
+
89
+ describe('buildRunAnnotationList', () => {
90
+ test('ranks by severity before emission order', () => {
91
+ const annotations = [
92
+ annotation({id: 'a', style: 'success', sequence: 1}),
93
+ annotation({id: 'b', style: 'error', sequence: 2}),
94
+ annotation({id: 'c', style: 'default', sequence: 3}),
95
+ annotation({id: 'd', style: 'warning', sequence: 4}),
96
+ annotation({id: 'e', style: 'info', sequence: 5}),
97
+ ];
98
+
99
+ const entries = buildRunAnnotationList({annotations, jobs: jobs()});
100
+
101
+ expect(entries.map((entry) => entry.annotation.id)).toEqual(['b', 'd', 'e', 'a', 'c']);
102
+ });
103
+
104
+ test('breaks a severity tie by job position before sequence', () => {
105
+ // `sequence` restarts per execution, so the later job's lower sequence must not lead.
106
+ const annotations = [
107
+ annotation({id: 'late-job', style: 'error', sequence: 1, jobId: TEST_JOB_ID}),
108
+ annotation({id: 'early-job', style: 'error', sequence: 9, jobId: BUILD_JOB_ID}),
109
+ ];
110
+
111
+ const entries = buildRunAnnotationList({annotations, jobs: jobs()});
112
+
113
+ expect(entries.map((entry) => entry.annotation.id)).toEqual(['early-job', 'late-job']);
114
+ });
115
+
116
+ test('breaks a job tie by execution sequence', () => {
117
+ const annotations = [
118
+ annotation({
119
+ id: 'second-execution',
120
+ style: 'error',
121
+ sequence: 1,
122
+ jobId: TEST_JOB_ID,
123
+ jobExecutionId: TEST_EXECUTION_TWO_ID,
124
+ }),
125
+ annotation({
126
+ id: 'first-execution',
127
+ style: 'error',
128
+ sequence: 2,
129
+ jobId: TEST_JOB_ID,
130
+ jobExecutionId: TEST_EXECUTION_ONE_ID,
131
+ }),
132
+ ];
133
+
134
+ const entries = buildRunAnnotationList({annotations, jobs: jobs()});
135
+
136
+ expect(entries.map((entry) => entry.annotation.id)).toEqual([
137
+ 'first-execution',
138
+ 'second-execution',
139
+ ]);
140
+ });
141
+
142
+ test('resolves provenance and an origin the run can route back to', () => {
143
+ const annotations = [annotation({id: 'a', style: 'error'})];
144
+
145
+ const [entry] = buildRunAnnotationList({annotations, jobs: jobs()});
146
+
147
+ expect(entry?.jobName).toBe('build');
148
+ expect(entry?.stepLabel).toBe('compile');
149
+ expect(entry?.attemptLabel).toBe('attempt 1');
150
+ expect(entry?.origin).toEqual({
151
+ jobId: BUILD_JOB_ID,
152
+ jobExecutionId: BUILD_EXECUTION_ID,
153
+ stepId: BUILD_STEP_ID,
154
+ stepAttemptId: BUILD_ATTEMPT_ID,
155
+ });
156
+ });
157
+
158
+ test('names an execution only when the job ran more than once', () => {
159
+ const annotations = [
160
+ annotation({id: 'single', style: 'info'}),
161
+ annotation({
162
+ id: 'multi',
163
+ style: 'info',
164
+ jobId: TEST_JOB_ID,
165
+ jobExecutionId: TEST_EXECUTION_TWO_ID,
166
+ originStepId: TEST_STEP_ID,
167
+ }),
168
+ ];
169
+
170
+ const entries = buildRunAnnotationList({annotations, jobs: jobs()});
171
+ const byId = new Map(entries.map((entry) => [entry.annotation.id, entry]));
172
+
173
+ expect(byId.get('single')?.executionLabel).toBeNull();
174
+ expect(byId.get('multi')?.executionLabel).toBe('execution #2');
175
+ });
176
+
177
+ test('keeps an annotation whose step the run attempt no longer holds, without an origin', () => {
178
+ const annotations = [
179
+ annotation({
180
+ id: 'orphan',
181
+ style: 'error',
182
+ originStepId: '55555555-5555-4555-8555-0000000000ff',
183
+ }),
184
+ ];
185
+
186
+ const [entry] = buildRunAnnotationList({annotations, jobs: jobs()});
187
+
188
+ expect(entry?.annotation.id).toBe('orphan');
189
+ expect(entry?.jobName).toBe('build');
190
+ expect(entry?.stepLabel).toBeNull();
191
+ expect(entry?.origin).toBeNull();
192
+ });
193
+
194
+ test('sorts an annotation from an unknown job last rather than first', () => {
195
+ const annotations = [
196
+ annotation({
197
+ id: 'unknown-job',
198
+ style: 'error',
199
+ jobId: '44444444-4444-4444-8444-0000000000ff',
200
+ }),
201
+ annotation({id: 'known-job', style: 'error'}),
202
+ ];
203
+
204
+ const entries = buildRunAnnotationList({annotations, jobs: jobs()});
205
+
206
+ expect(entries.map((entry) => entry.annotation.id)).toEqual(['known-job', 'unknown-job']);
207
+ });
208
+
209
+ test('filters by severity and by job', () => {
210
+ const annotations = [
211
+ annotation({id: 'build-error', style: 'error'}),
212
+ annotation({id: 'build-info', style: 'info'}),
213
+ annotation({id: 'test-error', style: 'error', jobId: TEST_JOB_ID}),
214
+ ];
215
+
216
+ expect(
217
+ buildRunAnnotationList({annotations, jobs: jobs(), severity: 'error'}).map(
218
+ (entry) => entry.annotation.id,
219
+ ),
220
+ ).toEqual(['build-error', 'test-error']);
221
+ expect(
222
+ buildRunAnnotationList({annotations, jobs: jobs(), jobId: BUILD_JOB_ID}).map(
223
+ (entry) => entry.annotation.id,
224
+ ),
225
+ ).toEqual(['build-error', 'build-info']);
226
+ });
227
+
228
+ test('never selects an unstyled annotation with a severity filter', () => {
229
+ const annotations = [annotation({id: 'plain', style: 'default'})];
230
+
231
+ expect(buildRunAnnotationList({annotations, jobs: jobs(), severity: 'info'})).toEqual([]);
232
+ });
233
+ });
234
+
235
+ function annotation(
236
+ overrides: Partial<RunAnnotationRecord> & {id: string; style: RunAnnotationStyle},
237
+ ): RunAnnotationRecord {
238
+ return {
239
+ jobId: BUILD_JOB_ID,
240
+ jobExecutionId: BUILD_EXECUTION_ID,
241
+ originStepId: BUILD_STEP_ID,
242
+ originStepAttempt: 1,
243
+ context: `context-${overrides.id}`,
244
+ sequence: 1,
245
+ body: 'Body',
246
+ ...overrides,
247
+ };
248
+ }
249
+
250
+ function jobs(): Job[] {
251
+ return [
252
+ workflowJob({
253
+ id: BUILD_JOB_ID,
254
+ key: 'build',
255
+ name: 'build',
256
+ position: 0,
257
+ job_executions: [
258
+ workflowJobExecutionDto({
259
+ id: BUILD_EXECUTION_ID,
260
+ job_id: BUILD_JOB_ID,
261
+ sequence: 1,
262
+ steps: [
263
+ workflowStepDto({
264
+ id: BUILD_STEP_ID,
265
+ job_execution_id: BUILD_EXECUTION_ID,
266
+ name: 'compile',
267
+ attempts: [
268
+ workflowStepAttemptDto({
269
+ id: BUILD_ATTEMPT_ID,
270
+ step_id: BUILD_STEP_ID,
271
+ attempt: 1,
272
+ status: 'succeeded',
273
+ }),
274
+ ],
275
+ }),
276
+ ],
277
+ }),
278
+ ],
279
+ }),
280
+ workflowJob({
281
+ id: TEST_JOB_ID,
282
+ key: 'test',
283
+ name: 'test',
284
+ position: 1,
285
+ job_executions: [
286
+ workflowJobExecutionDto({
287
+ id: TEST_EXECUTION_ONE_ID,
288
+ job_id: TEST_JOB_ID,
289
+ sequence: 1,
290
+ steps: [
291
+ workflowStepDto({
292
+ id: TEST_STEP_ID,
293
+ job_execution_id: TEST_EXECUTION_ONE_ID,
294
+ name: 'run tests',
295
+ attempts: [],
296
+ }),
297
+ ],
298
+ }),
299
+ workflowJobExecutionDto({
300
+ id: TEST_EXECUTION_TWO_ID,
301
+ job_id: TEST_JOB_ID,
302
+ sequence: 2,
303
+ steps: [
304
+ workflowStepDto({
305
+ id: TEST_STEP_ID,
306
+ job_execution_id: TEST_EXECUTION_TWO_ID,
307
+ name: 'run tests',
308
+ attempts: [],
309
+ }),
310
+ ],
311
+ }),
312
+ ],
313
+ }),
314
+ ];
315
+ }
@@ -0,0 +1,244 @@
1
+ import type {Job} from './workflow-run.js';
2
+
3
+ export type RunAnnotationStyle = 'default' | 'info' | 'success' | 'warning' | 'error';
4
+
5
+ /**
6
+ * The severities the run surfaces rank and filter by. The `default` style carries no severity:
7
+ * it counts toward the total and sorts last, but no severity filter selects it.
8
+ */
9
+ export const RUN_ANNOTATION_SEVERITIES = ['error', 'warning', 'info', 'success'] as const;
10
+
11
+ export type RunAnnotationSeverity = (typeof RUN_ANNOTATION_SEVERITIES)[number];
12
+
13
+ const SEVERITY_RANK: Record<RunAnnotationStyle, number> = {
14
+ error: 0,
15
+ warning: 1,
16
+ info: 2,
17
+ success: 3,
18
+ default: 4,
19
+ };
20
+
21
+ const SEVERITY_SET = new Set<string>(RUN_ANNOTATION_SEVERITIES);
22
+
23
+ export interface RunAnnotation {
24
+ id: string;
25
+ jobId: string;
26
+ jobExecutionId: string;
27
+ originStepId: string;
28
+ originStepAttempt: number;
29
+ context: string;
30
+ style: RunAnnotationStyle;
31
+ sequence: number;
32
+ }
33
+
34
+ export interface RunAnnotationRecord extends RunAnnotation {
35
+ body: string;
36
+ }
37
+
38
+ /** Counts shown by the run rail and the severity summary line. */
39
+ export interface RunAnnotationSummary {
40
+ total: number;
41
+ error: number;
42
+ warning: number;
43
+ info: number;
44
+ success: number;
45
+ /** True when the read hit its page budget, so every count is a lower bound. */
46
+ truncated: boolean;
47
+ /** Optional per-step counts used by the step inspector without loading annotation bodies. */
48
+ stepCounts?: readonly {
49
+ stepId: string;
50
+ attempt: number;
51
+ total: number;
52
+ }[];
53
+ }
54
+
55
+ /** Everything the annotations list needs to route back to the step that emitted an annotation. */
56
+ export interface RunAnnotationOrigin {
57
+ jobId: string;
58
+ jobExecutionId: string;
59
+ stepId: string;
60
+ stepAttemptId: string;
61
+ }
62
+
63
+ export interface RunAnnotationEntry {
64
+ annotation: RunAnnotationRecord;
65
+ jobName: string | null;
66
+ /** Only set when the job ran more than once, so a single-execution job stays uncluttered. */
67
+ executionLabel: string | null;
68
+ stepLabel: string | null;
69
+ attemptLabel: string;
70
+ /** `null` when the run attempt no longer contains the emitting step, so no link is offered. */
71
+ origin: RunAnnotationOrigin | null;
72
+ }
73
+
74
+ export function annotationSeverity(style: RunAnnotationStyle): RunAnnotationSeverity | null {
75
+ return SEVERITY_SET.has(style) ? (style as RunAnnotationSeverity) : null;
76
+ }
77
+
78
+ export function summarizeRunAnnotations(
79
+ annotations: readonly RunAnnotation[],
80
+ {truncated = false}: {truncated?: boolean | undefined} = {},
81
+ ): RunAnnotationSummary {
82
+ const summary: RunAnnotationSummary = {
83
+ total: annotations.length,
84
+ error: 0,
85
+ warning: 0,
86
+ info: 0,
87
+ success: 0,
88
+ truncated,
89
+ };
90
+
91
+ for (const annotation of annotations) {
92
+ const severity = annotationSeverity(annotation.style);
93
+ if (severity) summary[severity] += 1;
94
+ }
95
+
96
+ return summary;
97
+ }
98
+
99
+ export interface BuildRunAnnotationListInput {
100
+ annotations: readonly RunAnnotationRecord[];
101
+ jobs: readonly Job[];
102
+ severity?: RunAnnotationSeverity | undefined;
103
+ jobId?: string | undefined;
104
+ }
105
+
106
+ /**
107
+ * Ranks annotations for display.
108
+ *
109
+ * Severity leads: emission order is a log affordance, not a summary affordance. `sequence` is
110
+ * only stable within one job execution, so the tie-break walks down job position and execution
111
+ * sequence before using it, and ends on the id so the order never depends on input order.
112
+ */
113
+ export function buildRunAnnotationList({
114
+ annotations,
115
+ jobs,
116
+ severity,
117
+ jobId,
118
+ }: BuildRunAnnotationListInput): RunAnnotationEntry[] {
119
+ const index = indexJobs(jobs);
120
+ const entries: RunAnnotationEntry[] = [];
121
+
122
+ for (const annotation of annotations) {
123
+ if (jobId && annotation.jobId !== jobId) continue;
124
+ const annotationSeverityValue = annotationSeverity(annotation.style);
125
+ if (severity && annotationSeverityValue !== severity) continue;
126
+ entries.push(toEntry(annotation, index));
127
+ }
128
+
129
+ return entries.sort((left, right) => compareEntries(left, right, index));
130
+ }
131
+
132
+ /** Counts annotations owned by one job, for the job page's bounded reference chip. */
133
+ export function summarizeJobAnnotations(
134
+ annotations: readonly RunAnnotation[],
135
+ jobId: string,
136
+ options: {truncated?: boolean | undefined} = {},
137
+ ): RunAnnotationSummary {
138
+ return summarizeRunAnnotations(
139
+ annotations.filter((annotation) => annotation.jobId === jobId),
140
+ options,
141
+ );
142
+ }
143
+
144
+ /** The loudest severity present, which tints a count chip. */
145
+ export function highestRunAnnotationSeverity(
146
+ summary: RunAnnotationSummary,
147
+ ): RunAnnotationSeverity | null {
148
+ return RUN_ANNOTATION_SEVERITIES.find((severity) => summary[severity] > 0) ?? null;
149
+ }
150
+
151
+ interface JobIndexEntry {
152
+ position: number;
153
+ name: string;
154
+ executionCount: number;
155
+ executions: Map<string, JobExecutionIndexEntry>;
156
+ }
157
+
158
+ interface JobExecutionIndexEntry {
159
+ sequence: number;
160
+ steps: Map<string, {label: string; attemptIds: Map<number, string>}>;
161
+ }
162
+
163
+ function indexJobs(jobs: readonly Job[]): Map<string, JobIndexEntry> {
164
+ const index = new Map<string, JobIndexEntry>();
165
+
166
+ for (const job of jobs) {
167
+ const executions = new Map<string, JobExecutionIndexEntry>();
168
+ for (const execution of job.jobExecutions) {
169
+ const steps = new Map<string, {label: string; attemptIds: Map<number, string>}>();
170
+ for (const step of execution.steps) {
171
+ const attemptIds = new Map<number, string>();
172
+ for (const attempt of step.attempts) attemptIds.set(attempt.attempt, attempt.id);
173
+ steps.set(step.id, {label: step.name || step.key || step.id, attemptIds});
174
+ }
175
+ executions.set(execution.id, {sequence: execution.sequence, steps});
176
+ }
177
+
178
+ index.set(job.id, {
179
+ position: job.position,
180
+ name: job.displayName,
181
+ executionCount: job.jobExecutions.length,
182
+ executions,
183
+ });
184
+ }
185
+
186
+ return index;
187
+ }
188
+
189
+ function toEntry(
190
+ annotation: RunAnnotationRecord,
191
+ index: Map<string, JobIndexEntry>,
192
+ ): RunAnnotationEntry {
193
+ const job = index.get(annotation.jobId);
194
+ const execution = job?.executions.get(annotation.jobExecutionId);
195
+ const step = execution?.steps.get(annotation.originStepId);
196
+ const stepAttemptId = step?.attemptIds.get(annotation.originStepAttempt);
197
+
198
+ return {
199
+ annotation,
200
+ jobName: job?.name ?? null,
201
+ executionLabel:
202
+ execution && job && job.executionCount > 1 ? `execution #${execution.sequence}` : null,
203
+ stepLabel: step?.label ?? null,
204
+ attemptLabel: `attempt ${annotation.originStepAttempt}`,
205
+ origin: stepAttemptId
206
+ ? {
207
+ jobId: annotation.jobId,
208
+ jobExecutionId: annotation.jobExecutionId,
209
+ stepId: annotation.originStepId,
210
+ stepAttemptId,
211
+ }
212
+ : null,
213
+ };
214
+ }
215
+
216
+ function compareEntries(
217
+ left: RunAnnotationEntry,
218
+ right: RunAnnotationEntry,
219
+ index: Map<string, JobIndexEntry>,
220
+ ): number {
221
+ const bySeverity = SEVERITY_RANK[left.annotation.style] - SEVERITY_RANK[right.annotation.style];
222
+ if (bySeverity !== 0) return bySeverity;
223
+
224
+ const byJob = jobPosition(left, index) - jobPosition(right, index);
225
+ if (byJob !== 0) return byJob;
226
+
227
+ const byExecution = executionSequence(left, index) - executionSequence(right, index);
228
+ if (byExecution !== 0) return byExecution;
229
+
230
+ const bySequence = left.annotation.sequence - right.annotation.sequence;
231
+ if (bySequence !== 0) return bySequence;
232
+
233
+ return left.annotation.id.localeCompare(right.annotation.id);
234
+ }
235
+
236
+ /** An unmatched job sorts last rather than first, so known provenance leads the list. */
237
+ function jobPosition(entry: RunAnnotationEntry, index: Map<string, JobIndexEntry>): number {
238
+ return index.get(entry.annotation.jobId)?.position ?? Number.MAX_SAFE_INTEGER;
239
+ }
240
+
241
+ function executionSequence(entry: RunAnnotationEntry, index: Map<string, JobIndexEntry>): number {
242
+ const job = index.get(entry.annotation.jobId);
243
+ return job?.executions.get(entry.annotation.jobExecutionId)?.sequence ?? Number.MAX_SAFE_INTEGER;
244
+ }
@@ -46,6 +46,10 @@ export {
46
46
  STEP_ERROR_REASONS,
47
47
  } from './entities/step.js';
48
48
  export type {
49
+ EvaluationTraceEntry,
50
+ EvaluationTraceLimitEntry,
51
+ EvaluationTraceValueEntry,
52
+ StepAttemptDetail,
49
53
  StepAttemptDisplayDuration,
50
54
  StepGateResult,
51
55
  } from './entities/step-attempt.js';
@@ -0,0 +1,84 @@
1
+ import {
2
+ workflowRunAnnotationSummaryQueryOptions,
3
+ workflowRunAnnotationsQueryOptions,
4
+ } from './annotations.js';
5
+
6
+ const WORKFLOW_RUN_ID = '11111111-1111-4111-8111-111111111111';
7
+
8
+ describe('annotation query polling', () => {
9
+ it('polls enabled list and summary queries by default', () => {
10
+ const summary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1);
11
+ const list = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1);
12
+
13
+ expect(summary.refetchInterval).toBe(5_000);
14
+ expect(refetchIntervalForPages(list, 1)).toBe(5_000);
15
+ });
16
+
17
+ it('polls when enabled explicitly for both query builders', () => {
18
+ const summary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
19
+ polling: true,
20
+ });
21
+ const list = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
22
+ polling: true,
23
+ });
24
+
25
+ expect(summary.refetchInterval).toBe(5_000);
26
+ expect(refetchIntervalForPages(list, 1)).toBe(5_000);
27
+ });
28
+
29
+ it('does not poll disabled query builders', () => {
30
+ const summary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
31
+ enabled: false,
32
+ });
33
+ const list = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
34
+ enabled: false,
35
+ });
36
+
37
+ expect(summary.refetchInterval).toBe(false);
38
+ expect(list.refetchInterval).toBe(false);
39
+ });
40
+
41
+ it('stops summary polling when the owning run is terminal', () => {
42
+ const options = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
43
+ polling: false,
44
+ });
45
+
46
+ expect(options.refetchInterval).toBe(false);
47
+ });
48
+
49
+ it('stops annotation list polling when the owning run is terminal', () => {
50
+ const options = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1, undefined, {
51
+ polling: false,
52
+ });
53
+
54
+ expect(options.refetchInterval).toBe(false);
55
+ });
56
+
57
+ it('stops annotation list polling after the first page has been loaded', () => {
58
+ const options = workflowRunAnnotationsQueryOptions(WORKFLOW_RUN_ID, 1);
59
+
60
+ expect(refetchIntervalForPages(options, 2)).toBe(false);
61
+ });
62
+
63
+ it('keeps execution-scoped summaries on a distinct reusable query key', () => {
64
+ const runSummary = workflowRunAnnotationSummaryQueryOptions(WORKFLOW_RUN_ID, 1);
65
+ const executionSummary = workflowRunAnnotationSummaryQueryOptions(
66
+ WORKFLOW_RUN_ID,
67
+ 1,
68
+ '22222222-2222-4222-8222-222222222222',
69
+ );
70
+
71
+ expect(runSummary.queryKey).not.toEqual(executionSummary.queryKey);
72
+ });
73
+ });
74
+
75
+ function refetchIntervalForPages(
76
+ options: ReturnType<typeof workflowRunAnnotationsQueryOptions>,
77
+ pageCount: number,
78
+ ) {
79
+ if (typeof options.refetchInterval !== 'function') return options.refetchInterval;
80
+
81
+ return options.refetchInterval({
82
+ state: {data: {pages: Array.from({length: pageCount}, () => ({}))}},
83
+ } as never);
84
+ }