@0xsequence/catapult 1.3.17 → 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 +249 -0
- 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/lib/__tests__/deployer.spec.js +118 -1
- package/dist/lib/__tests__/deployer.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/lib/__tests__/deployer.spec.ts +177 -1
- 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
|
@@ -1679,7 +1679,7 @@ describe('Deployer', () => {
|
|
|
1679
1679
|
const jobAResult = results.get('job-a')
|
|
1680
1680
|
expect(jobAResult).toBeDefined()
|
|
1681
1681
|
expect(jobAResult.outputs.get(mockNetwork1.chainId).status).toBe('skipped')
|
|
1682
|
-
expect(jobAResult.outputs.get(mockNetwork1.chainId).data).toContain('
|
|
1682
|
+
expect(jobAResult.outputs.get(mockNetwork1.chainId).data).toContain('skip_condition')
|
|
1683
1683
|
|
|
1684
1684
|
// Verify that job B ran successfully
|
|
1685
1685
|
const jobBResult = results.get('job-b')
|
|
@@ -2090,4 +2090,180 @@ describe('Deployer', () => {
|
|
|
2090
2090
|
)
|
|
2091
2091
|
})
|
|
2092
2092
|
})
|
|
2093
|
+
|
|
2094
|
+
describe('skip_if functionality', () => {
|
|
2095
|
+
it('should skip job when skip_if condition is true', async () => {
|
|
2096
|
+
const jobWithSkipIf: Job = {
|
|
2097
|
+
name: 'job-with-skip-if',
|
|
2098
|
+
version: '1.0.0',
|
|
2099
|
+
description: 'Job with skip_if condition',
|
|
2100
|
+
skip_if: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
|
|
2101
|
+
actions: [
|
|
2102
|
+
{ name: 'action1', template: 'template1', arguments: {} }
|
|
2103
|
+
]
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
mockLoader.jobs.clear()
|
|
2107
|
+
mockLoader.jobs.set('job-with-skip-if', jobWithSkipIf)
|
|
2108
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job-with-skip-if'])
|
|
2109
|
+
|
|
2110
|
+
// Mock engine.evaluateSkipConditions to return true (condition met)
|
|
2111
|
+
mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(true)
|
|
2112
|
+
|
|
2113
|
+
const deployer = new Deployer(deployerOptions)
|
|
2114
|
+
await deployer.run()
|
|
2115
|
+
|
|
2116
|
+
// Job should be skipped
|
|
2117
|
+
const results = (deployer as any).results
|
|
2118
|
+
const jobResult = results.get('job-with-skip-if')
|
|
2119
|
+
expect(jobResult).toBeDefined()
|
|
2120
|
+
expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('skipped')
|
|
2121
|
+
expect(jobResult.outputs.get(mockNetwork1.chainId).data).toContain('skip_if')
|
|
2122
|
+
|
|
2123
|
+
// executeJob should NOT be called for skipped job
|
|
2124
|
+
expect(mockEngine.executeJob).not.toHaveBeenCalled()
|
|
2125
|
+
})
|
|
2126
|
+
|
|
2127
|
+
it('should run job when skip_if condition is false', async () => {
|
|
2128
|
+
const jobWithSkipIf: Job = {
|
|
2129
|
+
name: 'job-with-skip-if-false',
|
|
2130
|
+
version: '1.0.0',
|
|
2131
|
+
description: 'Job with skip_if condition that is false',
|
|
2132
|
+
skip_if: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
|
|
2133
|
+
actions: [
|
|
2134
|
+
{ name: 'action1', template: 'template1', arguments: {} }
|
|
2135
|
+
]
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
mockLoader.jobs.clear()
|
|
2139
|
+
mockLoader.jobs.set('job-with-skip-if-false', jobWithSkipIf)
|
|
2140
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job-with-skip-if-false'])
|
|
2141
|
+
|
|
2142
|
+
// Mock engine.evaluateSkipConditions to return false (condition not met)
|
|
2143
|
+
mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(false)
|
|
2144
|
+
|
|
2145
|
+
const deployer = new Deployer(deployerOptions)
|
|
2146
|
+
await deployer.run()
|
|
2147
|
+
|
|
2148
|
+
// Job should run normally
|
|
2149
|
+
const results = (deployer as any).results
|
|
2150
|
+
const jobResult = results.get('job-with-skip-if-false')
|
|
2151
|
+
expect(jobResult).toBeDefined()
|
|
2152
|
+
expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
|
|
2153
|
+
|
|
2154
|
+
// executeJob should be called
|
|
2155
|
+
expect(mockEngine.executeJob).toHaveBeenCalledWith(jobWithSkipIf, expect.anything())
|
|
2156
|
+
})
|
|
2157
|
+
|
|
2158
|
+
it('should skip job when skip_if OR skip_condition is true', async () => {
|
|
2159
|
+
const jobWithBoth: Job = {
|
|
2160
|
+
name: 'job-with-both',
|
|
2161
|
+
version: '1.0.0',
|
|
2162
|
+
description: 'Job with both skip_if and skip_condition',
|
|
2163
|
+
skip_condition: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
|
|
2164
|
+
skip_if: [{ type: 'job-completed', arguments: { job: 'other-job' } }],
|
|
2165
|
+
actions: [
|
|
2166
|
+
{ name: 'action1', template: 'template1', arguments: {} }
|
|
2167
|
+
]
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
mockLoader.jobs.clear()
|
|
2171
|
+
mockLoader.jobs.set('job-with-both', jobWithBoth)
|
|
2172
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job-with-both'])
|
|
2173
|
+
|
|
2174
|
+
// Mock engine.evaluateSkipConditions to return false for skip_condition but true for skip_if
|
|
2175
|
+
// The combined conditions should be evaluated together
|
|
2176
|
+
mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(true)
|
|
2177
|
+
|
|
2178
|
+
const deployer = new Deployer(deployerOptions)
|
|
2179
|
+
await deployer.run()
|
|
2180
|
+
|
|
2181
|
+
// Job should be skipped (skip_if is true)
|
|
2182
|
+
const results = (deployer as any).results
|
|
2183
|
+
const jobResult = results.get('job-with-both')
|
|
2184
|
+
expect(jobResult).toBeDefined()
|
|
2185
|
+
expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('skipped')
|
|
2186
|
+
|
|
2187
|
+
// executeJob should NOT be called
|
|
2188
|
+
expect(mockEngine.executeJob).not.toHaveBeenCalled()
|
|
2189
|
+
})
|
|
2190
|
+
|
|
2191
|
+
it('should NOT post-check skip_if after execution (only skip_condition is post-checked)', async () => {
|
|
2192
|
+
// This is the key test: skip_if should NOT cause post-execution failures
|
|
2193
|
+
const jobWithSkipIfOnly: Job = {
|
|
2194
|
+
name: 'job-skip-if-only',
|
|
2195
|
+
version: '1.0.0',
|
|
2196
|
+
description: 'Job with skip_if that remains false after execution',
|
|
2197
|
+
only_networks: [1], // Only run on mainnet to have predictable call count
|
|
2198
|
+
skip_if: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
|
|
2199
|
+
actions: [
|
|
2200
|
+
{ name: 'action1', template: 'template1', arguments: {} }
|
|
2201
|
+
]
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
mockLoader.jobs.clear()
|
|
2205
|
+
mockLoader.jobs.set('job-skip-if-only', jobWithSkipIfOnly)
|
|
2206
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job-skip-if-only'])
|
|
2207
|
+
|
|
2208
|
+
// Pre-skip: skip_if is false, so job runs
|
|
2209
|
+
mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(false)
|
|
2210
|
+
|
|
2211
|
+
// Job runs successfully - no post-check for skip_if
|
|
2212
|
+
mockEngine.executeJob = jest.fn().mockResolvedValue(undefined)
|
|
2213
|
+
|
|
2214
|
+
const deployer = new Deployer(deployerOptions)
|
|
2215
|
+
await deployer.run()
|
|
2216
|
+
|
|
2217
|
+
// Job should succeed (no post-check failure for skip_if)
|
|
2218
|
+
const results = (deployer as any).results
|
|
2219
|
+
const jobResult = results.get('job-skip-if-only')
|
|
2220
|
+
expect(jobResult).toBeDefined()
|
|
2221
|
+
expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
|
|
2222
|
+
|
|
2223
|
+
// evaluateSkipConditions called once for pre-skip check (skip_if on mainnet only)
|
|
2224
|
+
// skip_if is NOT post-checked, so no additional calls from engine
|
|
2225
|
+
expect(mockEngine.evaluateSkipConditions).toHaveBeenCalledTimes(1)
|
|
2226
|
+
// executeJob should be called once (job ran on mainnet only)
|
|
2227
|
+
expect(mockEngine.executeJob).toHaveBeenCalledTimes(1)
|
|
2228
|
+
})
|
|
2229
|
+
|
|
2230
|
+
it('should still post-check skip_condition (existing behavior unchanged)', async () => {
|
|
2231
|
+
// Verify that skip_condition still gets post-checked
|
|
2232
|
+
// For this test, we need to use the real ExecutionEngine behavior
|
|
2233
|
+
// Since the mock doesn't easily support this, we'll verify the behavior indirectly
|
|
2234
|
+
|
|
2235
|
+
// The key is that skip_condition triggers post-check in the engine,
|
|
2236
|
+
// while skip_if does not. This is verified by the engine.spec.ts tests.
|
|
2237
|
+
|
|
2238
|
+
const jobWithSkipCondition: Job = {
|
|
2239
|
+
name: 'job-skip-condition',
|
|
2240
|
+
version: '1.0.0',
|
|
2241
|
+
description: 'Job with skip_condition',
|
|
2242
|
+
skip_condition: [{ type: 'contract-exists', arguments: { address: '0xabc' } }],
|
|
2243
|
+
actions: [
|
|
2244
|
+
{ name: 'action1', template: 'template1', arguments: {} }
|
|
2245
|
+
]
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
mockLoader.jobs.clear()
|
|
2249
|
+
mockLoader.jobs.set('job-skip-condition', jobWithSkipCondition)
|
|
2250
|
+
mockGraph.getExecutionOrder.mockReturnValue(['job-skip-condition'])
|
|
2251
|
+
|
|
2252
|
+
// Pre-skip: skip_condition is false, so job runs
|
|
2253
|
+
mockEngine.evaluateSkipConditions = jest.fn().mockResolvedValue(false)
|
|
2254
|
+
|
|
2255
|
+
// Job runs successfully
|
|
2256
|
+
mockEngine.executeJob = jest.fn().mockResolvedValue(undefined)
|
|
2257
|
+
|
|
2258
|
+
const deployer = new Deployer(deployerOptions)
|
|
2259
|
+
await deployer.run()
|
|
2260
|
+
|
|
2261
|
+
// Job should succeed in this mock scenario
|
|
2262
|
+
// The actual post-check behavior is tested in engine.spec.ts
|
|
2263
|
+
const results = (deployer as any).results
|
|
2264
|
+
const jobResult = results.get('job-skip-condition')
|
|
2265
|
+
expect(jobResult).toBeDefined()
|
|
2266
|
+
expect(jobResult.outputs.get(mockNetwork1.chainId).status).toBe('success')
|
|
2267
|
+
})
|
|
2268
|
+
})
|
|
2093
2269
|
})
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import * as fs from 'fs/promises'
|
|
2
|
+
import * as os from 'os'
|
|
3
|
+
import * as path from 'path'
|
|
4
|
+
import { execFile } from 'child_process'
|
|
5
|
+
import { promisify } from 'util'
|
|
6
|
+
import {
|
|
7
|
+
collectSourceProvenanceEntries,
|
|
8
|
+
generateBuildInfoFromSourceProvenance,
|
|
9
|
+
verifySourceProvenance
|
|
10
|
+
} from '../provenance'
|
|
11
|
+
|
|
12
|
+
const execFileAsync = promisify(execFile)
|
|
13
|
+
|
|
14
|
+
async function git(cwd: string, args: string[]): Promise<string> {
|
|
15
|
+
const result = await execFileAsync('git', args, { cwd })
|
|
16
|
+
return String(result.stdout).trim()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function writeFile(filePath: string, content: string): Promise<void> {
|
|
20
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
21
|
+
await fs.writeFile(filePath, content)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sourceYaml(repo: string, commit: string, build: string): string {
|
|
25
|
+
return `
|
|
26
|
+
type: source
|
|
27
|
+
build_info:
|
|
28
|
+
"./stage1.json":
|
|
29
|
+
repo: ${JSON.stringify(repo)}
|
|
30
|
+
commit: ${JSON.stringify(commit)}
|
|
31
|
+
build: ${JSON.stringify(build)}
|
|
32
|
+
`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function jobYaml(name: string, dependsOn: string[] = []): string {
|
|
36
|
+
const depends = dependsOn.length > 0 ? `depends_on: ${JSON.stringify(dependsOn)}\n` : ''
|
|
37
|
+
return `
|
|
38
|
+
name: ${JSON.stringify(name)}
|
|
39
|
+
version: "1.0.0"
|
|
40
|
+
${depends}actions:
|
|
41
|
+
- name: "noop"
|
|
42
|
+
type: "static"
|
|
43
|
+
arguments:
|
|
44
|
+
value: true
|
|
45
|
+
`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('source provenance operations', () => {
|
|
49
|
+
let tempDir: string
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'catapult-provenance-test-'))
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
afterEach(async () => {
|
|
56
|
+
await fs.rm(tempDir, { recursive: true, force: true })
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('collects source provenance entries even when build-info is missing', async () => {
|
|
60
|
+
const projectRoot = path.join(tempDir, 'project')
|
|
61
|
+
const sourcePath = path.join(projectRoot, 'jobs', 'demo', 'build-info', 'source.yaml')
|
|
62
|
+
await writeFile(sourcePath, sourceYaml('https://github.com/example/repo', 'abc123', 'forge build --build-info'))
|
|
63
|
+
|
|
64
|
+
const result = await collectSourceProvenanceEntries(projectRoot)
|
|
65
|
+
|
|
66
|
+
expect(result.warnings).toEqual([])
|
|
67
|
+
expect(result.entries).toHaveLength(1)
|
|
68
|
+
expect(result.entries[0]).toMatchObject({
|
|
69
|
+
sourceDocumentPath: sourcePath,
|
|
70
|
+
buildInfoRef: './stage1.json',
|
|
71
|
+
buildInfoPath: path.join(projectRoot, 'jobs', 'demo', 'build-info', 'stage1.json')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('can scope provenance entries to a job and its dependencies', async () => {
|
|
76
|
+
const projectRoot = path.join(tempDir, 'project')
|
|
77
|
+
await writeFile(path.join(projectRoot, 'jobs', 'base.yaml'), jobYaml('base'))
|
|
78
|
+
await writeFile(path.join(projectRoot, 'jobs', 'child.yaml'), jobYaml('child', ['base']))
|
|
79
|
+
await writeFile(path.join(projectRoot, 'jobs', 'base', 'build-info', 'source.yaml'), sourceYaml('https://github.com/example/base', 'abc123', 'build-base'))
|
|
80
|
+
await writeFile(path.join(projectRoot, 'jobs', 'child', 'build-info', 'source.yaml'), sourceYaml('https://github.com/example/child', 'def456', 'build-child'))
|
|
81
|
+
|
|
82
|
+
const childOnly = await collectSourceProvenanceEntries(projectRoot, {
|
|
83
|
+
jobs: ['child'],
|
|
84
|
+
loadStdTemplates: false
|
|
85
|
+
})
|
|
86
|
+
const withDependencies = await collectSourceProvenanceEntries(projectRoot, {
|
|
87
|
+
jobs: ['child'],
|
|
88
|
+
includeDependencies: true,
|
|
89
|
+
loadStdTemplates: false
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(childOnly.entries.map(entry => entry.provenance.repo)).toEqual(['https://github.com/example/child'])
|
|
93
|
+
expect(withDependencies.entries.map(entry => entry.provenance.repo).sort()).toEqual([
|
|
94
|
+
'https://github.com/example/base',
|
|
95
|
+
'https://github.com/example/child'
|
|
96
|
+
])
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('generates missing build-info from a local Git provenance repo', async () => {
|
|
100
|
+
const { projectRoot, expectedBuildInfo } = await createProjectWithLocalProvenanceRepo()
|
|
101
|
+
|
|
102
|
+
const result = await generateBuildInfoFromSourceProvenance(projectRoot)
|
|
103
|
+
const generatedPath = path.join(projectRoot, 'jobs', 'demo', 'build-info', 'stage1.json')
|
|
104
|
+
const generatedJson = JSON.parse(await fs.readFile(generatedPath, 'utf-8'))
|
|
105
|
+
|
|
106
|
+
expect(result.results).toHaveLength(1)
|
|
107
|
+
expect(result.results[0].status).toBe('generated')
|
|
108
|
+
expect(generatedJson).toEqual(expectedBuildInfo)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('verifies generated build-info and reports mismatches', async () => {
|
|
112
|
+
const { projectRoot } = await createProjectWithLocalProvenanceRepo()
|
|
113
|
+
const targetPath = path.join(projectRoot, 'jobs', 'demo', 'build-info', 'stage1.json')
|
|
114
|
+
|
|
115
|
+
await generateBuildInfoFromSourceProvenance(projectRoot)
|
|
116
|
+
const verified = await verifySourceProvenance(projectRoot)
|
|
117
|
+
expect(verified.results[0].status).toBe('verified')
|
|
118
|
+
|
|
119
|
+
const changed = JSON.parse(await fs.readFile(targetPath, 'utf-8'))
|
|
120
|
+
changed.solcVersion = '0.8.1'
|
|
121
|
+
await fs.writeFile(targetPath, JSON.stringify(changed, null, 2))
|
|
122
|
+
|
|
123
|
+
const mismatch = await verifySourceProvenance(projectRoot)
|
|
124
|
+
expect(mismatch.results[0].status).toBe('failed')
|
|
125
|
+
expect(mismatch.results[0].message).toContain('does not match')
|
|
126
|
+
expect(mismatch.results[0].message).toContain('$.solcVersion')
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('normalizes checkout-local build-info paths and top-level ids while verifying', async () => {
|
|
130
|
+
const { projectRoot } = await createProjectWithLocalProvenanceRepo({ checkoutSensitiveBuildInfo: true })
|
|
131
|
+
const targetPath = path.join(projectRoot, 'jobs', 'demo', 'build-info', 'stage1.json')
|
|
132
|
+
|
|
133
|
+
await generateBuildInfoFromSourceProvenance(projectRoot)
|
|
134
|
+
const generatedJson = JSON.parse(await fs.readFile(targetPath, 'utf-8'))
|
|
135
|
+
expect(generatedJson.id).toContain('catapult-provenance-')
|
|
136
|
+
expect(generatedJson.input.basePath).toContain('catapult-provenance-')
|
|
137
|
+
|
|
138
|
+
const verified = await verifySourceProvenance(projectRoot)
|
|
139
|
+
expect(verified.results[0].status).toBe('verified')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
async function createProjectWithLocalProvenanceRepo(
|
|
143
|
+
options: { checkoutSensitiveBuildInfo?: boolean } = {}
|
|
144
|
+
): Promise<{ projectRoot: string; expectedBuildInfo: Record<string, unknown> }> {
|
|
145
|
+
const sourceRepo = path.join(tempDir, 'source-repo')
|
|
146
|
+
const projectRoot = path.join(tempDir, 'project')
|
|
147
|
+
await fs.mkdir(sourceRepo, { recursive: true })
|
|
148
|
+
|
|
149
|
+
const expectedBuildInfo = {
|
|
150
|
+
_format: 'hh-sol-build-info-1',
|
|
151
|
+
id: 'stage1',
|
|
152
|
+
solcVersion: '0.8.0',
|
|
153
|
+
input: {
|
|
154
|
+
language: 'Solidity',
|
|
155
|
+
sources: {}
|
|
156
|
+
},
|
|
157
|
+
output: {
|
|
158
|
+
contracts: {}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const buildInfoScript = options.checkoutSensitiveBuildInfo
|
|
163
|
+
? `
|
|
164
|
+
const fs = require('fs')
|
|
165
|
+
const path = require('path')
|
|
166
|
+
const buildInfo = {
|
|
167
|
+
_format: 'hh-sol-build-info-1',
|
|
168
|
+
id: path.basename(path.dirname(__dirname)),
|
|
169
|
+
solcVersion: '0.8.0',
|
|
170
|
+
input: {
|
|
171
|
+
language: 'Solidity',
|
|
172
|
+
basePath: __dirname,
|
|
173
|
+
allowPaths: [__dirname, path.join(__dirname, 'lib')],
|
|
174
|
+
includePaths: [__dirname],
|
|
175
|
+
sources: {}
|
|
176
|
+
},
|
|
177
|
+
output: {
|
|
178
|
+
contracts: {}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
fs.mkdirSync(path.join(__dirname, 'out', 'build-info'), { recursive: true })
|
|
182
|
+
fs.writeFileSync(path.join(__dirname, 'out', 'build-info', 'stage1.json'), JSON.stringify(buildInfo, null, 2))
|
|
183
|
+
`
|
|
184
|
+
: `
|
|
185
|
+
const fs = require('fs')
|
|
186
|
+
const path = require('path')
|
|
187
|
+
fs.mkdirSync(path.join(__dirname, 'out', 'build-info'), { recursive: true })
|
|
188
|
+
fs.writeFileSync(path.join(__dirname, 'out', 'build-info', 'stage1.json'), JSON.stringify(${JSON.stringify(expectedBuildInfo)}, null, 2))
|
|
189
|
+
`
|
|
190
|
+
|
|
191
|
+
await writeFile(path.join(sourceRepo, 'build-info.js'), `
|
|
192
|
+
${buildInfoScript.trim()}
|
|
193
|
+
`)
|
|
194
|
+
await git(sourceRepo, ['init'])
|
|
195
|
+
await git(sourceRepo, ['config', 'user.email', 'catapult@example.com'])
|
|
196
|
+
await git(sourceRepo, ['config', 'user.name', 'Catapult Test'])
|
|
197
|
+
await git(sourceRepo, ['add', 'build-info.js'])
|
|
198
|
+
await git(sourceRepo, ['commit', '-m', 'add build script'])
|
|
199
|
+
const commit = await git(sourceRepo, ['rev-parse', 'HEAD'])
|
|
200
|
+
|
|
201
|
+
await writeFile(
|
|
202
|
+
path.join(projectRoot, 'jobs', 'demo', 'build-info', 'source.yaml'),
|
|
203
|
+
sourceYaml(sourceRepo, commit, 'node build-info.js')
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return { projectRoot, expectedBuildInfo }
|
|
207
|
+
}
|
|
208
|
+
})
|
|
@@ -1,7 +1,64 @@
|
|
|
1
1
|
import * as fs from 'fs/promises'
|
|
2
2
|
import * as path from 'path'
|
|
3
3
|
import { ContractRepository } from '../repository'
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
function buildInfoContent(): string {
|
|
6
|
+
return JSON.stringify({
|
|
7
|
+
_format: 'hh-sol-build-info-1',
|
|
8
|
+
id: 'test-build-id',
|
|
9
|
+
solcVersion: '0.8.0',
|
|
10
|
+
solcLongVersion: '0.8.0+commit.c7dfd78e',
|
|
11
|
+
input: {
|
|
12
|
+
language: 'Solidity',
|
|
13
|
+
sources: {
|
|
14
|
+
'src/Stage1Module.sol': {
|
|
15
|
+
content: 'contract Stage1Module {}'
|
|
16
|
+
},
|
|
17
|
+
'src/Stage2Module.sol': {
|
|
18
|
+
content: 'contract Stage2Module {}'
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
settings: {
|
|
22
|
+
outputSelection: {
|
|
23
|
+
'*': {
|
|
24
|
+
'*': ['abi', 'evm.bytecode', 'evm.deployedBytecode']
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
output: {
|
|
30
|
+
contracts: {
|
|
31
|
+
'src/Stage1Module.sol': {
|
|
32
|
+
Stage1Module: {
|
|
33
|
+
abi: [],
|
|
34
|
+
evm: {
|
|
35
|
+
bytecode: {
|
|
36
|
+
object: '0x608060405234801561001057600080fd5b50111111'
|
|
37
|
+
},
|
|
38
|
+
deployedBytecode: {
|
|
39
|
+
object: '0x608060405234801561001057600080fd5b50111112'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
'src/Stage2Module.sol': {
|
|
45
|
+
Stage2Module: {
|
|
46
|
+
abi: [],
|
|
47
|
+
evm: {
|
|
48
|
+
bytecode: {
|
|
49
|
+
object: '0x608060405234801561001057600080fd5b50222222'
|
|
50
|
+
},
|
|
51
|
+
deployedBytecode: {
|
|
52
|
+
object: '0x608060405234801561001057600080fd5b50222223'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
sources: {}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
5
62
|
|
|
6
63
|
describe('ContractRepository', () => {
|
|
7
64
|
let repository: ContractRepository
|
|
@@ -111,6 +168,217 @@ describe('ContractRepository', () => {
|
|
|
111
168
|
expect(contract._sources.has(buildInfoPath)).toBe(true)
|
|
112
169
|
})
|
|
113
170
|
|
|
171
|
+
it('should attach source provenance from build-info sidecars', async () => {
|
|
172
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
173
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
174
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
175
|
+
const sourcePath = path.join(buildInfoDir, 'source.yaml')
|
|
176
|
+
|
|
177
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
178
|
+
await fs.writeFile(sourcePath, `
|
|
179
|
+
type: source
|
|
180
|
+
build_info:
|
|
181
|
+
"./stage1.json":
|
|
182
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
183
|
+
ref: "v3.0.0-rc.5"
|
|
184
|
+
commit: "0d9061f229da73edae890e6fdd1fbf753028df6d"
|
|
185
|
+
build: "forge build --build-info"
|
|
186
|
+
contracts:
|
|
187
|
+
"src/Stage1Module.sol:Stage1Module":
|
|
188
|
+
ref: "stage1-special"
|
|
189
|
+
`)
|
|
190
|
+
|
|
191
|
+
await repository.loadFrom(tempDir)
|
|
192
|
+
|
|
193
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
194
|
+
const stage2 = repository.lookup(`${buildInfoPath}:Stage2Module`)
|
|
195
|
+
|
|
196
|
+
expect(stage1).not.toBeNull()
|
|
197
|
+
expect(stage2).not.toBeNull()
|
|
198
|
+
|
|
199
|
+
expect(stage1!.sourceProvenance).toMatchObject({
|
|
200
|
+
repo: 'https://github.com/0xsequence/wallet-contracts-v3',
|
|
201
|
+
ref: 'stage1-special',
|
|
202
|
+
commit: '0d9061f229da73edae890e6fdd1fbf753028df6d',
|
|
203
|
+
build: 'forge build --build-info',
|
|
204
|
+
sourceDocumentPath: sourcePath,
|
|
205
|
+
buildInfoPath
|
|
206
|
+
})
|
|
207
|
+
expect(stage2!.sourceProvenance).toMatchObject({
|
|
208
|
+
repo: 'https://github.com/0xsequence/wallet-contracts-v3',
|
|
209
|
+
ref: 'v3.0.0-rc.5',
|
|
210
|
+
commit: '0d9061f229da73edae890e6fdd1fbf753028df6d'
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(stage1!._sourceProvenance?.get(buildInfoPath)?.ref).toBe('stage1-special')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('should skip source sidecars that point to missing build-info files', async () => {
|
|
217
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
218
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
219
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
220
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
221
|
+
type: source
|
|
222
|
+
build_info:
|
|
223
|
+
"./missing.json":
|
|
224
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
225
|
+
`)
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await expect(repository.loadFrom(tempDir)).resolves.toBeUndefined()
|
|
229
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('does not exist'))
|
|
230
|
+
} finally {
|
|
231
|
+
warnSpy.mockRestore()
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should skip malformed source sidecars without blocking build-info loading', async () => {
|
|
236
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
237
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
238
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
239
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
240
|
+
|
|
241
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
242
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
243
|
+
type: source
|
|
244
|
+
build_info:
|
|
245
|
+
"./stage1.json":
|
|
246
|
+
ref: "v3.0.0-rc.5"
|
|
247
|
+
`)
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await repository.loadFrom(tempDir)
|
|
251
|
+
} finally {
|
|
252
|
+
warnSpy.mockRestore()
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
256
|
+
expect(stage1).not.toBeNull()
|
|
257
|
+
expect(stage1!.sourceProvenance).toBeUndefined()
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('should keep valid source sidecar entries when sibling entries are invalid', async () => {
|
|
261
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
262
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
263
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
264
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
265
|
+
|
|
266
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
267
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
268
|
+
type: source
|
|
269
|
+
build_info:
|
|
270
|
+
"./stage1.json":
|
|
271
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
272
|
+
commit: "0d9061f229da73edae890e6fdd1fbf753028df6d"
|
|
273
|
+
"./bad.json":
|
|
274
|
+
typo_field: true
|
|
275
|
+
`)
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
await repository.loadFrom(tempDir)
|
|
279
|
+
|
|
280
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
281
|
+
expect(stage1).not.toBeNull()
|
|
282
|
+
expect(stage1!.sourceProvenance).toMatchObject({
|
|
283
|
+
repo: 'https://github.com/0xsequence/wallet-contracts-v3',
|
|
284
|
+
commit: '0d9061f229da73edae890e6fdd1fbf753028df6d'
|
|
285
|
+
})
|
|
286
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('typo_field is not supported'))
|
|
287
|
+
} finally {
|
|
288
|
+
warnSpy.mockRestore()
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should skip source sidecar entries that do not point to build-info JSON', async () => {
|
|
293
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
294
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
295
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
296
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
297
|
+
|
|
298
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
299
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
300
|
+
type: source
|
|
301
|
+
build_info:
|
|
302
|
+
"./stage1.txt":
|
|
303
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3"
|
|
304
|
+
`)
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
await repository.loadFrom(tempDir)
|
|
308
|
+
|
|
309
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
310
|
+
expect(stage1).not.toBeNull()
|
|
311
|
+
expect(stage1!.sourceProvenance).toBeUndefined()
|
|
312
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('does not point to a build-info JSON file'))
|
|
313
|
+
} finally {
|
|
314
|
+
warnSpy.mockRestore()
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('should skip duplicate source provenance entries for the same build-info file', async () => {
|
|
319
|
+
const buildInfoDir = path.join(tempDir, 'build-info', 'rc-5')
|
|
320
|
+
await fs.mkdir(buildInfoDir, { recursive: true })
|
|
321
|
+
const buildInfoPath = path.join(buildInfoDir, 'stage1.json')
|
|
322
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined)
|
|
323
|
+
|
|
324
|
+
await fs.writeFile(buildInfoPath, buildInfoContent())
|
|
325
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yaml'), `
|
|
326
|
+
type: source
|
|
327
|
+
build_info:
|
|
328
|
+
"./stage1.json":
|
|
329
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3-a"
|
|
330
|
+
`)
|
|
331
|
+
await fs.writeFile(path.join(buildInfoDir, 'source.yml'), `
|
|
332
|
+
type: source
|
|
333
|
+
build_info:
|
|
334
|
+
"./stage1.json":
|
|
335
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3-b"
|
|
336
|
+
`)
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
await repository.loadFrom(tempDir)
|
|
340
|
+
|
|
341
|
+
const stage1 = repository.lookup(`${buildInfoPath}:Stage1Module`)
|
|
342
|
+
expect(stage1).not.toBeNull()
|
|
343
|
+
expect(stage1!._sourceProvenance?.size).toBe(1)
|
|
344
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('duplicate provenance'))
|
|
345
|
+
} finally {
|
|
346
|
+
warnSpy.mockRestore()
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('should select preferred source provenance deterministically for duplicate bytecode', async () => {
|
|
351
|
+
const olderBuildInfoDir = path.join(tempDir, 'build-info', 'b-release')
|
|
352
|
+
const newerBuildInfoDir = path.join(tempDir, 'build-info', 'a-release')
|
|
353
|
+
await fs.mkdir(olderBuildInfoDir, { recursive: true })
|
|
354
|
+
await fs.mkdir(newerBuildInfoDir, { recursive: true })
|
|
355
|
+
|
|
356
|
+
const olderBuildInfoPath = path.join(olderBuildInfoDir, 'stage1.json')
|
|
357
|
+
const newerBuildInfoPath = path.join(newerBuildInfoDir, 'stage1.json')
|
|
358
|
+
await fs.writeFile(olderBuildInfoPath, buildInfoContent())
|
|
359
|
+
await fs.writeFile(newerBuildInfoPath, buildInfoContent())
|
|
360
|
+
|
|
361
|
+
await fs.writeFile(path.join(olderBuildInfoDir, 'source.yaml'), `
|
|
362
|
+
type: source
|
|
363
|
+
build_info:
|
|
364
|
+
"./stage1.json":
|
|
365
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3-old"
|
|
366
|
+
`)
|
|
367
|
+
await fs.writeFile(path.join(newerBuildInfoDir, 'source.yaml'), `
|
|
368
|
+
type: source
|
|
369
|
+
build_info:
|
|
370
|
+
"./stage1.json":
|
|
371
|
+
repo: "https://github.com/0xsequence/wallet-contracts-v3-new"
|
|
372
|
+
`)
|
|
373
|
+
|
|
374
|
+
await repository.loadFrom(tempDir)
|
|
375
|
+
|
|
376
|
+
const stage1 = repository.lookup(`${newerBuildInfoPath}:Stage1Module`)
|
|
377
|
+
expect(stage1).not.toBeNull()
|
|
378
|
+
expect(stage1!._sourceProvenance?.size).toBe(2)
|
|
379
|
+
expect(stage1!.sourceProvenance?.repo).toBe('https://github.com/0xsequence/wallet-contracts-v3-new')
|
|
380
|
+
})
|
|
381
|
+
|
|
114
382
|
it('should hydrate contracts from multiple source files', async () => {
|
|
115
383
|
// Create a basic artifact file (minimal info, will be hydrated by build-info)
|
|
116
384
|
const artifactContent = JSON.stringify({
|
|
@@ -341,4 +609,4 @@ describe('ContractRepository', () => {
|
|
|
341
609
|
expect(contracts).toHaveLength(0)
|
|
342
610
|
})
|
|
343
611
|
})
|
|
344
|
-
})
|
|
612
|
+
})
|