@0xsequence/catapult 1.3.16 → 1.4.0
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 +250 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +12 -0
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/provenance.d.ts +3 -0
- package/dist/commands/provenance.d.ts.map +1 -0
- package/dist/commands/provenance.js +138 -0
- package/dist/commands/provenance.js.map +1 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +7 -4
- package/dist/commands/run.js.map +1 -1
- package/dist/lib/__tests__/deployer.spec.js +118 -1
- package/dist/lib/__tests__/deployer.spec.js.map +1 -1
- package/dist/lib/__tests__/network-utils.spec.js +53 -8
- package/dist/lib/__tests__/network-utils.spec.js.map +1 -1
- package/dist/lib/__tests__/provenance.spec.d.ts +2 -0
- package/dist/lib/__tests__/provenance.spec.d.ts.map +1 -0
- package/dist/lib/__tests__/provenance.spec.js +205 -0
- package/dist/lib/__tests__/provenance.spec.js.map +1 -0
- package/dist/lib/contracts/__tests__/repository.spec.js +243 -0
- package/dist/lib/contracts/__tests__/repository.spec.js.map +1 -1
- package/dist/lib/contracts/repository.d.ts +9 -1
- package/dist/lib/contracts/repository.d.ts.map +1 -1
- package/dist/lib/contracts/repository.js +93 -7
- package/dist/lib/contracts/repository.js.map +1 -1
- package/dist/lib/core/__tests__/assert-action.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/assert-action.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/assert-action.spec.js +377 -0
- package/dist/lib/core/__tests__/assert-action.spec.js.map +1 -0
- package/dist/lib/core/__tests__/engine.spec.js +80 -0
- package/dist/lib/core/__tests__/engine.spec.js.map +1 -1
- package/dist/lib/core/__tests__/loader.spec.js +29 -0
- package/dist/lib/core/__tests__/loader.spec.js.map +1 -1
- package/dist/lib/core/__tests__/resolver.spec.js +383 -0
- package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
- package/dist/lib/core/engine.d.ts.map +1 -1
- package/dist/lib/core/engine.js +33 -0
- package/dist/lib/core/engine.js.map +1 -1
- package/dist/lib/core/loader.d.ts +1 -0
- package/dist/lib/core/loader.d.ts.map +1 -1
- package/dist/lib/core/loader.js +6 -1
- package/dist/lib/core/loader.js.map +1 -1
- package/dist/lib/core/resolver.d.ts +2 -0
- package/dist/lib/core/resolver.d.ts.map +1 -1
- package/dist/lib/core/resolver.js +89 -0
- package/dist/lib/core/resolver.js.map +1 -1
- package/dist/lib/deployer.d.ts.map +1 -1
- package/dist/lib/deployer.js +21 -4
- package/dist/lib/deployer.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/parsers/__tests__/job.spec.js +77 -0
- package/dist/lib/parsers/__tests__/job.spec.js.map +1 -1
- package/dist/lib/parsers/__tests__/source.spec.d.ts +2 -0
- package/dist/lib/parsers/__tests__/source.spec.d.ts.map +1 -0
- package/dist/lib/parsers/__tests__/source.spec.js +121 -0
- package/dist/lib/parsers/__tests__/source.spec.js.map +1 -0
- package/dist/lib/parsers/index.d.ts +1 -0
- package/dist/lib/parsers/index.d.ts.map +1 -1
- package/dist/lib/parsers/index.js +1 -0
- package/dist/lib/parsers/index.js.map +1 -1
- package/dist/lib/parsers/job.d.ts.map +1 -1
- package/dist/lib/parsers/job.js +11 -0
- package/dist/lib/parsers/job.js.map +1 -1
- package/dist/lib/parsers/source.d.ts +4 -0
- package/dist/lib/parsers/source.d.ts.map +1 -0
- package/dist/lib/parsers/source.js +107 -0
- package/dist/lib/parsers/source.js.map +1 -0
- package/dist/lib/provenance.d.ts +34 -0
- package/dist/lib/provenance.d.ts.map +1 -0
- package/dist/lib/provenance.js +645 -0
- package/dist/lib/provenance.js.map +1 -0
- package/dist/lib/types/actions.d.ts +18 -2
- package/dist/lib/types/actions.d.ts.map +1 -1
- package/dist/lib/types/actions.js +1 -0
- package/dist/lib/types/actions.js.map +1 -1
- package/dist/lib/types/contracts.d.ts +3 -0
- package/dist/lib/types/contracts.d.ts.map +1 -1
- package/dist/lib/types/definitions.d.ts +1 -0
- package/dist/lib/types/definitions.d.ts.map +1 -1
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/index.d.ts.map +1 -1
- package/dist/lib/types/index.js +1 -0
- package/dist/lib/types/index.js.map +1 -1
- package/dist/lib/types/source.d.ts +24 -0
- package/dist/lib/types/source.d.ts.map +1 -0
- package/dist/lib/types/source.js +3 -0
- package/dist/lib/types/source.js.map +1 -0
- package/dist/lib/types/values.d.ts +33 -1
- package/dist/lib/types/values.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +3 -2
- package/src/commands/index.ts +2 -1
- package/src/commands/list.ts +14 -1
- package/src/commands/provenance.ts +120 -0
- package/src/commands/run.ts +11 -6
- package/src/lib/__tests__/deployer.spec.ts +177 -1
- package/src/lib/__tests__/network-utils.spec.ts +63 -14
- package/src/lib/__tests__/provenance.spec.ts +208 -0
- package/src/lib/contracts/__tests__/repository.spec.ts +270 -2
- package/src/lib/contracts/repository.ts +112 -14
- package/src/lib/core/__tests__/assert-action.spec.ts +474 -0
- package/src/lib/core/__tests__/engine.spec.ts +116 -0
- package/src/lib/core/__tests__/loader.spec.ts +34 -1
- package/src/lib/core/__tests__/resolver.spec.ts +444 -1
- package/src/lib/core/engine.ts +52 -0
- package/src/lib/core/loader.ts +8 -2
- package/src/lib/core/resolver.ts +116 -0
- package/src/lib/deployer.ts +28 -4
- package/src/lib/index.ts +4 -1
- package/src/lib/parsers/__tests__/job.spec.ts +81 -0
- package/src/lib/parsers/__tests__/source.spec.ts +134 -0
- package/src/lib/parsers/index.ts +1 -0
- package/src/lib/parsers/job.ts +14 -2
- package/src/lib/parsers/source.ts +129 -0
- package/src/lib/provenance.ts +785 -0
- package/src/lib/types/actions.ts +22 -1
- package/src/lib/types/contracts.ts +4 -1
- package/src/lib/types/definitions.ts +7 -0
- package/src/lib/types/index.ts +1 -0
- package/src/lib/types/source.ts +26 -0
- package/src/lib/types/values.ts +71 -0
package/src/lib/deployer.ts
CHANGED
|
@@ -261,13 +261,37 @@ export class Deployer {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
// Check job-level skip conditions before execution
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
// skip_if is a pure gate (no post-check), skip_condition has both pre-skip and post-check
|
|
265
|
+
const skipIfConditions = job.skip_if
|
|
266
|
+
const skipConditions = job.skip_condition
|
|
267
|
+
|
|
268
|
+
if (skipIfConditions || skipConditions) {
|
|
269
|
+
// Evaluate skip_if first (pure gate)
|
|
270
|
+
let shouldSkip = false
|
|
271
|
+
let skipReason: string = 'skip_condition'
|
|
272
|
+
|
|
273
|
+
if (skipIfConditions) {
|
|
274
|
+
const skipIfResult = await engine.evaluateSkipConditions(skipIfConditions, context, new Map())
|
|
275
|
+
if (skipIfResult) {
|
|
276
|
+
shouldSkip = true
|
|
277
|
+
skipReason = 'skip_if'
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// If not skipped by skip_if, check skip_condition
|
|
282
|
+
if (!shouldSkip && skipConditions) {
|
|
283
|
+
const skipConditionResult = await engine.evaluateSkipConditions(skipConditions, context, new Map())
|
|
284
|
+
if (skipConditionResult) {
|
|
285
|
+
shouldSkip = true
|
|
286
|
+
skipReason = 'skip_condition'
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
266
290
|
if (shouldSkip) {
|
|
267
291
|
// Store skipped result
|
|
268
292
|
this.results.get(job.name)!.outputs.set(network.chainId, {
|
|
269
293
|
status: 'skipped',
|
|
270
|
-
data: `Job "${job.name}" skipped due to
|
|
294
|
+
data: `Job "${job.name}" skipped due to ${skipReason}`
|
|
271
295
|
})
|
|
272
296
|
|
|
273
297
|
this.events.emitEvent({
|
|
@@ -276,7 +300,7 @@ export class Deployer {
|
|
|
276
300
|
data: {
|
|
277
301
|
jobName: job.name,
|
|
278
302
|
networkName: network.name,
|
|
279
|
-
reason:
|
|
303
|
+
reason: skipReason
|
|
280
304
|
}
|
|
281
305
|
})
|
|
282
306
|
|
package/src/lib/index.ts
CHANGED
|
@@ -10,5 +10,8 @@ export * as parsers from './parsers'
|
|
|
10
10
|
// Export the deployer
|
|
11
11
|
export * from './deployer'
|
|
12
12
|
|
|
13
|
+
// Export provenance helpers
|
|
14
|
+
export * from './provenance'
|
|
15
|
+
|
|
13
16
|
// This will be the main entry point for the lib
|
|
14
|
-
// Additional functionality will be added here as the lib grows
|
|
17
|
+
// Additional functionality will be added here as the lib grows
|
|
@@ -355,4 +355,85 @@ actions:
|
|
|
355
355
|
arguments: { address: '0xabc' }
|
|
356
356
|
})
|
|
357
357
|
})
|
|
358
|
+
|
|
359
|
+
it('should parse job-level skip_if conditions', () => {
|
|
360
|
+
const yamlContent = `
|
|
361
|
+
name: "job-with-skip-if"
|
|
362
|
+
version: "1"
|
|
363
|
+
skip_if:
|
|
364
|
+
- type: "contract-exists"
|
|
365
|
+
arguments:
|
|
366
|
+
address: "0xdef"
|
|
367
|
+
actions:
|
|
368
|
+
- name: "a1"
|
|
369
|
+
template: "t1"
|
|
370
|
+
arguments: {}
|
|
371
|
+
`
|
|
372
|
+
const job = parseJob(yamlContent)
|
|
373
|
+
expect(job.skip_if).toHaveLength(1)
|
|
374
|
+
expect(job.skip_if?.[0]).toEqual({
|
|
375
|
+
type: 'contract-exists',
|
|
376
|
+
arguments: { address: '0xdef' }
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('should parse both skip_condition and skip_if on a job', () => {
|
|
381
|
+
const yamlContent = `
|
|
382
|
+
name: "job-with-both"
|
|
383
|
+
version: "1"
|
|
384
|
+
skip_condition:
|
|
385
|
+
- type: "contract-exists"
|
|
386
|
+
arguments:
|
|
387
|
+
address: "0xabc"
|
|
388
|
+
skip_if:
|
|
389
|
+
- type: "job-completed"
|
|
390
|
+
arguments:
|
|
391
|
+
job: "other-job"
|
|
392
|
+
actions:
|
|
393
|
+
- name: "a1"
|
|
394
|
+
template: "t1"
|
|
395
|
+
arguments: {}
|
|
396
|
+
`
|
|
397
|
+
const job = parseJob(yamlContent)
|
|
398
|
+
expect(job.skip_condition).toHaveLength(1)
|
|
399
|
+
expect(job.skip_condition?.[0]).toEqual({
|
|
400
|
+
type: 'contract-exists',
|
|
401
|
+
arguments: { address: '0xabc' }
|
|
402
|
+
})
|
|
403
|
+
expect(job.skip_if).toHaveLength(1)
|
|
404
|
+
expect(job.skip_if?.[0]).toEqual({
|
|
405
|
+
type: 'job-completed',
|
|
406
|
+
arguments: { job: 'other-job' }
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
it('should throw when skip_if is not an array', () => {
|
|
411
|
+
const yamlContent = `
|
|
412
|
+
name: "job-with-invalid-skip-if"
|
|
413
|
+
version: "1"
|
|
414
|
+
skip_if:
|
|
415
|
+
type: "contract-exists"
|
|
416
|
+
arguments:
|
|
417
|
+
address: "0xabc"
|
|
418
|
+
actions:
|
|
419
|
+
- name: "a1"
|
|
420
|
+
template: "t1"
|
|
421
|
+
arguments: {}
|
|
422
|
+
`
|
|
423
|
+
expect(() => parseJob(yamlContent)).toThrow('"skip_if" must be an array if provided')
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('should throw when skip_if contains invalid condition', () => {
|
|
427
|
+
const yamlContent = `
|
|
428
|
+
name: "job-with-invalid-skip-if"
|
|
429
|
+
version: "1"
|
|
430
|
+
skip_if:
|
|
431
|
+
- type: "invalid-type"
|
|
432
|
+
actions:
|
|
433
|
+
- name: "a1"
|
|
434
|
+
template: "t1"
|
|
435
|
+
arguments: {}
|
|
436
|
+
`
|
|
437
|
+
expect(() => parseJob(yamlContent)).toThrow('"skip_if" contains an invalid condition entry')
|
|
438
|
+
})
|
|
358
439
|
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { mergeSourceProvenance, parseSourceDocument } from '../source'
|
|
2
|
+
|
|
3
|
+
describe('Source Provenance Parsing', () => {
|
|
4
|
+
it('should return null for non-source YAML documents', () => {
|
|
5
|
+
const result = parseSourceDocument(`
|
|
6
|
+
type: constants
|
|
7
|
+
constants:
|
|
8
|
+
value: 1
|
|
9
|
+
`)
|
|
10
|
+
|
|
11
|
+
expect(result).toBeNull()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should parse source provenance documents', () => {
|
|
15
|
+
const result = parseSourceDocument(`
|
|
16
|
+
type: source
|
|
17
|
+
build_info:
|
|
18
|
+
"./stage1.json":
|
|
19
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
20
|
+
ref: "v3.0.0-rc.5"
|
|
21
|
+
commit: "0d9061f229da73edae890e6fdd1fbf753028df6d"
|
|
22
|
+
build: "forge build --build-info"
|
|
23
|
+
contracts:
|
|
24
|
+
"src/Stage1Module.sol:Stage1Module":
|
|
25
|
+
ref: "stage1-special"
|
|
26
|
+
`)
|
|
27
|
+
|
|
28
|
+
expect(result).not.toBeNull()
|
|
29
|
+
expect(result!.build_info['./stage1.json'].repo).toBe('https://github.com/0xsequence/wallet-contracts-v3')
|
|
30
|
+
expect(result!.build_info['./stage1.json'].contracts?.['src/Stage1Module.sol:Stage1Module'].ref).toBe('stage1-special')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should require repo on each build-info provenance entry', () => {
|
|
34
|
+
const result = parseSourceDocument(`
|
|
35
|
+
type: source
|
|
36
|
+
build_info:
|
|
37
|
+
"./stage1.json":
|
|
38
|
+
ref: "v3.0.0-rc.5"
|
|
39
|
+
`)
|
|
40
|
+
|
|
41
|
+
expect(result).not.toBeNull()
|
|
42
|
+
expect(result!.build_info).toEqual({})
|
|
43
|
+
expect(result!.warnings).toEqual([
|
|
44
|
+
expect.stringContaining('build_info["./stage1.json"].repo')
|
|
45
|
+
])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should validate optional string fields', () => {
|
|
49
|
+
const result = parseSourceDocument(`
|
|
50
|
+
type: source
|
|
51
|
+
build_info:
|
|
52
|
+
"./stage1.json":
|
|
53
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
54
|
+
commit: 123
|
|
55
|
+
`)
|
|
56
|
+
|
|
57
|
+
expect(result).not.toBeNull()
|
|
58
|
+
expect(result!.build_info).toEqual({})
|
|
59
|
+
expect(result!.warnings).toEqual([
|
|
60
|
+
expect.stringContaining('commit')
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should reject unsupported fields', () => {
|
|
65
|
+
const result = parseSourceDocument(`
|
|
66
|
+
type: source
|
|
67
|
+
build_info:
|
|
68
|
+
"./stage1.json":
|
|
69
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
70
|
+
path: "contracts"
|
|
71
|
+
`)
|
|
72
|
+
|
|
73
|
+
expect(result).not.toBeNull()
|
|
74
|
+
expect(result!.build_info).toEqual({})
|
|
75
|
+
expect(result!.warnings).toEqual([
|
|
76
|
+
expect.stringContaining('path is not supported')
|
|
77
|
+
])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should quote build-info paths in validation errors', () => {
|
|
81
|
+
const result = parseSourceDocument(`
|
|
82
|
+
type: source
|
|
83
|
+
build_info:
|
|
84
|
+
"./stage1.json":
|
|
85
|
+
ref: "v3.0.0-rc.5"
|
|
86
|
+
`)
|
|
87
|
+
|
|
88
|
+
expect(result).not.toBeNull()
|
|
89
|
+
expect(result!.warnings).toEqual([
|
|
90
|
+
expect.stringContaining('build_info["./stage1.json"].repo')
|
|
91
|
+
])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should keep valid entries when sibling entries are invalid', () => {
|
|
95
|
+
const result = parseSourceDocument(`
|
|
96
|
+
type: source
|
|
97
|
+
build_info:
|
|
98
|
+
"./stage1.json":
|
|
99
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
100
|
+
"./bad.json":
|
|
101
|
+
typo_field: true
|
|
102
|
+
`)
|
|
103
|
+
|
|
104
|
+
expect(result).not.toBeNull()
|
|
105
|
+
expect(Object.keys(result!.build_info)).toEqual(['./stage1.json'])
|
|
106
|
+
expect(result!.warnings).toEqual([
|
|
107
|
+
expect.stringContaining('typo_field is not supported')
|
|
108
|
+
])
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('mergeSourceProvenance', () => {
|
|
112
|
+
it('should merge contract overrides into build-info provenance', () => {
|
|
113
|
+
const result = mergeSourceProvenance({
|
|
114
|
+
repo: 'https://github.com/0xsequence/wallet-contracts-v3',
|
|
115
|
+
ref: 'v3.0.0-rc.5',
|
|
116
|
+
commit: '0d9061f229da73edae890e6fdd1fbf753028df6d',
|
|
117
|
+
contracts: {
|
|
118
|
+
'src/Stage1Module.sol:Stage1Module': {
|
|
119
|
+
ref: 'stage1-special'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, {
|
|
123
|
+
ref: 'stage1-special'
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(result).toEqual({
|
|
127
|
+
repo: 'https://github.com/0xsequence/wallet-contracts-v3',
|
|
128
|
+
ref: 'stage1-special',
|
|
129
|
+
commit: '0d9061f229da73edae890e6fdd1fbf753028df6d'
|
|
130
|
+
})
|
|
131
|
+
expect(result).not.toHaveProperty('contracts')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
})
|
package/src/lib/parsers/index.ts
CHANGED
package/src/lib/parsers/job.ts
CHANGED
|
@@ -110,7 +110,7 @@ export function parseJob(yamlContent: string): Job {
|
|
|
110
110
|
throw new Error(`Invalid job "${rawObject.name}": "min_evm_version" must be a string if provided.`)
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
// --- Optional: validate job-level skip_condition and
|
|
113
|
+
// --- Optional: validate job-level skip_condition and skip_if ---
|
|
114
114
|
if (rawObject.skip_condition !== undefined) {
|
|
115
115
|
if (!Array.isArray(rawObject.skip_condition)) {
|
|
116
116
|
throw new Error(`Invalid job "${rawObject.name}": "skip_condition" must be an array if provided.`)
|
|
@@ -122,6 +122,17 @@ export function parseJob(yamlContent: string): Job {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
if (rawObject.skip_if !== undefined) {
|
|
126
|
+
if (!Array.isArray(rawObject.skip_if)) {
|
|
127
|
+
throw new Error(`Invalid job "${rawObject.name}": "skip_if" must be an array if provided.`)
|
|
128
|
+
}
|
|
129
|
+
for (const condition of rawObject.skip_if) {
|
|
130
|
+
if (!isCondition(condition)) {
|
|
131
|
+
throw new Error(`Invalid job "${rawObject.name}": "skip_if" contains an invalid condition entry.`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
125
136
|
if (rawObject.constants !== undefined) {
|
|
126
137
|
if (typeof rawObject.constants !== 'object' || rawObject.constants === null || Array.isArray(rawObject.constants)) {
|
|
127
138
|
throw new Error(`Invalid job "${rawObject.name}": "constants" field must be an object if provided.`)
|
|
@@ -141,8 +152,9 @@ export function parseJob(yamlContent: string): Job {
|
|
|
141
152
|
min_evm_version: rawObject.min_evm_version,
|
|
142
153
|
deprecated: rawObject.deprecated === true,
|
|
143
154
|
skip_condition: rawObject.skip_condition as Condition[] | undefined,
|
|
155
|
+
skip_if: rawObject.skip_if as Condition[] | undefined,
|
|
144
156
|
constants: rawObject.constants,
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
return job
|
|
148
|
-
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { parse as parseYaml, YAMLParseError } from 'yaml'
|
|
2
|
+
import { BuildInfoSourceProvenance, SourceDocument, SourceProvenance, SourceProvenanceOverride } from '../types'
|
|
3
|
+
|
|
4
|
+
const STRING_FIELDS = ['repo', 'ref', 'commit', 'build'] as const
|
|
5
|
+
const BUILD_INFO_FIELDS = new Set<string>([...STRING_FIELDS, 'contracts'])
|
|
6
|
+
const CONTRACT_OVERRIDE_FIELDS = new Set<string>(STRING_FIELDS)
|
|
7
|
+
|
|
8
|
+
function isPlainObject(value: any): value is Record<string, any> {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildInfoLabel(buildInfoPath: string): string {
|
|
13
|
+
return `build_info[${JSON.stringify(buildInfoPath)}]`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function contractOverrideLabel(buildInfoPath: string, contractName: string): string {
|
|
17
|
+
return `${buildInfoLabel(buildInfoPath)}.contracts[${JSON.stringify(contractName)}]`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function validateKnownFields(value: Record<string, any>, label: string, allowedFields: Set<string>): void {
|
|
21
|
+
for (const field of Object.keys(value)) {
|
|
22
|
+
if (!allowedFields.has(field)) {
|
|
23
|
+
throw new Error(`Invalid source: ${label}.${field} is not supported.`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateStringFields(value: Record<string, any>, label: string, requiredRepo: boolean): void {
|
|
29
|
+
if (requiredRepo && (typeof value.repo !== 'string' || value.repo.length === 0)) {
|
|
30
|
+
throw new Error(`Invalid source: ${label}.repo field is required and must be a non-empty string.`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const field of STRING_FIELDS) {
|
|
34
|
+
if (value[field] !== undefined && typeof value[field] !== 'string') {
|
|
35
|
+
throw new Error(`Invalid source: ${label}.${field} must be a string if provided.`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateBuildInfoProvenance(buildInfoPath: string, provenance: unknown): BuildInfoSourceProvenance {
|
|
41
|
+
if (!isPlainObject(provenance)) {
|
|
42
|
+
throw new Error(`Invalid source: ${buildInfoLabel(buildInfoPath)} must be an object.`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const label = buildInfoLabel(buildInfoPath)
|
|
46
|
+
validateKnownFields(provenance, label, BUILD_INFO_FIELDS)
|
|
47
|
+
validateStringFields(provenance, label, true)
|
|
48
|
+
|
|
49
|
+
if (provenance.contracts !== undefined) {
|
|
50
|
+
if (!isPlainObject(provenance.contracts)) {
|
|
51
|
+
throw new Error(`Invalid source: ${label}.contracts must be an object if provided.`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [contractName, override] of Object.entries(provenance.contracts)) {
|
|
55
|
+
if (!contractName || typeof contractName !== 'string') {
|
|
56
|
+
throw new Error(`Invalid source: ${label}.contracts keys must be non-empty strings.`)
|
|
57
|
+
}
|
|
58
|
+
if (!isPlainObject(override)) {
|
|
59
|
+
throw new Error(`Invalid source: ${contractOverrideLabel(buildInfoPath, contractName)} must be an object.`)
|
|
60
|
+
}
|
|
61
|
+
const overrideLabel = contractOverrideLabel(buildInfoPath, contractName)
|
|
62
|
+
validateKnownFields(override, overrideLabel, CONTRACT_OVERRIDE_FIELDS)
|
|
63
|
+
validateStringFields(override, overrideLabel, false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return provenance as BuildInfoSourceProvenance
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parses a YAML source provenance document.
|
|
72
|
+
* Returns null for YAML documents that are not marked with `type: "source"`.
|
|
73
|
+
*/
|
|
74
|
+
export function parseSourceDocument(yamlContent: string): SourceDocument | null {
|
|
75
|
+
let rawObject: any
|
|
76
|
+
try {
|
|
77
|
+
rawObject = parseYaml(yamlContent)
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (e instanceof YAMLParseError) {
|
|
80
|
+
const line = (e as any).linePos?.[0]?.line ? ` at line ${(e as any).linePos[0].line}` : ''
|
|
81
|
+
throw new Error(`Failed to parse source YAML: ${e.message}${line}.`)
|
|
82
|
+
}
|
|
83
|
+
throw e
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!rawObject || typeof rawObject !== 'object') {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (rawObject.type !== 'source') {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!isPlainObject(rawObject.build_info)) {
|
|
95
|
+
throw new Error('Invalid source: "build_info" field is required and must be an object.')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const buildInfo: Record<string, BuildInfoSourceProvenance> = {}
|
|
99
|
+
const warnings: string[] = []
|
|
100
|
+
|
|
101
|
+
for (const [buildInfoPath, provenance] of Object.entries(rawObject.build_info)) {
|
|
102
|
+
if (!buildInfoPath || typeof buildInfoPath !== 'string') {
|
|
103
|
+
throw new Error('Invalid source: "build_info" keys must be non-empty strings.')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
buildInfo[buildInfoPath] = validateBuildInfoProvenance(buildInfoPath, provenance)
|
|
108
|
+
} catch (error) {
|
|
109
|
+
warnings.push(error instanceof Error ? error.message : String(error))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
type: 'source',
|
|
115
|
+
build_info: buildInfo,
|
|
116
|
+
warnings
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mergeSourceProvenance(
|
|
121
|
+
base: BuildInfoSourceProvenance,
|
|
122
|
+
override?: SourceProvenanceOverride
|
|
123
|
+
): SourceProvenance {
|
|
124
|
+
const { contracts, ...baseFields } = base
|
|
125
|
+
return {
|
|
126
|
+
...baseFields,
|
|
127
|
+
...(override || {})
|
|
128
|
+
}
|
|
129
|
+
}
|