@vzn/vx 0.0.520 → 0.0.522
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",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.522",
|
|
4
4
|
"description": "The fastest task runner and build cache for JavaScript monorepos.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"node": ">=18"
|
|
22
22
|
},
|
|
23
23
|
"optionalDependencies": {
|
|
24
|
-
"@vzn/vx-linux-x64": "0.0.
|
|
25
|
-
"@vzn/vx-linux-arm64": "0.0.
|
|
26
|
-
"@vzn/vx-darwin-x64": "0.0.
|
|
27
|
-
"@vzn/vx-darwin-arm64": "0.0.
|
|
24
|
+
"@vzn/vx-linux-x64": "0.0.522",
|
|
25
|
+
"@vzn/vx-linux-arm64": "0.0.522",
|
|
26
|
+
"@vzn/vx-darwin-x64": "0.0.522",
|
|
27
|
+
"@vzn/vx-darwin-arm64": "0.0.522"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@anthropic-ai/sandbox-runtime": "^0.0.76"
|
package/src/cache/git-inputs.ts
CHANGED
|
@@ -214,6 +214,22 @@ export class GitFilesCache extends Map<string, readonly string[]> {
|
|
|
214
214
|
this.dirty = dirty
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/** The populate's `git var -l`, for `configValue`. */
|
|
218
|
+
private vars: Map<string, string> | null = null
|
|
219
|
+
|
|
220
|
+
setGitVars(listing: string | null): void {
|
|
221
|
+
this.vars = listing === null ? null : gitVarList(listing)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* A config key's value as `git config --get` answers it (the last one),
|
|
226
|
+
* from the populate's `git var -l`: null when the config has none,
|
|
227
|
+
* undefined when no listing was read.
|
|
228
|
+
*/
|
|
229
|
+
configValue(key: string): string | null | undefined {
|
|
230
|
+
return this.vars === null ? undefined : (this.vars.get(key) ?? null)
|
|
231
|
+
}
|
|
232
|
+
|
|
217
233
|
markOutputsChanged(projectDir: string, relPaths: readonly string[]): void {
|
|
218
234
|
this.recordChanged(projectDir, relPaths)
|
|
219
235
|
// The workspace-wide partition sees the same files under
|
|
@@ -774,6 +790,12 @@ async function dropFilteredOids(
|
|
|
774
790
|
pathspecs: readonly string[]
|
|
775
791
|
gitVars: string
|
|
776
792
|
spawnGit: (a: string[], stdin?: string) => Promise<GitRun | null>
|
|
793
|
+
/**
|
|
794
|
+
* The same `check-attr`, already started over every listed index entry
|
|
795
|
+
* (a superset of `trusted`) because `attributesInIndex` fired before
|
|
796
|
+
* `status` was back.
|
|
797
|
+
*/
|
|
798
|
+
early?: Promise<GitRun | null> | undefined
|
|
777
799
|
},
|
|
778
800
|
): Promise<void> {
|
|
779
801
|
if (trusted.size === 0) return
|
|
@@ -823,14 +845,48 @@ async function dropFilteredOids(
|
|
|
823
845
|
// blob's OID keyed an edit git calls clean, and the run replayed the old
|
|
824
846
|
// output (item 978). Git LFS is a `filter` as well: its files are hashed
|
|
825
847
|
// from disk now, the one honest identity of what a task reads.
|
|
826
|
-
const res = await args.spawnGit(
|
|
827
|
-
['check-attr', '--stdin', '-z', 'text', 'eol', 'ident', 'filter', 'working-tree-encoding'],
|
|
828
|
-
[...trusted.keys()].join('\0'),
|
|
829
|
-
)
|
|
848
|
+
const res = await (args.early ?? args.spawnGit(CHECK_ATTR, [...trusted.keys()].join('\0')))
|
|
830
849
|
if (res === null || res.exitCode !== 0) return
|
|
831
850
|
for (const rel of parseCheckAttrOutput(res.stdout)) trusted.delete(rel)
|
|
832
851
|
}
|
|
833
852
|
|
|
853
|
+
const CHECK_ATTR = [
|
|
854
|
+
'check-attr',
|
|
855
|
+
'--stdin',
|
|
856
|
+
'-z',
|
|
857
|
+
'text',
|
|
858
|
+
'eol',
|
|
859
|
+
'ident',
|
|
860
|
+
'filter',
|
|
861
|
+
'working-tree-encoding',
|
|
862
|
+
]
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* The filter gate's half that needs only the index listing: an attributes
|
|
866
|
+
* file tracked in it, in `$GIT_DIR/info/attributes`, or above the scanned
|
|
867
|
+
* dirs. When it fires, `dropFilteredOids` is sure to ask `check-attr`, so the
|
|
868
|
+
* enumeration starts it beside `status` (~8 ms on this repo, U-3)
|
|
869
|
+
* instead of after it. When it does not, the whole gate decides later.
|
|
870
|
+
*/
|
|
871
|
+
function attributesInIndex(args: {
|
|
872
|
+
tracked: readonly string[]
|
|
873
|
+
workspaceRoot: string
|
|
874
|
+
gitDir: string
|
|
875
|
+
gitPrefix: string
|
|
876
|
+
pathspecs: readonly string[]
|
|
877
|
+
}): boolean {
|
|
878
|
+
if (
|
|
879
|
+
args.gitDir !== '' &&
|
|
880
|
+
existsSync(path.resolve(args.workspaceRoot, args.gitDir, 'info', 'attributes'))
|
|
881
|
+
) {
|
|
882
|
+
return true
|
|
883
|
+
}
|
|
884
|
+
for (const rel of args.tracked) {
|
|
885
|
+
if (rel === '.gitattributes' || rel.endsWith('/.gitattributes')) return true
|
|
886
|
+
}
|
|
887
|
+
return attributesAbove(args)
|
|
888
|
+
}
|
|
889
|
+
|
|
834
890
|
/**
|
|
835
891
|
* The repository root, derived from the workspace root and git's own
|
|
836
892
|
* `--show-prefix` (the repo→workspace path). Exact and free: the prefix
|
|
@@ -1095,6 +1151,8 @@ export interface GitEnumeration {
|
|
|
1095
1151
|
trusted: Map<string, string>
|
|
1096
1152
|
/** Whether the worktree had uncommitted changes; null when `git status` failed. */
|
|
1097
1153
|
dirty: boolean | null
|
|
1154
|
+
/** `git var -l`'s listing (merged config, then git's variables); null when it failed. */
|
|
1155
|
+
vars: string | null
|
|
1098
1156
|
/**
|
|
1099
1157
|
* What `git status` listed, workspace-relative: modified, staged or deleted
|
|
1100
1158
|
* paths (both sides of a rename) and untracked files. Null when it failed.
|
|
@@ -1259,7 +1317,20 @@ export async function startGitEnumeration(
|
|
|
1259
1317
|
}
|
|
1260
1318
|
}
|
|
1261
1319
|
const indexKeyRead = readIndexKey()
|
|
1262
|
-
const
|
|
1320
|
+
const gitPrefix = facts?.prefix ?? ''
|
|
1321
|
+
const gitDir = facts?.commonDir ?? ''
|
|
1322
|
+
const ls = await listing
|
|
1323
|
+
const parsedLs =
|
|
1324
|
+
ls !== null && ls.exitCode === 0
|
|
1325
|
+
? parseLsFilesOutput(ls.stdout.length === 0 ? [] : ls.stdout.split('\0'), ls.undecodable)
|
|
1326
|
+
: undefined
|
|
1327
|
+
const early =
|
|
1328
|
+
parsedLs !== undefined &&
|
|
1329
|
+
parsedLs.oids.size > 0 &&
|
|
1330
|
+
attributesInIndex({ tracked: parsedLs.files, workspaceRoot, gitDir, gitPrefix, pathspecs })
|
|
1331
|
+
? spawnGit(CHECK_ATTR, [...parsedLs.oids.keys()].join('\0'))
|
|
1332
|
+
: undefined
|
|
1333
|
+
const [, status, vars] = await running
|
|
1263
1334
|
// The key stands for the index `ls-files` listed only when that index
|
|
1264
1335
|
// was written before the enumeration began (git writes it by rename, so
|
|
1265
1336
|
// the stamp moves with any write) and is still in place after it: a
|
|
@@ -1281,20 +1352,14 @@ export async function startGitEnumeration(
|
|
|
1281
1352
|
if (ls === null) {
|
|
1282
1353
|
throw gitSpawnRefusal(workspaceRoot)
|
|
1283
1354
|
}
|
|
1284
|
-
if (
|
|
1355
|
+
if (parsedLs === undefined) {
|
|
1285
1356
|
const stderr = ls.stderr.trim()
|
|
1286
1357
|
throw new UserError(
|
|
1287
1358
|
`vx requires git: ${workspaceRoot} is not inside a git work tree. ` +
|
|
1288
1359
|
`Run 'git init' in your workspace root.${stderr ? ` (git: ${stderr})` : ''}`,
|
|
1289
1360
|
)
|
|
1290
1361
|
}
|
|
1291
|
-
const {
|
|
1292
|
-
files: tracked,
|
|
1293
|
-
oids,
|
|
1294
|
-
flagged,
|
|
1295
|
-
gitlinks,
|
|
1296
|
-
undecodable,
|
|
1297
|
-
} = parseLsFilesOutput(ls.stdout.length === 0 ? [] : ls.stdout.split('\0'), ls.undecodable)
|
|
1362
|
+
const { files: tracked, oids, flagged, gitlinks, undecodable } = parsedLs
|
|
1298
1363
|
// Normalize `status`'s repo-root-relative paths to workspace-relative (strip
|
|
1299
1364
|
// the `--show-prefix`) so the dirty set is keyed identically to the trusted
|
|
1300
1365
|
// OID map. Without this, when the workspace root is a git subdir, a modified
|
|
@@ -1302,8 +1367,6 @@ export async function startGitEnumeration(
|
|
|
1302
1367
|
// a STALE cache hit serving old outputs. Empty prefix (workspace == git root,
|
|
1303
1368
|
// the common case) is a zero-cost no-op. Paths above the workspace can't be
|
|
1304
1369
|
// inputs, so they drop out of the set.
|
|
1305
|
-
const gitPrefix = facts?.prefix ?? ''
|
|
1306
|
-
const gitDir = facts?.commonDir ?? ''
|
|
1307
1370
|
const parsedStatus =
|
|
1308
1371
|
status !== null && status.exitCode === 0
|
|
1309
1372
|
? parseStatusOutput(status.stdout, status.undecodable)
|
|
@@ -1357,6 +1420,7 @@ export async function startGitEnumeration(
|
|
|
1357
1420
|
pathspecs,
|
|
1358
1421
|
gitVars: vars !== null && vars.exitCode === 0 ? vars.stdout : '',
|
|
1359
1422
|
spawnGit,
|
|
1423
|
+
early,
|
|
1360
1424
|
})
|
|
1361
1425
|
if (vars !== null && vars.exitCode === 0 && fileModeIgnored(vars.stdout)) {
|
|
1362
1426
|
restampModes(trusted, workspaceRoot)
|
|
@@ -1368,6 +1432,7 @@ export async function startGitEnumeration(
|
|
|
1368
1432
|
all,
|
|
1369
1433
|
trusted,
|
|
1370
1434
|
dirty: worktreeDirty,
|
|
1435
|
+
vars: vars !== null && vars.exitCode === 0 ? vars.stdout : null,
|
|
1371
1436
|
changed: dirty === null ? null : [...dirty, ...untracked],
|
|
1372
1437
|
untracked: dirty === null ? null : untracked,
|
|
1373
1438
|
undecodable,
|
|
@@ -1436,6 +1501,7 @@ export async function applyGitEnumeration(
|
|
|
1436
1501
|
await dropResizedOids(enumeration, memo)
|
|
1437
1502
|
const { all, trusted } = enumeration
|
|
1438
1503
|
cache.setWorktreeDirty(enumeration.dirty)
|
|
1504
|
+
cache.setGitVars(enumeration.vars)
|
|
1439
1505
|
cache.enumeratedAtMs = enumeration.startedAtMs
|
|
1440
1506
|
cache.markUndecodable(enumeration.undecodable.map((rel) => path.join(workspaceRoot, rel)))
|
|
1441
1507
|
// Sort once, then each project's files are a contiguous range found
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
/** `mtime_ms` of a recorded output prefix that did not exist when the snapshot was taken. */
|
|
10
10
|
const ABSENT_DIR_MTIME = -1
|
|
11
|
+
/**
|
|
12
|
+
* `mtime_ms` of a recorded output prefix that was a regular file: a literal
|
|
13
|
+
* output naming one (`public/app.js`). The set under it is the file alone
|
|
14
|
+
* while it stays a regular file; its bytes are the per-file check's.
|
|
15
|
+
*/
|
|
16
|
+
const FILE_PREFIX_MTIME = -2
|
|
11
17
|
|
|
12
18
|
import type { Database } from 'bun:sqlite'
|
|
13
19
|
import { inHashes, lazyStatement } from './schema.js'
|
|
@@ -244,7 +250,14 @@ export class OutputIndex {
|
|
|
244
250
|
}
|
|
245
251
|
return false
|
|
246
252
|
}
|
|
247
|
-
if (!st.isDirectory())
|
|
253
|
+
if (!st.isDirectory()) {
|
|
254
|
+
// A literal output naming a file walked its glob on every hit: two
|
|
255
|
+
// of this repo's site tasks, ~6 ms of each warm run (U-5).
|
|
256
|
+
if (!isPrefix || !st.isFile()) return false
|
|
257
|
+
rows.push([rel, FILE_PREFIX_MTIME])
|
|
258
|
+
files.push(rel)
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
248
261
|
rows.push([rel, st.mtimeMs])
|
|
249
262
|
if (rows.length > OUTPUT_DIRS_CAP) return false
|
|
250
263
|
let entries
|
|
@@ -379,6 +392,7 @@ export class OutputIndex {
|
|
|
379
392
|
return r.mtimeMs === ABSENT_DIR_MTIME
|
|
380
393
|
}
|
|
381
394
|
if (r.mtimeMs === ABSENT_DIR_MTIME) return false
|
|
395
|
+
if (r.mtimeMs === FILE_PREFIX_MTIME) return st.isFile()
|
|
382
396
|
return st.isDirectory() && Math.abs(st.mtimeMs - r.mtimeMs) < 1
|
|
383
397
|
})
|
|
384
398
|
return results.every(Boolean)
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
// unset env degrades each field to null and NEVER throws. Telemetry must
|
|
5
5
|
// not be able to fail a build.
|
|
6
6
|
//
|
|
7
|
-
// Cost:
|
|
8
|
-
//
|
|
9
|
-
// declares the `telemetry` capability — nobody else
|
|
7
|
+
// Cost: HEAD read from `.git` on every run (`captureGitContext`), plus
|
|
8
|
+
// `refs/remotes/origin/HEAD` and the enumeration's `remote.origin.url`
|
|
9
|
+
// only when a plugin declares the `telemetry` capability — nobody else
|
|
10
|
+
// needs those fields. Each falls back to a `git` spawn where the files or
|
|
11
|
+
// the enumeration cannot answer.
|
|
10
12
|
|
|
11
13
|
import fs from 'node:fs'
|
|
12
14
|
import os from 'node:os'
|
|
@@ -115,28 +117,9 @@ function readHeadDirect(
|
|
|
115
117
|
workspaceRoot: string,
|
|
116
118
|
): { commitSha: string; branch: string | null } | null {
|
|
117
119
|
try {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const m = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(gitDir, 'utf8'))
|
|
122
|
-
if (m === null) return null
|
|
123
|
-
gitDir = path.resolve(workspaceRoot, m[1]!.trim())
|
|
124
|
-
}
|
|
125
|
-
let commonDir = gitDir
|
|
126
|
-
const commonFile = path.join(gitDir, 'commondir')
|
|
127
|
-
if (fs.existsSync(commonFile)) {
|
|
128
|
-
commonDir = path.resolve(gitDir, fs.readFileSync(commonFile, 'utf8').trim())
|
|
129
|
-
}
|
|
130
|
-
// Reftable storage (`git init --ref-format=reftable`, git 2.45): HEAD
|
|
131
|
-
// names `refs/heads/.invalid` and the refs live in `reftable/`, so the
|
|
132
|
-
// files say nothing true about HEAD. A leftover `packed-refs` there was
|
|
133
|
-
// read as the answer; git is asked instead.
|
|
134
|
-
if (
|
|
135
|
-
fs.existsSync(path.join(gitDir, 'reftable')) ||
|
|
136
|
-
fs.existsSync(path.join(commonDir, 'reftable'))
|
|
137
|
-
) {
|
|
138
|
-
return null
|
|
139
|
-
}
|
|
120
|
+
const dirs = refDirsOf(workspaceRoot)
|
|
121
|
+
if (dirs === null) return null
|
|
122
|
+
const { gitDir, commonDir } = dirs
|
|
140
123
|
const head = fs.readFileSync(path.join(gitDir, 'HEAD'), 'utf8').trim()
|
|
141
124
|
const SHA = /^[0-9a-f]{40,64}$/
|
|
142
125
|
if (SHA.test(head)) return { commitSha: head, branch: null }
|
|
@@ -166,6 +149,57 @@ function readHeadDirect(
|
|
|
166
149
|
}
|
|
167
150
|
}
|
|
168
151
|
|
|
152
|
+
/**
|
|
153
|
+
* The git dir and common dir behind `workspaceRoot` when its refs are files
|
|
154
|
+
* this code reads: a `.git` directory or a `.git` FILE (`gitdir: …`, a linked
|
|
155
|
+
* worktree, whose shared refs live under `commondir`). Null for a reftable
|
|
156
|
+
* repository (`git init --ref-format=reftable`, git 2.45): HEAD names
|
|
157
|
+
* `refs/heads/.invalid` and the refs live in `reftable/`, so the files say
|
|
158
|
+
* nothing true. A leftover `packed-refs` there was read as the answer.
|
|
159
|
+
* Throws when there is no `.git` at the root (a workspace in a subdirectory).
|
|
160
|
+
*/
|
|
161
|
+
function refDirsOf(workspaceRoot: string): { gitDir: string; commonDir: string } | null {
|
|
162
|
+
let gitDir = path.join(workspaceRoot, '.git')
|
|
163
|
+
const st = fs.statSync(gitDir)
|
|
164
|
+
if (st.isFile()) {
|
|
165
|
+
const m = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(gitDir, 'utf8'))
|
|
166
|
+
if (m === null) return null
|
|
167
|
+
gitDir = path.resolve(workspaceRoot, m[1]!.trim())
|
|
168
|
+
}
|
|
169
|
+
let commonDir = gitDir
|
|
170
|
+
const commonFile = path.join(gitDir, 'commondir')
|
|
171
|
+
if (fs.existsSync(commonFile)) {
|
|
172
|
+
commonDir = path.resolve(gitDir, fs.readFileSync(commonFile, 'utf8').trim())
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
fs.existsSync(path.join(gitDir, 'reftable')) ||
|
|
176
|
+
fs.existsSync(path.join(commonDir, 'reftable'))
|
|
177
|
+
) {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
return { gitDir, commonDir }
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `refs/remotes/origin/HEAD`'s target from the files, as `git symbolic-ref
|
|
185
|
+
* --short` names it (`origin/main`): `undefined` when the files cannot say
|
|
186
|
+
* (no `.git` at the root, reftable), null when there is no such symbolic
|
|
187
|
+
* ref. A symbolic ref is never packed, so a missing file is git's answer
|
|
188
|
+
* too.
|
|
189
|
+
*/
|
|
190
|
+
function readOriginHeadDirect(workspaceRoot: string): string | null | undefined {
|
|
191
|
+
try {
|
|
192
|
+
const dirs = refDirsOf(workspaceRoot)
|
|
193
|
+
if (dirs === null) return undefined
|
|
194
|
+
const file = path.join(dirs.commonDir, 'refs', 'remotes', 'origin', 'HEAD')
|
|
195
|
+
if (!fs.existsSync(file)) return null
|
|
196
|
+
const ref = /^ref:\s*refs\/remotes\/(.+)$/.exec(fs.readFileSync(file, 'utf8').trim())
|
|
197
|
+
return ref === null ? undefined : ref[1]!
|
|
198
|
+
} catch {
|
|
199
|
+
return undefined
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
169
203
|
/**
|
|
170
204
|
* The branch a CI provider says this checkout came from, or null.
|
|
171
205
|
*
|
|
@@ -221,30 +255,34 @@ export function captureDefaultBranch(
|
|
|
221
255
|
}
|
|
222
256
|
}
|
|
223
257
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (
|
|
258
|
+
// The file first, as `captureGitContext` reads HEAD: a spawn here was
|
|
259
|
+
// ~8 ms of every run a telemetry plugin is declared in.
|
|
260
|
+
let ref = readOriginHeadDirect(workspaceRoot)
|
|
261
|
+
if (ref === undefined) {
|
|
262
|
+
ref = null
|
|
263
|
+
try {
|
|
264
|
+
const proc = Bun.spawnSync({
|
|
265
|
+
cmd: [
|
|
266
|
+
executablePath('git'),
|
|
267
|
+
'-C',
|
|
268
|
+
workspaceRoot,
|
|
269
|
+
'symbolic-ref',
|
|
270
|
+
'--short',
|
|
271
|
+
'refs/remotes/origin/HEAD',
|
|
272
|
+
],
|
|
273
|
+
stdout: 'pipe',
|
|
274
|
+
stderr: 'ignore',
|
|
275
|
+
})
|
|
276
|
+
if (proc.exitCode === 0) ref = new TextDecoder().decode(proc.stdout).trim()
|
|
277
|
+
} catch {
|
|
278
|
+
// git unavailable / no remote HEAD ref: null.
|
|
243
279
|
}
|
|
244
|
-
} catch {
|
|
245
|
-
// git unavailable / no remote HEAD ref: null.
|
|
246
280
|
}
|
|
247
|
-
return null
|
|
281
|
+
if (ref === null) return null
|
|
282
|
+
// `refs/remotes/origin/HEAD` → `origin/main`; strip the remote prefix.
|
|
283
|
+
const slash = ref.indexOf('/')
|
|
284
|
+
const name = slash >= 0 ? ref.slice(slash + 1) : ref
|
|
285
|
+
return name.length > 0 ? name : null
|
|
248
286
|
}
|
|
249
287
|
|
|
250
288
|
// Recognized CI env vars in match-priority order. The first whose value
|
|
@@ -324,28 +362,38 @@ export function normalizeRemoteUrl(raw: string): string {
|
|
|
324
362
|
* checkout keeps a stable identity across runs; `.vx/` is already
|
|
325
363
|
* gitignored infrastructure);
|
|
326
364
|
* 3. unwritable `.vx/` → the root path itself (stable per machine).
|
|
327
|
-
*
|
|
328
|
-
*
|
|
365
|
+
* The URL comes from the run's own `git var -l` when it has one, else one
|
|
366
|
+
* `git` spawn behind try/catch; call sites gate on telemetry being active
|
|
367
|
+
* so a plain run never pays it. Never throws.
|
|
329
368
|
*/
|
|
330
|
-
export function captureWorkspaceIdentity(
|
|
369
|
+
export function captureWorkspaceIdentity(
|
|
370
|
+
workspaceRoot: string,
|
|
371
|
+
/**
|
|
372
|
+
* `remote.origin.url` as the run's enumeration read it (`git var -l`, the
|
|
373
|
+
* same merged config): null when it has none. Undefined asks git.
|
|
374
|
+
*/
|
|
375
|
+
originUrl?: string | null,
|
|
376
|
+
): WorkspaceIdentity {
|
|
331
377
|
const base = workspaceRoot.replace(/\/+$/, '').split('/').pop() || 'workspace'
|
|
332
378
|
try {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
379
|
+
let url = originUrl ?? ''
|
|
380
|
+
if (originUrl === undefined) {
|
|
381
|
+
const proc = Bun.spawnSync({
|
|
382
|
+
// `config --get`, NOT `remote get-url`: get-url applies insteadOf
|
|
383
|
+
// rewrites, so two developers mirroring the same repo through
|
|
384
|
+
// different proxies would derive different workspace ids. The raw
|
|
385
|
+
// configured URL is the identity.
|
|
386
|
+
cmd: [executablePath('git'), '-C', workspaceRoot, 'config', '--get', 'remote.origin.url'],
|
|
387
|
+
stdout: 'pipe',
|
|
388
|
+
stderr: 'ignore',
|
|
389
|
+
})
|
|
390
|
+
if (proc.exitCode === 0) url = new TextDecoder().decode(proc.stdout)
|
|
391
|
+
}
|
|
392
|
+
url = url.trim()
|
|
393
|
+
if (url.length > 0) {
|
|
394
|
+
const normalized = normalizeRemoteUrl(url)
|
|
395
|
+
const name = normalized.split('/').pop() || base
|
|
396
|
+
return { id: xxh3hex(normalized), name }
|
|
349
397
|
}
|
|
350
398
|
} catch {
|
|
351
399
|
// git unavailable — fall through to the salt.
|
package/src/orchestrator/run.ts
CHANGED
|
@@ -720,9 +720,13 @@ async function runOnBus(
|
|
|
720
720
|
// but declines still pays — its answer is only knowable by asking.
|
|
721
721
|
const hasTelemetryPlugin = prepared.plugins.some((p) => p.telemetry !== undefined)
|
|
722
722
|
if (hasTelemetryPlugin || options.telemetrySinks !== undefined) {
|
|
723
|
-
// Workspace identity (telemetry v2)
|
|
724
|
-
//
|
|
725
|
-
|
|
723
|
+
// Workspace identity (telemetry v2), paid only when a telemetry
|
|
724
|
+
// consumer can exist — a plain run never reaches here. The remote URL
|
|
725
|
+
// is the enumeration's `git var -l`: a spawn here cost ~4 ms.
|
|
726
|
+
const wsIdentity = captureWorkspaceIdentity(
|
|
727
|
+
workspaceRoot,
|
|
728
|
+
gitFilesCache.configValue('remote.origin.url'),
|
|
729
|
+
)
|
|
726
730
|
runContextRecord = {
|
|
727
731
|
runId,
|
|
728
732
|
vxVersion: VERSION,
|
|
@@ -192,6 +192,17 @@ export interface TaskTelemetry {
|
|
|
192
192
|
notReady?: 'timeout' | 'exited' | 'spawn'
|
|
193
193
|
/** On a task that retried: each attempt that failed before the last, in order. Additive. */
|
|
194
194
|
failedAttempts?: readonly FailedAttempt[]
|
|
195
|
+
/** On a task proved flaky this run: its key's passes and failures on record. Additive. */
|
|
196
|
+
flaky?: { passes: number; failures: number }
|
|
197
|
+
/** On a sandboxed task: the violation lines themselves, one per denial. Additive. */
|
|
198
|
+
sandboxViolationLines?: readonly string[]
|
|
199
|
+
/** On a cache hit: how long the run that stored the entry took — the time this hit saved. Additive. */
|
|
200
|
+
storedDurationMs?: number
|
|
201
|
+
/** On a cache hit: the CPU and peak memory of the run that stored it. Additive. */
|
|
202
|
+
storedCpuMs?: number
|
|
203
|
+
storedPeakRssBytes?: number
|
|
204
|
+
/** How long an `admit` policy held the task once it was ready. Additive. */
|
|
205
|
+
admissionHeldMs?: number
|
|
195
206
|
/**
|
|
196
207
|
* On a cache hit: whether outputs were written this run (`true`) or the
|
|
197
208
|
* disk already matched the entry and nothing was restored (`false`, an
|
|
@@ -718,6 +729,14 @@ export function taskTelemetryOf(o: TaskOutcome): TaskTelemetry {
|
|
|
718
729
|
if (o.sandboxViolations !== undefined) t.sandboxViolations = o.sandboxViolations
|
|
719
730
|
if (o.notReady !== undefined) t.notReady = o.notReady
|
|
720
731
|
if (o.failedAttempts !== undefined) t.failedAttempts = o.failedAttempts
|
|
732
|
+
if (o.flaky !== undefined) t.flaky = o.flaky
|
|
733
|
+
if (o.sandboxViolationLines !== undefined && o.sandboxViolationLines.length > 0) {
|
|
734
|
+
t.sandboxViolationLines = o.sandboxViolationLines
|
|
735
|
+
}
|
|
736
|
+
if (o.storedDurationMs !== undefined) t.storedDurationMs = o.storedDurationMs
|
|
737
|
+
if (o.storedCpuMs !== undefined) t.storedCpuMs = o.storedCpuMs
|
|
738
|
+
if (o.storedPeakRssBytes !== undefined) t.storedPeakRssBytes = o.storedPeakRssBytes
|
|
739
|
+
if (o.admissionHeldMs !== undefined) t.admissionHeldMs = o.admissionHeldMs
|
|
721
740
|
if (isCacheHit(o.status)) t.restored = o.restored === true
|
|
722
741
|
if (o.wallclockStartNs !== undefined) t.wallclockStartNs = o.wallclockStartNs.toString()
|
|
723
742
|
if (o.wallclockEndNs !== undefined) t.wallclockEndNs = o.wallclockEndNs.toString()
|
|
@@ -165,6 +165,10 @@ const IMPURE_LITERAL_RE = /constructor|__proto__|prototype/
|
|
|
165
165
|
const IMPORT_RE =
|
|
166
166
|
/\b(?:import|export)\b(?:(?!\b(?:import|export)\b)[^;])*?\bfrom\s*\0(\d+)\0|\bimport\s*\0(\d+)\0/g
|
|
167
167
|
|
|
168
|
+
// `import` as a keyword. Followed by `:` it is a property key (a task named
|
|
169
|
+
// `import`): no import form, static or dynamic, puts a colon after the word.
|
|
170
|
+
const IMPORT_WORD_RE = /\bimport\b(?!\s*:)/g
|
|
171
|
+
|
|
168
172
|
/**
|
|
169
173
|
* Every static import in `source` as its specifier and statement, or `null`
|
|
170
174
|
* when the source cannot be lexed or holds an `import` no form above
|
|
@@ -181,9 +185,9 @@ function staticImports(
|
|
|
181
185
|
let seen = 0
|
|
182
186
|
for (const m of code.matchAll(IMPORT_RE)) {
|
|
183
187
|
imports.push({ spec: strings[Number(m[1] ?? m[2])]!, statement: m[0] })
|
|
184
|
-
seen += m[0].match(
|
|
188
|
+
seen += m[0].match(IMPORT_WORD_RE)?.length ?? 0
|
|
185
189
|
}
|
|
186
|
-
if (seen !== (code.match(
|
|
190
|
+
if (seen !== (code.match(IMPORT_WORD_RE)?.length ?? 0)) return null
|
|
187
191
|
return { code, strings, imports }
|
|
188
192
|
}
|
|
189
193
|
|