@vzn/vx-ci 0.0.643 → 0.0.645

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 vx contributors
3
+ Copyright (c) 2026 VZN
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  Website: <https://vznjs.github.io/vx/> · this plugin: <https://vznjs.github.io/vx/plugins/vx-ci/> · source: <https://github.com/vznjs/vx>
4
4
 
5
- GitHub Actions integration for [`@vzn/vx`](https://vznjs.github.io/vx/) — a
6
- telemetry plugin that writes every `vx run` as a **job summary** on the
7
- workflow run page.
5
+ GitHub Actions and GitLab CI integration for [`@vzn/vx`](https://vznjs.github.io/vx/):
6
+ `github()` writes every `vx run` as a **job summary** on the workflow run
7
+ page and names the run's cache scope; `gitlab()` names the run's cache
8
+ scope on GitLab CI ([below](#gitlab-ci)).
8
9
 
9
10
  ```sh
10
11
  npm install -D @vzn/vx @vzn/vx-ci # or: pnpm add -D -w · yarn add -D (-W on Yarn 1) · bun add -d
@@ -39,8 +40,8 @@ checked and nothing loads, so declaring it unconditionally is safe.
39
40
 
40
41
  The options type is `GithubPluginOptions`; `renderJobSummary` renders the
41
42
  summary lines the plugin posts (the site's CI guide sample is rendered from it).
42
- The package exports `github`, `renderJobSummary` and the types `GithubPluginOptions`
43
- and `FetchFn`; nothing else. `fetchFn`, `append` and `sizeOf` are test seams:
43
+ The package exports `github`, `gitlab`, `renderJobSummary` and the types
44
+ `GithubPluginOptions`, `GitlabPluginOptions` and `FetchFn`; nothing else. `fetchFn`, `append` and `sizeOf` are test seams:
44
45
  they replace the Checks API transport, the summary writer and its size probe.
45
46
 
46
47
  ```ts
@@ -49,7 +50,7 @@ github({
49
50
  title: 'build & test', // default: 'vx run'
50
51
  checks: true, // default: on with GITHUB_TOKEN; true warns when it is missing, false opts out
51
52
  checkName: 'ci', // default: 'vx', the check run's name
52
- cacheScope: false, // default: on; scope remote cache writes by ref
53
+ cacheScope: false, // default: on; scope plugin cache entries by ref
53
54
  })
54
55
  ```
55
56
 
@@ -58,23 +59,46 @@ github({
58
59
  On Actions (`GITHUB_ACTIONS=true`) the plugin's `config` stage names the
59
60
  run's branch through `ctx.setBranch`, from the ref (`GITHUB_REF`,
60
61
  `GITHUB_REF_NAME`, `GITHUB_HEAD_REF`, `GITHUB_REPOSITORY`) and the
61
- `GITHUB_EVENT_PATH` payload. A push to the default branch names no scope (reads and writes
62
- the default keys). A push to, or a pull request from, a branch names
62
+ `GITHUB_EVENT_PATH` payload. A push to the default branch names no scope.
63
+ A push to, or a pull request from, a branch names
63
64
  `<owner>/<repo>:<branch>` of the repository the branch lives in, so a
64
65
  fork's pull request names the fork; so does a `pull_request_target` or
65
66
  `workflow_run` run on a PR's behalf (by `GITHUB_EVENT_NAME`, though its
66
67
  ref is main's), and an `issue_comment` on a PR, which names no branch,
67
- `pr-<n>`. A pull request also names its base commit
68
- (`pull_request.base.sha`), so vx diffs the merge checkout from it with
69
- no git call. A scoped run's changed tasks read the scope's remote entry,
70
- the rest the default keys, and every task it builds is saved to the
71
- remote under the scope; the local cache never knows a scope. A
72
- PR never writes what main reads, and a laptop on the branch derives the
73
- same scope. Whether a run writes at all is `cache.policy`'s call.
74
- `VX_CACHE_SCOPE` overrides it. This is a client-side convention: the
75
- real boundary is a cache token the server limits, so give PR jobs one
76
- that cannot write the default keys (`docs/security.md` § Cache
77
- poisoning).
68
+ `pr-<n>`. A merge queue's run (`merge_group`) is trusted: no scope,
69
+ whatever its `gh-readonly-queue/...` branch, compared with
70
+ `merge_group.base_sha`. A pull request also names its base commit
71
+ (`pull_request.base.sha`), the base of a bare `--affected`. The scope
72
+ keys only what plugins store themselves (`ctx.cache`, run records):
73
+ task keys never carry it, and the cache server decides from the CI OIDC
74
+ token where a task's entry lands. `VX_CACHE_SCOPE` overrides it.
75
+
76
+ ## GitLab CI
77
+
78
+ ```ts
79
+ // .vx/workspace.ts
80
+ import { defineWorkspace } from '@vzn/vx/config'
81
+ import { github, gitlab } from '@vzn/vx-ci'
82
+
83
+ export default defineWorkspace({ plugins: [github(), gitlab()] })
84
+ ```
85
+
86
+ On GitLab CI (`GITLAB_CI=true`) `gitlab()`'s `config` stage names the
87
+ run's branch through `ctx.setBranch`, from GitLab's predefined
88
+ variables, as `github()` does on Actions. The default branch
89
+ (`CI_DEFAULT_BRANCH`) names no scope. Another branch or a tag (`CI_COMMIT_TAG`) names
90
+ `<project path>:<ref>` (`CI_PROJECT_PATH`, `CI_COMMIT_REF_NAME`), so a
91
+ subgroup's project is `group/sub/app:feat-x`, the scope a laptop on
92
+ `feat-x` derives. A merge request pipeline names its source branch
93
+ (`CI_MERGE_REQUEST_SOURCE_BRANCH_NAME`) in the project it lives in (a
94
+ fork's `CI_MERGE_REQUEST_SOURCE_PROJECT_PATH`) and its diff base
95
+ (`CI_MERGE_REQUEST_DIFF_BASE_SHA`), the base of a bare `--affected`.
96
+ A merge train's pipeline (`CI_MERGE_REQUEST_EVENT_TYPE=merge_train`)
97
+ builds what lands on the target, so it is trusted: no scope. As on
98
+ Actions, the scope keys only plugin entries. `gitlab({ cacheScope: false })` (`GitlabPluginOptions`) turns it off;
99
+ `VX_CACHE_SCOPE` overrides it. Anywhere else `gitlab()` returns
100
+ `undefined`, so declaring both plugins is safe. Merge request comments
101
+ and reports are not part of it yet.
78
102
 
79
103
  ## How it works
80
104
 
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@vzn/vx-ci",
3
- "version": "0.0.643",
3
+ "version": "0.0.645",
4
4
  "license": "MIT",
5
- "description": "GitHub Actions integration for vx — a telemetry plugin that writes the run as a GitHub Actions job summary.",
5
+ "description": "GitHub Actions and GitLab CI integration for vx — a job summary on Actions and the branch cache scope on both.",
6
6
  "keywords": [
7
7
  "vx",
8
8
  "vx-plugin",
9
9
  "monorepo",
10
10
  "github-actions",
11
+ "gitlab-ci",
11
12
  "job-summary",
12
13
  "checks"
13
14
  ],
@@ -33,7 +34,7 @@
33
34
  "access": "public"
34
35
  },
35
36
  "peerDependencies": {
36
- "@vzn/vx": "^0.0.643"
37
+ "@vzn/vx": "^0.0.645"
37
38
  },
38
39
  "repository": {
39
40
  "type": "git",
@@ -1,8 +1,8 @@
1
1
  // The cache scope and base commit a GitHub Actions run names through
2
- // `setBranch`: the default branch writes the default keys, a branch its own
3
- // scope, the one a laptop on that branch reads (owner, 2026-10-17).
2
+ // `setBranch`: none on the default branch, a branch its own scope, the one
3
+ // a laptop on that branch derives (owner, 2026-10-17).
4
4
  import { readFile } from 'node:fs/promises'
5
- import { branchCacheScope } from '@vzn/vx'
5
+ import { type BranchFacts, branchCacheScope } from '@vzn/vx'
6
6
 
7
7
  /**
8
8
  * What an Actions run knows of its branch (owner, 2026-10-17). `scope`:
@@ -13,17 +13,23 @@ import { branchCacheScope } from '@vzn/vx'
13
13
  * derives. An event that runs in the default branch's context on a pull
14
14
  * request's behalf (`pull_request_target`, `issue_comment` on a PR, a
15
15
  * `workflow_run` a PR started) is the PR's, whatever `GITHUB_REF` says: a
16
- * workflow that checks out the PR's head there would otherwise write the
17
- * default keys. `issue_comment` names no branch, so it writes `pr-<n>`.
16
+ * workflow that checks out the PR's head there would otherwise share main's
17
+ * scope. `issue_comment` names no branch, so it writes `pr-<n>`.
18
+ * A merge queue's run (`merge_group`) is trusted (owner, 2026-10-18): no
19
+ * scope, compared with `merge_group.base_sha`.
18
20
  * `base`: a pull request's base commit (`pull_request.base.sha`), what its
19
21
  * merge checkout is compared with, no git call needed.
20
22
  */
21
23
  export async function githubBranch(
22
24
  env: Readonly<Record<string, string | undefined>>,
23
- ): Promise<{ scope?: string; base?: string }> {
25
+ ): Promise<BranchFacts> {
24
26
  if (env['GITHUB_ACTIONS'] !== 'true') return {}
25
27
  const ref = env['GITHUB_REF'] ?? ''
26
28
  const event = await eventOf(env['GITHUB_EVENT_PATH'])
29
+ if (env['GITHUB_EVENT_NAME'] === 'merge_group') {
30
+ const base = stringOf(event?.merge_group?.base_sha)
31
+ return { trusted: true, ...(base !== undefined ? { base } : {}) }
32
+ }
27
33
  const repo = stringOf(event?.repository?.full_name) ?? env['GITHUB_REPOSITORY']
28
34
  const base = stringOf(event?.pull_request?.base?.sha)
29
35
  const withBase = (scope: string | undefined): { scope?: string; base?: string } => ({
@@ -54,6 +60,7 @@ interface ActionsEvent {
54
60
  base?: { sha?: unknown }
55
61
  }
56
62
  issue?: { number?: unknown; pull_request?: unknown }
63
+ merge_group?: { base_sha?: unknown }
57
64
  workflow_run?: {
58
65
  event?: unknown
59
66
  head_branch?: unknown
package/src/gitlab.ts ADDED
@@ -0,0 +1,63 @@
1
+ // The `gitlab()` plugin (owner, 2026-10-18): on GitLab CI it names the
2
+ // run's branch through `setBranch`, as `github()` does on Actions. Off
3
+ // GitLab CI (`GITLAB_CI` not 'true') the factory returns undefined, which
4
+ // `plugins` skips, so declaring it costs nothing on a laptop.
5
+ import {
6
+ type BranchFacts,
7
+ branchCacheScope,
8
+ definePlugin,
9
+ type PluginOptionKinds,
10
+ refuseUnknownOptions,
11
+ type VxPlugin,
12
+ } from '@vzn/vx'
13
+
14
+ export interface GitlabPluginOptions {
15
+ /**
16
+ * Name the run's cache scope and base commit from GitLab's predefined
17
+ * variables: none on the default branch or a merge train, else
18
+ * `<project path>:<branch>`; a merge request is compared with its diff
19
+ * base. The scope keys plugin entries only. `VX_CACHE_SCOPE` beats it.
20
+ * Default: on; `false` opts out.
21
+ */
22
+ cacheScope?: boolean
23
+ }
24
+
25
+ const GITLAB_PLUGIN_KEYS: PluginOptionKinds<GitlabPluginOptions> = {
26
+ cacheScope: 'boolean',
27
+ }
28
+
29
+ export function gitlab(options: GitlabPluginOptions = {}): VxPlugin | undefined {
30
+ if (process.env['GITLAB_CI'] !== 'true') return undefined
31
+ refuseUnknownOptions('gitlab()', options, GITLAB_PLUGIN_KEYS)
32
+ return definePlugin(import.meta, {
33
+ config(_workspace, ctx) {
34
+ if (options.cacheScope === false) return
35
+ ctx.setBranch(gitlabBranch(process.env))
36
+ },
37
+ })
38
+ }
39
+
40
+ /**
41
+ * What a GitLab CI job knows of its branch. A merge request pipeline names
42
+ * its source branch in the project it lives in (a fork's own,
43
+ * `CI_MERGE_REQUEST_SOURCE_PROJECT_PATH`) and its diff base; a merge
44
+ * train's pipeline builds what lands on the target, so it is trusted; a
45
+ * branch or tag pipeline names its ref, none on the default branch.
46
+ */
47
+ function gitlabBranch(env: Readonly<Record<string, string | undefined>>): BranchFacts {
48
+ const value = (key: string): string | undefined => env[key] || undefined
49
+ const base = value('CI_MERGE_REQUEST_DIFF_BASE_SHA')
50
+ const withBase = (facts: BranchFacts): BranchFacts =>
51
+ base === undefined ? facts : { ...facts, base }
52
+ if (value('CI_MERGE_REQUEST_EVENT_TYPE') === 'merge_train') return withBase({ trusted: true })
53
+ const project = value('CI_PROJECT_PATH')
54
+ const source = value('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME')
55
+ if (source !== undefined) {
56
+ const from = value('CI_MERGE_REQUEST_SOURCE_PROJECT_PATH') ?? project
57
+ return withBase(from === undefined ? {} : { scope: branchCacheScope(from, source) })
58
+ }
59
+ const ref = value('CI_COMMIT_REF_NAME')
60
+ if (project === undefined || ref === undefined) return {}
61
+ if (value('CI_COMMIT_TAG') === undefined && ref === value('CI_DEFAULT_BRANCH')) return {}
62
+ return { scope: branchCacheScope(project, ref) }
63
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Public API for @vzn/vx-ci — the GitHub Actions integration plugin.
1
+ // Public API for @vzn/vx-ci — the GitHub Actions and GitLab CI plugins.
2
2
  //
3
3
  // Usage in .vx/workspace.ts:
4
4
  // import { defineWorkspace } from '@vzn/vx'
@@ -9,6 +9,7 @@
9
9
  // appends a job summary: verdict, stats, failures, and the per-task table.
10
10
  // Anywhere else the plugin declines and costs nothing.
11
11
  export { github, type GithubPluginOptions } from './plugin.js'
12
+ export { gitlab, type GitlabPluginOptions } from './gitlab.js'
12
13
  export { renderJobSummary } from './summary.js'
13
14
 
14
15
  // The Checks API helpers are the plugin's own; only the transport type
package/src/plugin.ts CHANGED
@@ -40,11 +40,11 @@ export interface GithubPluginOptions {
40
40
  /** Check-run name. Default: `'vx'`. */
41
41
  checkName?: string
42
42
  /**
43
- * Name the run's cache scope and base commit from the Actions event: a
44
- * push to the default branch keeps the default keys, a branch or its pull
45
- * request writes to `<owner>/<repo>:<branch>` (a fork's names the fork),
46
- * and a pull request is compared with its base commit. `VX_CACHE_SCOPE`
47
- * beats it. Default: on; `false` opts out.
43
+ * Name the run's cache scope and base commit from the Actions event: none
44
+ * on the default branch, else `<owner>/<repo>:<branch>` (a fork's names
45
+ * the fork); a pull request is compared with its base commit. The scope
46
+ * keys plugin entries only. `VX_CACHE_SCOPE` beats it. Default: on;
47
+ * `false` opts out.
48
48
  */
49
49
  cacheScope?: boolean
50
50
  /** Test seam — inject the append. Defaults to fs appendFile. */