@vzn/vx-ci 0.0.524 → 0.0.526

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vzn/vx-ci",
3
- "version": "0.0.524",
3
+ "version": "0.0.526",
4
4
  "license": "MIT",
5
5
  "description": "GitHub Actions integration for vx — a telemetry plugin that writes the run as a GitHub Actions job summary.",
6
6
  "keywords": [
@@ -33,7 +33,7 @@
33
33
  "access": "public"
34
34
  },
35
35
  "peerDependencies": {
36
- "@vzn/vx": "^0.0.524"
36
+ "@vzn/vx": "^0.0.526"
37
37
  },
38
38
  "repository": {
39
39
  "type": "git",
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  // Anywhere else the plugin declines and costs nothing.
11
11
  export { github, type GithubPluginOptions } from './plugin.js'
12
12
  export { renderJobSummary } from './summary.js'
13
+
13
14
  // The Checks API helpers are the plugin's own; only the transport type
14
15
  // the options name is public (1.0 freezes what this file exports).
15
16
  export { type FetchFn } from './checks.js'
package/src/plugin.ts CHANGED
@@ -4,23 +4,15 @@
4
4
  // and no cost — so declaring `github()` is safe in every environment, the
5
5
  // same decline pattern as `otel()`.
6
6
  import { appendFile, stat } from 'node:fs/promises'
7
- import {
8
- refuseUnknownOptions,
9
- type PluginOptionKinds,
10
- definePlugin,
11
- type RunSummaryRecord,
12
- type TelemetrySink,
13
- type VxPlugin,
14
- } from '@vzn/vx'
15
- import {
16
- buildCheckRunPayload,
17
- postCheckRun,
18
- resolveCheckRunEnv,
19
- type CheckRunEnv,
20
- type FetchFn,
21
- } from './checks.js'
7
+ import { createRequire } from 'node:module'
8
+ import { refuseUnknownOptions, type PluginOptionKinds, definePlugin, type VxPlugin } from '@vzn/vx'
9
+ import type { FetchFn } from './checks.js'
22
10
  import { githubCacheScope } from './cache-scope.js'
23
- import { clampJobSummary, MAX_JOB_SUMMARY_BYTES, renderJobSummary } from './summary.js'
11
+
12
+ // Every run evaluates `vx.workspace.ts`; the sink, the summary renderer
13
+ // and the Checks API client load only where a summary file is set. The
14
+ // telemetry hook is synchronous, so the module loads by `require`.
15
+ const loadSink = (): typeof import('./sink.js') => createRequire(import.meta.url)('./sink.js')
24
16
 
25
17
  export interface GithubPluginOptions {
26
18
  /**
@@ -58,75 +50,6 @@ export interface GithubPluginOptions {
58
50
  sizeOf?: (file: string) => Promise<number>
59
51
  }
60
52
 
61
- export class GithubSummarySink implements TelemetrySink {
62
- readonly name = 'github-job-summary'
63
- /** Summary-only: no streaming records at all — zero per-event cost. */
64
- readonly wants: [] = []
65
- private summary: RunSummaryRecord | undefined
66
-
67
- constructor(
68
- private readonly file: string,
69
- private readonly title: string,
70
- private readonly append: (file: string, markdown: string) => Promise<void>,
71
- private readonly warn: (m: string) => void,
72
- private readonly check?: { env: CheckRunEnv; name: string; fetchFn: FetchFn },
73
- /** Bytes the summary file already holds. */
74
- private readonly sizeOf: (file: string) => Promise<number> = async () => 0,
75
- ) {}
76
-
77
- onRunSummary(summary: RunSummaryRecord): void {
78
- // MUST return promptly (contract): stash, render + write in flush().
79
- this.summary = summary
80
- }
81
-
82
- async flush(signal?: AbortSignal): Promise<void> {
83
- if (this.summary === undefined) return
84
- const markdown = renderJobSummary(this.summary, this.title)
85
- // The two artifacts are INDEPENDENT, and the plugin already says so in
86
- // one direction: it declines the check-run without a token and still
87
- // writes the summary. The reverse has to hold, or a full disk on the
88
- // runner costs the PR its check — the more visible of the two. Reported,
89
- // not thrown: a telemetry sink may never break a run.
90
- try {
91
- // Clamped: past GitHub's 1 MiB cap the runner drops the summary whole,
92
- // so a bounded page beats none. The cap covers the step's whole file,
93
- // and a page clamped to 1 MiB after another writer's output lost both
94
- // (F-42): the room is what is left. The check-run payload has its own,
95
- // smaller cap and is clamped where it is built.
96
- const used = await this.sizeOf(this.file)
97
- // After another writer the page starts on its own line: one that left
98
- // no final newline ran vx's heading into its paragraph (F-51).
99
- const lead = used > 0 ? '\n' : ''
100
- const page = clampJobSummary(markdown, MAX_JOB_SUMMARY_BYTES - used - lead.length)
101
- if (page === '') {
102
- this.warn(
103
- `vx-ci: ${this.file} already holds ${used} bytes of GitHub's 1 MiB job summary cap — no room for vx's page`,
104
- )
105
- } else {
106
- await this.append(this.file, lead + page)
107
- }
108
- } catch (err) {
109
- this.warn(
110
- `vx-ci: could not write the job summary to ${this.file}: ${err instanceof Error ? err.message : String(err)}`,
111
- )
112
- }
113
- if (this.check !== undefined) {
114
- await postCheckRun({
115
- env: this.check.env,
116
- payload: buildCheckRunPayload({
117
- summary: this.summary,
118
- markdown,
119
- name: this.check.name,
120
- sha: this.check.env.sha,
121
- }),
122
- fetchFn: this.check.fetchFn,
123
- warn: this.warn,
124
- ...(signal === undefined ? {} : { signal }),
125
- })
126
- }
127
- }
128
- }
129
-
130
53
  /**
131
54
  * Why a string cannot be an HTTP header value, or null. Bun's `fetch`
132
55
  * refuses a line break, a NUL or a character past Latin-1 once it has
@@ -166,6 +89,7 @@ export function github(options: GithubPluginOptions = {}): VxPlugin {
166
89
  const file = options.summaryFile ?? process.env['GITHUB_STEP_SUMMARY']
167
90
  if (file === undefined || file === '') return undefined
168
91
  const append = options.append ?? (async (f: string, md: string) => appendFile(f, md, 'utf8'))
92
+ const { GithubSummarySink, resolveCheckRunEnv } = loadSink()
169
93
  let check: ConstructorParameters<typeof GithubSummarySink>[4]
170
94
  if (options.checks !== false) {
171
95
  const env = resolveCheckRunEnv(process.env)
package/src/sink.ts ADDED
@@ -0,0 +1,76 @@
1
+ // The job summary sink: loaded only on a runner that sets a summary file,
2
+ // so a run elsewhere pays no module of it.
3
+ import { type RunSummaryRecord, type TelemetrySink } from '@vzn/vx'
4
+ import { buildCheckRunPayload, postCheckRun, type CheckRunEnv, type FetchFn } from './checks.js'
5
+ import { clampJobSummary, MAX_JOB_SUMMARY_BYTES, renderJobSummary } from './summary.js'
6
+
7
+ export { resolveCheckRunEnv } from './checks.js'
8
+
9
+ export class GithubSummarySink implements TelemetrySink {
10
+ readonly name = 'github-job-summary'
11
+ /** Summary-only: no streaming records at all — zero per-event cost. */
12
+ readonly wants: [] = []
13
+ private summary: RunSummaryRecord | undefined
14
+
15
+ constructor(
16
+ private readonly file: string,
17
+ private readonly title: string,
18
+ private readonly append: (file: string, markdown: string) => Promise<void>,
19
+ private readonly warn: (m: string) => void,
20
+ private readonly check?: { env: CheckRunEnv; name: string; fetchFn: FetchFn },
21
+ /** Bytes the summary file already holds. */
22
+ private readonly sizeOf: (file: string) => Promise<number> = async () => 0,
23
+ ) {}
24
+
25
+ onRunSummary(summary: RunSummaryRecord): void {
26
+ // MUST return promptly (contract): stash, render + write in flush().
27
+ this.summary = summary
28
+ }
29
+
30
+ async flush(signal?: AbortSignal): Promise<void> {
31
+ if (this.summary === undefined) return
32
+ const markdown = renderJobSummary(this.summary, this.title)
33
+ // The two artifacts are INDEPENDENT, and the plugin already says so in
34
+ // one direction: it declines the check-run without a token and still
35
+ // writes the summary. The reverse has to hold, or a full disk on the
36
+ // runner costs the PR its check — the more visible of the two. Reported,
37
+ // not thrown: a telemetry sink may never break a run.
38
+ try {
39
+ // Clamped: past GitHub's 1 MiB cap the runner drops the summary whole,
40
+ // so a bounded page beats none. The cap covers the step's whole file,
41
+ // and a page clamped to 1 MiB after another writer's output lost both
42
+ // (F-42): the room is what is left. The check-run payload has its own,
43
+ // smaller cap and is clamped where it is built.
44
+ const used = await this.sizeOf(this.file)
45
+ // After another writer the page starts on its own line: one that left
46
+ // no final newline ran vx's heading into its paragraph (F-51).
47
+ const lead = used > 0 ? '\n' : ''
48
+ const page = clampJobSummary(markdown, MAX_JOB_SUMMARY_BYTES - used - lead.length)
49
+ if (page === '') {
50
+ this.warn(
51
+ `vx-ci: ${this.file} already holds ${used} bytes of GitHub's 1 MiB job summary cap — no room for vx's page`,
52
+ )
53
+ } else {
54
+ await this.append(this.file, lead + page)
55
+ }
56
+ } catch (err) {
57
+ this.warn(
58
+ `vx-ci: could not write the job summary to ${this.file}: ${err instanceof Error ? err.message : String(err)}`,
59
+ )
60
+ }
61
+ if (this.check !== undefined) {
62
+ await postCheckRun({
63
+ env: this.check.env,
64
+ payload: buildCheckRunPayload({
65
+ summary: this.summary,
66
+ markdown,
67
+ name: this.check.name,
68
+ sha: this.check.env.sha,
69
+ }),
70
+ fetchFn: this.check.fetchFn,
71
+ warn: this.warn,
72
+ ...(signal === undefined ? {} : { signal }),
73
+ })
74
+ }
75
+ }
76
+ }