@0xsequence/catapult 1.3.17 → 1.5.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 +276 -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__/network-loader.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 +405 -0
- package/dist/lib/core/__tests__/resolver.spec.js.map +1 -1
- package/dist/lib/core/__tests__/sign-actions.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/sign-actions.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/sign-actions.spec.js +128 -0
- package/dist/lib/core/__tests__/sign-actions.spec.js.map +1 -0
- package/dist/lib/core/__tests__/signer.spec.d.ts +2 -0
- package/dist/lib/core/__tests__/signer.spec.d.ts.map +1 -0
- package/dist/lib/core/__tests__/signer.spec.js +40 -0
- package/dist/lib/core/__tests__/signer.spec.js.map +1 -0
- package/dist/lib/core/context.d.ts +3 -2
- package/dist/lib/core/context.d.ts.map +1 -1
- package/dist/lib/core/context.js +3 -2
- package/dist/lib/core/context.js.map +1 -1
- package/dist/lib/core/engine.d.ts +4 -0
- package/dist/lib/core/engine.d.ts.map +1 -1
- package/dist/lib/core/engine.js +206 -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/core/signer.d.ts +7 -0
- package/dist/lib/core/signer.d.ts.map +1 -0
- package/dist/lib/core/signer.js +60 -0
- package/dist/lib/core/signer.js.map +1 -0
- 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 +158 -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 +694 -0
- package/dist/lib/provenance.js.map +1 -0
- package/dist/lib/types/actions.d.ts +42 -2
- package/dist/lib/types/actions.d.ts.map +1 -1
- package/dist/lib/types/actions.js +4 -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 +26 -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 +4 -1
- package/.eslintrc.json +0 -29
- package/.github/workflows/ci.yml +0 -181
- package/CONCEPT.md +0 -24
- package/contracts/checked-call.huff +0 -65
- package/eslint.config.js +0 -48
- package/examples/jobs/guards-v1.yaml +0 -17
- package/examples/jobs/sequence-seq-0001-patch.yaml +0 -59
- package/examples/jobs/sequence-v1.yaml +0 -59
- package/examples/templates/sequence-factory-v1.yaml +0 -56
- package/jest.config.js +0 -25
- package/src/cli.ts +0 -17
- package/src/commands/common.ts +0 -61
- package/src/commands/dry.ts +0 -209
- package/src/commands/etherscan.ts +0 -360
- package/src/commands/index.ts +0 -5
- package/src/commands/list.ts +0 -249
- package/src/commands/run.ts +0 -146
- package/src/commands/utils.ts +0 -215
- package/src/index.ts +0 -67
- package/src/lib/__tests__/deployer-events.spec.ts +0 -338
- package/src/lib/__tests__/deployer.spec.ts +0 -2093
- package/src/lib/__tests__/network-loader.spec.ts +0 -150
- package/src/lib/__tests__/network-selection.spec.ts +0 -41
- package/src/lib/__tests__/network-utils.spec.ts +0 -230
- package/src/lib/artifacts/__tests__/fixtures/contract1.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/contract2.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/duplicate-name.json +0 -19
- package/src/lib/artifacts/__tests__/fixtures/nested/nested-contract.json +0 -18
- package/src/lib/artifacts/__tests__/fixtures/not-an-artifact.json +0 -8
- package/src/lib/artifacts/__tests__/fixtures/readme.txt +0 -2
- package/src/lib/contracts/__tests__/repository.spec.ts +0 -344
- package/src/lib/contracts/repository.ts +0 -313
- package/src/lib/core/__tests__/context.spec.ts +0 -37
- package/src/lib/core/__tests__/engine.spec.ts +0 -1889
- package/src/lib/core/__tests__/graph.spec.ts +0 -125
- package/src/lib/core/__tests__/json-integration.spec.ts +0 -425
- package/src/lib/core/__tests__/loader.spec.ts +0 -334
- package/src/lib/core/__tests__/multi-platform-verification.spec.ts +0 -406
- package/src/lib/core/__tests__/resolver.spec.ts +0 -2053
- package/src/lib/core/__tests__/static-action.spec.ts +0 -172
- package/src/lib/core/context.ts +0 -127
- package/src/lib/core/engine.ts +0 -1782
- package/src/lib/core/graph.ts +0 -252
- package/src/lib/core/loader.ts +0 -247
- package/src/lib/core/resolver.ts +0 -757
- package/src/lib/deployer.ts +0 -981
- package/src/lib/events/__tests__/event-system.spec.ts +0 -392
- package/src/lib/events/cli-adapter.ts +0 -369
- package/src/lib/events/emitter.ts +0 -62
- package/src/lib/events/index.ts +0 -3
- package/src/lib/events/types.ts +0 -520
- package/src/lib/index.ts +0 -14
- package/src/lib/network-loader.ts +0 -90
- package/src/lib/network-selection.ts +0 -73
- package/src/lib/network-utils.ts +0 -64
- package/src/lib/parsers/__tests__/buildinfo.spec.ts +0 -122
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-bytecode-buildinfo.json +0 -62
- package/src/lib/parsers/__tests__/fixtures/buildinfo/invalid-json.txt +0 -2
- package/src/lib/parsers/__tests__/fixtures/buildinfo/multi-contract-buildinfo.json +0 -89
- package/src/lib/parsers/__tests__/fixtures/buildinfo/no-contracts-buildinfo.json +0 -17
- package/src/lib/parsers/__tests__/fixtures/buildinfo/simple-buildinfo.json +0 -63
- package/src/lib/parsers/__tests__/fixtures/buildinfo/wrong-format.json +0 -4
- package/src/lib/parsers/__tests__/job.spec.ts +0 -358
- package/src/lib/parsers/__tests__/template.spec.ts +0 -111
- package/src/lib/parsers/artifact/__tests__/artifact.spec.ts +0 -117
- package/src/lib/parsers/artifact/__tests__/fixtures/empty-bytecode.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/hardhat-artifact.json +0 -67
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-bytecode.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/invalid-json.txt +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/minimal-artifact.json +0 -5
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-abi.json +0 -4
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-bytecode.json +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/missing-contract-name.json +0 -11
- package/src/lib/parsers/artifact/__tests__/fixtures/simple-artifact.json +0 -40
- package/src/lib/parsers/artifact/__tests__/fixtures/wrong-types.json +0 -7
- package/src/lib/parsers/artifact/foundry-1.2.ts +0 -72
- package/src/lib/parsers/artifact/index.ts +0 -27
- package/src/lib/parsers/artifact/types.ts +0 -9
- package/src/lib/parsers/buildinfo.ts +0 -127
- package/src/lib/parsers/constants.ts +0 -56
- package/src/lib/parsers/index.ts +0 -5
- package/src/lib/parsers/job.ts +0 -148
- package/src/lib/parsers/template.ts +0 -135
- package/src/lib/std/templates/arachnid-deterministic-deployment-proxy.yaml +0 -68
- package/src/lib/std/templates/assured-deployment.yaml +0 -46
- package/src/lib/std/templates/era-evm-predeploy.yaml +0 -35
- package/src/lib/std/templates/erc-2470.yaml +0 -70
- package/src/lib/std/templates/min-balance.yaml +0 -35
- package/src/lib/std/templates/nano-universal-deployer.yaml +0 -61
- package/src/lib/std/templates/raw-erc-2470.yaml +0 -62
- package/src/lib/std/templates/raw-nano-universal-deployer.yaml +0 -54
- package/src/lib/std/templates/raw-sequence-universal-deployer-2.yaml +0 -52
- package/src/lib/std/templates/sequence-universal-deployer-2.yaml +0 -61
- package/src/lib/types/__tests__/json-request-action.spec.ts +0 -243
- package/src/lib/types/__tests__/read-json-value.spec.ts +0 -278
- package/src/lib/types/__tests__/resolve-json-value.spec.ts +0 -769
- package/src/lib/types/actions.ts +0 -127
- package/src/lib/types/artifacts.ts +0 -21
- package/src/lib/types/buildinfo.ts +0 -116
- package/src/lib/types/conditions.ts +0 -50
- package/src/lib/types/contracts.ts +0 -23
- package/src/lib/types/definitions.ts +0 -70
- package/src/lib/types/index.ts +0 -8
- package/src/lib/types/network.ts +0 -33
- package/src/lib/types/project.ts +0 -9
- package/src/lib/types/task.ts +0 -9
- package/src/lib/types/values.ts +0 -150
- package/src/lib/utils/assertion.ts +0 -24
- package/src/lib/utils/validation.ts +0 -116
- package/src/lib/validation/contract-references.ts +0 -210
- package/src/lib/validation/index.ts +0 -1
- package/src/lib/verification/__tests__/etherscan.spec.ts +0 -710
- package/src/lib/verification/__tests__/sourcify.spec.ts +0 -288
- package/src/lib/verification/etherscan.ts +0 -547
- package/src/lib/verification/sourcify.ts +0 -248
- package/test_validation/artifacts/TestContract.json +0 -9
- package/test_validation/jobs/test-missing.yaml +0 -16
- package/test_validation/networks.yaml +0 -3
- package/tsconfig.json +0 -36
package/src/lib/deployer.ts
DELETED
|
@@ -1,981 +0,0 @@
|
|
|
1
|
-
import * as fs from 'fs/promises'
|
|
2
|
-
import * as path from 'path'
|
|
3
|
-
|
|
4
|
-
import { ProjectLoader, ProjectLoaderOptions } from './core/loader'
|
|
5
|
-
import { DependencyGraph } from './core/graph'
|
|
6
|
-
import { ExecutionEngine } from './core/engine'
|
|
7
|
-
import { createDefaultVerificationRegistry } from './verification/etherscan'
|
|
8
|
-
import { ExecutionContext } from './core/context'
|
|
9
|
-
import { Network, Job } from './types'
|
|
10
|
-
import { DeploymentEventEmitter, deploymentEvents } from './events'
|
|
11
|
-
import type { RunSummaryEvent } from './events'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Options for configuring a Deployer instance.
|
|
15
|
-
*/
|
|
16
|
-
export interface DeployerOptions {
|
|
17
|
-
/** The root directory of the deployment project. */
|
|
18
|
-
projectRoot: string
|
|
19
|
-
|
|
20
|
-
/** The private key of the EOA to be used as the signer/relayer. Optional if an implicit sender from RPC is desired. */
|
|
21
|
-
privateKey?: string
|
|
22
|
-
|
|
23
|
-
/** An array of network configurations to use for deployment. */
|
|
24
|
-
networks: Network[]
|
|
25
|
-
|
|
26
|
-
/** Optional: An array of job names to execute. If not provided, all jobs are considered. */
|
|
27
|
-
runJobs?: string[]
|
|
28
|
-
|
|
29
|
-
/** Optional: An array of chain IDs to run on. If not provided, all configured networks are used. */
|
|
30
|
-
runOnNetworks?: number[]
|
|
31
|
-
|
|
32
|
-
/** Optional: Custom event emitter instance. If not provided, uses the global singleton. */
|
|
33
|
-
eventEmitter?: DeploymentEventEmitter
|
|
34
|
-
|
|
35
|
-
/** Optional: Project loader options (e.g., whether to load standard templates). */
|
|
36
|
-
loaderOptions?: ProjectLoaderOptions
|
|
37
|
-
|
|
38
|
-
/** Optional Etherscan API key for contract verification. */
|
|
39
|
-
etherscanApiKey?: string
|
|
40
|
-
|
|
41
|
-
/** Optional: Stop execution as soon as any job fails. Defaults to false. */
|
|
42
|
-
failEarly?: boolean
|
|
43
|
-
|
|
44
|
-
/** Optional: Skip post-execution check of skip conditions. Defaults to false (post-check enabled). */
|
|
45
|
-
noPostCheckConditions?: boolean
|
|
46
|
-
|
|
47
|
-
/** Optional: When true, write outputs in a flat directory instead of mirroring the jobs dir structure. */
|
|
48
|
-
flatOutput?: boolean
|
|
49
|
-
|
|
50
|
-
/** Optional: Allow running jobs marked as deprecated when true. */
|
|
51
|
-
runDeprecated?: boolean
|
|
52
|
-
|
|
53
|
-
/** Optional: Show end-of-run summary (default: true). */
|
|
54
|
-
showSummary?: boolean
|
|
55
|
-
|
|
56
|
-
/** Optional: Convert verification errors to warnings instead of failing (default: false). */
|
|
57
|
-
ignoreVerifyErrors?: boolean
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The Deployer is the top-level orchestrator for the entire deployment process.
|
|
62
|
-
* It loads a project, builds the dependency graph, and executes jobs across
|
|
63
|
-
* specified networks in the correct order.
|
|
64
|
-
*/
|
|
65
|
-
export class Deployer {
|
|
66
|
-
private readonly options: DeployerOptions
|
|
67
|
-
public readonly events: DeploymentEventEmitter
|
|
68
|
-
private readonly loader: ProjectLoader
|
|
69
|
-
private readonly noPostCheckConditions: boolean
|
|
70
|
-
private readonly showSummary: boolean
|
|
71
|
-
|
|
72
|
-
// Store both successful and failed execution results
|
|
73
|
-
private readonly results = new Map<string, {
|
|
74
|
-
job: Job;
|
|
75
|
-
outputs: Map<number, { status: 'success' | 'error' | 'skipped'; data: Map<string, unknown> | string }>
|
|
76
|
-
}>()
|
|
77
|
-
private graph?: DependencyGraph
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
constructor(options: DeployerOptions) {
|
|
81
|
-
this.options = options
|
|
82
|
-
this.events = options.eventEmitter || deploymentEvents
|
|
83
|
-
this.loader = new ProjectLoader(options.projectRoot, options.loaderOptions)
|
|
84
|
-
this.noPostCheckConditions = options.noPostCheckConditions ?? false
|
|
85
|
-
this.showSummary = options.showSummary !== false
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Runs the entire deployment process from loading to execution and outputting results.
|
|
91
|
-
*/
|
|
92
|
-
public async run(): Promise<void> {
|
|
93
|
-
this.events.emitEvent({
|
|
94
|
-
type: 'deployment_started',
|
|
95
|
-
level: 'info',
|
|
96
|
-
data: {
|
|
97
|
-
projectRoot: this.options.projectRoot
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
// 1. Load all project artifacts, templates, and jobs.
|
|
103
|
-
this.events.emitEvent({
|
|
104
|
-
type: 'project_loading_started',
|
|
105
|
-
level: 'info',
|
|
106
|
-
data: {
|
|
107
|
-
projectRoot: this.options.projectRoot
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
await this.loader.load()
|
|
112
|
-
|
|
113
|
-
this.events.emitEvent({
|
|
114
|
-
type: 'project_loaded',
|
|
115
|
-
level: 'info',
|
|
116
|
-
data: {
|
|
117
|
-
jobCount: this.loader.jobs.size,
|
|
118
|
-
templateCount: this.loader.templates.size
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
// 2. Build the dependency graph and determine execution order.
|
|
123
|
-
const graph = new DependencyGraph(this.loader.jobs, this.loader.templates)
|
|
124
|
-
this.graph = graph
|
|
125
|
-
const jobOrder = graph.getExecutionOrder()
|
|
126
|
-
|
|
127
|
-
// 3. Filter jobs and networks based on user options.
|
|
128
|
-
const jobsToRun = this.getJobExecutionPlan(jobOrder)
|
|
129
|
-
|
|
130
|
-
// Inform about skipped deprecated jobs (when applicable)
|
|
131
|
-
if (!this.options.runDeprecated) {
|
|
132
|
-
const skippedDeprecated = jobOrder.filter(name => {
|
|
133
|
-
const j = this.loader.jobs.get(name) as { deprecated?: boolean } | undefined
|
|
134
|
-
return !jobsToRun.includes(name) && j?.deprecated === true
|
|
135
|
-
})
|
|
136
|
-
if (skippedDeprecated.length > 0) {
|
|
137
|
-
this.events.emitEvent({
|
|
138
|
-
type: 'deprecated_jobs_skipped',
|
|
139
|
-
level: 'warn',
|
|
140
|
-
data: { jobs: skippedDeprecated }
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const targetNetworks = this.getTargetNetworks()
|
|
145
|
-
|
|
146
|
-
this.events.emitEvent({
|
|
147
|
-
type: 'execution_plan',
|
|
148
|
-
level: 'info',
|
|
149
|
-
data: {
|
|
150
|
-
targetNetworks: targetNetworks.map(n => ({
|
|
151
|
-
name: n.name,
|
|
152
|
-
chainId: n.chainId
|
|
153
|
-
})),
|
|
154
|
-
jobExecutionOrder: jobsToRun
|
|
155
|
-
}
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
// 4. Execute the plan.
|
|
159
|
-
const verificationRegistry = createDefaultVerificationRegistry(this.options.etherscanApiKey)
|
|
160
|
-
const engine = new ExecutionEngine(this.loader.templates, {
|
|
161
|
-
eventEmitter: this.events,
|
|
162
|
-
verificationRegistry,
|
|
163
|
-
noPostCheckConditions: this.noPostCheckConditions,
|
|
164
|
-
ignoreVerifyErrors: this.options.ignoreVerifyErrors ?? false
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// Track if any jobs have failed
|
|
168
|
-
let hasFailures = false
|
|
169
|
-
// Emit signer info once per network (chainId)
|
|
170
|
-
const signerInfoPrintedForChain = new Set<number>()
|
|
171
|
-
|
|
172
|
-
for (const network of targetNetworks) {
|
|
173
|
-
this.events.emitEvent({
|
|
174
|
-
type: 'network_started',
|
|
175
|
-
level: 'info',
|
|
176
|
-
data: {
|
|
177
|
-
networkName: network.name,
|
|
178
|
-
chainId: network.chainId
|
|
179
|
-
}
|
|
180
|
-
})
|
|
181
|
-
for (const jobName of jobsToRun) {
|
|
182
|
-
const job = this.loader.jobs.get(jobName)!
|
|
183
|
-
|
|
184
|
-
// Initialize results storage for this job if not exists
|
|
185
|
-
if (!this.results.has(job.name)) {
|
|
186
|
-
this.results.set(job.name, { job, outputs: new Map() })
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (this.shouldSkipJobOnNetwork(job, network)) {
|
|
190
|
-
this.events.emitEvent({
|
|
191
|
-
type: 'job_skipped',
|
|
192
|
-
level: 'warn',
|
|
193
|
-
data: {
|
|
194
|
-
jobName,
|
|
195
|
-
networkName: network.name,
|
|
196
|
-
reason: 'configuration'
|
|
197
|
-
}
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
// Store skipped result
|
|
201
|
-
this.results.get(job.name)!.outputs.set(network.chainId, {
|
|
202
|
-
status: 'skipped',
|
|
203
|
-
data: 'Job skipped due to network configuration'
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
continue
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let context: ExecutionContext | undefined
|
|
210
|
-
try {
|
|
211
|
-
context = new ExecutionContext(
|
|
212
|
-
network,
|
|
213
|
-
this.options.privateKey,
|
|
214
|
-
this.loader.contractRepository,
|
|
215
|
-
this.options.etherscanApiKey,
|
|
216
|
-
this.loader.constants
|
|
217
|
-
)
|
|
218
|
-
// Set job-level constants if present (guard for mocked contexts in tests)
|
|
219
|
-
if (typeof (context as unknown as { setJobConstants?: (constants: unknown) => void }).setJobConstants === 'function') {
|
|
220
|
-
(context as unknown as { setJobConstants: (constants: unknown) => void }).setJobConstants(job.constants)
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Emit signer info once per network using the first job's context
|
|
224
|
-
if (!signerInfoPrintedForChain.has(network.chainId)) {
|
|
225
|
-
try {
|
|
226
|
-
const getSignerFn = (context as unknown as {
|
|
227
|
-
getResolvedSigner?: () => Promise<{ getAddress: () => Promise<string> }>
|
|
228
|
-
signer?: { getAddress: () => Promise<string> }
|
|
229
|
-
}).getResolvedSigner
|
|
230
|
-
const signer = getSignerFn
|
|
231
|
-
? await getSignerFn.call(context)
|
|
232
|
-
: (context as unknown as { signer?: { getAddress: () => Promise<string> } }).signer
|
|
233
|
-
if (signer && typeof signer.getAddress === 'function') {
|
|
234
|
-
const address = await signer.getAddress()
|
|
235
|
-
// provider may not exist on mocked contexts; guard for it
|
|
236
|
-
const provider = (context as unknown as {
|
|
237
|
-
provider?: { getBalance: (addr: string) => Promise<bigint | number | { toString: () => string }> }
|
|
238
|
-
}).provider
|
|
239
|
-
if (provider && typeof provider.getBalance === 'function') {
|
|
240
|
-
const balanceBn = await provider.getBalance(address)
|
|
241
|
-
const balanceWei = balanceBn.toString()
|
|
242
|
-
const balanceEth = (Number(balanceBn) / 1e18).toString()
|
|
243
|
-
this.events.emitEvent({
|
|
244
|
-
type: 'network_signer_info',
|
|
245
|
-
level: 'info',
|
|
246
|
-
data: {
|
|
247
|
-
networkName: network.name,
|
|
248
|
-
chainId: network.chainId,
|
|
249
|
-
address,
|
|
250
|
-
balanceWei,
|
|
251
|
-
balance: balanceEth
|
|
252
|
-
}
|
|
253
|
-
})
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
} catch {
|
|
257
|
-
// ignore non-fatal signer info errors
|
|
258
|
-
} finally {
|
|
259
|
-
signerInfoPrintedForChain.add(network.chainId)
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Check job-level skip conditions before execution
|
|
264
|
-
if (job.skip_condition) {
|
|
265
|
-
const shouldSkip = await engine.evaluateSkipConditions(job.skip_condition, context, new Map())
|
|
266
|
-
if (shouldSkip) {
|
|
267
|
-
// Store skipped result
|
|
268
|
-
this.results.get(job.name)!.outputs.set(network.chainId, {
|
|
269
|
-
status: 'skipped',
|
|
270
|
-
data: `Job "${job.name}" skipped due to skip condition`
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
this.events.emitEvent({
|
|
274
|
-
type: 'job_skipped',
|
|
275
|
-
level: 'warn',
|
|
276
|
-
data: {
|
|
277
|
-
jobName: job.name,
|
|
278
|
-
networkName: network.name,
|
|
279
|
-
reason: 'skip_condition'
|
|
280
|
-
}
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
continue // Skip to next job
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Populate context with outputs from previously executed dependent jobs
|
|
288
|
-
this.populateContextWithDependentJobOutputs(job, context, network)
|
|
289
|
-
|
|
290
|
-
await engine.executeJob(job, context)
|
|
291
|
-
|
|
292
|
-
// Store successful results
|
|
293
|
-
this.results.get(job.name)!.outputs.set(network.chainId, {
|
|
294
|
-
status: 'success',
|
|
295
|
-
data: (context as { getOutputs(): Map<string, unknown> }).getOutputs()
|
|
296
|
-
})
|
|
297
|
-
} catch (error) {
|
|
298
|
-
// Store error results
|
|
299
|
-
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
300
|
-
this.results.get(job.name)!.outputs.set(network.chainId, {
|
|
301
|
-
status: 'error',
|
|
302
|
-
data: errorMessage
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
this.events.emitEvent({
|
|
306
|
-
type: 'job_execution_failed',
|
|
307
|
-
level: 'error',
|
|
308
|
-
data: {
|
|
309
|
-
jobName: job.name,
|
|
310
|
-
networkName: network.name,
|
|
311
|
-
chainId: network.chainId,
|
|
312
|
-
error: errorMessage
|
|
313
|
-
}
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
// Mark that we have failures
|
|
317
|
-
hasFailures = true
|
|
318
|
-
|
|
319
|
-
// If fail-early is enabled, throw the error immediately
|
|
320
|
-
if (this.options.failEarly) {
|
|
321
|
-
throw error
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Otherwise, continue to next job/network
|
|
325
|
-
} finally {
|
|
326
|
-
// Clean up the context to prevent hanging connections
|
|
327
|
-
if (context) {
|
|
328
|
-
try {
|
|
329
|
-
await context.dispose()
|
|
330
|
-
} catch (disposeError) {
|
|
331
|
-
// Log disposal errors but don't let them interrupt the flow
|
|
332
|
-
this.events.emitEvent({
|
|
333
|
-
type: 'context_disposal_warning',
|
|
334
|
-
level: 'warn',
|
|
335
|
-
data: {
|
|
336
|
-
jobName: job.name,
|
|
337
|
-
networkName: network.name,
|
|
338
|
-
error: disposeError instanceof Error ? disposeError.message : String(disposeError)
|
|
339
|
-
}
|
|
340
|
-
})
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// 5. Write results to output files.
|
|
348
|
-
await this.writeOutputFiles()
|
|
349
|
-
|
|
350
|
-
// Show verification warnings report if ignoreVerifyErrors is enabled
|
|
351
|
-
if (this.options.ignoreVerifyErrors) {
|
|
352
|
-
this.emitVerificationWarningsReport(engine)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Emit end-of-run summary before final status
|
|
356
|
-
if (this.showSummary) {
|
|
357
|
-
this.emitRunSummary(hasFailures)
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// Check if any jobs failed and exit with error if so
|
|
361
|
-
if (hasFailures) {
|
|
362
|
-
const error = new Error('One or more jobs failed during execution')
|
|
363
|
-
|
|
364
|
-
// Build a flat list of failed jobs with network context and error messages
|
|
365
|
-
const failedJobs: Array<{ jobName: string; networkName: string; chainId: number; error: string }> = []
|
|
366
|
-
for (const [, result] of this.results) {
|
|
367
|
-
const job = result.job
|
|
368
|
-
for (const [chainId, netResult] of result.outputs) {
|
|
369
|
-
if (netResult.status === 'error') {
|
|
370
|
-
// Resolve network name from configured networks (fallback to chainId if missing)
|
|
371
|
-
const network = this.options.networks.find(n => n.chainId === chainId)
|
|
372
|
-
failedJobs.push({
|
|
373
|
-
jobName: job.name,
|
|
374
|
-
networkName: network?.name || `chain-${chainId}`,
|
|
375
|
-
chainId,
|
|
376
|
-
error: String(netResult.data)
|
|
377
|
-
})
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
this.events.emitEvent({
|
|
383
|
-
type: 'deployment_failed',
|
|
384
|
-
level: 'error',
|
|
385
|
-
data: {
|
|
386
|
-
error: error.message,
|
|
387
|
-
stack: error.stack,
|
|
388
|
-
failedJobs
|
|
389
|
-
}
|
|
390
|
-
})
|
|
391
|
-
throw error
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
this.events.emitEvent({
|
|
395
|
-
type: 'deployment_completed',
|
|
396
|
-
level: 'info'
|
|
397
|
-
})
|
|
398
|
-
} catch (error) {
|
|
399
|
-
this.events.emitEvent({
|
|
400
|
-
type: 'deployment_failed',
|
|
401
|
-
level: 'error',
|
|
402
|
-
data: {
|
|
403
|
-
error: error instanceof Error ? error.message : String(error),
|
|
404
|
-
stack: error instanceof Error ? error.stack : undefined
|
|
405
|
-
}
|
|
406
|
-
})
|
|
407
|
-
// Re-throw to allow CLI to exit with a non-zero code
|
|
408
|
-
throw error
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* Emit a concise run summary event for the CLI to render at the end.
|
|
414
|
-
*/
|
|
415
|
-
private emitRunSummary(hasFailures: boolean): void {
|
|
416
|
-
// Compute counts
|
|
417
|
-
const jobCount = this.results.size
|
|
418
|
-
let successCount = 0
|
|
419
|
-
let failedCount = 0
|
|
420
|
-
let skippedCount = 0
|
|
421
|
-
|
|
422
|
-
for (const [, result] of this.results) {
|
|
423
|
-
for (const [, netResult] of result.outputs) {
|
|
424
|
-
if (netResult.status === 'success') successCount++
|
|
425
|
-
else if (netResult.status === 'skipped') skippedCount++
|
|
426
|
-
else if (netResult.status === 'error') failedCount++
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// Collect key contract addresses from outputs for quick visibility
|
|
431
|
-
const keyContracts: Array<{ job: string; action: string; address: string }> = []
|
|
432
|
-
for (const [, result] of this.results) {
|
|
433
|
-
for (const [, netResult] of result.outputs) {
|
|
434
|
-
if (netResult.status !== 'success') continue
|
|
435
|
-
const outputs = netResult.data as Map<string, unknown>
|
|
436
|
-
for (const [k, v] of outputs) {
|
|
437
|
-
if (k.endsWith('.address') && typeof v === 'string') {
|
|
438
|
-
const action = k.split('.')[0]
|
|
439
|
-
keyContracts.push({ job: result.job.name, action, address: v })
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const summaryEvent = {
|
|
446
|
-
type: 'run_summary',
|
|
447
|
-
level: (hasFailures ? 'warn' : 'info') as 'info' | 'warn',
|
|
448
|
-
data: {
|
|
449
|
-
networkCount: this.options.networks.length,
|
|
450
|
-
jobCount,
|
|
451
|
-
successCount,
|
|
452
|
-
failedCount,
|
|
453
|
-
skippedCount,
|
|
454
|
-
keyContracts: keyContracts.slice(0, 10)
|
|
455
|
-
}
|
|
456
|
-
} satisfies Omit<RunSummaryEvent, 'timestamp'>
|
|
457
|
-
|
|
458
|
-
this.events.emitEvent(summaryEvent)
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Emit verification warnings report when ignoreVerifyErrors is enabled
|
|
463
|
-
*/
|
|
464
|
-
private emitVerificationWarningsReport(engine: ExecutionEngine): void {
|
|
465
|
-
const warnings = engine.getVerificationWarnings()
|
|
466
|
-
|
|
467
|
-
if (warnings.length > 0) {
|
|
468
|
-
this.events.emitEvent({
|
|
469
|
-
type: 'verification_warnings_report',
|
|
470
|
-
level: 'warn',
|
|
471
|
-
data: {
|
|
472
|
-
totalWarnings: warnings.length,
|
|
473
|
-
warnings: warnings
|
|
474
|
-
}
|
|
475
|
-
})
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
/**
|
|
480
|
-
* Determines the final, ordered list of jobs to execute based on user input.
|
|
481
|
-
* If a user requests specific jobs, this ensures all their dependencies are also included.
|
|
482
|
-
*/
|
|
483
|
-
private getJobExecutionPlan(fullOrder: string[]): string[] {
|
|
484
|
-
// Expand provided runJobs to concrete job names by supporting simple glob patterns
|
|
485
|
-
const expandRunJobs = (patterns: string[]): string[] => {
|
|
486
|
-
const allJobNames = Array.from(this.loader.jobs.keys())
|
|
487
|
-
|
|
488
|
-
const isPattern = (s: string): boolean => /[*?]/.test(s)
|
|
489
|
-
const escapeRegex = (s: string): string => s.replace(/[-\\^$+?.()|[\]{}*?]/g, '\\$&')
|
|
490
|
-
const patternToRegex = (pattern: string): RegExp => {
|
|
491
|
-
// Escape regex metacharacters, then translate wildcard tokens
|
|
492
|
-
const escaped = escapeRegex(pattern)
|
|
493
|
-
.replace(/\\\*/g, '.*') // escaped '*' -> '.*'
|
|
494
|
-
.replace(/\\\?/g, '.') // escaped '?' -> '.'
|
|
495
|
-
return new RegExp(`^${escaped}$`)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
const expanded: string[] = []
|
|
499
|
-
const seen = new Set<string>()
|
|
500
|
-
|
|
501
|
-
for (const p of patterns) {
|
|
502
|
-
if (!isPattern(p)) {
|
|
503
|
-
// Exact name; validate exists
|
|
504
|
-
if (!this.loader.jobs.has(p)) {
|
|
505
|
-
throw new Error(`Specified job "${p}" not found in project.`)
|
|
506
|
-
}
|
|
507
|
-
if (!seen.has(p)) {
|
|
508
|
-
seen.add(p)
|
|
509
|
-
expanded.push(p)
|
|
510
|
-
}
|
|
511
|
-
continue
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const re = patternToRegex(p)
|
|
515
|
-
const matches = allJobNames.filter(name => re.test(name))
|
|
516
|
-
if (matches.length === 0) {
|
|
517
|
-
throw new Error(`Job pattern "${p}" did not match any jobs in project.`)
|
|
518
|
-
}
|
|
519
|
-
for (const m of matches) {
|
|
520
|
-
if (!seen.has(m)) {
|
|
521
|
-
seen.add(m)
|
|
522
|
-
expanded.push(m)
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
return expanded
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Helper to decide if a job is deprecated
|
|
531
|
-
const isDeprecated = (jobName: string): boolean => {
|
|
532
|
-
const j = this.loader.jobs.get(jobName)
|
|
533
|
-
return !!(j && (j as { deprecated?: boolean }).deprecated === true)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// If user didn't specify jobs explicitly, include all non-deprecated jobs.
|
|
537
|
-
// Additionally, ALWAYS include deprecated jobs when they are dependencies of any non-deprecated job.
|
|
538
|
-
if (!this.options.runJobs || this.options.runJobs.length === 0) {
|
|
539
|
-
if (this.options.runDeprecated) {
|
|
540
|
-
return fullOrder
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const nonDeprecatedJobs = new Set(fullOrder.filter(name => !isDeprecated(name)))
|
|
544
|
-
|
|
545
|
-
// Collect deprecated jobs that are required by any non-deprecated job
|
|
546
|
-
const requiredDeprecated = new Set<string>()
|
|
547
|
-
for (const jobName of nonDeprecatedJobs) {
|
|
548
|
-
const deps = this.graph?.getDependencies(jobName) || new Set<string>()
|
|
549
|
-
for (const dep of deps) {
|
|
550
|
-
if (isDeprecated(dep)) {
|
|
551
|
-
requiredDeprecated.add(dep)
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const allowed = new Set<string>([...nonDeprecatedJobs, ...requiredDeprecated])
|
|
557
|
-
return fullOrder.filter(name => allowed.has(name))
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Expand patterns to concrete names
|
|
561
|
-
const expandedRunJobs = expandRunJobs(this.options.runJobs)
|
|
562
|
-
const explicitlyRequested = new Set<string>(expandedRunJobs)
|
|
563
|
-
|
|
564
|
-
const jobsToRun = new Set<string>()
|
|
565
|
-
for (const jobName of expandedRunJobs) {
|
|
566
|
-
jobsToRun.add(jobName)
|
|
567
|
-
const dependencies = this.graph?.getDependencies(jobName) || new Set()
|
|
568
|
-
dependencies.forEach((dep: string) => jobsToRun.add(dep))
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Deprecated dependencies must be kept even when --run-deprecated is not set.
|
|
572
|
-
// Only drop deprecated jobs that are neither explicitly requested nor required as a dependency.
|
|
573
|
-
const depsOfRequested = new Set<string>()
|
|
574
|
-
for (const jobName of expandedRunJobs) {
|
|
575
|
-
const deps = this.graph?.getDependencies(jobName) || new Set<string>()
|
|
576
|
-
deps.forEach(d => depsOfRequested.add(d))
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
const filtered = Array.from(jobsToRun).filter(name => {
|
|
580
|
-
if (!isDeprecated(name)) return true
|
|
581
|
-
if (explicitlyRequested.has(name)) return true
|
|
582
|
-
if (depsOfRequested.has(name)) return true // keep deprecated dependency
|
|
583
|
-
return this.options.runDeprecated === true
|
|
584
|
-
})
|
|
585
|
-
const allowedSet = new Set(filtered)
|
|
586
|
-
|
|
587
|
-
// Filter the original execution order to only include the required jobs, preserving the correct sequence.
|
|
588
|
-
return fullOrder.filter(jobName => allowedSet.has(jobName))
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/**
|
|
592
|
-
* Determines the final list of networks to run on based on user input.
|
|
593
|
-
*/
|
|
594
|
-
private getTargetNetworks(): Network[] {
|
|
595
|
-
if (!this.options.runOnNetworks || this.options.runOnNetworks.length === 0) {
|
|
596
|
-
return this.options.networks // Run on all configured networks
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const targetChainIds = new Set(this.options.runOnNetworks)
|
|
600
|
-
const filteredNetworks = this.options.networks.filter(n => targetChainIds.has(n.chainId))
|
|
601
|
-
|
|
602
|
-
if (filteredNetworks.length !== this.options.runOnNetworks.length) {
|
|
603
|
-
const foundIds = new Set(filteredNetworks.map(n => n.chainId))
|
|
604
|
-
const missingIds = this.options.runOnNetworks.filter(id => !foundIds.has(id))
|
|
605
|
-
this.events.emitEvent({
|
|
606
|
-
type: 'missing_network_config_warning',
|
|
607
|
-
level: 'warn',
|
|
608
|
-
data: {
|
|
609
|
-
missingChainIds: missingIds
|
|
610
|
-
}
|
|
611
|
-
})
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
return filteredNetworks
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* Checks a job's `only_networks` and `skip_networks` fields to see if it should run on the given network.
|
|
619
|
-
*/
|
|
620
|
-
private shouldSkipJobOnNetwork(job: Job, network: Network): boolean {
|
|
621
|
-
// Note: This relies on `only_networks` and `skip_networks` being present on the Job type.
|
|
622
|
-
const jobWithNetworkFilters = job as Job & { only_networks?: number[]; skip_networks?: number[]; min_evm_version?: string }
|
|
623
|
-
|
|
624
|
-
// Check only_networks: if present, the job only runs on these networks.
|
|
625
|
-
// If the network is NOT in only_networks, skip immediately. If it IS included, continue to min_evm_version checks.
|
|
626
|
-
const hasOnly = !!(jobWithNetworkFilters.only_networks && jobWithNetworkFilters.only_networks.length > 0)
|
|
627
|
-
if (hasOnly) {
|
|
628
|
-
if (!jobWithNetworkFilters.only_networks!.includes(network.chainId)) {
|
|
629
|
-
return true
|
|
630
|
-
}
|
|
631
|
-
// When only_networks is present and the network is allowed, skip_networks is ignored by design.
|
|
632
|
-
} else {
|
|
633
|
-
// Only consider skip_networks when only_networks is not set.
|
|
634
|
-
if (jobWithNetworkFilters.skip_networks && jobWithNetworkFilters.skip_networks.length > 0) {
|
|
635
|
-
if (jobWithNetworkFilters.skip_networks.includes(network.chainId)) {
|
|
636
|
-
return true
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
// Check minimal EVM hardfork requirement if present on job and network declares an EVM version
|
|
642
|
-
if (jobWithNetworkFilters.min_evm_version) {
|
|
643
|
-
const jobMin = this.normalizeEvmVersion(jobWithNetworkFilters.min_evm_version)
|
|
644
|
-
const chainEvm = network.evmVersion ? this.normalizeEvmVersion(network.evmVersion) : undefined
|
|
645
|
-
if (jobMin && chainEvm) {
|
|
646
|
-
// Skip when chain's EVM is older than job's minimal requirement
|
|
647
|
-
return this.compareEvmVersions(chainEvm, jobMin) < 0
|
|
648
|
-
}
|
|
649
|
-
// If network has no evmVersion declared, do not skip (assume compatible)
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
return false // Run by default
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
/**
|
|
656
|
-
* Normalizes common EVM hardfork identifiers to a canonical lowercase token.
|
|
657
|
-
*/
|
|
658
|
-
private normalizeEvmVersion(identifier: string | undefined): string | undefined {
|
|
659
|
-
if (!identifier) return undefined
|
|
660
|
-
const id = String(identifier).trim().toLowerCase()
|
|
661
|
-
const aliasMap: Record<string, string> = {
|
|
662
|
-
frontier: 'frontier',
|
|
663
|
-
homestead: 'homestead',
|
|
664
|
-
'tangerine whistle': 'tangerine',
|
|
665
|
-
tangerine: 'tangerine',
|
|
666
|
-
'spurious dragon': 'spuriousdragon',
|
|
667
|
-
spuriousdragon: 'spuriousdragon',
|
|
668
|
-
byzantium: 'byzantium',
|
|
669
|
-
constantinople: 'constantinople',
|
|
670
|
-
petersburg: 'petersburg',
|
|
671
|
-
istanbul: 'istanbul',
|
|
672
|
-
berlin: 'berlin',
|
|
673
|
-
london: 'london',
|
|
674
|
-
// The Merge hardfork is referred to as Paris in solidity's evmVersion naming
|
|
675
|
-
merge: 'paris',
|
|
676
|
-
paris: 'paris',
|
|
677
|
-
shanghai: 'shanghai',
|
|
678
|
-
// a.k.a. Cancun + Deneb (Dencun)
|
|
679
|
-
cancun: 'cancun',
|
|
680
|
-
dencun: 'cancun',
|
|
681
|
-
prague: 'prague',
|
|
682
|
-
}
|
|
683
|
-
return aliasMap[id] || undefined
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Compares canonical EVM hardfork tokens. Returns -1 if a < b, 0 if equal, 1 if a > b.
|
|
688
|
-
* Unknown tokens are treated as incomparable; caller should guard for undefined.
|
|
689
|
-
*/
|
|
690
|
-
private compareEvmVersions(a: string, b: string): number {
|
|
691
|
-
const order = [
|
|
692
|
-
'frontier',
|
|
693
|
-
'homestead',
|
|
694
|
-
'tangerine',
|
|
695
|
-
'spuriousdragon',
|
|
696
|
-
'byzantium',
|
|
697
|
-
'constantinople',
|
|
698
|
-
'petersburg',
|
|
699
|
-
'istanbul',
|
|
700
|
-
'berlin',
|
|
701
|
-
'london',
|
|
702
|
-
'paris',
|
|
703
|
-
'shanghai',
|
|
704
|
-
'cancun',
|
|
705
|
-
'prague'
|
|
706
|
-
]
|
|
707
|
-
const ia = order.indexOf(a)
|
|
708
|
-
const ib = order.indexOf(b)
|
|
709
|
-
if (ia === -1 || ib === -1) return 0
|
|
710
|
-
if (ia < ib) return -1
|
|
711
|
-
if (ia > ib) return 1
|
|
712
|
-
return 0
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
/**
|
|
716
|
-
* Populates the execution context with outputs from previously executed dependent jobs.
|
|
717
|
-
* Throws an error if any dependency failed.
|
|
718
|
-
*/
|
|
719
|
-
private populateContextWithDependentJobOutputs(job: Job, context: ExecutionContext, network: Network): void {
|
|
720
|
-
if (!job.depends_on) return
|
|
721
|
-
|
|
722
|
-
for (const dependentJobName of job.depends_on) {
|
|
723
|
-
const dependentJobResults = this.results.get(dependentJobName)
|
|
724
|
-
if (!dependentJobResults) {
|
|
725
|
-
throw new Error(`Job "${job.name}" depends on "${dependentJobName}", but "${dependentJobName}" has not been executed yet.`)
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const networkResult = dependentJobResults.outputs.get(network.chainId)
|
|
729
|
-
if (!networkResult) {
|
|
730
|
-
throw new Error(`Job "${job.name}" depends on "${dependentJobName}", but "${dependentJobName}" has not been executed on network ${network.name} (chainId: ${network.chainId}).`)
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// Skip jobs don't provide outputs, but they don't prevent dependent jobs from running
|
|
734
|
-
if (networkResult.status === 'skipped') {
|
|
735
|
-
continue
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
if (networkResult.status !== 'success') {
|
|
739
|
-
const errorMessage = typeof networkResult.data === 'string' ? networkResult.data : 'Unknown error'
|
|
740
|
-
throw new Error(`Job "${job.name}" depends on "${dependentJobName}", but "${dependentJobName}" failed: ${errorMessage}`)
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Add outputs with job name prefixes for cross-job access
|
|
744
|
-
const outputs = networkResult.data as Map<string, unknown>
|
|
745
|
-
for (const [key, value] of outputs.entries()) {
|
|
746
|
-
const prefixedKey = `${dependentJobName}.${key}`
|
|
747
|
-
context.setOutput(prefixedKey, value)
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
/**
|
|
755
|
-
* Writes the collected deployment results to JSON files in the output directory.
|
|
756
|
-
* By default, mirrors the jobs directory structure under output/. When flatOutput
|
|
757
|
-
* is true, writes all job JSONs directly under output/.
|
|
758
|
-
*/
|
|
759
|
-
private async writeOutputFiles(): Promise<void> {
|
|
760
|
-
if (this.results.size === 0) {
|
|
761
|
-
this.events.emitEvent({
|
|
762
|
-
type: 'no_outputs',
|
|
763
|
-
level: 'warn'
|
|
764
|
-
})
|
|
765
|
-
return
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
const outputRoot = path.join(this.options.projectRoot, 'output')
|
|
769
|
-
await fs.mkdir(outputRoot, { recursive: true })
|
|
770
|
-
|
|
771
|
-
this.events.emitEvent({
|
|
772
|
-
type: 'output_writing_started',
|
|
773
|
-
level: 'info'
|
|
774
|
-
})
|
|
775
|
-
|
|
776
|
-
for (const [jobName, resultData] of this.results.entries()) {
|
|
777
|
-
// Determine relative subpath for this job based on its source path under jobs/
|
|
778
|
-
let relativeJobSubpath = `${jobName}.json`
|
|
779
|
-
if (!this.options.flatOutput && resultData.job._path) {
|
|
780
|
-
// Find jobs directory within project
|
|
781
|
-
const jobsDir = path.join(this.options.projectRoot, 'jobs')
|
|
782
|
-
const normalizedJobPath = path.normalize(resultData.job._path)
|
|
783
|
-
const normalizedJobsDir = path.normalize(jobsDir)
|
|
784
|
-
if (normalizedJobPath.startsWith(normalizedJobsDir)) {
|
|
785
|
-
// Compute relative path from jobs dir to the yaml file, and replace extension with .json
|
|
786
|
-
const relFromJobs = path.relative(normalizedJobsDir, normalizedJobPath)
|
|
787
|
-
const dirPart = path.dirname(relFromJobs)
|
|
788
|
-
const fileBase = path.basename(relFromJobs, path.extname(relFromJobs))
|
|
789
|
-
relativeJobSubpath = dirPart === '.' ? `${fileBase}.json` : path.join(dirPart, `${fileBase}.json`)
|
|
790
|
-
} else {
|
|
791
|
-
// Fallback to job name if path isn't within jobs dir
|
|
792
|
-
relativeJobSubpath = `${jobName}.json`
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
const outputFilePath = path.join(outputRoot, relativeJobSubpath)
|
|
797
|
-
const outputFileDir = path.dirname(outputFilePath)
|
|
798
|
-
await fs.mkdir(outputFileDir, { recursive: true })
|
|
799
|
-
|
|
800
|
-
// Group networks by identical status and outputs
|
|
801
|
-
const groupedResults = this.groupNetworkResults(resultData.outputs, resultData.job)
|
|
802
|
-
|
|
803
|
-
const fileContent = {
|
|
804
|
-
jobName: resultData.job.name,
|
|
805
|
-
jobVersion: resultData.job.version,
|
|
806
|
-
lastRun: new Date().toISOString(),
|
|
807
|
-
networks: groupedResults
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
await fs.writeFile(outputFilePath, JSON.stringify(fileContent, null, 2))
|
|
811
|
-
this.events.emitEvent({
|
|
812
|
-
type: 'output_file_written',
|
|
813
|
-
level: 'info',
|
|
814
|
-
data: {
|
|
815
|
-
relativePath: path.relative(this.options.projectRoot, outputFilePath)
|
|
816
|
-
}
|
|
817
|
-
})
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* Filters outputs according to job actions' output selection:
|
|
823
|
-
* - output: true -> include all outputs for that action
|
|
824
|
-
* - output: false -> exclude outputs for that action
|
|
825
|
-
* - output: object -> include ONLY the specified keys, resolved from context if they are placeholders
|
|
826
|
-
*
|
|
827
|
-
* If no actions have output: true or object (i.e., only false/undefined), includes all outputs (backward compatibility),
|
|
828
|
-
* but excludes dependency outputs when there are explicit dependencies defined.
|
|
829
|
-
*/
|
|
830
|
-
private filterOutputsByActionFlags(outputs: Map<string, unknown>, job: Job): Record<string, unknown> {
|
|
831
|
-
// Partition actions by output config
|
|
832
|
-
const actionsWithCustomMap = job.actions.filter(a => a.output && typeof a.output === 'object' && a.output !== null) as Array<Job['actions'][number] & { output: Record<string, unknown> }>
|
|
833
|
-
const actionsWithTrue = job.actions.filter(a => a.output === true)
|
|
834
|
-
const actionsWithFalse = new Set(job.actions.filter(a => a.output === false).map(a => a.name))
|
|
835
|
-
|
|
836
|
-
// If there are any custom maps, include only those mapped keys for those actions.
|
|
837
|
-
// Collect explicit inclusions here.
|
|
838
|
-
const result = new Map<string, unknown>()
|
|
839
|
-
|
|
840
|
-
// Helper to include by prefix
|
|
841
|
-
const includeAllForAction = (actionName: string) => {
|
|
842
|
-
for (const [key, value] of outputs) {
|
|
843
|
-
if (key.startsWith(`${actionName}.`)) {
|
|
844
|
-
result.set(key, value)
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
// 1) Handle custom output maps (highest precedence and explicit selection)
|
|
850
|
-
if (actionsWithCustomMap.length > 0) {
|
|
851
|
-
for (const action of actionsWithCustomMap) {
|
|
852
|
-
const prefix = `${action.name}.`
|
|
853
|
-
// For mapped keys, accept either fully qualified keys (e.g., "txHash") which we map to `${action.name}.txHash`
|
|
854
|
-
// or already-qualified keys (rare). We'll normalize to prefixed keys in the output.
|
|
855
|
-
for (const mappedKey of Object.keys(action.output)) {
|
|
856
|
-
// If user provided fully-qualified "action.key", strip if redundant
|
|
857
|
-
const normalizedKey = mappedKey.startsWith(prefix) ? mappedKey : `${prefix}${mappedKey}`
|
|
858
|
-
// Only include if present in outputs map
|
|
859
|
-
if (outputs.has(normalizedKey)) {
|
|
860
|
-
result.set(normalizedKey, outputs.get(normalizedKey)!)
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
// Note: when any custom maps exist, we DO NOT automatically include actionsWithTrue;
|
|
865
|
-
// the requirement states "if action specifies custom output, then the output is defined by them and not by the template".
|
|
866
|
-
// That means for those actions, only mapped keys are included. For other actions (without custom maps),
|
|
867
|
-
// they will be handled by output:true rules below.
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
// 2) Include all for actions marked output: true (that do not have a custom map)
|
|
871
|
-
const actionsWithTrueNames = new Set(actionsWithTrue.map(a => a.name))
|
|
872
|
-
for (const actionName of actionsWithTrueNames) {
|
|
873
|
-
// If this action also had a custom map, custom map already handled it and should be authoritative.
|
|
874
|
-
const hadCustom = actionsWithCustomMap.some(a => a.name === actionName)
|
|
875
|
-
if (!hadCustom) {
|
|
876
|
-
includeAllForAction(actionName)
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// 3) Exclude any actions explicitly marked false (they won't be included by rules above anyway)
|
|
881
|
-
|
|
882
|
-
const hasExplicitOutputSelection = actionsWithCustomMap.length > 0 || actionsWithTrue.length > 0
|
|
883
|
-
|
|
884
|
-
// If the job has any explicit output selection, return the selected outputs even when empty.
|
|
885
|
-
if (hasExplicitOutputSelection) {
|
|
886
|
-
// Additionally, filter out any accidentally included outputs from actions marked false
|
|
887
|
-
for (const falseActionName of actionsWithFalse) {
|
|
888
|
-
for (const key of Array.from(result.keys())) {
|
|
889
|
-
if (key.startsWith(`${falseActionName}.`)) {
|
|
890
|
-
result.delete(key)
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
return Object.fromEntries(result)
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
// 4) Backward compatibility: include all outputs if no action opted-in via true/object.
|
|
898
|
-
// Exclude dependency outputs if the job has explicit dependencies.
|
|
899
|
-
if (job.depends_on && job.depends_on.length > 0) {
|
|
900
|
-
return this.filterOutDependencyOutputs(outputs, job)
|
|
901
|
-
}
|
|
902
|
-
return Object.fromEntries(outputs)
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Filters out dependency outputs from the outputs map.
|
|
907
|
-
* Dependency outputs are identified by being prefixed with dependency job names.
|
|
908
|
-
*/
|
|
909
|
-
private filterOutDependencyOutputs(outputs: Map<string, unknown>, job: Job): Record<string, unknown> {
|
|
910
|
-
const filtered = new Map<string, unknown>()
|
|
911
|
-
|
|
912
|
-
// Get list of dependency job names
|
|
913
|
-
const dependencyNames = job.depends_on || []
|
|
914
|
-
|
|
915
|
-
for (const [key, value] of outputs) {
|
|
916
|
-
// Check if this output key starts with any dependency job name prefix
|
|
917
|
-
const isDependencyOutput = dependencyNames.some(depName => key.startsWith(`${depName}.`))
|
|
918
|
-
|
|
919
|
-
// Only include outputs that are NOT from dependencies
|
|
920
|
-
if (!isDependencyOutput) {
|
|
921
|
-
filtered.set(key, value)
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
return Object.fromEntries(filtered)
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
/**
|
|
929
|
-
* Groups network results by status and outputs.
|
|
930
|
-
* - Success states with identical outputs are grouped together with chainIds array
|
|
931
|
-
* - Error states are kept separate (one entry per network)
|
|
932
|
-
*/
|
|
933
|
-
private groupNetworkResults(outputs: Map<number, { status: 'success' | 'error' | 'skipped'; data: Map<string, unknown> | string }>, job: Job): Array<{
|
|
934
|
-
status: 'success' | 'error' | 'skipped';
|
|
935
|
-
chainIds?: string[];
|
|
936
|
-
chainId?: string;
|
|
937
|
-
outputs?: Record<string, unknown>;
|
|
938
|
-
error?: string;
|
|
939
|
-
}> {
|
|
940
|
-
const successGroups = new Map<string, { chainIds: string[], outputs: Record<string, unknown> }>()
|
|
941
|
-
const errorEntries: Array<{
|
|
942
|
-
status: 'error';
|
|
943
|
-
chainId: string;
|
|
944
|
-
error: string;
|
|
945
|
-
}> = []
|
|
946
|
-
|
|
947
|
-
for (const [chainId, result] of outputs.entries()) {
|
|
948
|
-
if (result.status === 'success') {
|
|
949
|
-
// Group successful results by identical outputs, filtered by action output flags
|
|
950
|
-
const outputsObj = result.data instanceof Map ? this.filterOutputsByActionFlags(result.data, job) : {}
|
|
951
|
-
const key = JSON.stringify(outputsObj)
|
|
952
|
-
|
|
953
|
-
if (!successGroups.has(key)) {
|
|
954
|
-
successGroups.set(key, {
|
|
955
|
-
chainIds: [],
|
|
956
|
-
outputs: outputsObj
|
|
957
|
-
})
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
successGroups.get(key)!.chainIds.push(chainId.toString())
|
|
961
|
-
} else {
|
|
962
|
-
// Keep error results separate - one entry per network
|
|
963
|
-
errorEntries.push({
|
|
964
|
-
status: 'error',
|
|
965
|
-
chainId: chainId.toString(),
|
|
966
|
-
error: result.data as string
|
|
967
|
-
})
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
// Convert success groups to array format
|
|
972
|
-
const successEntries = Array.from(successGroups.values()).map(group => ({
|
|
973
|
-
status: 'success' as const,
|
|
974
|
-
chainIds: group.chainIds.sort(), // Sort for consistent output
|
|
975
|
-
outputs: group.outputs
|
|
976
|
-
}))
|
|
977
|
-
|
|
978
|
-
// Return all entries: successes first, then errors
|
|
979
|
-
return [...successEntries, ...errorEntries]
|
|
980
|
-
}
|
|
981
|
-
}
|