@0xsequence/catapult 1.3.7 → 1.3.8

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.
@@ -16,31 +16,31 @@ import type { RunSummaryEvent } from './events'
16
16
  export interface DeployerOptions {
17
17
  /** The root directory of the deployment project. */
18
18
  projectRoot: string
19
-
19
+
20
20
  /** The private key of the EOA to be used as the signer/relayer. Optional if an implicit sender from RPC is desired. */
21
21
  privateKey?: string
22
-
22
+
23
23
  /** An array of network configurations to use for deployment. */
24
24
  networks: Network[]
25
-
25
+
26
26
  /** Optional: An array of job names to execute. If not provided, all jobs are considered. */
27
27
  runJobs?: string[]
28
-
28
+
29
29
  /** Optional: An array of chain IDs to run on. If not provided, all configured networks are used. */
30
30
  runOnNetworks?: number[]
31
-
31
+
32
32
  /** Optional: Custom event emitter instance. If not provided, uses the global singleton. */
33
33
  eventEmitter?: DeploymentEventEmitter
34
-
34
+
35
35
  /** Optional: Project loader options (e.g., whether to load standard templates). */
36
36
  loaderOptions?: ProjectLoaderOptions
37
-
37
+
38
38
  /** Optional Etherscan API key for contract verification. */
39
39
  etherscanApiKey?: string
40
-
40
+
41
41
  /** Optional: Stop execution as soon as any job fails. Defaults to false. */
42
42
  failEarly?: boolean
43
-
43
+
44
44
  /** Optional: Skip post-execution check of skip conditions. Defaults to false (post-check enabled). */
45
45
  noPostCheckConditions?: boolean
46
46
 
@@ -68,11 +68,11 @@ export class Deployer {
68
68
  private readonly loader: ProjectLoader
69
69
  private readonly noPostCheckConditions: boolean
70
70
  private readonly showSummary: boolean
71
-
71
+
72
72
  // Store both successful and failed execution results
73
73
  private readonly results = new Map<string, {
74
74
  job: Job;
75
- outputs: Map<number, { status: 'success' | 'error'; data: Map<string, unknown> | string }>
75
+ outputs: Map<number, { status: 'success' | 'error' | 'skipped'; data: Map<string, unknown> | string }>
76
76
  }>()
77
77
  private graph?: DependencyGraph
78
78
 
@@ -97,7 +97,7 @@ export class Deployer {
97
97
  projectRoot: this.options.projectRoot
98
98
  }
99
99
  })
100
-
100
+
101
101
  try {
102
102
  // 1. Load all project artifacts, templates, and jobs.
103
103
  this.events.emitEvent({
@@ -107,9 +107,9 @@ export class Deployer {
107
107
  projectRoot: this.options.projectRoot
108
108
  }
109
109
  })
110
-
110
+
111
111
  await this.loader.load()
112
-
112
+
113
113
  this.events.emitEvent({
114
114
  type: 'project_loaded',
115
115
  level: 'info',
@@ -118,7 +118,7 @@ export class Deployer {
118
118
  templateCount: this.loader.templates.size
119
119
  }
120
120
  })
121
-
121
+
122
122
  // 2. Build the dependency graph and determine execution order.
123
123
  const graph = new DependencyGraph(this.loader.jobs, this.loader.templates)
124
124
  this.graph = graph
@@ -142,7 +142,7 @@ export class Deployer {
142
142
  }
143
143
  }
144
144
  const targetNetworks = this.getTargetNetworks()
145
-
145
+
146
146
  this.events.emitEvent({
147
147
  type: 'execution_plan',
148
148
  level: 'info',
@@ -163,12 +163,12 @@ export class Deployer {
163
163
  noPostCheckConditions: this.noPostCheckConditions,
164
164
  ignoreVerifyErrors: this.options.ignoreVerifyErrors ?? false
165
165
  })
166
-
166
+
167
167
  // Track if any jobs have failed
168
168
  let hasFailures = false
169
169
  // Emit signer info once per network (chainId)
170
170
  const signerInfoPrintedForChain = new Set<number>()
171
-
171
+
172
172
  for (const network of targetNetworks) {
173
173
  this.events.emitEvent({
174
174
  type: 'network_started',
@@ -180,7 +180,12 @@ export class Deployer {
180
180
  })
181
181
  for (const jobName of jobsToRun) {
182
182
  const job = this.loader.jobs.get(jobName)!
183
-
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
+
184
189
  if (this.shouldSkipJobOnNetwork(job, network)) {
185
190
  this.events.emitEvent({
186
191
  type: 'job_skipped',
@@ -191,14 +196,16 @@ export class Deployer {
191
196
  reason: 'configuration'
192
197
  }
193
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
+
194
206
  continue
195
207
  }
196
-
197
- // Initialize results storage for this job if not exists
198
- if (!this.results.has(job.name)) {
199
- this.results.set(job.name, { job, outputs: new Map() })
200
- }
201
-
208
+
202
209
  let context: ExecutionContext | undefined
203
210
  try {
204
211
  context = new ExecutionContext(
@@ -212,7 +219,7 @@ export class Deployer {
212
219
  if (typeof (context as unknown as { setJobConstants?: (constants: unknown) => void }).setJobConstants === 'function') {
213
220
  (context as unknown as { setJobConstants: (constants: unknown) => void }).setJobConstants(job.constants)
214
221
  }
215
-
222
+
216
223
  // Emit signer info once per network using the first job's context
217
224
  if (!signerInfoPrintedForChain.has(network.chainId)) {
218
225
  try {
@@ -253,11 +260,35 @@ export class Deployer {
253
260
  }
254
261
  }
255
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
+
256
287
  // Populate context with outputs from previously executed dependent jobs
257
288
  this.populateContextWithDependentJobOutputs(job, context, network)
258
-
289
+
259
290
  await engine.executeJob(job, context)
260
-
291
+
261
292
  // Store successful results
262
293
  this.results.get(job.name)!.outputs.set(network.chainId, {
263
294
  status: 'success',
@@ -270,7 +301,7 @@ export class Deployer {
270
301
  status: 'error',
271
302
  data: errorMessage
272
303
  })
273
-
304
+
274
305
  this.events.emitEvent({
275
306
  type: 'job_execution_failed',
276
307
  level: 'error',
@@ -281,15 +312,15 @@ export class Deployer {
281
312
  error: errorMessage
282
313
  }
283
314
  })
284
-
315
+
285
316
  // Mark that we have failures
286
317
  hasFailures = true
287
-
318
+
288
319
  // If fail-early is enabled, throw the error immediately
289
320
  if (this.options.failEarly) {
290
321
  throw error
291
322
  }
292
-
323
+
293
324
  // Otherwise, continue to next job/network
294
325
  } finally {
295
326
  // Clean up the context to prevent hanging connections
@@ -312,7 +343,7 @@ export class Deployer {
312
343
  }
313
344
  }
314
345
  }
315
-
346
+
316
347
  // 5. Write results to output files.
317
348
  await this.writeOutputFiles()
318
349
 
@@ -386,18 +417,13 @@ export class Deployer {
386
417
  const jobCount = this.results.size
387
418
  let successCount = 0
388
419
  let failedCount = 0
389
- const skippedCount = 0
390
-
391
- // Detect skipped by comparing planned jobs across networks vs executed entries
392
- // Here we approximate: an entry exists per job and per network outcome. We count
393
- // successes/errors; skips were emitted as events during execution and are not persisted
394
- // in results. We cannot perfectly reconstruct skipped count without tracking, so we
395
- // expose it as 0 for now; could be improved by tracking per-network skip events.
420
+ let skippedCount = 0
396
421
 
397
422
  for (const [, result] of this.results) {
398
423
  for (const [, netResult] of result.outputs) {
399
424
  if (netResult.status === 'success') successCount++
400
- else failedCount++
425
+ else if (netResult.status === 'skipped') skippedCount++
426
+ else if (netResult.status === 'error') failedCount++
401
427
  }
402
428
  }
403
429
 
@@ -437,7 +463,7 @@ export class Deployer {
437
463
  */
438
464
  private emitVerificationWarningsReport(engine: ExecutionEngine): void {
439
465
  const warnings = engine.getVerificationWarnings()
440
-
466
+
441
467
  if (warnings.length > 0) {
442
468
  this.events.emitEvent({
443
469
  type: 'verification_warnings_report',
@@ -530,7 +556,7 @@ export class Deployer {
530
556
  const allowed = new Set<string>([...nonDeprecatedJobs, ...requiredDeprecated])
531
557
  return fullOrder.filter(name => allowed.has(name))
532
558
  }
533
-
559
+
534
560
  // Expand patterns to concrete names
535
561
  const expandedRunJobs = expandRunJobs(this.options.runJobs)
536
562
  const explicitlyRequested = new Set<string>(expandedRunJobs)
@@ -557,7 +583,7 @@ export class Deployer {
557
583
  return this.options.runDeprecated === true
558
584
  })
559
585
  const allowedSet = new Set(filtered)
560
-
586
+
561
587
  // Filter the original execution order to only include the required jobs, preserving the correct sequence.
562
588
  return fullOrder.filter(jobName => allowedSet.has(jobName))
563
589
  }
@@ -569,10 +595,10 @@ export class Deployer {
569
595
  if (!this.options.runOnNetworks || this.options.runOnNetworks.length === 0) {
570
596
  return this.options.networks // Run on all configured networks
571
597
  }
572
-
598
+
573
599
  const targetChainIds = new Set(this.options.runOnNetworks)
574
600
  const filteredNetworks = this.options.networks.filter(n => targetChainIds.has(n.chainId))
575
-
601
+
576
602
  if (filteredNetworks.length !== this.options.runOnNetworks.length) {
577
603
  const foundIds = new Set(filteredNetworks.map(n => n.chainId))
578
604
  const missingIds = this.options.runOnNetworks.filter(id => !foundIds.has(id))
@@ -584,7 +610,7 @@ export class Deployer {
584
610
  }
585
611
  })
586
612
  }
587
-
613
+
588
614
  return filteredNetworks
589
615
  }
590
616
 
@@ -611,7 +637,7 @@ export class Deployer {
611
637
  }
612
638
  }
613
639
  }
614
-
640
+
615
641
  // Check minimal EVM hardfork requirement if present on job and network declares an EVM version
616
642
  if (jobWithNetworkFilters.min_evm_version) {
617
643
  const jobMin = this.normalizeEvmVersion(jobWithNetworkFilters.min_evm_version)
@@ -622,7 +648,7 @@ export class Deployer {
622
648
  }
623
649
  // If network has no evmVersion declared, do not skip (assume compatible)
624
650
  }
625
-
651
+
626
652
  return false // Run by default
627
653
  }
628
654
 
@@ -704,6 +730,11 @@ export class Deployer {
704
730
  throw new Error(`Job "${job.name}" depends on "${dependentJobName}", but "${dependentJobName}" has not been executed on network ${network.name} (chainId: ${network.chainId}).`)
705
731
  }
706
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
+
707
738
  if (networkResult.status !== 'success') {
708
739
  const errorMessage = typeof networkResult.data === 'string' ? networkResult.data : 'Unknown error'
709
740
  throw new Error(`Job "${job.name}" depends on "${dependentJobName}", but "${dependentJobName}" failed: ${errorMessage}`)
@@ -736,7 +767,7 @@ export class Deployer {
736
767
 
737
768
  const outputRoot = path.join(this.options.projectRoot, 'output')
738
769
  await fs.mkdir(outputRoot, { recursive: true })
739
-
770
+
740
771
  this.events.emitEvent({
741
772
  type: 'output_writing_started',
742
773
  level: 'info'
@@ -765,7 +796,7 @@ export class Deployer {
765
796
  const outputFilePath = path.join(outputRoot, relativeJobSubpath)
766
797
  const outputFileDir = path.dirname(outputFilePath)
767
798
  await fs.mkdir(outputFileDir, { recursive: true })
768
-
799
+
769
800
  // Group networks by identical status and outputs
770
801
  const groupedResults = this.groupNetworkResults(resultData.outputs, resultData.job)
771
802
 
@@ -775,7 +806,7 @@ export class Deployer {
775
806
  lastRun: new Date().toISOString(),
776
807
  networks: groupedResults
777
808
  }
778
-
809
+
779
810
  await fs.writeFile(outputFilePath, JSON.stringify(fileContent, null, 2))
780
811
  this.events.emitEvent({
781
812
  type: 'output_file_written',
@@ -875,20 +906,20 @@ export class Deployer {
875
906
  */
876
907
  private filterOutDependencyOutputs(outputs: Map<string, unknown>, job: Job): Record<string, unknown> {
877
908
  const filtered = new Map<string, unknown>()
878
-
909
+
879
910
  // Get list of dependency job names
880
911
  const dependencyNames = job.depends_on || []
881
-
912
+
882
913
  for (const [key, value] of outputs) {
883
914
  // Check if this output key starts with any dependency job name prefix
884
915
  const isDependencyOutput = dependencyNames.some(depName => key.startsWith(`${depName}.`))
885
-
916
+
886
917
  // Only include outputs that are NOT from dependencies
887
918
  if (!isDependencyOutput) {
888
919
  filtered.set(key, value)
889
920
  }
890
921
  }
891
-
922
+
892
923
  return Object.fromEntries(filtered)
893
924
  }
894
925
 
@@ -897,8 +928,8 @@ export class Deployer {
897
928
  * - Success states with identical outputs are grouped together with chainIds array
898
929
  * - Error states are kept separate (one entry per network)
899
930
  */
900
- private groupNetworkResults(outputs: Map<number, { status: 'success' | 'error'; data: Map<string, unknown> | string }>, job: Job): Array<{
901
- status: 'success' | 'error';
931
+ private groupNetworkResults(outputs: Map<number, { status: 'success' | 'error' | 'skipped'; data: Map<string, unknown> | string }>, job: Job): Array<{
932
+ status: 'success' | 'error' | 'skipped';
902
933
  chainIds?: string[];
903
934
  chainId?: string;
904
935
  outputs?: Record<string, unknown>;
@@ -910,20 +941,20 @@ export class Deployer {
910
941
  chainId: string;
911
942
  error: string;
912
943
  }> = []
913
-
944
+
914
945
  for (const [chainId, result] of outputs.entries()) {
915
946
  if (result.status === 'success') {
916
947
  // Group successful results by identical outputs, filtered by action output flags
917
948
  const outputsObj = result.data instanceof Map ? this.filterOutputsByActionFlags(result.data, job) : {}
918
949
  const key = JSON.stringify(outputsObj)
919
-
950
+
920
951
  if (!successGroups.has(key)) {
921
952
  successGroups.set(key, {
922
953
  chainIds: [],
923
954
  outputs: outputsObj
924
955
  })
925
956
  }
926
-
957
+
927
958
  successGroups.get(key)!.chainIds.push(chainId.toString())
928
959
  } else {
929
960
  // Keep error results separate - one entry per network
@@ -934,14 +965,14 @@ export class Deployer {
934
965
  })
935
966
  }
936
967
  }
937
-
968
+
938
969
  // Convert success groups to array format
939
970
  const successEntries = Array.from(successGroups.values()).map(group => ({
940
971
  status: 'success' as const,
941
972
  chainIds: group.chainIds.sort(), // Sort for consistent output
942
973
  outputs: group.outputs
943
974
  }))
944
-
975
+
945
976
  // Return all entries: successes first, then errors
946
977
  return [...successEntries, ...errorEntries]
947
978
  }
@@ -1,3 +0,0 @@
1
- import { Network } from './types';
2
- export declare function resolveRequestedNetworks(requests: string[], networks: Network[]): number[];
3
- //# sourceMappingURL=network-match.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"network-match.d.ts","sourceRoot":"","sources":["../../src/lib/network-match.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AA8BjC,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAyC1F"}
@@ -1,62 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.resolveRequestedNetworks = resolveRequestedNetworks;
4
- function normalize(input) {
5
- return input
6
- .toLowerCase()
7
- .replace(/[^a-z0-9]+/g, ' ')
8
- .replace(/\s+/g, ' ')
9
- .trim();
10
- }
11
- function scoreNameMatch(candidateName, query) {
12
- const c = normalize(candidateName);
13
- const q = normalize(query);
14
- if (!q || !c)
15
- return 0;
16
- if (c === q)
17
- return 100;
18
- if (c.startsWith(q))
19
- return 90;
20
- if (c.includes(q))
21
- return 75;
22
- const qTokens = q.split(' ');
23
- const allTokensPresent = qTokens.every(t => c.includes(t));
24
- if (allTokensPresent)
25
- return 70;
26
- return 0;
27
- }
28
- function resolveRequestedNetworks(requests, networks) {
29
- const chainIds = [];
30
- for (const req of requests) {
31
- const asNumber = Number(req);
32
- if (!Number.isNaN(asNumber)) {
33
- const found = networks.find(n => n.chainId === asNumber);
34
- if (found) {
35
- chainIds.push(found.chainId);
36
- continue;
37
- }
38
- }
39
- let bestScore = -1;
40
- let bestMatches = [];
41
- for (const n of networks) {
42
- const s = scoreNameMatch(n.name, req);
43
- if (s > bestScore) {
44
- bestScore = s;
45
- bestMatches = [n];
46
- }
47
- else if (s === bestScore && s > 0) {
48
- bestMatches.push(n);
49
- }
50
- }
51
- if (bestScore < 60 || bestMatches.length === 0) {
52
- throw new Error(`No network matched "${req}". Available: ${networks.map(n => `${n.name} (${n.chainId})`).join(', ')}`);
53
- }
54
- if (bestMatches.length > 1) {
55
- const candidates = bestMatches.map(n => `${n.name} (${n.chainId})`).join(', ');
56
- throw new Error(`Ambiguous network "${req}". Possible matches: ${candidates}`);
57
- }
58
- chainIds.push(bestMatches[0].chainId);
59
- }
60
- return chainIds;
61
- }
62
- //# sourceMappingURL=network-match.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"network-match.js","sourceRoot":"","sources":["../../src/lib/network-match.ts"],"names":[],"mappings":"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"}