@vzn/vx-ci 0.0.645 → 0.0.648

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/src/checks.ts DELETED
@@ -1,199 +0,0 @@
1
- // The Checks API half: one completed `check-run` on the built commit, whose
2
- // output is the same summary markdown the job summary shows. Needs
3
- // `GITHUB_TOKEN` with `checks: write`; the plugin declines the check (not
4
- // the whole sink) without one, so the summary still works token-less.
5
- import type { RunSummaryRecord } from '@vzn/vx'
6
- import { truncateUtf8 } from './summary.js'
7
-
8
- export type FetchFn = (
9
- url: string,
10
- init: { method: string; headers: Record<string, string>; body: string; signal?: AbortSignal },
11
- ) => Promise<{ ok: boolean; status: number; text(): Promise<string> }>
12
-
13
- export interface CheckRunEnv {
14
- token: string
15
- repository: string
16
- /** The commit the check attaches to. On `pull_request` events
17
- * `GITHUB_SHA` is the MERGE commit; GitHub still renders the check on
18
- * the PR, so that is the documented default. */
19
- sha: string
20
- apiUrl: string
21
- }
22
-
23
- /** Read the Actions environment; `null` when any piece is missing. */
24
- export function resolveCheckRunEnv(env: Record<string, string | undefined>): CheckRunEnv | null {
25
- const token = env['GITHUB_TOKEN']
26
- const repository = env['GITHUB_REPOSITORY']
27
- const sha = env['GITHUB_SHA']
28
- // All three treated alike: an EMPTY var is as absent as a missing one.
29
- // Only the token used to be checked for empty, so `GITHUB_REPOSITORY=''`
30
- // built a POST to `/repos//check-runs` and `GITHUB_SHA=''` sent
31
- // `head_sha: ''` — a 404 or 422 warning where a clean decline was meant.
32
- if (
33
- token === undefined ||
34
- token === '' ||
35
- repository === undefined ||
36
- repository === '' ||
37
- sha === undefined ||
38
- sha === ''
39
- ) {
40
- return null
41
- }
42
- // The same for the API URL: `??` kept an empty one, and the POST went to a
43
- // relative URL fetch refuses (item 924).
44
- const apiUrl = env['GITHUB_API_URL']
45
- return {
46
- token,
47
- repository,
48
- sha,
49
- apiUrl: apiUrl === undefined || apiUrl === '' ? 'https://api.github.com' : apiUrl,
50
- }
51
- }
52
-
53
- /** fetch refused the server's certificate (untrusted, expired, wrong name). */
54
- function isCertificateRefusal(err: unknown): boolean {
55
- const code = (err as { code?: unknown } | null)?.code
56
- return (
57
- typeof code === 'string' &&
58
- (code.includes('CERT') || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE')
59
- )
60
- }
61
-
62
- /**
63
- * GitHub caps `output.summary` at 65535 characters; truncate with a tell.
64
- * Counted in UTF-8 bytes, which is never fewer than the characters, so
65
- * the page fits whichever unit GitHub counts. The cut was in UTF-16 units
66
- * and split an emoji (the aborted label 🛑) into a lone surrogate the JSON
67
- * body carried (item 924); it now backs off a continuation byte, as the job
68
- * summary's does.
69
- */
70
- export function clampSummary(markdown: string): string {
71
- return truncateUtf8(
72
- markdown,
73
- 65_535,
74
- '\n\n…truncated by @vzn/vx-ci (65535-char Checks API limit)',
75
- )
76
- }
77
-
78
- export function buildCheckRunPayload(args: {
79
- summary: RunSummaryRecord
80
- markdown: string
81
- name: string
82
- sha: string
83
- }): Record<string, unknown> {
84
- const ok = args.summary.exitOk
85
- // Stopped, not broken: a cancelled CI job's run has aborted tasks and
86
- // nothing failed, and GitHub has a conclusion for exactly that.
87
- const cancelled = !ok && args.summary.failedCount === 0 && args.summary.abortedCount > 0
88
- return {
89
- name: args.name,
90
- head_sha: args.sha,
91
- status: 'completed',
92
- conclusion: ok ? 'success' : cancelled ? 'cancelled' : 'failure',
93
- started_at: new Date(args.summary.startedAt).toISOString(),
94
- completed_at: new Date(args.summary.endedAt).toISOString(),
95
- output: {
96
- title: ok
97
- ? `${args.summary.taskCount} task${args.summary.taskCount === 1 ? '' : 's'} · ${args.summary.hitCount} cached` +
98
- (args.summary.hitCount > 0
99
- ? ` (${args.summary.upToDateCount} up-to-date, ${args.summary.restoredLocalCount + args.summary.restoredRemoteCount} restored)`
100
- : '')
101
- : cancelled
102
- ? `cancelled · ${args.summary.abortedCount} aborted`
103
- : `${args.summary.failedCount} failed`,
104
- summary: clampSummary(args.markdown),
105
- },
106
- }
107
- }
108
-
109
- /** Resolves true when `signal` ended the wait. */
110
- function sleepUnless(ms: number, signal: AbortSignal | undefined): Promise<boolean> {
111
- return new Promise((resolve) => {
112
- const done = (): void => {
113
- clearTimeout(timer)
114
- signal?.removeEventListener('abort', done)
115
- resolve(signal?.aborted === true)
116
- }
117
- const timer = setTimeout(done, ms)
118
- signal?.addEventListener('abort', done, { once: true })
119
- })
120
- }
121
-
122
- const CHECK_RETRY_DELAYS_MS = [200, 800] as const
123
- const CHECK_RETRY_STATUS = new Set([502, 503, 504])
124
-
125
- /**
126
- * POST the check run. Failures are REPORTED via `warn`, never thrown —
127
- * observability must never break a run. `signal` is core's flush deadline:
128
- * the deadline alone abandoned the request, and a hanging API kept the
129
- * process alive with no end (item 1055).
130
- */
131
- export async function postCheckRun(args: {
132
- env: CheckRunEnv
133
- payload: Record<string, unknown>
134
- fetchFn: FetchFn
135
- warn: (m: string) => void
136
- signal?: AbortSignal
137
- }): Promise<void> {
138
- const url = `${args.env.apiUrl}/repos/${args.env.repository}/check-runs`
139
- const post = () =>
140
- args.fetchFn(url, {
141
- method: 'POST',
142
- headers: {
143
- authorization: `Bearer ${args.env.token}`,
144
- accept: 'application/vnd.github+json',
145
- 'content-type': 'application/json',
146
- 'x-github-api-version': '2022-11-28',
147
- 'user-agent': 'vzn-vx-ci',
148
- },
149
- body: JSON.stringify(args.payload),
150
- ...(args.signal === undefined ? {} : { signal: args.signal }),
151
- })
152
- try {
153
- // GitHub's own blips (502, 503, 504, a dropped connection) are retried
154
- // twice, 200 then 800 ms apart, inside the flush deadline: one of them
155
- // cost the run its check (F-29). A retry after a 502 that GitHub did
156
- // process adds a second run of the same name, which GitHub shows as one.
157
- let res: Awaited<ReturnType<FetchFn>> | undefined
158
- for (let attempt = 0; ; attempt++) {
159
- const delay = CHECK_RETRY_DELAYS_MS[attempt]
160
- try {
161
- res = await post()
162
- if (!CHECK_RETRY_STATUS.has(res.status) || delay === undefined) break
163
- } catch (err) {
164
- // A refused certificate (a GHES host behind a private CA) fails the
165
- // same way every time: not retried (F-40).
166
- if (delay === undefined || args.signal?.aborted === true || isCertificateRefusal(err)) {
167
- throw err
168
- }
169
- }
170
- if (args.signal?.aborted === true) break
171
- // The deadline ended the wait: GitHub's last answer is what to tell,
172
- // not the AbortError a further POST would throw (F-46).
173
- if (await sleepUnless(delay, args.signal)) break
174
- }
175
- if (res === undefined) throw new Error('the flush deadline passed before it could be retried')
176
- if (!res.ok) {
177
- const body = await res.text().catch(() => '')
178
- // GitHub answers a rate limit (primary or secondary) with 403 as often
179
- // as 429, and the permissions hint sent every such run to its workflow
180
- // file. Not retried: its retry-after is seconds to minutes, past core's
181
- // flush deadline (F-13).
182
- const limited = res.status === 429 || /rate limit/i.test(body)
183
- const hint = limited
184
- ? ' — rate-limited by GitHub; this run has no check'
185
- : res.status === 403
186
- ? ' — does the workflow grant `permissions: checks: write`?'
187
- : ''
188
- args.warn(`vx-ci: check-run POST failed (${res.status})${hint}: ${body.slice(0, 200)}`)
189
- }
190
- } catch (err) {
191
- // A GHES host behind a private CA: Bun's fetch trusts a CA named there.
192
- const hint = isCertificateRefusal(err)
193
- ? " — for a host behind a private CA, set NODE_EXTRA_CA_CERTS to its CA's PEM file"
194
- : ''
195
- args.warn(
196
- `vx-ci: check-run POST failed: ${err instanceof Error ? err.message : String(err)}${hint}`,
197
- )
198
- }
199
- }