From dab128486560c2dbe0ca2fdbd52d551ff6f7916f Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 28 Sep 2026 10:15:19 +1300 Subject: [PATCH 1/4] feat: add per-network job filters --- CHANGELOG.md | 6 ++ README.md | 20 +++++++ src/commands/dry.ts | 8 +++ src/commands/run.ts | 2 + src/lib/__tests__/deployer.spec.ts | 16 +++++ src/lib/__tests__/network-job-filters.spec.ts | 59 +++++++++++++++++++ src/lib/__tests__/network-loader.spec.ts | 37 ++++++++++++ src/lib/deployer.ts | 18 +++--- src/lib/network-job-filters.ts | 50 ++++++++++++++++ src/lib/network-loader.ts | 7 +++ src/lib/types/network.ts | 11 ++++ 11 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 src/lib/__tests__/network-job-filters.spec.ts create mode 100644 src/lib/network-job-filters.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 07c6212..7ed4c1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Added + +- Add `onlyJobs` and `skipJobs` to `networks.yaml` entries for filtering jobs per network by name pattern. `onlyJobs` keeps the transitive dependencies of the jobs it matches. + ## 2.0.0 ### Breaking changes diff --git a/README.md b/README.md index 827ecbd..8b786cb 100644 --- a/README.md +++ b/README.md @@ -239,6 +239,26 @@ Rules: - Else, if `skip_networks` is set and non-empty, the job is skipped on those chain IDs. - Otherwise, the job runs on all networks selected for the run (via `networks.yaml` or `--network`). +#### Per-network job filters + +A network can restrict or exclude jobs by name in `networks.yaml`. Patterns support `*` and `?` wildcards: + +```yaml +- name: "Monad" + chainId: 143 + rpcUrl: "https://rpc.monad.xyz" + # Run only these jobs (plus their dependencies) on this network + onlyJobs: ["trails-*"] + # Skip these jobs on this network, applied after onlyJobs + skipJobs: ["trails-rc-1"] +``` + +Rules: +- If `onlyJobs` is set, jobs it matches run along with their transitive dependencies; every other job is skipped on that network. +- `skipJobs` then skips every job it matches, including dependencies pulled in by `onlyJobs`. +- A pattern that matches no job is an error, so a typo cannot silently allow or skip nothing. +- Network job filters combine with the per-job filters above: a job runs only if both allow it. + #### Minimum EVM version per job Jobs can declare a minimum EVM hardfork they require. When a network’s `evmVersion` is older than the job’s `min_evm_version`, the job is skipped on that network. diff --git a/src/commands/dry.ts b/src/commands/dry.ts index 83f3a7c..74e6f05 100644 --- a/src/commands/dry.ts +++ b/src/commands/dry.ts @@ -7,6 +7,7 @@ import { projectOption, noStdOption, verbosityOption } from './common' import { validateContractReferences, extractUsedContractReferences } from '../lib/validation/contract-references' import { setVerbosity } from '../index' import { resolveSelectedChainIds } from '../lib/network-selection' +import { getNetworkExcludedJobs } from '../lib/network-job-filters' import { Template } from '../lib/types' interface DryRunOptions { @@ -195,6 +196,13 @@ export function makeDryRunCommand(): Command { console.log(chalk.blue('\nExecution Plan:')) console.log(chalk.gray(` - Target Networks: ${targetNetworks.map(n => `${n.name} (ChainID: ${n.chainId})`).join(', ')}`)) console.log(chalk.gray(` - Job Execution Order: ${jobExecutionPlan.join(' -> ')}`)) + for (const network of targetNetworks) { + const excluded = getNetworkExcludedJobs(network, fullOrder, graph) + const skipped = jobExecutionPlan.filter(jobName => excluded.has(jobName)) + if (skipped.length > 0) { + console.log(chalk.gray(` - Skipped on ${network.name} (ChainID: ${network.chainId}) by network job filters: ${skipped.join(', ')}`)) + } + } console.log(chalk.green.bold('\n✅ Dry run successful. All job and template definitions appear to be valid.')) diff --git a/src/commands/run.ts b/src/commands/run.ts index 232f564..bc42c93 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -98,6 +98,8 @@ export function makeRunCommand(): Command { gasLimit: detectedNetwork.gasLimit || knownNetwork?.gasLimit, testnet: detectedNetwork.testnet !== undefined ? detectedNetwork.testnet : knownNetwork?.testnet, evmVersion: detectedNetwork.evmVersion || knownNetwork?.evmVersion, + onlyJobs: knownNetwork?.onlyJobs, + skipJobs: knownNetwork?.skipJobs, params: detectedNetwork.params || knownNetwork?.params, } diff --git a/src/lib/__tests__/deployer.spec.ts b/src/lib/__tests__/deployer.spec.ts index 30a6bec..af27fa6 100644 --- a/src/lib/__tests__/deployer.spec.ts +++ b/src/lib/__tests__/deployer.spec.ts @@ -259,6 +259,22 @@ describe('Deployer', () => { expect(usedNetwork.chainId).toBe(1) }) + it('should skip jobs excluded by network job filters', async () => { + const options: DeployerOptions = { + ...deployerOptions, + networks: [mockNetwork1, { ...mockNetwork2, skipJobs: ['job2'] }] + } + + const deployer = new Deployer(options) + await deployer.run() + + const job2Networks = mockEngine.executeJob.mock.calls + .map((call, index) => ({ job: call[0].name, chainId: MockExecutionContext.mock.calls[index][0].chainId })) + .filter(c => c.job === 'job2') + .map(c => c.chainId) + expect(job2Networks).toEqual([1]) + }) + it('should create correct output files in flat mode', async () => { const deployer = new Deployer({ ...deployerOptions, flatOutput: true }) await deployer.run() diff --git a/src/lib/__tests__/network-job-filters.spec.ts b/src/lib/__tests__/network-job-filters.spec.ts new file mode 100644 index 0000000..cc9e41a --- /dev/null +++ b/src/lib/__tests__/network-job-filters.spec.ts @@ -0,0 +1,59 @@ +import { getNetworkExcludedJobs, jobPatternToRegex } from '../network-job-filters' +import { DependencyGraph } from '../core/graph' +import { Network } from '../types' + +const jobNames = ['erc2470', 'multicall3', 'sequence-v3', 'trails-rc-1', 'trails-utils', 'builder-factories'] + +const dependencies: Record = { + 'trails-rc-1': ['erc2470', 'multicall3'], + 'trails-utils': ['erc2470'], + 'builder-factories': ['erc2470'], +} + +const graph = { + getDependencies: (jobName: string) => new Set(dependencies[jobName] ?? []), +} as unknown as DependencyGraph + +const network = (filters: Pick): Network => ({ + name: 'monad', + chainId: 143, + rpcUrl: 'http://127.0.0.1:8545', + ...filters, +}) + +describe('jobPatternToRegex', () => { + it('matches wildcards and escapes regex metacharacters', () => { + expect(jobPatternToRegex('trails-*').test('trails-rc-1')).toBe(true) + expect(jobPatternToRegex('trails-rc-?').test('trails-rc-1')).toBe(true) + expect(jobPatternToRegex('sequence_v3/rc.1').test('sequence_v3/rcx1')).toBe(false) + expect(jobPatternToRegex('trails').test('trails-rc-1')).toBe(false) + }) +}) + +describe('getNetworkExcludedJobs', () => { + it('excludes nothing without filters', () => { + expect(getNetworkExcludedJobs(network({}), jobNames, graph)).toEqual(new Set()) + }) + + it('keeps onlyJobs matches and their dependencies', () => { + const excluded = getNetworkExcludedJobs(network({ onlyJobs: ['trails-*'] }), jobNames, graph) + expect(excluded).toEqual(new Set(['sequence-v3', 'builder-factories'])) + }) + + it('excludes skipJobs matches', () => { + const excluded = getNetworkExcludedJobs(network({ skipJobs: ['builder-*', 'sequence-v3'] }), jobNames, graph) + expect(excluded).toEqual(new Set(['builder-factories', 'sequence-v3'])) + }) + + it('applies skipJobs after onlyJobs, including to dependencies', () => { + const excluded = getNetworkExcludedJobs(network({ onlyJobs: ['trails-*'], skipJobs: ['multicall3'] }), jobNames, graph) + expect(excluded).toEqual(new Set(['sequence-v3', 'builder-factories', 'multicall3'])) + }) + + it('throws when a pattern matches no job', () => { + expect(() => getNetworkExcludedJobs(network({ onlyJobs: ['trials-*'] }), jobNames, graph)) + .toThrow('Network "monad" (chainId: 143) onlyJobs pattern "trials-*" did not match any jobs in project.') + expect(() => getNetworkExcludedJobs(network({ skipJobs: ['missing'] }), jobNames, graph)) + .toThrow('skipJobs pattern "missing"') + }) +}) diff --git a/src/lib/__tests__/network-loader.spec.ts b/src/lib/__tests__/network-loader.spec.ts index f7b58a5..916d98e 100644 --- a/src/lib/__tests__/network-loader.spec.ts +++ b/src/lib/__tests__/network-loader.spec.ts @@ -149,3 +149,40 @@ describe('network-loader params', () => { await expect(loadNetworks(projectRoot)).rejects.toThrow(/Failed to load or parse networks.yaml/) }) }) + +describe('network-loader job filters', () => { + afterAll(async () => { + try { + await fs.rm(tmpDir, { recursive: true, force: true }) + } catch {} + }) + + test('passes through onlyJobs and skipJobs', async () => { + const projectRoot = path.join(tmpDir, 'job-filters-valid') + const yaml = ` +- name: "MyNet" + chainId: 143 + rpcUrl: "http://127.0.0.1:8545" + onlyJobs: ["trails-*"] + skipJobs: ["trails-rc-1"] +` + await writeNetworksYaml(projectRoot, yaml) + + const networks = await loadNetworks(projectRoot) + expect(networks[0].onlyJobs).toEqual(['trails-*']) + expect(networks[0].skipJobs).toEqual(['trails-rc-1']) + }) + + test('rejects skipJobs when not an array of strings', async () => { + const projectRoot = path.join(tmpDir, 'job-filters-invalid') + const yaml = ` +- name: "BadNet" + chainId: 143 + rpcUrl: "http://127.0.0.1:8545" + skipJobs: "trails-*" +` + await writeNetworksYaml(projectRoot, yaml) + + await expect(loadNetworks(projectRoot)).rejects.toThrow(/Failed to load or parse networks.yaml/) + }) +}) diff --git a/src/lib/deployer.ts b/src/lib/deployer.ts index f740c07..c18b68a 100644 --- a/src/lib/deployer.ts +++ b/src/lib/deployer.ts @@ -7,6 +7,7 @@ import { ExecutionEngine } from './core/engine' import { createDefaultVerificationRegistry } from './verification/etherscan' import { ExecutionContext } from './core/context' import { Network, Job } from './types' +import { getNetworkExcludedJobs, jobPatternToRegex } from './network-job-filters' import { DeploymentEventEmitter, deploymentEvents } from './events' import type { RunSummaryEvent } from './events' @@ -181,6 +182,11 @@ export class Deployer { // Emit signer info once per network (chainId) const signerInfoPrintedForChain = new Set() + const allJobNames = Array.from(this.loader.jobs.keys()) + const networkExcludedJobs = new Map>( + targetNetworks.map(n => [n.chainId, getNetworkExcludedJobs(n, allJobNames, graph)]) + ) + for (const network of targetNetworks) { this.events.emitEvent({ type: 'network_started', @@ -198,7 +204,7 @@ export class Deployer { this.results.set(job.name, { job, outputs: new Map() }) } - if (this.shouldSkipJobOnNetwork(job, network)) { + if (networkExcludedJobs.get(network.chainId)!.has(jobName) || this.shouldSkipJobOnNetwork(job, network)) { this.events.emitEvent({ type: 'job_skipped', level: 'warn', @@ -523,14 +529,6 @@ export class Deployer { const allJobNames = Array.from(this.loader.jobs.keys()) const isPattern = (s: string): boolean => /[*?]/.test(s) - const escapeRegex = (s: string): string => s.replace(/[-\\^$+?.()|[\]{}*?]/g, '\\$&') - const patternToRegex = (pattern: string): RegExp => { - // Escape regex metacharacters, then translate wildcard tokens - const escaped = escapeRegex(pattern) - .replace(/\\\*/g, '.*') // escaped '*' -> '.*' - .replace(/\\\?/g, '.') // escaped '?' -> '.' - return new RegExp(`^${escaped}$`) - } const expanded: string[] = [] const seen = new Set() @@ -548,7 +546,7 @@ export class Deployer { continue } - const re = patternToRegex(p) + const re = jobPatternToRegex(p) const matches = allJobNames.filter(name => re.test(name)) if (matches.length === 0) { throw new Error(`Job pattern "${p}" did not match any jobs in project.`) diff --git a/src/lib/network-job-filters.ts b/src/lib/network-job-filters.ts new file mode 100644 index 0000000..a6753c7 --- /dev/null +++ b/src/lib/network-job-filters.ts @@ -0,0 +1,50 @@ +import { DependencyGraph } from './core/graph' +import { Network } from './types' + +/** + * Converts a job name pattern into an anchored regex. `*` matches any run of characters, `?` one character. + */ +export function jobPatternToRegex(pattern: string): RegExp { + const escaped = pattern + .replace(/[-\\^$+?.()|[\]{}*]/g, '\\$&') + .replace(/\\\*/g, '.*') + .replace(/\\\?/g, '.') + return new RegExp(`^${escaped}$`) +} + +function matchJobs(patterns: string[], jobNames: string[], network: Network, field: string): string[] { + const matched: string[] = [] + for (const pattern of patterns) { + const re = jobPatternToRegex(pattern) + const matches = jobNames.filter(name => re.test(name)) + if (matches.length === 0) { + throw new Error(`Network "${network.name}" (chainId: ${network.chainId}) ${field} pattern "${pattern}" did not match any jobs in project.`) + } + matched.push(...matches) + } + return matched +} + +/** + * Returns the jobs a network excludes through its `onlyJobs` and `skipJobs` fields. + * Jobs matched by `onlyJobs` keep their transitive dependencies; `skipJobs` is applied afterwards. + * Throws when a pattern matches no job, so a mistyped pattern cannot silently allow or skip nothing. + */ +export function getNetworkExcludedJobs(network: Network, jobNames: string[], graph: DependencyGraph): Set { + const excluded = new Set() + + if (network.onlyJobs && network.onlyJobs.length > 0) { + const allowed = new Set() + for (const name of matchJobs(network.onlyJobs, jobNames, network, 'onlyJobs')) { + allowed.add(name) + graph.getDependencies(name).forEach(dep => allowed.add(dep)) + } + jobNames.filter(name => !allowed.has(name)).forEach(name => excluded.add(name)) + } + + if (network.skipJobs && network.skipJobs.length > 0) { + matchJobs(network.skipJobs, jobNames, network, 'skipJobs').forEach(name => excluded.add(name)) + } + + return excluded +} diff --git a/src/lib/network-loader.ts b/src/lib/network-loader.ts index 3a7bb76..1a07a32 100644 --- a/src/lib/network-loader.ts +++ b/src/lib/network-loader.ts @@ -3,6 +3,10 @@ import * as path from 'path' import { parse as parseYaml } from 'yaml' import { Network } from './types' +function isStringArray(value: unknown): boolean { + return Array.isArray(value) && value.every((item: unknown) => typeof item === 'string') +} + function isValidNetwork(obj: unknown): obj is Network { return ( typeof obj === 'object' && @@ -23,6 +27,9 @@ function isValidNetwork(obj: unknown): obj is Network { (!('testnet' in obj) || typeof (obj as Record).testnet === 'boolean') && // evmVersion field is optional and should be a string if present (!('evmVersion' in obj) || typeof (obj as Record).evmVersion === 'string') && + // onlyJobs and skipJobs are optional arrays of job name patterns + (!('onlyJobs' in obj) || isStringArray((obj as Record).onlyJobs)) && + (!('skipJobs' in obj) || isStringArray((obj as Record).skipJobs)) && // params field is optional and must be a plain object if present (!('params' in obj) || ( typeof (obj as Record).params === 'object' && diff --git a/src/lib/types/network.ts b/src/lib/types/network.ts index 3fdf498..df71484 100644 --- a/src/lib/types/network.ts +++ b/src/lib/types/network.ts @@ -26,6 +26,17 @@ export interface Network { */ evmVersion?: string + /** + * Job name patterns allowed on this network (`*` and `?` wildcards). Matched jobs keep their + * dependencies; every other job is skipped. + */ + onlyJobs?: string[] + + /** + * Job name patterns skipped on this network (`*` and `?` wildcards). Applied after `onlyJobs`. + */ + skipJobs?: string[] + /** * Integrator-owned metadata bag. Keys are not validated by Catapult. */ From b7728bcd80fd1d311c24430cb14cac9d82d57d27 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 28 Sep 2026 10:18:24 +1300 Subject: [PATCH 2/4] fix: ignore job filter patterns that match nothing --- README.md | 2 +- src/lib/__tests__/network-job-filters.spec.ts | 8 +++----- src/lib/network-job-filters.ts | 19 +++++-------------- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8b786cb..7f960c4 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ A network can restrict or exclude jobs by name in `networks.yaml`. Patterns supp Rules: - If `onlyJobs` is set, jobs it matches run along with their transitive dependencies; every other job is skipped on that network. - `skipJobs` then skips every job it matches, including dependencies pulled in by `onlyJobs`. -- A pattern that matches no job is an error, so a typo cannot silently allow or skip nothing. +- A pattern that matches no job matches nothing. With no `onlyJobs` matches, every job is skipped on that network. - Network job filters combine with the per-job filters above: a job runs only if both allow it. #### Minimum EVM version per job diff --git a/src/lib/__tests__/network-job-filters.spec.ts b/src/lib/__tests__/network-job-filters.spec.ts index cc9e41a..ad7e351 100644 --- a/src/lib/__tests__/network-job-filters.spec.ts +++ b/src/lib/__tests__/network-job-filters.spec.ts @@ -50,10 +50,8 @@ describe('getNetworkExcludedJobs', () => { expect(excluded).toEqual(new Set(['sequence-v3', 'builder-factories', 'multicall3'])) }) - it('throws when a pattern matches no job', () => { - expect(() => getNetworkExcludedJobs(network({ onlyJobs: ['trials-*'] }), jobNames, graph)) - .toThrow('Network "monad" (chainId: 143) onlyJobs pattern "trials-*" did not match any jobs in project.') - expect(() => getNetworkExcludedJobs(network({ skipJobs: ['missing'] }), jobNames, graph)) - .toThrow('skipJobs pattern "missing"') + it('ignores patterns that match no job', () => { + expect(getNetworkExcludedJobs(network({ onlyJobs: ['missing-*'] }), jobNames, graph)).toEqual(new Set(jobNames)) + expect(getNetworkExcludedJobs(network({ skipJobs: ['missing'] }), jobNames, graph)).toEqual(new Set()) }) }) diff --git a/src/lib/network-job-filters.ts b/src/lib/network-job-filters.ts index a6753c7..54ee7a1 100644 --- a/src/lib/network-job-filters.ts +++ b/src/lib/network-job-filters.ts @@ -12,30 +12,21 @@ export function jobPatternToRegex(pattern: string): RegExp { return new RegExp(`^${escaped}$`) } -function matchJobs(patterns: string[], jobNames: string[], network: Network, field: string): string[] { - const matched: string[] = [] - for (const pattern of patterns) { - const re = jobPatternToRegex(pattern) - const matches = jobNames.filter(name => re.test(name)) - if (matches.length === 0) { - throw new Error(`Network "${network.name}" (chainId: ${network.chainId}) ${field} pattern "${pattern}" did not match any jobs in project.`) - } - matched.push(...matches) - } - return matched +function matchJobs(patterns: string[], jobNames: string[]): string[] { + const regexes = patterns.map(jobPatternToRegex) + return jobNames.filter(name => regexes.some(re => re.test(name))) } /** * Returns the jobs a network excludes through its `onlyJobs` and `skipJobs` fields. * Jobs matched by `onlyJobs` keep their transitive dependencies; `skipJobs` is applied afterwards. - * Throws when a pattern matches no job, so a mistyped pattern cannot silently allow or skip nothing. */ export function getNetworkExcludedJobs(network: Network, jobNames: string[], graph: DependencyGraph): Set { const excluded = new Set() if (network.onlyJobs && network.onlyJobs.length > 0) { const allowed = new Set() - for (const name of matchJobs(network.onlyJobs, jobNames, network, 'onlyJobs')) { + for (const name of matchJobs(network.onlyJobs, jobNames)) { allowed.add(name) graph.getDependencies(name).forEach(dep => allowed.add(dep)) } @@ -43,7 +34,7 @@ export function getNetworkExcludedJobs(network: Network, jobNames: string[], gra } if (network.skipJobs && network.skipJobs.length > 0) { - matchJobs(network.skipJobs, jobNames, network, 'skipJobs').forEach(name => excluded.add(name)) + matchJobs(network.skipJobs, jobNames).forEach(name => excluded.add(name)) } return excluded From 0c98251df3a0765dd932be3a499ec3ab923a85d4 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Mon, 28 Sep 2026 12:52:25 +1300 Subject: [PATCH 3/4] test: cover onlyJobs job with a skipJobs dependency --- src/lib/__tests__/deployer.spec.ts | 17 +++++++++++++++++ src/lib/__tests__/network-job-filters.spec.ts | 10 ++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/lib/__tests__/deployer.spec.ts b/src/lib/__tests__/deployer.spec.ts index af27fa6..96e48ed 100644 --- a/src/lib/__tests__/deployer.spec.ts +++ b/src/lib/__tests__/deployer.spec.ts @@ -275,6 +275,23 @@ describe('Deployer', () => { expect(job2Networks).toEqual([1]) }) + it('should run an onlyJobs job without its skipJobs dependency', async () => { + // job2 depends on job1 + mockGraph.getDependencies.mockImplementation((jobName: string) => + new Set(jobName === 'job2' ? ['job1'] : []) + ) + const options: DeployerOptions = { + ...deployerOptions, + networks: [{ ...mockNetwork2, onlyJobs: ['job2'], skipJobs: ['job1'] }] + } + + const deployer = new Deployer(options) + await deployer.run() + + const executedJobs = mockEngine.executeJob.mock.calls.map(call => call[0].name) + expect(executedJobs).toEqual(['job2']) + }) + it('should create correct output files in flat mode', async () => { const deployer = new Deployer({ ...deployerOptions, flatOutput: true }) await deployer.run() diff --git a/src/lib/__tests__/network-job-filters.spec.ts b/src/lib/__tests__/network-job-filters.spec.ts index ad7e351..7827015 100644 --- a/src/lib/__tests__/network-job-filters.spec.ts +++ b/src/lib/__tests__/network-job-filters.spec.ts @@ -50,6 +50,16 @@ describe('getNetworkExcludedJobs', () => { expect(excluded).toEqual(new Set(['sequence-v3', 'builder-factories', 'multicall3'])) }) + it('runs only an onlyJobs job when skipJobs removes its dependency', () => { + const jobs = ['job-a', 'job-b'] + const abGraph = { + getDependencies: (jobName: string) => new Set(jobName === 'job-a' ? ['job-b'] : []), + } as unknown as DependencyGraph + + const excluded = getNetworkExcludedJobs(network({ onlyJobs: ['job-a'], skipJobs: ['job-b'] }), jobs, abGraph) + expect(jobs.filter(name => !excluded.has(name))).toEqual(['job-a']) + }) + it('ignores patterns that match no job', () => { expect(getNetworkExcludedJobs(network({ onlyJobs: ['missing-*'] }), jobNames, graph)).toEqual(new Set(jobNames)) expect(getNetworkExcludedJobs(network({ skipJobs: ['missing'] }), jobNames, graph)).toEqual(new Set()) From d34c5b4c6bbbd8a4a2876fec721d6f6ed5791a19 Mon Sep 17 00:00:00 2001 From: Michael Standen Date: Tue, 29 Sep 2026 09:03:44 +1300 Subject: [PATCH 4/4] test: cover network and job filter combinations --- src/lib/__tests__/deployer.spec.ts | 83 ++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/src/lib/__tests__/deployer.spec.ts b/src/lib/__tests__/deployer.spec.ts index 96e48ed..50fe8e8 100644 --- a/src/lib/__tests__/deployer.spec.ts +++ b/src/lib/__tests__/deployer.spec.ts @@ -292,6 +292,89 @@ describe('Deployer', () => { expect(executedJobs).toEqual(['job2']) }) + describe('network and job filter combinations', () => { + // Each network filter crossed with each job filter, for a single job on polygon (137). + // The job runs only when both the network and the job allow it. + const networkFilters: { name: string; filters: Pick; allows: boolean }[] = [ + { name: 'no network filters', filters: {}, allows: true }, + { name: 'empty onlyJobs and skipJobs', filters: { onlyJobs: [], skipJobs: [] }, allows: true }, + { name: 'onlyJobs matching the job', filters: { onlyJobs: ['target*'] }, allows: true }, + { name: 'onlyJobs not matching the job', filters: { onlyJobs: ['other-*'] }, allows: false }, + { name: 'skipJobs matching the job', filters: { skipJobs: ['target'] }, allows: false }, + { name: 'skipJobs not matching the job', filters: { skipJobs: ['other-*'] }, allows: true }, + { name: 'onlyJobs and skipJobs both matching the job', filters: { onlyJobs: ['target*'], skipJobs: ['target'] }, allows: false }, + ] + const jobFilters: { name: string; filters: Partial; allows: boolean }[] = [ + { name: 'no job filters', filters: {}, allows: true }, + { name: 'empty only_networks and skip_networks', filters: { only_networks: [], skip_networks: [] }, allows: true }, + { name: 'only_networks including the network', filters: { only_networks: [137] }, allows: true }, + { name: 'only_networks excluding the network', filters: { only_networks: [1] }, allows: false }, + { name: 'skip_networks including the network', filters: { skip_networks: [137] }, allows: false }, + { name: 'skip_networks excluding the network', filters: { skip_networks: [1] }, allows: true }, + { name: 'only_networks and skip_networks both including the network', filters: { only_networks: [137], skip_networks: [137] }, allows: true }, + { name: 'min_evm_version above the network evmVersion', filters: { min_evm_version: 'prague' }, allows: false }, + { name: 'min_evm_version at the network evmVersion', filters: { min_evm_version: 'cancun' }, allows: true }, + ] + const matrix = networkFilters.flatMap(n => jobFilters.map(j => ({ + networkCase: n.name, + jobCase: j.name, + networkFilters: n.filters, + jobFilters: j.filters, + runs: n.allows && j.allows, + }))) + + it.each(matrix)('$networkCase + $jobCase: runs=$runs', async ({ networkFilters, jobFilters, runs }) => { + const job: Job = { ...mockJob1, name: 'target', ...jobFilters } + mockLoader.jobs = new Map([['target', job]]) + mockGraph.getExecutionOrder.mockReturnValue(['target']) + + const deployer = new Deployer({ + ...deployerOptions, + networks: [{ ...mockNetwork2, evmVersion: 'cancun', ...networkFilters }] + }) + await deployer.run() + + expect(mockEngine.executeJob).toHaveBeenCalledTimes(runs ? 1 : 0) + }) + + describe('dependencies kept by onlyJobs', () => { + const runOnPolygon = async (depFilters: Partial, targetFilters: Partial): Promise => { + mockLoader.jobs = new Map([ + ['dep', { ...mockJob1, name: 'dep', ...depFilters }], + ['target', { ...mockJob1, name: 'target', depends_on: ['dep'], ...targetFilters }] + ]) + mockGraph.getExecutionOrder.mockReturnValue(['dep', 'target']) + mockGraph.getDependencies.mockImplementation((jobName: string) => + new Set(jobName === 'target' ? ['dep'] : []) + ) + + const deployer = new Deployer({ + ...deployerOptions, + networks: [{ ...mockNetwork2, onlyJobs: ['target'] }] + }) + await deployer.run() + + return mockEngine.executeJob.mock.calls.map(call => call[0].name) + } + + it('runs the dependency when it has no job filters', async () => { + expect(await runOnPolygon({}, {})).toEqual(['dep', 'target']) + }) + + it('skips the dependency when its skip_networks includes the network', async () => { + expect(await runOnPolygon({ skip_networks: [137] }, {})).toEqual(['target']) + }) + + it('skips the dependency when its only_networks excludes the network', async () => { + expect(await runOnPolygon({ only_networks: [1] }, {})).toEqual(['target']) + }) + + it('runs the dependency when the matched job is skipped by its own skip_networks', async () => { + expect(await runOnPolygon({}, { skip_networks: [137] })).toEqual(['dep']) + }) + }) + }) + it('should create correct output files in flat mode', async () => { const deployer = new Deployer({ ...deployerOptions, flatOutput: true }) await deployer.run()