@vzn/vx-migrate 0.0.643 → 0.0.644
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 +4 -3
- package/package.json +2 -2
- package/src/adoption-plugin.ts +1 -1
- package/src/glob-grammar.ts +15 -50
- package/src/migrate-nx.ts +2 -10
- package/src/nx/index.ts +0 -24
- package/src/nx/nx-map.ts +0 -17
- package/src/nx-cache/index.ts +16 -10
- package/src/remote-scope.ts +12 -0
- package/src/turbo/index.ts +5 -54
- package/src/turbo/turbo-map.ts +7 -10
- package/src/turbo-cache/index.ts +30 -7
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ Rules:
|
|
|
75
75
|
- `interruptible` maps to nothing: `vx watch` keeps a persistent task up across cycles and restarts it only when its config changes or it dies.
|
|
76
76
|
- A task's `tags` (Turbo main, after 2.11.5) map to nothing: Turbo keeps them out of the hash and the run.
|
|
77
77
|
- `envMode: "loose"` (top-level or in `global`, or `TURBO_ENV_MODE=loose`, which wins) is a note: Turbo hands every task the whole environment, vx only the declared names.
|
|
78
|
-
- The workspace keys vx has a home for, top level or in `global`, fill what `.vx/workspace.ts` leaves unset: `run.concurrency` (`"10"`, `"50%"` of the cores) → `run.concurrency
|
|
78
|
+
- The workspace keys vx has a home for, top level or in `global`, fill what `.vx/workspace.ts` leaves unset: `run.concurrency` (`"10"`, `"50%"` of the cores) → `run.concurrency`. `TURBO_CONCURRENCY` wins over turbo.json, as under Turbo; a set one decides, its `0` included. `cacheMaxSize` / `cacheMaxAge` map to nothing: no vx run evicts, `vx cache clean --max-size / --older-than` does.
|
|
79
79
|
- `TURBO_SCM_BASE`, the base `turbo run --affected` compares with, is the run's base commit (`setBranch({ base })`): a bare `vx run build --affected` compares with the same ref. With it unset on GitHub Actions (`GITHUB_ACTIONS`), Turbo's own base applies: a pull request's `GITHUB_BASE_REF`, else the push event's `before` from `GITHUB_EVENT_PATH`.
|
|
80
80
|
- Turbo hashes each package's `microfrontends.json` (or `.jsonc`) into every task, or the one file `VC_MICROFRONTENDS_CONFIG_FILE_NAME` names; so does `turbo()`.
|
|
81
81
|
- Turbo 2.11's `global` block (`futureFlags.globalConfiguration`) is read as the `globalDependencies`, `globalEnv` and `globalPassThroughEnv` it replaces.
|
|
@@ -173,7 +173,7 @@ Two cached tasks on one workspace path cannot both keep their cache, and the fir
|
|
|
173
173
|
|
|
174
174
|
### nx.json `maxCacheSize`
|
|
175
175
|
|
|
176
|
-
`maxCacheSize`
|
|
176
|
+
`maxCacheSize` maps to nothing: no vx run evicts, `vx cache clean --max-size` does.
|
|
177
177
|
|
|
178
178
|
### Lerna
|
|
179
179
|
|
|
@@ -259,7 +259,7 @@ Reads the root pipeline (`tasks` in Turbo 2, `pipeline` in Turbo 1), per-package
|
|
|
259
259
|
|
|
260
260
|
### Nx
|
|
261
261
|
|
|
262
|
-
Reads the **resolved** project graph only (`.nx/workspace-data/project-graph.json` when exported, else the one the workspace's own `nx graph` exports into a temp file, as `nx()` does; with no `node_modules/.bin/nx`, as in a fresh clone, it stops and names the repo manager's install), through the same mapper `nx()` runs live, executors aside (below). Targets Nx plugins infer at runtime are frozen as the snapshot saw them. `nx:run-commands` is the one shell line `nx()` runs (see [`nx:run-commands`](#nxrun-commands) above: where, parallel or in order, forwarded arguments, `env`, `readyWhen`) — storybook's `compile` is `cd ../../.. && node ./scripts/build/build-package.ts --cwd code/lib/cli`; a plain `command` is that shorthand; `nx:run-script` is the package's script body with its `pre<name>` / `post<name>` hooks folded in (or `yarn run <name>` when the body calls yarn's `run` builtin; an empty script is the placeholder with a todo), `nx:noop` is a group task; Nx 15–16's `@nrwl/workspace:run-commands` and `run-script` (and their `@nx/workspace:` names) are the `nx:` executors they re-exported; **an executor target is its `nx-exec` line**, as `nx()` runs it: the migrator translates no executor, so Nx and `@vzn/vx-migrate` stay installed until each such line is rewritten as the command it runs (a server executor Nx knows, `@nx/js:node` or a dev server, is still a persistent task). The report also says what `.vx/workspace.ts` still holds, as the Turbo migration does: the `nx()` a keep adoption declared, which keeps reading nx.json, and a lockfile with no `@vzn/vx-lockfile` plugin, where Nx keyed each project on the npm packages it depends on. A written command that still runs Nx itself (a run-commands `nx run b:build`, `npx nx test b`, a script's `nx exec -- tsc`) carries a TODO: it works only while Nx is installed. An Nx project no workspace glob lists (an integrated repo's `project.json` library) gets a `package.json` (`name`, `private`) where it has none, and a note names the directories to add to `workspaces` (or `pnpm-workspace.yaml`), so the package manager installs it and a lockfile plugin names it; the migrator never edits the root manifest. A repo with no `package.json` at all (Nx's `nx init` outside JS: Nx pinned in `.nx/installation`, projects as `project.json`) is migrated too, natively only: the root is the directory holding `nx.json`, the graph is exported by `.nx/installation`'s `nx`, each config writes the project's Nx `name`, every edge is the explicit `project#target` Nx's graph draws (a `^` edge needs a lockfile plugin, which has no lockfile to read), and `.vx/workspace.ts` declares no plugin, so nothing is installed and core runs the configs alone; `--keep` refuses, as `nx()` would have no `package.json` to install into, and an executor target's `nx-exec` line is a note to rewrite as a command. A target with `configurations` writes one task per configuration (`build`, `build:ci`). A project's Nx `tags` are written as its `tags`. Named inputs expand from `nx.json` when readable. An output path is kept as written, a `!` one too (`{projectRoot}/dist` → `dist`, `{projectRoot}/bin/tool` → `bin/tool`; one naming an unset `{options.x}` is dropped, as Nx drops it; a dotted `{options.outputPath.base}` walks the options, as Nx does; an extglob is put in vx's grammar: Next's inferred `.next/!(cache)/**/*` is `.next/*/**/*` with `!.next/cache/**/*`, `@(js|map)` is `{js,map}`, and a form with no vx spelling is dropped with a TODO): vx reads a bare path as the file or the whole tree under it, so a directory and an extensionless binary both save and restore. nx.json's `parallel
|
|
262
|
+
Reads the **resolved** project graph only (`.nx/workspace-data/project-graph.json` when exported, else the one the workspace's own `nx graph` exports into a temp file, as `nx()` does; with no `node_modules/.bin/nx`, as in a fresh clone, it stops and names the repo manager's install), through the same mapper `nx()` runs live, executors aside (below). Targets Nx plugins infer at runtime are frozen as the snapshot saw them. `nx:run-commands` is the one shell line `nx()` runs (see [`nx:run-commands`](#nxrun-commands) above: where, parallel or in order, forwarded arguments, `env`, `readyWhen`) — storybook's `compile` is `cd ../../.. && node ./scripts/build/build-package.ts --cwd code/lib/cli`; a plain `command` is that shorthand; `nx:run-script` is the package's script body with its `pre<name>` / `post<name>` hooks folded in (or `yarn run <name>` when the body calls yarn's `run` builtin; an empty script is the placeholder with a todo), `nx:noop` is a group task; Nx 15–16's `@nrwl/workspace:run-commands` and `run-script` (and their `@nx/workspace:` names) are the `nx:` executors they re-exported; **an executor target is its `nx-exec` line**, as `nx()` runs it: the migrator translates no executor, so Nx and `@vzn/vx-migrate` stay installed until each such line is rewritten as the command it runs (a server executor Nx knows, `@nx/js:node` or a dev server, is still a persistent task). The report also says what `.vx/workspace.ts` still holds, as the Turbo migration does: the `nx()` a keep adoption declared, which keeps reading nx.json, and a lockfile with no `@vzn/vx-lockfile` plugin, where Nx keyed each project on the npm packages it depends on. A written command that still runs Nx itself (a run-commands `nx run b:build`, `npx nx test b`, a script's `nx exec -- tsc`) carries a TODO: it works only while Nx is installed. An Nx project no workspace glob lists (an integrated repo's `project.json` library) gets a `package.json` (`name`, `private`) where it has none, and a note names the directories to add to `workspaces` (or `pnpm-workspace.yaml`), so the package manager installs it and a lockfile plugin names it; the migrator never edits the root manifest. A repo with no `package.json` at all (Nx's `nx init` outside JS: Nx pinned in `.nx/installation`, projects as `project.json`) is migrated too, natively only: the root is the directory holding `nx.json`, the graph is exported by `.nx/installation`'s `nx`, each config writes the project's Nx `name`, every edge is the explicit `project#target` Nx's graph draws (a `^` edge needs a lockfile plugin, which has no lockfile to read), and `.vx/workspace.ts` declares no plugin, so nothing is installed and core runs the configs alone; `--keep` refuses, as `nx()` would have no `package.json` to install into, and an executor target's `nx-exec` line is a note to rewrite as a command. A target with `configurations` writes one task per configuration (`build`, `build:ci`). A project's Nx `tags` are written as its `tags`. Named inputs expand from `nx.json` when readable. An output path is kept as written, a `!` one too (`{projectRoot}/dist` → `dist`, `{projectRoot}/bin/tool` → `bin/tool`; one naming an unset `{options.x}` is dropped, as Nx drops it; a dotted `{options.outputPath.base}` walks the options, as Nx does; an extglob is put in vx's grammar: Next's inferred `.next/!(cache)/**/*` is `.next/*/**/*` with `!.next/cache/**/*`, `@(js|map)` is `{js,map}`, and a form with no vx spelling is dropped with a TODO): vx reads a bare path as the file or the whole tree under it, so a directory and an extensionless binary both save and restore. nx.json's `parallel` and `defaultBase`, which `nx()` applies live, are each a field of the `.vx/workspace.ts` it writes (`run.concurrency`, `baseBranch`), read from nx.json alone, never the environment; beside a workspace file already there, each is a note naming the field to add. vx derives package edges from `package.json`; an Nx graph edge with no manifest path (`implicitDependencies`, a tsconfig path) becomes, for each `^target` of the dependant, an explicit `pkg#target` edge to what Nx's own walk reaches — each dependency that has the target, and through one that lacks it, its dependencies — so the order and the key are Nx's. The reverse too: where a project's manifest names a workspace package its Nx graph does not reach (an `implicitDependencies: ["!a"]` that breaks a manifest cycle), its `^target` becomes the explicit edges Nx draws, since vx's `^` would follow the manifest and bring the cycle back.
|
|
263
263
|
|
|
264
264
|
### Vite Task
|
|
265
265
|
|
|
@@ -620,6 +620,7 @@ With no token from an option, a variable or a config file, both plugins read the
|
|
|
620
620
|
|
|
621
621
|
## Remote-cache behaviour (both)
|
|
622
622
|
|
|
623
|
+
- Every request (`HEAD`, `GET`, `PUT`, the batch `POST`) carries `x-vx-cache-scope`: `trusted` for a task's own key, else the branch scope its key was derived in (`acme/app:feat-x`). The key is the same either way. A server that keeps one bucket per scope uses it to skip its own layering; whether a write to `trusted` is allowed stays the server's call, from the token (a CI ID token). A request without the header, from an older client, leaves the layering to the server. A batch that mixes scopes is one `POST` per scope.
|
|
623
624
|
- A remote error degrades to a **miss** and a warning that names the request, the artifact and the server — `[vx] plugin '@vzn/vx-migrate': upload 32248a2a7c89e241 to https://cache.example.com/v8/artifacts failed: no answer within 60000 ms` — never the token. The run never fails because of the cache.
|
|
624
625
|
- The same failure is said once per run: an unreachable server fails the probe, the download and the upload alike, and the run prints the first and, at its end, `[vx] plugin '@vzn/vx-migrate': 2 more requests failed the same way: Unable to connect. Is the computer able to access the url?` (core does the counting, for every cache plugin).
|
|
625
626
|
- A request answered `5xx` (not `501`), or one that never connected (a refused port, an unresolved host), is sent again after 2 s, `retries` times. A `429` pauses every request to that remote for its `Retry-After` (capped at 10 s a wait, 2 s without one), and the request is sent again when the pause ends, for up to 20 s from its first `429`, as Turbo's client does. A spent deadline is not resent: it has already cost its wait.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vzn/vx-migrate",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.644",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Adopt @vzn/vx from Turborepo, Nx or package.json scripts: run the repo unchanged, migrate it to vx.config.ts, and keep your Turbo or Nx remote cache",
|
|
6
6
|
"keywords": [
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@vzn/vx": "^0.0.
|
|
43
|
+
"@vzn/vx": "^0.0.644"
|
|
44
44
|
},
|
|
45
45
|
"repository": {
|
|
46
46
|
"type": "git",
|
package/src/adoption-plugin.ts
CHANGED
|
@@ -60,7 +60,7 @@ export function adoptionPlugin(
|
|
|
60
60
|
mapRun: (ctx: ProjectHookContext) => Promise<AdoptionRun>,
|
|
61
61
|
reads: readonly string[] = [],
|
|
62
62
|
// `config`: a workspace-wide key of the adopted tool's (nx.json `parallel`,
|
|
63
|
-
// Turbo's `
|
|
63
|
+
// Turbo's `concurrency`) that the workspace itself leaves unset.
|
|
64
64
|
// `discover`: a project the tool has that no member glob lists.
|
|
65
65
|
stages: Pick<VxPlugin, 'config' | 'discover'> = {},
|
|
66
66
|
): VxPlugin {
|
package/src/glob-grammar.ts
CHANGED
|
@@ -1,28 +1,22 @@
|
|
|
1
|
-
// The glob grammar Nx and Turbo share and vx's own differ in
|
|
2
|
-
//
|
|
1
|
+
// The glob grammar Nx and Turbo share and vx's own differ in extglobs;
|
|
2
|
+
// both adapters translate through here (items 914, 1031).
|
|
3
3
|
|
|
4
4
|
const EXTGLOB = /([?@+*!])\(([^()]*)\)/g
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A minimatch-family glob (Nx's minimatch, Turbo's wax) in vx's grammar,
|
|
8
|
-
* or null when it has no safe form.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - `[jt]` is the brace set `{j,t}`, plus the literal `[jt]` itself in a
|
|
13
|
-
* positive glob (a superset of inputs is safe; the route dir may be
|
|
14
|
-
* meant); a range within digits or one case of letters (`[a-c]`,
|
|
15
|
-
* `[0-9]`) is its members;
|
|
16
|
-
* - a negated class (`[!a]`) is `?` in a positive glob, a superset;
|
|
8
|
+
* or null when it has no safe form. Character classes pass through (vx
|
|
9
|
+
* reads them the same way); vx has no extglobs, so Nx's own default
|
|
10
|
+
* `production` negation, `?(*.)+(spec|test).[jt]s?(x)`, excluded nothing
|
|
11
|
+
* (item 914):
|
|
17
12
|
* - `?(a|b)` is `{a,b,}` and `@(a|b)` is `{a,b}`;
|
|
18
13
|
* - `+(a|b)` and `*(a|b)` narrow to one repetition, which only a
|
|
19
14
|
* NEGATION may do — it then excludes fewer files, never more;
|
|
20
|
-
* -
|
|
21
|
-
* null.
|
|
15
|
+
* - `!(…)` or nesting is null.
|
|
22
16
|
*/
|
|
23
17
|
export function minimatchToVx(glob: string, negated: boolean): string | null {
|
|
24
|
-
if (
|
|
25
|
-
|
|
18
|
+
if (!glob.includes('(')) return glob
|
|
19
|
+
const out = glob.replace(EXTGLOB, (_whole, kind: string, body: string) => {
|
|
26
20
|
const alts = body.split('|')
|
|
27
21
|
if (alts.some((a) => /[{},]/.test(a))) return '\0'
|
|
28
22
|
if (kind === '?') return `{${alts.join(',')},}`
|
|
@@ -30,36 +24,7 @@ export function minimatchToVx(glob: string, negated: boolean): string | null {
|
|
|
30
24
|
if (!negated || kind === '!') return '\0'
|
|
31
25
|
return kind === '+' ? `{${alts.join(',')}}` : `{${alts.join(',')},}`
|
|
32
26
|
})
|
|
33
|
-
|
|
34
|
-
out = out.replace(/\[([^\]]*)\]/g, (whole, body: string) => {
|
|
35
|
-
if (body === '' || /[{},/]/.test(body)) return '\0'
|
|
36
|
-
if (/^[!^]/.test(body)) return negated || body.length === 1 ? '\0' : '?'
|
|
37
|
-
const chars = classMembers(body)
|
|
38
|
-
if (chars === null) return '\0'
|
|
39
|
-
return negated ? `{${chars.join(',')}}` : `{${whole},${chars.join(',')}}`
|
|
40
|
-
})
|
|
41
|
-
return out.includes('\0') ? null : out
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const RANGES = ['0123456789', 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ']
|
|
45
|
-
|
|
46
|
-
/** A class body's characters, ranges spelled out; null for a range across kinds or backwards. */
|
|
47
|
-
function classMembers(body: string): string[] | null {
|
|
48
|
-
const out = new Set<string>()
|
|
49
|
-
for (let i = 0; i < body.length; i++) {
|
|
50
|
-
const c = body[i]!
|
|
51
|
-
// A `-` first or last is itself, as in minimatch.
|
|
52
|
-
if (body[i + 1] === '-' && i + 2 < body.length) {
|
|
53
|
-
const end = body[i + 2]!
|
|
54
|
-
const kind = RANGES.find((r) => r.includes(c))
|
|
55
|
-
const from = kind?.indexOf(c) ?? -1
|
|
56
|
-
const to = kind?.indexOf(end) ?? -1
|
|
57
|
-
if (kind === undefined || to < from) return null
|
|
58
|
-
for (const m of kind.slice(from, to + 1)) out.add(m)
|
|
59
|
-
i += 2
|
|
60
|
-
} else out.add(c)
|
|
61
|
-
}
|
|
62
|
-
return [...out]
|
|
27
|
+
return out.includes('\0') || /[?@+*!]\(/.test(out) ? null : out
|
|
63
28
|
}
|
|
64
29
|
|
|
65
30
|
/**
|
|
@@ -73,7 +38,7 @@ function classMembers(body: string): string[] | null {
|
|
|
73
38
|
*/
|
|
74
39
|
export function withoutTakenBack<T>(entries: readonly T[]): T[] {
|
|
75
40
|
const list = [...new Set(entries)]
|
|
76
|
-
const wild = /[*?{}]/
|
|
41
|
+
const wild = /[*?{}[\]\\]/
|
|
77
42
|
const negative = list.flatMap((e) =>
|
|
78
43
|
typeof e === 'string' && e.startsWith('!') ? [e.slice(1)] : [],
|
|
79
44
|
)
|
|
@@ -90,10 +55,10 @@ export function withoutTakenBack<T>(entries: readonly T[]): T[] {
|
|
|
90
55
|
|
|
91
56
|
/**
|
|
92
57
|
* A path from the file system as a task glob that names only itself: a dir
|
|
93
|
-
* named `my{app}` read as a brace matched `myapp` instead
|
|
94
|
-
*
|
|
95
|
-
*
|
|
58
|
+
* named `my{app}` read as a brace matched `myapp` instead, and `app/[id]` a
|
|
59
|
+
* class. `*` and `?` stay, since the schema reads `\*` as a Windows
|
|
60
|
+
* separator, and they match the name too (a superset).
|
|
96
61
|
*/
|
|
97
62
|
export function literalGlob(p: string): string {
|
|
98
|
-
return p.replace(/^!|[{}]/g, '\\$&')
|
|
63
|
+
return p.replace(/^!|[{}[\]]/g, '\\$&')
|
|
99
64
|
}
|
package/src/migrate-nx.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { stat } from 'node:fs/promises'
|
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import { type GeneratedProject, type MigrationFormat, type MigrationPlan } from '@vzn/vx'
|
|
7
7
|
import type { PackageMeta } from './packages.js'
|
|
8
|
-
import { mapNxWorkspace,
|
|
8
|
+
import { mapNxWorkspace, parseNxGraph, readNxJson } from './nx/nx-map.js'
|
|
9
9
|
import { spareTrackedOutputs, trackedFiles, trackedKinds } from './tracked-outputs.js'
|
|
10
10
|
import { adoptedToolNotes } from './workspace-notes.js'
|
|
11
11
|
import { relPosix } from './paths.js'
|
|
@@ -165,7 +165,7 @@ async function unlistedProjects(
|
|
|
165
165
|
/** A `.vx/workspace.ts` field nx.json's run settings call for. */
|
|
166
166
|
export interface NxWorkspaceField {
|
|
167
167
|
/** The setting's path: its group, then its field, or a top-level key. */
|
|
168
|
-
readonly field: 'run.concurrency' | 'baseBranch'
|
|
168
|
+
readonly field: 'run.concurrency' | 'baseBranch'
|
|
169
169
|
/** The value as TypeScript source; undefined when vx cannot read it. */
|
|
170
170
|
readonly source: string | undefined
|
|
171
171
|
/** What to tell a user whose workspace file this run does not write. */
|
|
@@ -187,7 +187,6 @@ export function nxWorkspaceFields(json: Record<string, unknown> | undefined): Nx
|
|
|
187
187
|
tasksRunnerOptions?: { default?: { options?: { parallel?: unknown } } }
|
|
188
188
|
defaultBase?: unknown
|
|
189
189
|
affected?: { defaultBase?: unknown }
|
|
190
|
-
maxCacheSize?: unknown
|
|
191
190
|
}
|
|
192
191
|
| undefined
|
|
193
192
|
const out: NxWorkspaceField[] = []
|
|
@@ -207,12 +206,5 @@ export function nxWorkspaceFields(json: Record<string, unknown> | undefined): Nx
|
|
|
207
206
|
note: `nx.json \`defaultBase\` ${JSON.stringify(base.trim())}: add \`baseBranch: ${JSON.stringify(branch)}\` to .vx/workspace.ts`,
|
|
208
207
|
})
|
|
209
208
|
}
|
|
210
|
-
const size = nxSizeText(nx?.maxCacheSize)
|
|
211
|
-
if (size !== undefined)
|
|
212
|
-
out.push({
|
|
213
|
-
field: 'cache.retention',
|
|
214
|
-
source: /^\d+[KMG]?B$/.test(size) ? `{ maxSize: ${quoted(size)} }` : undefined,
|
|
215
|
-
note: `nx.json \`maxCacheSize\`: add \`cache: { retention: { maxSize: ${JSON.stringify(size)} } }\` to .vx/workspace.ts`,
|
|
216
|
-
})
|
|
217
209
|
return out
|
|
218
210
|
}
|
package/src/nx/index.ts
CHANGED
|
@@ -39,7 +39,6 @@ import {
|
|
|
39
39
|
mapNxWorkspace,
|
|
40
40
|
type NxExecutors,
|
|
41
41
|
type NxGraph,
|
|
42
|
-
nxSizeText,
|
|
43
42
|
parseNxGraph,
|
|
44
43
|
readNxJson,
|
|
45
44
|
} from './nx-map.js'
|
|
@@ -166,11 +165,6 @@ export function nx(options: NxPluginOptions = {}): VxPlugin {
|
|
|
166
165
|
rules: { ...workspace.run?.rules, dependencies },
|
|
167
166
|
}
|
|
168
167
|
}
|
|
169
|
-
if (workspace.cache?.retention === undefined) {
|
|
170
|
-
const maxSize = await nxMaxCacheSize(root)
|
|
171
|
-
if (maxSize !== undefined)
|
|
172
|
-
workspace.cache = { ...workspace.cache, retention: { maxSize } }
|
|
173
|
-
}
|
|
174
168
|
},
|
|
175
169
|
// The package manager's members are projects, each with no vx.config
|
|
176
170
|
// written. An integrated Nx repo keeps projects out of that list
|
|
@@ -294,24 +288,6 @@ async function nxDefaultBase(root: string): Promise<string | undefined> {
|
|
|
294
288
|
return typeof b === 'string' && b.trim() !== '' ? b.trim().replace(/^origin\//, '') : undefined
|
|
295
289
|
}
|
|
296
290
|
|
|
297
|
-
/**
|
|
298
|
-
* nx.json's `maxCacheSize`, or `NX_MAX_CACHE_SIZE` above it, as core's
|
|
299
|
-
* size: Nx caps its local cache there (`resolveMaxCacheSize`), and read by
|
|
300
|
-
* nothing a capped cache grew without bound under vx, as turbo.json's
|
|
301
|
-
* `cacheMaxSize` once did (G-49). 1024-based, a fraction truncated to
|
|
302
|
-
* bytes; `0` is no cap. A value Nx would refuse is left to core's
|
|
303
|
-
* refusal, which names the field.
|
|
304
|
-
*/
|
|
305
|
-
async function nxMaxCacheSize(root: string): Promise<string | undefined> {
|
|
306
|
-
const env = process.env['NX_MAX_CACHE_SIZE']
|
|
307
|
-
const raw =
|
|
308
|
-
env !== undefined
|
|
309
|
-
? env
|
|
310
|
-
: ((await readNxJson(root).catch(() => null))?.json as { maxCacheSize?: unknown } | undefined)
|
|
311
|
-
?.maxCacheSize
|
|
312
|
-
return nxSizeText(raw)
|
|
313
|
-
}
|
|
314
|
-
|
|
315
291
|
interface LoadedGraph {
|
|
316
292
|
readonly text: string
|
|
317
293
|
readonly graph: NxGraph
|
package/src/nx/nx-map.ts
CHANGED
|
@@ -1522,20 +1522,3 @@ function followNxGraph(
|
|
|
1522
1522
|
}
|
|
1523
1523
|
}
|
|
1524
1524
|
}
|
|
1525
|
-
|
|
1526
|
-
/** Nx's cache-size grammar (`10GB`, `1.5 GB`, bare bytes) as core's size; `0` is no cap. */
|
|
1527
|
-
export function nxSizeText(raw: unknown): string | undefined {
|
|
1528
|
-
if (typeof raw !== 'string' && typeof raw !== 'number') return undefined
|
|
1529
|
-
const text = String(raw).trim()
|
|
1530
|
-
const m = /^(\d+\.?\d*|\.\d+)\s?([KMG]?B)?$/.exec(text)
|
|
1531
|
-
if (m === null) return text
|
|
1532
|
-
const units = ['B', 'KB', 'MB', 'GB']
|
|
1533
|
-
let n = Math.floor(Number(m[1]) * 1024 ** units.indexOf(m[2] ?? 'B'))
|
|
1534
|
-
if (n === 0) return undefined
|
|
1535
|
-
let u = 0
|
|
1536
|
-
while (u < units.length - 1 && n % 1024 === 0) {
|
|
1537
|
-
n /= 1024
|
|
1538
|
-
u++
|
|
1539
|
-
}
|
|
1540
|
-
return `${n}${units[u]}`
|
|
1541
|
-
}
|
package/src/nx-cache/index.ts
CHANGED
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
import {
|
|
15
15
|
definePlugin,
|
|
16
16
|
type RemoteCacheLayer,
|
|
17
|
+
type RemoteKey,
|
|
17
18
|
type RemoteOpenContext,
|
|
18
19
|
type VxPlugin,
|
|
19
20
|
refuseUnknownOptions,
|
|
20
21
|
type PluginOptionKinds,
|
|
21
22
|
} from '@vzn/vx'
|
|
22
23
|
import { deadline } from '../remote-deadline.js'
|
|
24
|
+
import { scopeHeader } from '../remote-scope.js'
|
|
23
25
|
import { OutageBreaker } from '../remote-breaker.js'
|
|
24
26
|
import { Cooldown, withRetry } from '../remote-retry.js'
|
|
25
27
|
import {
|
|
@@ -153,10 +155,11 @@ export class NxRemoteCache implements RemoteCacheLayer {
|
|
|
153
155
|
private async request(
|
|
154
156
|
method: 'GET' | 'PUT',
|
|
155
157
|
hash: string,
|
|
158
|
+
at: RemoteKey | undefined,
|
|
156
159
|
body?: Blob,
|
|
157
160
|
): Promise<Response | undefined> {
|
|
158
161
|
if (this.token === undefined) return undefined
|
|
159
|
-
const headers: Record<string, string> =
|
|
162
|
+
const headers: Record<string, string> = scopeHeader(at)
|
|
160
163
|
if (this.token !== '') headers['Authorization'] = `Bearer ${this.token}`
|
|
161
164
|
if (body !== undefined) {
|
|
162
165
|
headers['Content-Type'] = 'application/octet-stream'
|
|
@@ -209,15 +212,18 @@ export class NxRemoteCache implements RemoteCacheLayer {
|
|
|
209
212
|
return res
|
|
210
213
|
}
|
|
211
214
|
|
|
212
|
-
async get(
|
|
213
|
-
|
|
215
|
+
async get(
|
|
216
|
+
hash: string,
|
|
217
|
+
at?: RemoteKey,
|
|
218
|
+
): Promise<{ body: Response; durationMs: number | undefined } | null> {
|
|
219
|
+
const res = await this.fetch(hash, at)
|
|
214
220
|
// The Nx wire carries no producing-task duration.
|
|
215
221
|
return res === null ? null : { body: res, durationMs: undefined }
|
|
216
222
|
}
|
|
217
223
|
|
|
218
|
-
private async fetch(hash: string): Promise<Response | null> {
|
|
224
|
+
private async fetch(hash: string, at: RemoteKey | undefined): Promise<Response | null> {
|
|
219
225
|
if (this.disabled) return null
|
|
220
|
-
const res = await this.request('GET', hash)
|
|
226
|
+
const res = await this.request('GET', hash, at)
|
|
221
227
|
if (res === undefined) return null
|
|
222
228
|
if (res.status !== 200) {
|
|
223
229
|
await res.body?.cancel()
|
|
@@ -227,17 +233,17 @@ export class NxRemoteCache implements RemoteCacheLayer {
|
|
|
227
233
|
return res
|
|
228
234
|
}
|
|
229
235
|
|
|
230
|
-
async has(hash: string): Promise<boolean> {
|
|
231
|
-
const res = await this.fetch(hash)
|
|
236
|
+
async has(hash: string, at?: RemoteKey): Promise<boolean> {
|
|
237
|
+
const res = await this.fetch(hash, at)
|
|
232
238
|
if (res === null) return false
|
|
233
239
|
await res.body?.cancel()
|
|
234
240
|
return true
|
|
235
241
|
}
|
|
236
242
|
|
|
237
|
-
// Nx's record carries no duration;
|
|
238
|
-
async put(hash: string, body: Blob,
|
|
243
|
+
// Nx's record carries no duration; only `meta`'s scope is sent.
|
|
244
|
+
async put(hash: string, body: Blob, meta: { durationMs: number } & RemoteKey): Promise<void> {
|
|
239
245
|
if (this.disabled || this.writesRefused) return
|
|
240
|
-
const res = await this.request('PUT', hash, body)
|
|
246
|
+
const res = await this.request('PUT', hash, meta, body)
|
|
241
247
|
if (res === undefined) return
|
|
242
248
|
if (res.status === 200 || res.status === 202 || res.status === 409) return
|
|
243
249
|
throw new Error(`HTTP ${res.status}`)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RemoteKey } from '@vzn/vx'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The bucket a request means (owner, 2026-10-18): `trusted` for a task's
|
|
5
|
+
* own key, else the branch scope its key was derived in. It only states
|
|
6
|
+
* intent; a server checks it against the caller's identity (a CI ID
|
|
7
|
+
* token). A request without it, from an older client, leaves the layering
|
|
8
|
+
* to the server. Shared by `turboCache()` and `nxCache()`.
|
|
9
|
+
*/
|
|
10
|
+
export function scopeHeader(at: RemoteKey | undefined): Record<string, string> {
|
|
11
|
+
return { 'x-vx-cache-scope': at?.scope ?? 'trusted' }
|
|
12
|
+
}
|
package/src/turbo/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { readFileSync } from 'node:fs'
|
|
12
12
|
import { availableParallelism } from 'node:os'
|
|
13
13
|
import path from 'node:path'
|
|
14
|
-
import type { ProjectMeta, VxPlugin
|
|
14
|
+
import type { ProjectMeta, VxPlugin } from '@vzn/vx'
|
|
15
15
|
import type { PackageMeta } from '../packages.js'
|
|
16
16
|
import { memberProjects, withManifests } from '../manifests.js'
|
|
17
17
|
import { refuseUnknownOptions, type PluginOptionKinds } from '@vzn/vx'
|
|
@@ -65,14 +65,6 @@ export function turbo(options: TurboPluginOptions = {}): VxPlugin {
|
|
|
65
65
|
const keys = await workspaceKeys(options.root ?? ctx.workspaceRoot)
|
|
66
66
|
if (workspace.run?.concurrency === undefined && keys.concurrency !== undefined)
|
|
67
67
|
workspace.run = { ...workspace.run, concurrency: keys.concurrency }
|
|
68
|
-
if (workspace.cache?.retention === undefined && keys.cacheRetention !== undefined)
|
|
69
|
-
workspace.cache = {
|
|
70
|
-
...workspace.cache,
|
|
71
|
-
// `olderThan` is built with its unit below.
|
|
72
|
-
retention: keys.cacheRetention as NonNullable<
|
|
73
|
-
NonNullable<WorkspaceConfig['cache']>['retention']
|
|
74
|
-
>,
|
|
75
|
-
}
|
|
76
68
|
// `turbo run --affected` compares with TURBO_SCM_BASE when set, and
|
|
77
69
|
// on GitHub Actions with what the event names.
|
|
78
70
|
const base =
|
|
@@ -99,36 +91,6 @@ export function turbo(options: TurboPluginOptions = {}): VxPlugin {
|
|
|
99
91
|
)
|
|
100
92
|
}
|
|
101
93
|
|
|
102
|
-
/**
|
|
103
|
-
* turbo.json's workspace keys vx has a home for, top level or under
|
|
104
|
-
* `global`: `concurrency` (`"10"`, `"50%"` of the cores) and the local
|
|
105
|
-
* cache's `cacheMaxSize` / `cacheMaxAge` (`"0"` is off; weeks become days).
|
|
106
|
-
* Read by nothing, a repo that capped its cache at 10GB (formbricks) grew
|
|
107
|
-
* it without bound under vx. A value core cannot parse is left to core's
|
|
108
|
-
* refusal, which names the field.
|
|
109
|
-
*/
|
|
110
|
-
const SIZE_UNITS = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Turbo's size (turborepo-cache `parse_human_size`): 1024-based, a bare
|
|
114
|
-
* number is bytes, a fraction is truncated to bytes. Core takes a whole
|
|
115
|
-
* number with a unit, so `7.5GB` (langfuse) is restated as `7680MB` and
|
|
116
|
-
* `1000000` as `1000000B`; core refused both. Undefined when Turbo reads
|
|
117
|
-
* it as off (under one byte).
|
|
118
|
-
*/
|
|
119
|
-
function turboSize(v: string): string | undefined {
|
|
120
|
-
const m = /^(\d+\.?\d*|\.\d+)([KMGT]?B)?$/i.exec(v)
|
|
121
|
-
if (!m) return v
|
|
122
|
-
let n = Math.floor(Number(m[1]) * 1024 ** SIZE_UNITS.indexOf((m[2] ?? 'B').toUpperCase()))
|
|
123
|
-
if (n === 0) return undefined
|
|
124
|
-
let u = 0
|
|
125
|
-
while (u < SIZE_UNITS.length - 1 && n % 1024 === 0) {
|
|
126
|
-
n /= 1024
|
|
127
|
-
u++
|
|
128
|
-
}
|
|
129
|
-
return `${n}${SIZE_UNITS[u]}`
|
|
130
|
-
}
|
|
131
|
-
|
|
132
94
|
const UNKNOWN_SHA = '0'.repeat(40)
|
|
133
95
|
|
|
134
96
|
/**
|
|
@@ -173,9 +135,12 @@ function githubActionsBase(root: string, remoteFallback: boolean): string | unde
|
|
|
173
135
|
return before === '' ? undefined : before
|
|
174
136
|
}
|
|
175
137
|
|
|
138
|
+
/**
|
|
139
|
+
* turbo.json's workspace keys vx has a home for, top level or under
|
|
140
|
+
* `global`: `concurrency` (`"10"`, `"50%"` of the cores).
|
|
141
|
+
*/
|
|
176
142
|
async function workspaceKeys(root: string): Promise<{
|
|
177
143
|
concurrency?: number
|
|
178
|
-
cacheRetention?: { maxSize?: string; olderThan?: string }
|
|
179
144
|
remoteBaseFallback?: boolean
|
|
180
145
|
}> {
|
|
181
146
|
const file = await turboConfigFile(root)
|
|
@@ -199,7 +164,6 @@ async function workspaceKeys(root: string): Promise<{
|
|
|
199
164
|
}
|
|
200
165
|
const out: {
|
|
201
166
|
concurrency?: number
|
|
202
|
-
cacheRetention?: { maxSize?: string; olderThan?: string }
|
|
203
167
|
remoteBaseFallback?: boolean
|
|
204
168
|
} = {}
|
|
205
169
|
const flags = raw['futureFlags'] as { githubActionsRemoteBaseRefFallback?: unknown } | undefined
|
|
@@ -212,19 +176,6 @@ async function workspaceKeys(root: string): Promise<{
|
|
|
212
176
|
: Number(c)
|
|
213
177
|
if (Number.isInteger(n) && n > 0) out.concurrency = n
|
|
214
178
|
}
|
|
215
|
-
const size = read('cacheMaxSize', 'TURBO_CACHE_MAX_SIZE')
|
|
216
|
-
const maxSize = size === undefined ? undefined : turboSize(size)
|
|
217
|
-
const age = read('cacheMaxAge', 'TURBO_CACHE_MAX_AGE')
|
|
218
|
-
// Turbo reads a bare number as days (`parse_human_duration`).
|
|
219
|
-
const olderThan = age
|
|
220
|
-
?.replace(/^(\d+)w$/i, (_, w: string) => `${Number(w) * 7}d`)
|
|
221
|
-
.replace(/^\d+$/, (d) => `${d}d`)
|
|
222
|
-
if (maxSize !== undefined || olderThan !== undefined) {
|
|
223
|
-
out.cacheRetention = {
|
|
224
|
-
...(maxSize !== undefined ? { maxSize } : {}),
|
|
225
|
-
...(olderThan !== undefined ? { olderThan } : {}),
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
179
|
return out
|
|
229
180
|
}
|
|
230
181
|
|
package/src/turbo/turbo-map.ts
CHANGED
|
@@ -2023,9 +2023,9 @@ function buildTask(
|
|
|
2023
2023
|
continue
|
|
2024
2024
|
}
|
|
2025
2025
|
const neg = i.startsWith('!')
|
|
2026
|
-
// Turbo's globs have
|
|
2027
|
-
//
|
|
2028
|
-
//
|
|
2026
|
+
// Turbo's globs have extglobs and vx's do not, so
|
|
2027
|
+
// `src/**/*.@(js|ts)` keyed on nothing and an edit replayed the old
|
|
2028
|
+
// build (item 1031). A positive glob with no safe
|
|
2029
2029
|
// form widens to every package file; a negation with none is dropped
|
|
2030
2030
|
// (it would exclude fewer files, never more).
|
|
2031
2031
|
const wax = minimatchToVx(neg ? i.slice(1) : i, neg)
|
|
@@ -2103,19 +2103,16 @@ function buildTask(
|
|
|
2103
2103
|
const zeroed = new Set<string>()
|
|
2104
2104
|
const wsOutFiles: string[] = []
|
|
2105
2105
|
for (const raw of def.outputs ?? []) {
|
|
2106
|
-
//
|
|
2107
|
-
//
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
const rest = cut < 0 ? null : minimatchToVx(raw.slice(cut + 1), raw.startsWith('!'))
|
|
2111
|
-
if (cut >= 0 && rest === null) {
|
|
2106
|
+
// Turbo's grammar: an extglob in `dist/**/*.@(js|mjs)` matched
|
|
2107
|
+
// nothing, so a hit restored nothing (item 1031).
|
|
2108
|
+
const mapped = minimatchToVx(raw, raw.startsWith('!'))
|
|
2109
|
+
if (mapped === null) {
|
|
2112
2110
|
todos.push(
|
|
2113
2111
|
`output ${JSON.stringify(raw)}: glob syntax vx cannot take — task runs uncached; ` +
|
|
2114
2112
|
'declare the exact outputs in a vx.config to cache it',
|
|
2115
2113
|
)
|
|
2116
2114
|
return { name, todos, task, uses }
|
|
2117
2115
|
}
|
|
2118
|
-
const mapped = cut < 0 ? raw : `${raw.slice(0, cut + 1)}${rest}`
|
|
2119
2116
|
// A `!` output takes a path back, as Turbo's does (A-44): Next's
|
|
2120
2117
|
// `.next/**` minus `!.next/cache/**` saved the cache and cleaned it
|
|
2121
2118
|
// before every run while vx outputs could not exclude.
|
package/src/turbo-cache/index.ts
CHANGED
|
@@ -18,12 +18,14 @@ import path from 'node:path'
|
|
|
18
18
|
import {
|
|
19
19
|
definePlugin,
|
|
20
20
|
type RemoteCacheLayer,
|
|
21
|
+
type RemoteKey,
|
|
21
22
|
type RemoteOpenContext,
|
|
22
23
|
type VxPlugin,
|
|
23
24
|
refuseUnknownOptions,
|
|
24
25
|
type PluginOptionKinds,
|
|
25
26
|
} from '@vzn/vx'
|
|
26
27
|
import { deadline } from '../remote-deadline.js'
|
|
28
|
+
import { scopeHeader } from '../remote-scope.js'
|
|
27
29
|
import { OutageBreaker } from '../remote-breaker.js'
|
|
28
30
|
import { Cooldown, withRetry } from '../remote-retry.js'
|
|
29
31
|
import {
|
|
@@ -614,9 +616,9 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
614
616
|
await revokeOidc(this.config.oidc!, token, this.fetchImpl, this.wait)
|
|
615
617
|
}
|
|
616
618
|
|
|
617
|
-
async has(hash: string): Promise<boolean> {
|
|
619
|
+
async has(hash: string, at?: RemoteKey): Promise<boolean> {
|
|
618
620
|
if (this.disabled) return false
|
|
619
|
-
const res = await this.request('HEAD', `/${hash}
|
|
621
|
+
const res = await this.request('HEAD', `/${hash}`, { headers: scopeHeader(at) })
|
|
620
622
|
if (res === undefined) return false
|
|
621
623
|
if (res.status === 200) return true
|
|
622
624
|
if (res.status === 404) {
|
|
@@ -626,11 +628,27 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
626
628
|
throw new Error(`HTTP ${res.status}`)
|
|
627
629
|
}
|
|
628
630
|
|
|
629
|
-
async hasMany(hashes: readonly string[]): Promise<Set<string> | null> {
|
|
631
|
+
async hasMany(hashes: readonly string[], at?: readonly RemoteKey[]): Promise<Set<string> | null> {
|
|
630
632
|
if (this.disabled || hashes.length === 0) return this.disabled ? new Set() : null
|
|
633
|
+
// One query names one scope, so a batch asks once per scope in it.
|
|
634
|
+
const byScope = new Map<string | undefined, string[]>()
|
|
635
|
+
hashes.forEach((h, i) => {
|
|
636
|
+
const scope = at?.[i]?.scope
|
|
637
|
+
byScope.set(scope, [...(byScope.get(scope) ?? []), h])
|
|
638
|
+
})
|
|
639
|
+
const held = new Set<string>()
|
|
640
|
+
for (const [scope, group] of byScope) {
|
|
641
|
+
const found = await this.queryHeld(group, scope === undefined ? {} : { scope })
|
|
642
|
+
if (found === null) return null
|
|
643
|
+
for (const h of found) held.add(h)
|
|
644
|
+
}
|
|
645
|
+
return held
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private async queryHeld(hashes: readonly string[], at: RemoteKey): Promise<Set<string> | null> {
|
|
631
649
|
const res = await this.request('POST', '', {
|
|
632
650
|
body: JSON.stringify({ hashes }),
|
|
633
|
-
headers: { 'Content-Type': 'application/json' },
|
|
651
|
+
headers: { 'Content-Type': 'application/json', ...scopeHeader(at) },
|
|
634
652
|
})
|
|
635
653
|
if (res === undefined) return new Set()
|
|
636
654
|
if (res.status !== 200) return null
|
|
@@ -653,12 +671,15 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
653
671
|
return held
|
|
654
672
|
}
|
|
655
673
|
|
|
656
|
-
async get(
|
|
674
|
+
async get(
|
|
675
|
+
hash: string,
|
|
676
|
+
at?: RemoteKey,
|
|
677
|
+
): Promise<{ body: Response; durationMs: number | undefined } | null> {
|
|
657
678
|
if (this.disabled) return null
|
|
658
679
|
// A gateway that keys binary media on Accept base64-encodes a `*/*`
|
|
659
680
|
// response, and every hit read as a corrupt artifact (nx#33092's class).
|
|
660
681
|
const res = await this.request('GET', `/${hash}`, {
|
|
661
|
-
headers: { Accept: 'application/octet-stream' },
|
|
682
|
+
headers: { Accept: 'application/octet-stream', ...scopeHeader(at) },
|
|
662
683
|
})
|
|
663
684
|
if (res === undefined) return null
|
|
664
685
|
if (res.status !== 200) {
|
|
@@ -716,13 +737,14 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
716
737
|
return new Response(unlinkingStream(temp))
|
|
717
738
|
}
|
|
718
739
|
|
|
719
|
-
async put(hash: string, body: Blob, meta: { durationMs: number }): Promise<void> {
|
|
740
|
+
async put(hash: string, body: Blob, meta: { durationMs: number } & RemoteKey): Promise<void> {
|
|
720
741
|
if (this.disabled || this.writesRefused) return
|
|
721
742
|
if (!this.absent.has(hash)) {
|
|
722
743
|
// Part of the upload, so under its deadline. Any answer but 200 or 404
|
|
723
744
|
// uploads: a lost store costs more. A server the HEAD cannot reach
|
|
724
745
|
// fails the put, as the PUT would.
|
|
725
746
|
const head = await this.request('HEAD', `/${hash}`, {
|
|
747
|
+
headers: scopeHeader(meta),
|
|
726
748
|
timeoutMs: this.config.uploadTimeoutMs,
|
|
727
749
|
})
|
|
728
750
|
if (head === undefined || head.status === 200) return
|
|
@@ -732,6 +754,7 @@ export class TurboRemoteCache implements RemoteCacheLayer {
|
|
|
732
754
|
'Content-Type': 'application/octet-stream',
|
|
733
755
|
'Content-Length': String(body.size),
|
|
734
756
|
'x-artifact-duration': String(Math.max(0, Math.round(meta.durationMs))),
|
|
757
|
+
...scopeHeader(meta),
|
|
735
758
|
}
|
|
736
759
|
if (this.key !== undefined) {
|
|
737
760
|
headers['x-artifact-tag'] = await artifactTag(this.key, hash, this.config.teamId ?? '', body)
|