@patronage/factory-ci 0.2.1 → 1.0.0-alpha.13
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/README.md +183 -3
- package/dist/index.d.ts +522 -2
- package/dist/index.js +1530 -35
- package/package.json +6 -6
- package/src/bundle-alchemy-entry.ts +94 -1
- package/src/candidate-lifecycle.ts +29 -0
- package/src/factory-workflow.ts +27 -28
- package/src/github-app-token.ts +162 -0
- package/src/index.ts +80 -0
- package/src/pinned-action.ts +30 -0
- package/src/production-impact-workflow.ts +109 -0
- package/src/proof-reuse-gate.ts +141 -10
- package/src/proof-reuse-presentation.ts +125 -0
- package/src/push-identity-workflow.ts +448 -0
- package/src/vitest-profile-reader.test.ts +208 -0
- package/src/vitest-profile-reader.ts +220 -0
- package/src/vitest-profile.ts +631 -0
- package/src/workflow-shell-lint.ts +462 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import type { PinnedAction } from "./actions.ts";
|
|
2
|
+
import type { WorkflowStep } from "./factory-workflow.ts";
|
|
3
|
+
import { assertPinnedAction } from "./pinned-action.ts";
|
|
4
|
+
|
|
5
|
+
export const FACTORY_PUSH_IDENTITY_SCHEMA_VERSION = 1;
|
|
6
|
+
export const FACTORY_PUSH_IDENTITY_ARTIFACT_PREFIX = "factory-push-identity";
|
|
7
|
+
export const FACTORY_PUSH_IDENTITY_RECORD_STEP_ID =
|
|
8
|
+
"factory_push_identity_record";
|
|
9
|
+
export const FACTORY_PUSH_IDENTITY_LOOKUP_STEP_ID =
|
|
10
|
+
"factory_push_identity_lookup";
|
|
11
|
+
export const FACTORY_PUSH_IDENTITY_DOWNLOAD_STEP_ID =
|
|
12
|
+
"factory_push_identity_download";
|
|
13
|
+
export const FACTORY_PUSH_IDENTITY_CHECKOUT_STEP_ID =
|
|
14
|
+
"factory_push_identity_checkout";
|
|
15
|
+
export const FACTORY_PUSH_IDENTITY_VALIDATE_STEP_ID = "factory_push_identity";
|
|
16
|
+
|
|
17
|
+
/** Versioned document uploaded by a push-triggered verification run. */
|
|
18
|
+
export interface FactoryPushIdentityEnvelope {
|
|
19
|
+
readonly after: string;
|
|
20
|
+
readonly before: string;
|
|
21
|
+
readonly repository: string;
|
|
22
|
+
readonly runId: string;
|
|
23
|
+
readonly schemaVersion: typeof FACTORY_PUSH_IDENTITY_SCHEMA_VERSION;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type FactoryPushIdentityDisposition = "refused" | "usable";
|
|
27
|
+
|
|
28
|
+
const ARTIFACT_MAX_BYTES = 16_384;
|
|
29
|
+
const ENVELOPE_MAX_BYTES = 4096;
|
|
30
|
+
|
|
31
|
+
const expression = (value: string): string => `\${{ ${value} }}`;
|
|
32
|
+
const artifactName = (runId: string): string =>
|
|
33
|
+
`${FACTORY_PUSH_IDENTITY_ARTIFACT_PREFIX}-${runId}`;
|
|
34
|
+
|
|
35
|
+
const producerScript = String.raw`identity="$RUNNER_TEMP/factory-push-identity.json"
|
|
36
|
+
produced=false
|
|
37
|
+
reason='producer_error'
|
|
38
|
+
before=''
|
|
39
|
+
event_after=''
|
|
40
|
+
rm -f "$identity"
|
|
41
|
+
|
|
42
|
+
is_hex_sha() {
|
|
43
|
+
[ "$(printf '%s' "$1" | wc -c | tr -d '[:space:]')" -eq 40 ] &&
|
|
44
|
+
case "$1" in *[!0-9a-f]*) false;; *) true;; esac
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
is_nonzero_sha() {
|
|
48
|
+
is_hex_sha "$1" && [ "$1" != '0000000000000000000000000000000000000000' ]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if [ "$GITHUB_EVENT_NAME" != 'push' ]; then
|
|
52
|
+
reason='not_push_event'
|
|
53
|
+
elif ! printf '%s' "$GITHUB_REPOSITORY" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
|
|
54
|
+
reason='repository_invalid'
|
|
55
|
+
elif ! printf '%s' "$GITHUB_RUN_ID" | grep -Eq '^[1-9][0-9]*$'; then
|
|
56
|
+
reason='run_id_invalid'
|
|
57
|
+
elif ! before="$(jq -er '.before | select(type == "string")' "$GITHUB_EVENT_PATH" 2>/dev/null)"; then
|
|
58
|
+
reason='event_before_missing'
|
|
59
|
+
elif ! event_after="$(jq -er '.after | select(type == "string")' "$GITHUB_EVENT_PATH" 2>/dev/null)"; then
|
|
60
|
+
reason='event_after_missing'
|
|
61
|
+
elif ! is_hex_sha "$before"; then
|
|
62
|
+
reason='event_before_invalid'
|
|
63
|
+
elif ! is_nonzero_sha "$event_after" || ! is_nonzero_sha "$GITHUB_SHA"; then
|
|
64
|
+
reason='event_after_invalid'
|
|
65
|
+
elif [ "$event_after" != "$GITHUB_SHA" ]; then
|
|
66
|
+
reason='event_after_mismatch'
|
|
67
|
+
elif jq -cn \
|
|
68
|
+
--argjson schemaVersion '${FACTORY_PUSH_IDENTITY_SCHEMA_VERSION}' \
|
|
69
|
+
--arg repository "$GITHUB_REPOSITORY" \
|
|
70
|
+
--arg runId "$GITHUB_RUN_ID" \
|
|
71
|
+
--arg before "$before" \
|
|
72
|
+
--arg after "$event_after" \
|
|
73
|
+
'{schemaVersion: $schemaVersion, repository: $repository, runId: $runId, before: $before, after: $after}' > "$identity"; then
|
|
74
|
+
produced=true
|
|
75
|
+
reason='produced'
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
printf 'produced=%s\nreason=%s\n' "$produced" "$reason" >> "$GITHUB_OUTPUT"
|
|
79
|
+
{
|
|
80
|
+
printf '## Exact push identity producer\n\n'
|
|
81
|
+
printf -- '- Status: %s\n' "$reason"
|
|
82
|
+
printf -- '- Repository/run: %s / %s\n' "$GITHUB_REPOSITORY" "$GITHUB_RUN_ID"
|
|
83
|
+
if [ "$produced" = true ]; then
|
|
84
|
+
printf -- '- Bound push: %s → %s\n' "$before" "$event_after"
|
|
85
|
+
else
|
|
86
|
+
printf -- '- Identity transport was not produced; downstream classification will refuse withdrawal.\n'
|
|
87
|
+
fi
|
|
88
|
+
} >> "$GITHUB_STEP_SUMMARY"`;
|
|
89
|
+
|
|
90
|
+
const lookupScript = String.raw`status='refused'
|
|
91
|
+
reason='artifact_lookup_failed'
|
|
92
|
+
artifact_id=''
|
|
93
|
+
matches="$RUNNER_TEMP/factory-push-identity-artifacts.tsv"
|
|
94
|
+
artifact_name='${FACTORY_PUSH_IDENTITY_ARTIFACT_PREFIX}-'"$EXPECTED_RUN_ID"
|
|
95
|
+
rm -f "$matches"
|
|
96
|
+
|
|
97
|
+
is_sha() {
|
|
98
|
+
[ "$(printf '%s' "$1" | wc -c | tr -d '[:space:]')" -eq 40 ] &&
|
|
99
|
+
[ "$1" != '0000000000000000000000000000000000000000' ] &&
|
|
100
|
+
case "$1" in *[!0-9a-f]*) false;; *) true;; esac
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if [ "$EXPECTED_EVENT_NAME" != 'workflow_run' ]; then
|
|
104
|
+
reason='event_not_workflow_run'
|
|
105
|
+
elif [ "$EXPECTED_WORKFLOW_EVENT" != 'push' ]; then
|
|
106
|
+
reason='triggering_workflow_not_push'
|
|
107
|
+
elif [ "$EXPECTED_CONCLUSION" != 'success' ]; then
|
|
108
|
+
reason='triggering_workflow_not_successful'
|
|
109
|
+
elif [ "$EXPECTED_WORKFLOW_REPOSITORY" != "$EXPECTED_REPOSITORY" ]; then
|
|
110
|
+
reason='triggering_repository_mismatch'
|
|
111
|
+
elif ! printf '%s' "$EXPECTED_REPOSITORY" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
|
|
112
|
+
reason='triggering_repository_invalid'
|
|
113
|
+
elif ! printf '%s' "$EXPECTED_RUN_ID" | grep -Eq '^[1-9][0-9]*$'; then
|
|
114
|
+
reason='triggering_run_id_invalid'
|
|
115
|
+
elif ! is_sha "$EXPECTED_AFTER_SHA"; then
|
|
116
|
+
reason='triggering_head_sha_invalid'
|
|
117
|
+
elif ! gh api --paginate \
|
|
118
|
+
"repos/$EXPECTED_REPOSITORY/actions/runs/$EXPECTED_RUN_ID/artifacts?per_page=100" \
|
|
119
|
+
--jq ".artifacts[] | select(.name == \"$artifact_name\") | [.id, .size_in_bytes, .expired] | @tsv" \
|
|
120
|
+
> "$matches" 2>/dev/null; then
|
|
121
|
+
reason='artifact_lookup_failed'
|
|
122
|
+
else
|
|
123
|
+
count="$(awk 'END { print NR + 0 }' "$matches")"
|
|
124
|
+
if [ "$count" -eq 0 ]; then
|
|
125
|
+
reason='artifact_missing'
|
|
126
|
+
elif [ "$count" -ne 1 ]; then
|
|
127
|
+
reason='artifact_duplicate'
|
|
128
|
+
else
|
|
129
|
+
IFS="$(printf '\t')" read -r artifact_id artifact_size artifact_expired < "$matches" || true
|
|
130
|
+
if ! printf '%s' "$artifact_id" | grep -Eq '^[1-9][0-9]*$' ||
|
|
131
|
+
! printf '%s' "$artifact_size" | grep -Eq '^[0-9]+$' ||
|
|
132
|
+
{ [ "$artifact_expired" != 'false' ] && [ "$artifact_expired" != 'true' ]; }; then
|
|
133
|
+
reason='artifact_metadata_invalid'
|
|
134
|
+
artifact_id=''
|
|
135
|
+
elif [ "$artifact_expired" = 'true' ]; then
|
|
136
|
+
reason='artifact_expired'
|
|
137
|
+
artifact_id=''
|
|
138
|
+
elif [ "$artifact_size" -gt '${ARTIFACT_MAX_BYTES}' ]; then
|
|
139
|
+
reason='artifact_oversized'
|
|
140
|
+
artifact_id=''
|
|
141
|
+
else
|
|
142
|
+
status='available'
|
|
143
|
+
reason='artifact_available'
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
printf 'status=%s\nreason=%s\nartifact_id=%s\n' "$status" "$reason" "$artifact_id" >> "$GITHUB_OUTPUT"`;
|
|
149
|
+
|
|
150
|
+
const validationScript = String.raw`disposition='refused'
|
|
151
|
+
reason='validation_error'
|
|
152
|
+
before=''
|
|
153
|
+
after=''
|
|
154
|
+
identity_directory="$RUNNER_TEMP/factory-push-identity"
|
|
155
|
+
identity="$identity_directory/factory-push-identity.json"
|
|
156
|
+
artifact_name='${FACTORY_PUSH_IDENTITY_ARTIFACT_PREFIX}-'"$EXPECTED_RUN_ID"
|
|
157
|
+
|
|
158
|
+
is_sha() {
|
|
159
|
+
[ "$(printf '%s' "$1" | wc -c | tr -d '[:space:]')" -eq 40 ] &&
|
|
160
|
+
[ "$1" != '0000000000000000000000000000000000000000' ] &&
|
|
161
|
+
case "$1" in *[!0-9a-f]*) false;; *) true;; esac
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if [ "$EXPECTED_EVENT_NAME" != 'workflow_run' ]; then
|
|
165
|
+
reason='event_not_workflow_run'
|
|
166
|
+
elif [ "$EXPECTED_WORKFLOW_EVENT" != 'push' ]; then
|
|
167
|
+
reason='triggering_workflow_not_push'
|
|
168
|
+
elif [ "$EXPECTED_CONCLUSION" != 'success' ]; then
|
|
169
|
+
reason='triggering_workflow_not_successful'
|
|
170
|
+
elif [ "$EXPECTED_WORKFLOW_REPOSITORY" != "$EXPECTED_REPOSITORY" ]; then
|
|
171
|
+
reason='triggering_repository_mismatch'
|
|
172
|
+
elif ! printf '%s' "$EXPECTED_REPOSITORY" | grep -Eq '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$'; then
|
|
173
|
+
reason='triggering_repository_invalid'
|
|
174
|
+
elif ! printf '%s' "$EXPECTED_RUN_ID" | grep -Eq '^[1-9][0-9]*$'; then
|
|
175
|
+
reason='triggering_run_id_invalid'
|
|
176
|
+
elif ! is_sha "$EXPECTED_AFTER_SHA"; then
|
|
177
|
+
reason='triggering_head_sha_invalid'
|
|
178
|
+
elif [ "$LOOKUP_STATUS" != 'available' ]; then
|
|
179
|
+
if [ -n "$LOOKUP_REASON" ]; then
|
|
180
|
+
reason="$LOOKUP_REASON"
|
|
181
|
+
else
|
|
182
|
+
reason='artifact_lookup_failed'
|
|
183
|
+
fi
|
|
184
|
+
elif [ "$DOWNLOAD_OUTCOME" != 'success' ]; then
|
|
185
|
+
reason='artifact_download_failed'
|
|
186
|
+
elif [ "$CHECKOUT_OUTCOME" != 'success' ]; then
|
|
187
|
+
reason='checkout_failed'
|
|
188
|
+
elif [ ! -d "$identity_directory" ]; then
|
|
189
|
+
reason='artifact_contents_missing'
|
|
190
|
+
else
|
|
191
|
+
entry_count="$(find "$identity_directory" -mindepth 1 -maxdepth 1 -print 2>/dev/null | awk 'END { print NR + 0 }')"
|
|
192
|
+
if [ "$entry_count" -ne 1 ] || [ ! -f "$identity" ] || [ -L "$identity" ]; then
|
|
193
|
+
reason='artifact_contents_invalid'
|
|
194
|
+
else
|
|
195
|
+
envelope_size="$(wc -c < "$identity" | tr -d '[:space:]')"
|
|
196
|
+
if ! printf '%s' "$envelope_size" | grep -Eq '^[0-9]+$'; then
|
|
197
|
+
reason='envelope_size_unreadable'
|
|
198
|
+
elif [ "$envelope_size" -gt '${ENVELOPE_MAX_BYTES}' ]; then
|
|
199
|
+
reason='envelope_oversized'
|
|
200
|
+
elif ! jq -e --argjson schemaVersion '${FACTORY_PUSH_IDENTITY_SCHEMA_VERSION}' '
|
|
201
|
+
type == "object" and
|
|
202
|
+
keys == ["after", "before", "repository", "runId", "schemaVersion"] and
|
|
203
|
+
.schemaVersion == $schemaVersion and
|
|
204
|
+
(.repository | type == "string") and
|
|
205
|
+
(.runId | type == "string" and test("^[1-9][0-9]*$")) and
|
|
206
|
+
(.before | type == "string" and test("^[0-9a-f]{40}$") and (test("^0{40}$") | not)) and
|
|
207
|
+
(.after | type == "string" and test("^[0-9a-f]{40}$") and (test("^0{40}$") | not))
|
|
208
|
+
' "$identity" >/dev/null 2>&1; then
|
|
209
|
+
reason='envelope_malformed'
|
|
210
|
+
else
|
|
211
|
+
repository="$(jq -r '.repository' "$identity")"
|
|
212
|
+
run_id="$(jq -r '.runId' "$identity")"
|
|
213
|
+
before="$(jq -r '.before' "$identity")"
|
|
214
|
+
after="$(jq -r '.after' "$identity")"
|
|
215
|
+
|
|
216
|
+
if [ "$repository" != "$EXPECTED_REPOSITORY" ]; then
|
|
217
|
+
reason='envelope_repository_mismatch'
|
|
218
|
+
elif [ "$run_id" != "$EXPECTED_RUN_ID" ]; then
|
|
219
|
+
reason='envelope_run_id_mismatch'
|
|
220
|
+
elif [ "$after" != "$EXPECTED_AFTER_SHA" ]; then
|
|
221
|
+
reason='envelope_after_mismatch'
|
|
222
|
+
elif ! actual_head="$(git rev-parse --verify HEAD 2>/dev/null)" || [ "$actual_head" != "$EXPECTED_AFTER_SHA" ]; then
|
|
223
|
+
reason='checkout_head_mismatch'
|
|
224
|
+
elif ! git cat-file -e "$before^{commit}" 2>/dev/null || ! git cat-file -e "$after^{commit}" 2>/dev/null; then
|
|
225
|
+
reason='envelope_commit_unreachable'
|
|
226
|
+
elif [ "$before" = "$after" ]; then
|
|
227
|
+
reason='envelope_commits_contradictory'
|
|
228
|
+
elif ! git merge-base --is-ancestor "$before" "$after"; then
|
|
229
|
+
reason='before_not_ancestor'
|
|
230
|
+
else
|
|
231
|
+
disposition='usable'
|
|
232
|
+
reason='identity_bound'
|
|
233
|
+
fi
|
|
234
|
+
fi
|
|
235
|
+
fi
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
if [ "$disposition" != 'usable' ]; then
|
|
239
|
+
before=''
|
|
240
|
+
after=''
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
provenance="$(jq -cn \
|
|
244
|
+
--argjson schemaVersion '${FACTORY_PUSH_IDENTITY_SCHEMA_VERSION}' \
|
|
245
|
+
--arg disposition "$disposition" \
|
|
246
|
+
--arg reason "$reason" \
|
|
247
|
+
--arg repository "$EXPECTED_REPOSITORY" \
|
|
248
|
+
--arg runId "$EXPECTED_RUN_ID" \
|
|
249
|
+
--arg expectedAfter "$EXPECTED_AFTER_SHA" \
|
|
250
|
+
--arg artifactName "$artifact_name" \
|
|
251
|
+
--arg artifactId "$ARTIFACT_ID" \
|
|
252
|
+
--arg before "$before" \
|
|
253
|
+
--arg after "$after" \
|
|
254
|
+
'{schemaVersion: $schemaVersion, disposition: $disposition, reason: $reason, repository: $repository, runId: $runId, expectedAfter: $expectedAfter, artifactName: $artifactName, artifactId: (if $artifactId == "" then null else $artifactId end), before: (if $before == "" then null else $before end), after: (if $after == "" then null else $after end)}')"
|
|
255
|
+
|
|
256
|
+
printf 'disposition=%s\nreason=%s\nbefore=%s\nafter=%s\nprovenance=%s\n' \
|
|
257
|
+
"$disposition" "$reason" "$before" "$after" "$provenance" >> "$GITHUB_OUTPUT"
|
|
258
|
+
{
|
|
259
|
+
printf '## Exact push identity\n\n'
|
|
260
|
+
printf -- '- Disposition: %s\n' "$disposition"
|
|
261
|
+
printf -- '- Reason: %s\n' "$reason"
|
|
262
|
+
printf -- '- Repository/run: %s / %s\n' "$EXPECTED_REPOSITORY" "$EXPECTED_RUN_ID"
|
|
263
|
+
printf -- '- Expected verified head: %s\n' "$EXPECTED_AFTER_SHA"
|
|
264
|
+
if [ "$disposition" = 'usable' ]; then
|
|
265
|
+
printf -- '- Bound push: %s → %s\n' "$before" "$after"
|
|
266
|
+
else
|
|
267
|
+
printf -- '- Production impact classification refused; every consumer-owned production target remains demanded.\n'
|
|
268
|
+
fi
|
|
269
|
+
printf '\nFactory telemetry provenance: %s\n' "$provenance"
|
|
270
|
+
} >> "$GITHUB_STEP_SUMMARY"`;
|
|
271
|
+
|
|
272
|
+
export interface FactoryPushIdentityProducerOptions {
|
|
273
|
+
/** Explicit caller-owned upload-artifact pin from its workflow artifact. */
|
|
274
|
+
readonly uploadArtifact: PinnedAction;
|
|
275
|
+
/** Optional consumer trigger policy combined with the required push event. */
|
|
276
|
+
readonly if?: string;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export interface FactoryPushIdentityProducer {
|
|
280
|
+
readonly artifactName: string;
|
|
281
|
+
readonly steps: readonly WorkflowStep[];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Record and upload the exact push-event identity without changing the Verify
|
|
286
|
+
* result when transport is unavailable. Place these steps after verification.
|
|
287
|
+
*/
|
|
288
|
+
export const factoryPushIdentityProducer = (
|
|
289
|
+
options: FactoryPushIdentityProducerOptions
|
|
290
|
+
): FactoryPushIdentityProducer => {
|
|
291
|
+
assertPinnedAction(
|
|
292
|
+
"uploadArtifact",
|
|
293
|
+
options.uploadArtifact,
|
|
294
|
+
"actions/upload-artifact"
|
|
295
|
+
);
|
|
296
|
+
const condition = options.if
|
|
297
|
+
? `github.event_name == 'push' && (${options.if})`
|
|
298
|
+
: "github.event_name == 'push'";
|
|
299
|
+
const name = artifactName(expression("github.run_id"));
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
artifactName: name,
|
|
302
|
+
steps: Object.freeze([
|
|
303
|
+
{
|
|
304
|
+
continueOnError: true,
|
|
305
|
+
id: FACTORY_PUSH_IDENTITY_RECORD_STEP_ID,
|
|
306
|
+
if: condition,
|
|
307
|
+
name: "Record exact push identity",
|
|
308
|
+
run: producerScript,
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
continueOnError: true,
|
|
312
|
+
if: `${condition} && steps.${FACTORY_PUSH_IDENTITY_RECORD_STEP_ID}.outputs.produced == 'true'`,
|
|
313
|
+
name: "Upload exact push identity",
|
|
314
|
+
uses: options.uploadArtifact.uses,
|
|
315
|
+
with: {
|
|
316
|
+
"if-no-files-found": "error",
|
|
317
|
+
name,
|
|
318
|
+
path: `${expression("runner.temp")}/factory-push-identity.json`,
|
|
319
|
+
"retention-days": "7",
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
]),
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export interface FactoryPushIdentityConsumer {
|
|
327
|
+
/** Job outputs suitable for a caller-owned decision job and telemetry. */
|
|
328
|
+
readonly outputs: Readonly<
|
|
329
|
+
Record<"after" | "before" | "disposition" | "provenance" | "reason", string>
|
|
330
|
+
>;
|
|
331
|
+
readonly requiredPermissions: Readonly<{
|
|
332
|
+
actions: "read";
|
|
333
|
+
contents: "read";
|
|
334
|
+
}>;
|
|
335
|
+
readonly steps: readonly WorkflowStep[];
|
|
336
|
+
/** Run classification only when exact identity transport is usable. */
|
|
337
|
+
readonly usableIf: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export interface FactoryPushIdentityConsumerOptions {
|
|
341
|
+
/** Explicit caller-owned checkout pin from its action family. */
|
|
342
|
+
readonly checkout: PinnedAction;
|
|
343
|
+
/** Explicit caller-owned download-artifact pin from its workflow artifact. */
|
|
344
|
+
readonly downloadArtifact: PinnedAction;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Download and validate identity from exactly the triggering workflow run in
|
|
349
|
+
* the current repository. Every failure becomes a typed refusal; consumers
|
|
350
|
+
* retain deploy policy, credentials, commands, topology, and convergence.
|
|
351
|
+
*/
|
|
352
|
+
export const factoryPushIdentityConsumer = (
|
|
353
|
+
options: FactoryPushIdentityConsumerOptions
|
|
354
|
+
): FactoryPushIdentityConsumer => {
|
|
355
|
+
assertPinnedAction("checkout", options.checkout, "actions/checkout");
|
|
356
|
+
assertPinnedAction(
|
|
357
|
+
"downloadArtifact",
|
|
358
|
+
options.downloadArtifact,
|
|
359
|
+
"actions/download-artifact"
|
|
360
|
+
);
|
|
361
|
+
const currentRepository = expression("github.repository");
|
|
362
|
+
const triggeringRun = expression("github.event.workflow_run.id");
|
|
363
|
+
const triggeringHead = expression("github.event.workflow_run.head_sha");
|
|
364
|
+
const lookupOutput = (name: string): string =>
|
|
365
|
+
expression(`steps.${FACTORY_PUSH_IDENTITY_LOOKUP_STEP_ID}.outputs.${name}`);
|
|
366
|
+
const validateOutput = (name: string): string =>
|
|
367
|
+
expression(
|
|
368
|
+
`steps.${FACTORY_PUSH_IDENTITY_VALIDATE_STEP_ID}.outputs.${name}`
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const contextEnvironment = {
|
|
372
|
+
EXPECTED_AFTER_SHA: triggeringHead,
|
|
373
|
+
EXPECTED_CONCLUSION: expression("github.event.workflow_run.conclusion"),
|
|
374
|
+
EXPECTED_EVENT_NAME: expression("github.event_name"),
|
|
375
|
+
EXPECTED_REPOSITORY: currentRepository,
|
|
376
|
+
EXPECTED_RUN_ID: triggeringRun,
|
|
377
|
+
EXPECTED_WORKFLOW_EVENT: expression("github.event.workflow_run.event"),
|
|
378
|
+
EXPECTED_WORKFLOW_REPOSITORY: expression(
|
|
379
|
+
"github.event.workflow_run.repository.full_name"
|
|
380
|
+
),
|
|
381
|
+
};
|
|
382
|
+
const steps: readonly WorkflowStep[] = [
|
|
383
|
+
{
|
|
384
|
+
continueOnError: true,
|
|
385
|
+
env: {
|
|
386
|
+
GH_TOKEN: expression("github.token"),
|
|
387
|
+
...contextEnvironment,
|
|
388
|
+
},
|
|
389
|
+
id: FACTORY_PUSH_IDENTITY_LOOKUP_STEP_ID,
|
|
390
|
+
name: "Resolve exact push identity artifact",
|
|
391
|
+
run: lookupScript,
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
continueOnError: true,
|
|
395
|
+
id: FACTORY_PUSH_IDENTITY_DOWNLOAD_STEP_ID,
|
|
396
|
+
if: `steps.${FACTORY_PUSH_IDENTITY_LOOKUP_STEP_ID}.outputs.status == 'available'`,
|
|
397
|
+
name: "Download exact push identity",
|
|
398
|
+
uses: options.downloadArtifact.uses,
|
|
399
|
+
with: {
|
|
400
|
+
"artifact-ids": lookupOutput("artifact_id"),
|
|
401
|
+
"github-token": expression("github.token"),
|
|
402
|
+
path: `${expression("runner.temp")}/factory-push-identity`,
|
|
403
|
+
repository: currentRepository,
|
|
404
|
+
"run-id": triggeringRun,
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
continueOnError: true,
|
|
409
|
+
id: FACTORY_PUSH_IDENTITY_CHECKOUT_STEP_ID,
|
|
410
|
+
if: `steps.${FACTORY_PUSH_IDENTITY_LOOKUP_STEP_ID}.outputs.status == 'available'`,
|
|
411
|
+
name: "Checkout exact verified head",
|
|
412
|
+
uses: options.checkout.uses,
|
|
413
|
+
with: { "fetch-depth": "0", ref: triggeringHead },
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
continueOnError: true,
|
|
417
|
+
env: {
|
|
418
|
+
ARTIFACT_ID: lookupOutput("artifact_id"),
|
|
419
|
+
CHECKOUT_OUTCOME: expression(
|
|
420
|
+
`steps.${FACTORY_PUSH_IDENTITY_CHECKOUT_STEP_ID}.outcome`
|
|
421
|
+
),
|
|
422
|
+
DOWNLOAD_OUTCOME: expression(
|
|
423
|
+
`steps.${FACTORY_PUSH_IDENTITY_DOWNLOAD_STEP_ID}.outcome`
|
|
424
|
+
),
|
|
425
|
+
LOOKUP_REASON: lookupOutput("reason"),
|
|
426
|
+
LOOKUP_STATUS: lookupOutput("status"),
|
|
427
|
+
...contextEnvironment,
|
|
428
|
+
},
|
|
429
|
+
id: FACTORY_PUSH_IDENTITY_VALIDATE_STEP_ID,
|
|
430
|
+
if: "always()",
|
|
431
|
+
name: "Validate exact push identity",
|
|
432
|
+
run: validationScript,
|
|
433
|
+
},
|
|
434
|
+
];
|
|
435
|
+
|
|
436
|
+
return Object.freeze({
|
|
437
|
+
outputs: Object.freeze({
|
|
438
|
+
after: validateOutput("after"),
|
|
439
|
+
before: validateOutput("before"),
|
|
440
|
+
disposition: validateOutput("disposition"),
|
|
441
|
+
provenance: validateOutput("provenance"),
|
|
442
|
+
reason: validateOutput("reason"),
|
|
443
|
+
}),
|
|
444
|
+
requiredPermissions: Object.freeze({ actions: "read", contents: "read" }),
|
|
445
|
+
steps: Object.freeze(steps),
|
|
446
|
+
usableIf: `steps.${FACTORY_PUSH_IDENTITY_VALIDATE_STEP_ID}.outputs.disposition == 'usable'`,
|
|
447
|
+
});
|
|
448
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { readVitestProfileDocument } from "./vitest-profile-reader.ts";
|
|
4
|
+
import type { VitestProfile } from "./vitest-profile.ts";
|
|
5
|
+
|
|
6
|
+
const durations = {
|
|
7
|
+
maximum: 16_200,
|
|
8
|
+
mean: 15_800,
|
|
9
|
+
median: 15_700,
|
|
10
|
+
minimum: 15_300,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** A document shaped exactly as `runVitestProfile` writes it. */
|
|
14
|
+
const validProfile = (): VitestProfile => ({
|
|
15
|
+
command: ["vitest", "run", "--reporter=json", "--maxWorkers=2"],
|
|
16
|
+
endedAt: "2026-08-06T12:01:00.000Z",
|
|
17
|
+
environment: {
|
|
18
|
+
arch: "x64",
|
|
19
|
+
availableParallelism: 4,
|
|
20
|
+
cpuCount: 4,
|
|
21
|
+
cpuModel: "AMD EPYC 9R45",
|
|
22
|
+
gitDirty: false,
|
|
23
|
+
gitHead: "0123456789abcdef0123456789abcdef01234567",
|
|
24
|
+
node: "v24.0.0",
|
|
25
|
+
osRelease: "6.8.0",
|
|
26
|
+
platform: "linux",
|
|
27
|
+
totalMemoryBytes: 16_000_000_000,
|
|
28
|
+
vitest: "3.0.0",
|
|
29
|
+
},
|
|
30
|
+
options: { maxWorkers: 2, samples: 3, slowLimit: 20 },
|
|
31
|
+
rawReportDirectory: "/tmp/profile.raw/abc",
|
|
32
|
+
runs: [
|
|
33
|
+
{
|
|
34
|
+
counts: {
|
|
35
|
+
failed: 0,
|
|
36
|
+
passed: 900,
|
|
37
|
+
pending: 0,
|
|
38
|
+
suites: 80,
|
|
39
|
+
tests: 900,
|
|
40
|
+
todo: 0,
|
|
41
|
+
},
|
|
42
|
+
durationMs: 15_300,
|
|
43
|
+
endedAt: "2026-08-06T12:00:20.000Z",
|
|
44
|
+
exitCode: 0,
|
|
45
|
+
failure: null,
|
|
46
|
+
files: [{ durationMs: 900, path: "src/slow.test.ts", status: "passed" }],
|
|
47
|
+
reportAvailable: true,
|
|
48
|
+
sample: 1,
|
|
49
|
+
startedAt: "2026-08-06T12:00:00.000Z",
|
|
50
|
+
tests: [
|
|
51
|
+
{
|
|
52
|
+
durationMs: 450,
|
|
53
|
+
file: "src/slow.test.ts",
|
|
54
|
+
name: "slow case",
|
|
55
|
+
status: "passed",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
schemaVersion: 1,
|
|
61
|
+
startedAt: "2026-08-06T12:00:00.000Z",
|
|
62
|
+
summary: {
|
|
63
|
+
durationMs: { ...durations },
|
|
64
|
+
slowFiles: [
|
|
65
|
+
{ durationMs: { ...durations }, path: "src/slow.test.ts", samples: 3 },
|
|
66
|
+
],
|
|
67
|
+
slowTests: [
|
|
68
|
+
{
|
|
69
|
+
durationMs: { ...durations },
|
|
70
|
+
file: "src/slow.test.ts",
|
|
71
|
+
name: "slow case",
|
|
72
|
+
samples: 3,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
tool: "factory-ci-vitest-profile",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("readVitestProfileDocument", () => {
|
|
80
|
+
it("accepts a document the writer emitted", () => {
|
|
81
|
+
const result = readVitestProfileDocument(validProfile());
|
|
82
|
+
|
|
83
|
+
expect(result.kind).toBe("profile");
|
|
84
|
+
if (result.kind === "profile") {
|
|
85
|
+
expect(result.profile.environment.cpuModel).toBe("AMD EPYC 9R45");
|
|
86
|
+
expect(result.profile.summary.durationMs.median).toBe(15_700);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("names the tool mismatch instead of guessing", () => {
|
|
91
|
+
const result = readVitestProfileDocument({
|
|
92
|
+
...validProfile(),
|
|
93
|
+
tool: "someone-elses-artifact",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(result).toEqual({
|
|
97
|
+
kind: "unrecognized",
|
|
98
|
+
reason:
|
|
99
|
+
'tool is "someone-elses-artifact" rather than "factory-ci-vitest-profile"',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("refuses a future schema version rather than misreading it", () => {
|
|
104
|
+
const result = readVitestProfileDocument({
|
|
105
|
+
...validProfile(),
|
|
106
|
+
schemaVersion: 2,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(result).toEqual({
|
|
110
|
+
kind: "unrecognized",
|
|
111
|
+
reason: "schemaVersion is 2 rather than 1",
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("refuses a document whose environment lost the cpuModel field", () => {
|
|
116
|
+
const document = validProfile() as unknown as {
|
|
117
|
+
environment: Record<string, unknown>;
|
|
118
|
+
};
|
|
119
|
+
delete document.environment.cpuModel;
|
|
120
|
+
|
|
121
|
+
expect(readVitestProfileDocument(document)).toEqual({
|
|
122
|
+
kind: "unrecognized",
|
|
123
|
+
reason: "environment.cpuModel is neither a string nor null",
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("refuses a run without a finite duration", () => {
|
|
128
|
+
const document = validProfile();
|
|
129
|
+
(document.runs[0] as unknown as Record<string, unknown>).durationMs =
|
|
130
|
+
"fast";
|
|
131
|
+
|
|
132
|
+
expect(readVitestProfileDocument(document)).toEqual({
|
|
133
|
+
kind: "unrecognized",
|
|
134
|
+
reason: "runs[0].durationMs is not a finite number",
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("refuses a summary without duration statistics", () => {
|
|
139
|
+
const document = validProfile() as unknown as {
|
|
140
|
+
summary: { durationMs: Record<string, unknown> };
|
|
141
|
+
};
|
|
142
|
+
delete document.summary.durationMs.median;
|
|
143
|
+
|
|
144
|
+
expect(readVitestProfileDocument(document)).toEqual({
|
|
145
|
+
kind: "unrecognized",
|
|
146
|
+
reason: "summary.durationMs.median is not a finite number",
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("refuses empty counts rather than serving undefined fields", () => {
|
|
151
|
+
const document = validProfile();
|
|
152
|
+
(document.runs[0] as unknown as Record<string, unknown>).counts = {};
|
|
153
|
+
|
|
154
|
+
expect(readVitestProfileDocument(document)).toEqual({
|
|
155
|
+
kind: "unrecognized",
|
|
156
|
+
reason: "runs[0].counts.failed is not a finite number",
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("refuses a null slowFiles entry rather than crashing projection", () => {
|
|
161
|
+
const document = validProfile() as unknown as {
|
|
162
|
+
summary: { slowFiles: unknown[] };
|
|
163
|
+
};
|
|
164
|
+
document.summary.slowFiles = [null];
|
|
165
|
+
|
|
166
|
+
expect(readVitestProfileDocument(document)).toEqual({
|
|
167
|
+
kind: "unrecognized",
|
|
168
|
+
reason: "summary.slowFiles[0] is not an object",
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("refuses a slow test whose duration summary is incomplete", () => {
|
|
173
|
+
const document = validProfile() as unknown as {
|
|
174
|
+
summary: { slowTests: { durationMs: Record<string, unknown> }[] };
|
|
175
|
+
};
|
|
176
|
+
const [slowTest] = document.summary.slowTests;
|
|
177
|
+
if (slowTest) {
|
|
178
|
+
delete slowTest.durationMs.median;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
expect(readVitestProfileDocument(document)).toEqual({
|
|
182
|
+
kind: "unrecognized",
|
|
183
|
+
reason: "summary.slowTests[0].durationMs.median is not a finite number",
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("refuses a slow file without a path string", () => {
|
|
188
|
+
const document = validProfile() as unknown as {
|
|
189
|
+
summary: { slowFiles: Record<string, unknown>[] };
|
|
190
|
+
};
|
|
191
|
+
const [slowFile] = document.summary.slowFiles;
|
|
192
|
+
if (slowFile) {
|
|
193
|
+
delete slowFile.path;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
expect(readVitestProfileDocument(document)).toEqual({
|
|
197
|
+
kind: "unrecognized",
|
|
198
|
+
reason: "summary.slowFiles[0].path is not a string",
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("refuses non-object input", () => {
|
|
203
|
+
expect(readVitestProfileDocument(null)).toEqual({
|
|
204
|
+
kind: "unrecognized",
|
|
205
|
+
reason: "the document is not an object",
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|