diff --git a/.eslintrc b/.eslintrc index a1c18e3..7e0e673 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,14 +1,14 @@ { - "extends": ["plugin:@typescript-eslint/recommended", "prettier"], - "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], - "rules": { - "@typescript-eslint/explicit-function-return-type": [ - "error", - { "allowExpressions": true } - ], - "@typescript-eslint/explicit-member-accessibility": "off", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-parameter-properties": "off" - } + "extends": ["plugin:@typescript-eslint/recommended", "prettier"], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "rules": { + "@typescript-eslint/explicit-function-return-type": [ + "error", + { "allowExpressions": true } + ], + "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-parameter-properties": "off" + } } diff --git a/.prettierrc.json b/.prettierrc.json index 64a6294..69cd794 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,6 +1,6 @@ { - "trailingComma": "all", - "tabWidth": 4, - "singleQuote": true, - "semi": true + "trailingComma": "all", + "tabWidth": 2, + "singleQuote": true, + "semi": true } diff --git a/.travis.yml b/.travis.yml index 9a4f9a7..f059f4f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,17 @@ language: node_js node_js: - - 12 - - 14 - - 16 + - 12 + - 14 + - 16 jobs: - include: - # Define the release stage that runs semantic-release - - stage: release - node_js: lts/* - # Advanced: optionally overwrite your default `script` step to skip the tests - # script: skip - deploy: - provider: script - skip_cleanup: true - script: - - npx semantic-release + include: + # Define the release stage that runs semantic-release + - stage: release + node_js: lts/* + # Advanced: optionally overwrite your default `script` step to skip the tests + # script: skip + deploy: + provider: script + skip_cleanup: true + script: + - npx semantic-release diff --git a/README.md b/README.md index c2524f7..e79536a 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ All standard Buildkite features are supported. The main advantage of using this module is: -- Easy reuse and recombination of steps -- Defining dependencies between steps explicitly -- Wait steps are not defined explicitly and manually managed but derived from the graph, always providing the most optimized graph -- Steps can be defined conditionally via an acceptor function, allowing for completely dynamic pipelines -- The graph can be serialzed into [dot](https://www.graphviz.org/) format, allowing you to see the whole of the pipeline in one glance. Clusters denote which parts of the graph are dependendent. -- Timeouts can be defined on a per-command basis, the step will then accumulate the timeouts accordingly +- Easy reuse and recombination of steps +- Defining dependencies between steps explicitly +- Wait steps are not defined explicitly and manually managed but derived from the graph, always providing the most optimized graph +- Steps can be defined conditionally via an acceptor function, allowing for completely dynamic pipelines +- The graph can be serialzed into [dot](https://www.graphviz.org/) format, allowing you to see the whole of the pipeline in one glance. Clusters denote which parts of the graph are dependendent. +- Timeouts can be defined on a per-command basis, the step will then accumulate the timeouts accordingly ## Example in a nutshell @@ -20,20 +20,20 @@ The main advantage of using this module is: const install = new Command('yarn', 2); const lint = new CommandStep([install, new Command('yarn lint', 1)]).withKey( - 'lint', + 'lint', ); const test = new CommandStep([install, new Command('yarn test', 2)]) - .withKey('test') - .dependsOn(lint); + .withKey('test') + .dependsOn(lint); const build = new CommandStep([install, new Command('yarn build', 5)]) - .withKey('build') - .dependsOn(lint); + .withKey('build') + .dependsOn(lint); const integration = new CommandStep([ - install, - new Command('yarn integration', 10), + install, + new Command('yarn integration', 10), ]) - .withKey('integration-test') - .dependsOn(build); + .withKey('integration-test') + .dependsOn(build); const pipeline = new Pipeline('My pipeline').add(test).add(integration); ``` @@ -44,24 +44,24 @@ will serialize to: ```yaml steps: - - command: - - yarn - - yarn lint - timeout_in_minutes: 3 - - wait: ~ - - command: - - yarn - - yarn build - timeout_in_minutes: 7 - - command: - - yarn - - yarn test - timeout_in_minutes: 4 - - wait: ~ - - command: - - yarn - - yarn integration - timeout_in_minutes: 12 + - command: + - yarn + - yarn lint + timeout_in_minutes: 3 + - wait: ~ + - command: + - yarn + - yarn build + timeout_in_minutes: 7 + - command: + - yarn + - yarn test + timeout_in_minutes: 4 + - wait: ~ + - command: + - yarn + - yarn integration + timeout_in_minutes: 12 ``` > Did you see how the `wait` step got added for you? How cool is that, hey :) @@ -72,41 +72,39 @@ Since version 5 we also support the [new `depends_on` syntax](https://buildkite. ```yaml steps: - - key: lint - command: - - yarn - - yarn lint - timeout_in_minutes: 3 - - key: build - depends_on: - - step: lint - command: - - yarn - - yarn build - timeout_in_minutes: 7 - - key: test - depends_on: - - step: lint - command: - - yarn - - yarn test - timeout_in_minutes: 4 - - key: integration-test - depends_on: - - step: build - command: - - yarn - - yarn integration - timeout_in_minutes: 12 + - key: lint + command: + - yarn + - yarn lint + timeout_in_minutes: 3 + - key: build + depends_on: + - step: lint + command: + - yarn + - yarn build + timeout_in_minutes: 7 + - key: test + depends_on: + - step: lint + command: + - yarn + - yarn test + timeout_in_minutes: 4 + - key: integration-test + depends_on: + - step: build + command: + - yarn + - yarn integration + timeout_in_minutes: 12 ``` you can get this format by using a flag on the yaml serializer: ```ts console.log( - await new YamlSerializer({ explicitDependencies: true }).serialize( - pipeline, - ), + await new YamlSerializer({ explicitDependencies: true }).serialize(pipeline), ); ``` diff --git a/examples/conditional.ts b/examples/conditional.ts index 5cbb92a..46dd28d 100644 --- a/examples/conditional.ts +++ b/examples/conditional.ts @@ -8,18 +8,18 @@ import { YamlSerializer } from '../src/serializers/yaml'; * This Conditional will accept when there is at least one changed file ending in .feature */ class FeatureFileChangedConditional extends Conditional { - accept() { - const changedFiles = execSync( - 'git --no-pager diff master --name-only --no-renames', - { encoding: 'utf8' }, - ).split(EOL); - for (const changedFile of changedFiles) { - if (extname(changedFile) === '.feature') { - return true; - } - } - return false; + accept() { + const changedFiles = execSync( + 'git --no-pager diff master --name-only --no-renames', + { encoding: 'utf8' }, + ).split(EOL); + for (const changedFile of changedFiles) { + if (extname(changedFile) === '.feature') { + return true; + } } + return false; + } } const install = new Command('yarn', 2); @@ -27,18 +27,18 @@ const install = new Command('yarn', 2); const lint = new CommandStep([install, new Command('yarn lint', 1)]); const test = new CommandStep([install, new Command('yarn test', 2)]).dependsOn( - lint, + lint, ); const build = new CommandStep([install, new Command('yarn build', 5)]); const integration = new CommandStep([ - install, - new Command('yarn integration', 10), + install, + new Command('yarn integration', 10), ]).dependsOn(build); const pipeline = new Pipeline('My pipeline') - .add(test) - .add(new FeatureFileChangedConditional(integration)); + .add(test) + .add(new FeatureFileChangedConditional(integration)); console.log(await new YamlSerializer().serialize(pipeline)); diff --git a/examples/retry_command.ts b/examples/retry_command.ts index a82328d..7a27ed0 100644 --- a/examples/retry_command.ts +++ b/examples/retry_command.ts @@ -2,28 +2,28 @@ import { Command, CommandStep, Pipeline } from '../src'; import { YamlSerializer } from '../src/serializers/yaml'; class RetryCommand extends Command { - constructor(private retries: number, command: Command) { - super(command.toString(), command.timeout * (retries + 1)); - } + constructor(private retries: number, command: Command) { + super(command.toString(), command.timeout * (retries + 1)); + } - protected serialize(): string { - return new Array(this.retries + 1).fill(this.command).join(' || '); - } + protected serialize(): string { + return new Array(this.retries + 1).fill(this.command).join(' || '); + } - public toString(): string { - return `${this.command} [retry = ${this.retries}]`; - } + public toString(): string { + return `${this.command} [retry = ${this.retries}]`; + } } const install = new Command('yarn'); const test = new CommandStep([ - install, - new RetryCommand(1, new Command('yarn test-integration')), + install, + new RetryCommand(1, new Command('yarn test-integration')), ]).withKey('test'); const pipeline = new Pipeline('My pipeline').add(test); new YamlSerializer({ explicitDependencies: true }) - .serialize(pipeline) - .then(console.log); + .serialize(pipeline) + .then(console.log); diff --git a/examples/simple.ts b/examples/simple.ts index 9dac583..1140062 100644 --- a/examples/simple.ts +++ b/examples/simple.ts @@ -2,30 +2,29 @@ import { Command, CommandStep, Pipeline } from '../src'; import { YamlSerializer } from '../src/serializers/yaml'; async function main() { - const install = new Command('yarn', 2); + const install = new Command('yarn', 2); - const lint = new CommandStep([ - install, - new Command('yarn lint', 1), - ]).withKey('lint'); - const test = new CommandStep([install, new Command('yarn test', 2)]) - .withKey('test') - .dependsOn(lint); - const build = new CommandStep([install, new Command('yarn build', 5)]) - .withKey('build') - .dependsOn(lint); - const integration = new CommandStep([ - install, - new Command('yarn integration', 10), - ]) - .withKey('integration-test') - .dependsOn(build); + const lint = new CommandStep([install, new Command('yarn lint', 1)]).withKey( + 'lint', + ); + const test = new CommandStep([install, new Command('yarn test', 2)]) + .withKey('test') + .dependsOn(lint); + const build = new CommandStep([install, new Command('yarn build', 5)]) + .withKey('build') + .dependsOn(lint); + const integration = new CommandStep([ + install, + new Command('yarn integration', 10), + ]) + .withKey('integration-test') + .dependsOn(build); - const pipeline = new Pipeline('My pipeline').add(test).add(integration); + const pipeline = new Pipeline('My pipeline').add(test).add(integration); - console.log(await new YamlSerializer().serialize(pipeline)); - // console.log(await new YamlSerializer({ explicitDependencies: true }).serialize(pipeline)); - // console.log(await new DotSerializer().serialize(pipeline)); + console.log(await new YamlSerializer().serialize(pipeline)); + // console.log(await new YamlSerializer({ explicitDependencies: true }).serialize(pipeline)); + // console.log(await new DotSerializer().serialize(pipeline)); } main(); diff --git a/jest.config.js b/jest.config.js index e93de6d..7bf6426 100644 --- a/jest.config.js +++ b/jest.config.js @@ -2,177 +2,175 @@ // https://jestjs.io/docs/en/configuration.html module.exports = { - // All imported modules in your tests should be mocked automatically - // automock: false, + // All imported modules in your tests should be mocked automatically + // automock: false, - // Stop running tests after `n` failures - // bail: 0, + // Stop running tests after `n` failures + // bail: 0, - // Respect "browser" field in package.json when resolving modules - // browser: false, + // Respect "browser" field in package.json when resolving modules + // browser: false, - // The directory where Jest should store its cached dependency information - // cacheDirectory: "/private/var/folders/7s/6nyztzkj6nxfy4z6sqkfyn0c0000gp/T/jest_dy", + // The directory where Jest should store its cached dependency information + // cacheDirectory: "/private/var/folders/7s/6nyztzkj6nxfy4z6sqkfyn0c0000gp/T/jest_dy", - // Automatically clear mock calls and instances between every test - clearMocks: true, + // Automatically clear mock calls and instances between every test + clearMocks: true, - // Indicates whether the coverage information should be collected while executing the test - collectCoverage: false, + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: false, - // An array of glob patterns indicating a set of files for which coverage information should be collected - collectCoverageFrom: ['src/**/*.ts'], + // An array of glob patterns indicating a set of files for which coverage information should be collected + collectCoverageFrom: ['src/**/*.ts'], - // The directory where Jest should output its coverage files - coverageDirectory: 'coverage', + // The directory where Jest should output its coverage files + coverageDirectory: 'coverage', - // An array of regexp pattern strings used to skip coverage collection - coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/'], + // An array of regexp pattern strings used to skip coverage collection + coveragePathIgnorePatterns: ['/node_modules/', '/__tests__/'], - // A list of reporter names that Jest uses when writing coverage reports - //coverageReporters: ['text'], + // A list of reporter names that Jest uses when writing coverage reports + //coverageReporters: ['text'], - // An object that configures minimum threshold enforcement for coverage results - // coverageThreshold: null, + // An object that configures minimum threshold enforcement for coverage results + // coverageThreshold: null, - // A path to a custom dependency extractor - // dependencyExtractor: null, + // A path to a custom dependency extractor + // dependencyExtractor: null, - // Make calling deprecated APIs throw helpful error messages - // errorOnDeprecated: false, + // Make calling deprecated APIs throw helpful error messages + // errorOnDeprecated: false, - // Force coverage collection from ignored files using an array of glob patterns - // forceCoverageMatch: [], + // Force coverage collection from ignored files using an array of glob patterns + // forceCoverageMatch: [], - // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: null, + // A path to a module which exports an async function that is triggered once before all test suites + // globalSetup: null, - // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: null, + // A path to a module which exports an async function that is triggered once after all test suites + // globalTeardown: null, - // A set of global variables that need to be available in all test environments - // globals: {}, + // A set of global variables that need to be available in all test environments + // globals: {}, - // An array of directory names to be searched recursively up from the requiring module's location - // moduleDirectories: [ - // "node_modules" - // ], + // An array of directory names to be searched recursively up from the requiring module's location + // moduleDirectories: [ + // "node_modules" + // ], - // An array of file extensions your modules use - // moduleFileExtensions: [ - // "js", - // "json", - // "jsx", - // "ts", - // "tsx", - // "node" - // ], + // An array of file extensions your modules use + // moduleFileExtensions: [ + // "js", + // "json", + // "jsx", + // "ts", + // "tsx", + // "node" + // ], - // A map from regular expressions to module names that allow to stub out resources with a single module - // moduleNameMapper: {}, + // A map from regular expressions to module names that allow to stub out resources with a single module + // moduleNameMapper: {}, - // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader - // modulePathIgnorePatterns: [], + // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader + // modulePathIgnorePatterns: [], - // Activates notifications for test results - // notify: false, + // Activates notifications for test results + // notify: false, - // An enum that specifies notification mode. Requires { notify: true } - // notifyMode: "failure-change", + // An enum that specifies notification mode. Requires { notify: true } + // notifyMode: "failure-change", - // A preset that is used as a base for Jest's configuration - preset: 'ts-jest', + // A preset that is used as a base for Jest's configuration + preset: 'ts-jest', - // Run tests from one or more projects - // projects: null, + // Run tests from one or more projects + // projects: null, - // Use this configuration option to add custom reporters to Jest - // reporters: undefined, + // Use this configuration option to add custom reporters to Jest + // reporters: undefined, - // Automatically reset mock state between every test - // resetMocks: false, + // Automatically reset mock state between every test + // resetMocks: false, - // Reset the module registry before running each individual test - // resetModules: false, + // Reset the module registry before running each individual test + // resetModules: false, - // A path to a custom resolver - // resolver: null, + // A path to a custom resolver + // resolver: null, - // Automatically restore mock state between every test - // restoreMocks: false, + // Automatically restore mock state between every test + // restoreMocks: false, - // The root directory that Jest should scan for tests and modules within - // rootDir: null, + // The root directory that Jest should scan for tests and modules within + // rootDir: null, - // A list of paths to directories that Jest should use to search for files in - // roots: [ - // "" - // ], + // A list of paths to directories that Jest should use to search for files in + // roots: [ + // "" + // ], - // Allows you to use a custom runner instead of Jest's default test runner - // runner: "jest-runner", + // Allows you to use a custom runner instead of Jest's default test runner + // runner: "jest-runner", - // The paths to modules that run some code to configure or set up the testing environment before each test - setupFiles: ['/src/__tests__/setup.ts'], + // The paths to modules that run some code to configure or set up the testing environment before each test + setupFiles: ['/src/__tests__/setup.ts'], - // A list of paths to modules that run some code to configure or set up the testing framework before each test - // setupFilesAfterEnv: [], + // A list of paths to modules that run some code to configure or set up the testing framework before each test + // setupFilesAfterEnv: [], - // A list of paths to snapshot serializer modules Jest should use for snapshot testing - // snapshotSerializers: [], + // A list of paths to snapshot serializer modules Jest should use for snapshot testing + // snapshotSerializers: [], - // The test environment that will be used for testing - testEnvironment: 'node', + // The test environment that will be used for testing + testEnvironment: 'node', - // Options that will be passed to the testEnvironment - // testEnvironmentOptions: {}, + // Options that will be passed to the testEnvironment + // testEnvironmentOptions: {}, - // Adds a location field to test results - // testLocationInResults: false, + // Adds a location field to test results + // testLocationInResults: false, - // The glob patterns Jest uses to detect test files - testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - '**/?(*.)+(spec|test).ts?(x)', - ], + // The glob patterns Jest uses to detect test files + testMatch: [ + // "**/__tests__/**/*.[jt]s?(x)", + '**/?(*.)+(spec|test).ts?(x)', + ], - // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped - testPathIgnorePatterns: ['/node_modules/', '/dist/'], + // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped + testPathIgnorePatterns: ['/node_modules/', '/dist/'], - // The regexp pattern or array of patterns that Jest uses to detect test files - // testRegex: [], + // The regexp pattern or array of patterns that Jest uses to detect test files + // testRegex: [], - // This option allows the use of a custom results processor - // testResultsProcessor: null, + // This option allows the use of a custom results processor + // testResultsProcessor: null, - // This option allows use of a custom test runner - // testRunner: "jasmine2", + // This option allows use of a custom test runner + // testRunner: "jasmine2", - // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href - // testURL: "http://localhost", + // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href + // testURL: "http://localhost", - // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" - // timers: "real", + // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" + // timers: "real", - // A map from regular expressions to paths to transformers - transform: { - '^.+\\.(ts|tsx)?$': 'ts-jest', - }, + // A map from regular expressions to paths to transformers + // transform: null, - // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation - // transformIgnorePatterns: [ - // "/node_modules/" - // ], + // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation + // transformIgnorePatterns: [ + // "/node_modules/" + // ], - // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them - // unmockedModulePathPatterns: undefined, + // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them + // unmockedModulePathPatterns: undefined, - // Indicates whether each individual test should be reported during the run - // verbose: null, + // Indicates whether each individual test should be reported during the run + // verbose: null, - // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode - // watchPathIgnorePatterns: [], + // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode + // watchPathIgnorePatterns: [], - // Whether to use watchman for file crawling - // watchman: true, + // Whether to use watchman for file crawling + // watchman: true, }; diff --git a/package.json b/package.json index ca7e1f0..404e709 100644 --- a/package.json +++ b/package.json @@ -1,68 +1,68 @@ { - "name": "buildkite-graph", - "version": "0.0.0-development", - "main": "dist/index.js", - "typings": "dist/index.d.ts", - "license": "MIT", - "private": false, - "files": [ - "dist", - "!dist/__tests__" - ], - "devDependencies": { - "@types/graphviz": "^0.0.32", - "@types/jest": "^26.0.23", - "@types/js-yaml": "^4.0.1", - "@types/lodash.sortby": "^4.7.6", - "@types/node": "^15.12.5", - "@types/uniqid": "^5.3.0", - "@typescript-eslint/eslint-plugin": "^4.28.1", - "@typescript-eslint/parser": "^4.28.1", - "cz-conventional-changelog": "3.3.0", - "eslint": "^7.29.0", - "eslint-config-prettier": "^8.3.0", - "husky": "^6.0.0", - "jest": "^27.0.6", - "prettier": "^2.3.2", - "pretty-quick": "^3.1.1", - "rimraf": "^3.0.2", - "semantic-release": "^17.4.4", - "ts-jest": "^27.0.3", - "ts-node": "^10.0.0", - "typescript": "^4.3.4" - }, - "scripts": { - "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", - "prettier": "prettier --write .", - "preprepare": "rimraf dist/", - "prepare": "husky install & tsc", - "lint": "eslint --ext .ts src", - "pretest": "prettier --check . && yarn lint", - "test": "jest --ci --coverage", - "test:watch": "jest --watch --notify", - "semantic-release": "semantic-release" - }, - "dependencies": { - "@sindresorhus/slugify": "^1.1.0", - "graphviz": "^0.0.9", - "js-yaml": "^4.1.0", - "lodash.sortby": "^4.7.0", - "ow": "^0.25.0", - "topological-sort": "^0.3.0", - "uniqid": "^5.3.0" - }, - "husky": { - "hooks": { - "pre-commit": "pretty-quick --staged" - } - }, - "config": { - "commitizen": { - "path": "./node_modules/cz-conventional-changelog" - } - }, - "repository": { - "type": "git", - "url": "git@github.com:joscha/buildkite-graph.git" + "name": "buildkite-graph", + "version": "0.0.0-development", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "license": "MIT", + "private": false, + "files": [ + "dist", + "!dist/__tests__" + ], + "devDependencies": { + "@types/graphviz": "^0.0.32", + "@types/jest": "^26.0.23", + "@types/js-yaml": "^4.0.1", + "@types/lodash.sortby": "^4.7.6", + "@types/node": "^15.12.5", + "@types/uniqid": "^5.3.0", + "@typescript-eslint/eslint-plugin": "^4.28.1", + "@typescript-eslint/parser": "^4.28.1", + "cz-conventional-changelog": "3.3.0", + "eslint": "^7.29.0", + "eslint-config-prettier": "^8.3.0", + "husky": "^6.0.0", + "jest": "^27.0.6", + "prettier": "^2.3.2", + "pretty-quick": "^3.1.1", + "rimraf": "^3.0.2", + "semantic-release": "^17.4.4", + "ts-jest": "^27.0.3", + "ts-node": "^10.0.0", + "typescript": "^4.3.4" + }, + "scripts": { + "eslint": "eslint . --ext .js,.jsx,.ts,.tsx", + "prettier": "prettier --write .", + "preprepare": "rimraf dist/", + "prepare": "husky install & tsc", + "lint": "eslint --ext .ts src", + "pretest": "prettier --check . && yarn lint", + "test": "jest --ci --coverage", + "test:watch": "jest --watch --notify", + "semantic-release": "semantic-release" + }, + "dependencies": { + "@sindresorhus/slugify": "^1.1.0", + "graphviz": "^0.0.9", + "js-yaml": "^4.1.0", + "lodash.sortby": "^4.7.0", + "ow": "^0.25.0", + "topological-sort": "^0.3.0", + "uniqid": "^5.3.0" + }, + "husky": { + "hooks": { + "pre-commit": "pretty-quick --staged" } + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-conventional-changelog" + } + }, + "repository": { + "type": "git", + "url": "git@github.com:joscha/buildkite-graph.git" + } } diff --git a/src/__tests__/block.spec.ts b/src/__tests__/block.spec.ts index 9c90673..edc1c54 100644 --- a/src/__tests__/block.spec.ts +++ b/src/__tests__/block.spec.ts @@ -2,78 +2,73 @@ import { BlockStep, Option, Pipeline, SelectField, TextField } from '../'; import { createTest } from './helpers'; describe('buildkite-graph', () => { - describe('Steps', () => { - describe('Block', () => { - createTest('can be added', () => [ - new Pipeline('whatever').add( - new BlockStep(':rocket: Release!'), - ), - new Pipeline('whatever').add( - new BlockStep( - ':rocket: Release!', - 'Release to production?', - ), - ), - ]); + describe('Steps', () => { + describe('Block', () => { + createTest('can be added', () => [ + new Pipeline('whatever').add(new BlockStep(':rocket: Release!')), + new Pipeline('whatever').add( + new BlockStep(':rocket: Release!', 'Release to production?'), + ), + ]); - createTest('branches', () => - new Pipeline('whatever').add( - new BlockStep('my title') - .withBranch('master') - .withBranch('stable/*') - .withBranch('!release/*'), - ), - ); + createTest('branches', () => + new Pipeline('whatever').add( + new BlockStep('my title') + .withBranch('master') + .withBranch('stable/*') + .withBranch('!release/*'), + ), + ); - createTest('with fields', () => [ - new Pipeline('whatever').add( - new BlockStep('my title').fields - .add(new TextField('field-1', 'Label 1')) - .fields.add(new SelectField('field-2', 'Label 2')) - .fields.add(new TextField('field-3', 'Label 3')), - ), - new Pipeline('whatever').add( - new BlockStep('my title').fields.add( - new TextField( - 'release-name', - 'Code Name', - 'What’s the code name for this release? :name_badge:', - false, - 'Flying Dolphin', - ), - ), - ), - new Pipeline('whatever').add( - new BlockStep('my title').fields.add( - new SelectField( - 'release-stream', - 'Stream', - 'What’s the release stream?', - false, - false, - 'beta', - ) - .addOption(new Option('Beta', 'beta')) - .addOption(new Option('Stable', 'stable')), - ), - ), - new Pipeline('whatever').add( - new BlockStep('my title').fields.add( - new SelectField( - 'regions', - 'Regions', - 'Which regions should we deploy this to? :earth_asia:', - true, - true, - ['na', 'eur', 'asia', 'aunz'], - ) - .addOption(new Option('North America', 'na')) - .addOption(new Option('Europe', 'eur')) - .addOption(new Option('Asia', 'asia')) - .addOption(new Option('Oceania', 'aunz')), - ), - ), - ]); - }); + createTest('with fields', () => [ + new Pipeline('whatever').add( + new BlockStep('my title').fields + .add(new TextField('field-1', 'Label 1')) + .fields.add(new SelectField('field-2', 'Label 2')) + .fields.add(new TextField('field-3', 'Label 3')), + ), + new Pipeline('whatever').add( + new BlockStep('my title').fields.add( + new TextField( + 'release-name', + 'Code Name', + 'What’s the code name for this release? :name_badge:', + false, + 'Flying Dolphin', + ), + ), + ), + new Pipeline('whatever').add( + new BlockStep('my title').fields.add( + new SelectField( + 'release-stream', + 'Stream', + 'What’s the release stream?', + false, + false, + 'beta', + ) + .addOption(new Option('Beta', 'beta')) + .addOption(new Option('Stable', 'stable')), + ), + ), + new Pipeline('whatever').add( + new BlockStep('my title').fields.add( + new SelectField( + 'regions', + 'Regions', + 'Which regions should we deploy this to? :earth_asia:', + true, + true, + ['na', 'eur', 'asia', 'aunz'], + ) + .addOption(new Option('North America', 'na')) + .addOption(new Option('Europe', 'eur')) + .addOption(new Option('Asia', 'asia')) + .addOption(new Option('Oceania', 'aunz')), + ), + ), + ]); }); + }); }); diff --git a/src/__tests__/command.spec.ts b/src/__tests__/command.spec.ts index 2f94680..0b08b5c 100644 --- a/src/__tests__/command.spec.ts +++ b/src/__tests__/command.spec.ts @@ -2,402 +2,379 @@ import { Command, CommandStep, ExitStatus, Pipeline, Plugin } from '../'; import { createTest } from './helpers'; describe('buildkite-graph', () => { - describe('Steps', () => { - describe('dependencies', () => { - createTest('step dependency', () => [ - new Pipeline('whatever').add( - new CommandStep('b').dependsOn(new CommandStep('a')), - ), - new Pipeline('whatever').add( - new CommandStep('d').dependsOn( - new CommandStep('a'), - new CommandStep('b'), - new CommandStep('c'), - ), - ), - ]); - it('can not depend on itself', () => { - const c = new CommandStep('c'); - expect(() => c.dependsOn(c)).toThrowError(); - }); - }); + describe('Steps', () => { + describe('dependencies', () => { + createTest('step dependency', () => [ + new Pipeline('whatever').add( + new CommandStep('b').dependsOn(new CommandStep('a')), + ), + new Pipeline('whatever').add( + new CommandStep('d').dependsOn( + new CommandStep('a'), + new CommandStep('b'), + new CommandStep('c'), + ), + ), + ]); + it('can not depend on itself', () => { + const c = new CommandStep('c'); + expect(() => c.dependsOn(c)).toThrowError(); + }); + }); - describe('Command', () => { - createTest('step addition', () => [ - new Pipeline('whatever').add( - new CommandStep('yarn').add('yarn test'), - ), - new Pipeline('x').add(new CommandStep('')), - ]); - - describe('continue on failure', () => { - createTest( - 'multiple subsequent always-executed subsequent steps do not get an additional wait step', - () => { - const command = new CommandStep('command.sh'); - const always = new CommandStep( - 'echo This runs regardless of the success or failure', - ) - .alwaysExecute() - .dependsOn(command); - const always2 = new CommandStep( - 'echo This runs regardless of the success or failure 2', - ) - .alwaysExecute() - .dependsOn(command); - const always3 = new CommandStep( - 'echo This runs regardless of the success or failure 3', - ) - .alwaysExecute() - .dependsOn(command); - - return new Pipeline('test') - .add(command) - .add(always) - .add(always2) - .add(always3); - }, - ); - - createTest( - 'subsequent depending steps that are not always executed get an additional wait step', - () => { - const command = new CommandStep('command.sh'); - const always = new CommandStep( - 'echo This runs regardless of the success or failure', - ) - .alwaysExecute() - .dependsOn(command); - const passed = new CommandStep( - 'echo The command passed', - ).dependsOn(command); - - return new Pipeline('test') - .add(command) - .add(always) - .add(passed); - }, - ); - }); - - createTest('dependency failures', () => { - const a = new CommandStep('a.sh'); - const b = new CommandStep('b.sh') - .dependsOn(a) - .allowDependencyFailure(); - const c = new CommandStep('c.sh').dependsOn(a).alwaysExecute(); - const d = new CommandStep('c.sh') - .dependsOn(a) - .alwaysExecute() - .allowDependencyFailure(); - return new Pipeline('test').add(a).add(b).add(c).add(d); - }); - - describe('timeouts', () => { - createTest( - 'commands with timeouts set step timeout total', - () => { - const command1 = new Command('yarn install', 10); - const command2 = new Command('yarn test', 10); - return new Pipeline('test').add( - new CommandStep([command1, command2]), - ); - }, - ); - - createTest('step timeout total trumps commands', () => { - const command1 = new Command('yarn install', 10); - const command2 = new Command('yarn test', 10); - return new Pipeline('test').add( - new CommandStep([command1, command2]) - .withTimeout(100) - .withTimeout(2), - ); - }); - - createTest( - 'one infinite timeout will cancel out the others', - () => { - const command1 = new Command('yarn install', 10); - const command2 = new Command('yarn test'); - return new Pipeline('test').add( - new CommandStep([command1, command2]), - ); - }, - ); - - createTest('can be infinite', () => { - return new Pipeline('test').add( - new CommandStep('noop').withTimeout(), - ); - }); - }); - - createTest('agents', () => - new Pipeline('whatever').add( - new CommandStep('noop').withAgent('npm', 'true'), - ), + describe('Command', () => { + createTest('step addition', () => [ + new Pipeline('whatever').add(new CommandStep('yarn').add('yarn test')), + new Pipeline('x').add(new CommandStep('')), + ]); + + describe('continue on failure', () => { + createTest( + 'multiple subsequent always-executed subsequent steps do not get an additional wait step', + () => { + const command = new CommandStep('command.sh'); + const always = new CommandStep( + 'echo This runs regardless of the success or failure', + ) + .alwaysExecute() + .dependsOn(command); + const always2 = new CommandStep( + 'echo This runs regardless of the success or failure 2', + ) + .alwaysExecute() + .dependsOn(command); + const always3 = new CommandStep( + 'echo This runs regardless of the success or failure 3', + ) + .alwaysExecute() + .dependsOn(command); + + return new Pipeline('test') + .add(command) + .add(always) + .add(always2) + .add(always3); + }, + ); + + createTest( + 'subsequent depending steps that are not always executed get an additional wait step', + () => { + const command = new CommandStep('command.sh'); + const always = new CommandStep( + 'echo This runs regardless of the success or failure', + ) + .alwaysExecute() + .dependsOn(command); + const passed = new CommandStep('echo The command passed').dependsOn( + command, ); - createTest('artifact_paths', () => [ - new Pipeline('whatever').add( - new CommandStep('noop') - .withArtifactPath('logs/**/*') - .withArtifactPath('coverage/**/*'), - ), - new Pipeline('whatever').add( - new CommandStep('noop').withArtifactPath( - 'logs/**/*', - 'coverage/**/*', - ), - ), - ]); - - createTest('branches', () => - new Pipeline('whatever').add( - new CommandStep('noop') - .withBranch('master') - .withBranch('stable/*') - .withBranch('!release/*'), - ), - ); + return new Pipeline('test').add(command).add(always).add(passed); + }, + ); + }); + + createTest('dependency failures', () => { + const a = new CommandStep('a.sh'); + const b = new CommandStep('b.sh').dependsOn(a).allowDependencyFailure(); + const c = new CommandStep('c.sh').dependsOn(a).alwaysExecute(); + const d = new CommandStep('c.sh') + .dependsOn(a) + .alwaysExecute() + .allowDependencyFailure(); + return new Pipeline('test').add(a).add(b).add(c).add(d); + }); + + describe('timeouts', () => { + createTest('commands with timeouts set step timeout total', () => { + const command1 = new Command('yarn install', 10); + const command2 = new Command('yarn test', 10); + return new Pipeline('test').add( + new CommandStep([command1, command2]), + ); + }); - createTest('concurrency', () => [ - new Pipeline('whatever').add( - new CommandStep('noop') - .withConcurrency(10, 'will/be/overridden') - .withConcurrency(3, 'my-app/deploy'), - ), - new Pipeline('whatever').add( - new CommandStep('noop') - .withConcurrencyMethod('eager') - .withConcurrency(3, 'my-app/deploy'), - ), - new Pipeline('whatever').add( - new CommandStep('noop') - .withConcurrencyMethod('ordered') - .withConcurrency(3, 'my-app/deploy'), - ), - ]); - - createTest('env', () => - new Pipeline('whatever').add( - new CommandStep('noop').env - .set('RAILS_ENV', 'test') - .env.set('DEBUG', 'true'), - ), - ); + createTest('step timeout total trumps commands', () => { + const command1 = new Command('yarn install', 10); + const command2 = new Command('yarn test', 10); + return new Pipeline('test').add( + new CommandStep([command1, command2]) + .withTimeout(100) + .withTimeout(2), + ); + }); - createTest('key', () => - new Pipeline('whatever').add( - new CommandStep('noop') - .withKey('my-key-overridden') - .withKey('my-key'), - ), - ); + createTest('one infinite timeout will cancel out the others', () => { + const command1 = new Command('yarn install', 10); + const command2 = new Command('yarn test'); + return new Pipeline('test').add( + new CommandStep([command1, command2]), + ); + }); - createTest('label', () => - new Pipeline('whatever').add( - new CommandStep('noop') - .withLabel('my label overridden') - .withLabel('my label'), - ), - ); + createTest('can be infinite', () => { + return new Pipeline('test').add( + new CommandStep('noop').withTimeout(), + ); + }); + }); + + createTest('agents', () => + new Pipeline('whatever').add( + new CommandStep('noop').withAgent('npm', 'true'), + ), + ); + + createTest('artifact_paths', () => [ + new Pipeline('whatever').add( + new CommandStep('noop') + .withArtifactPath('logs/**/*') + .withArtifactPath('coverage/**/*'), + ), + new Pipeline('whatever').add( + new CommandStep('noop').withArtifactPath( + 'logs/**/*', + 'coverage/**/*', + ), + ), + ]); + + createTest('branches', () => + new Pipeline('whatever').add( + new CommandStep('noop') + .withBranch('master') + .withBranch('stable/*') + .withBranch('!release/*'), + ), + ); + + createTest('concurrency', () => [ + new Pipeline('whatever').add( + new CommandStep('noop') + .withConcurrency(10, 'will/be/overridden') + .withConcurrency(3, 'my-app/deploy'), + ), + new Pipeline('whatever').add( + new CommandStep('noop') + .withConcurrencyMethod('eager') + .withConcurrency(3, 'my-app/deploy'), + ), + new Pipeline('whatever').add( + new CommandStep('noop') + .withConcurrencyMethod('ordered') + .withConcurrency(3, 'my-app/deploy'), + ), + ]); + + createTest('env', () => + new Pipeline('whatever').add( + new CommandStep('noop').env + .set('RAILS_ENV', 'test') + .env.set('DEBUG', 'true'), + ), + ); + + createTest('key', () => + new Pipeline('whatever').add( + new CommandStep('noop') + .withKey('my-key-overridden') + .withKey('my-key'), + ), + ); + + createTest('label', () => + new Pipeline('whatever').add( + new CommandStep('noop') + .withLabel('my label overridden') + .withLabel('my label'), + ), + ); + + createTest('parallelism', () => [ + new Pipeline('whatever').add( + new CommandStep('noop').withParallelism(100).withParallelism(123), + ), + new Pipeline('whatever').add( + new CommandStep('noop').withParallelism(1), + ), + ]); + + describe('plugins', () => { + let plugins: Plugin[]; + let stepWithPlugins: CommandStep; + + beforeEach(() => { + plugins = [ + new Plugin('bugcrowd/test-summary#v1.5.0', { + inputs: [ + { + label: ':htmllint: HTML lint', + artifact_path: 'web/target/htmllint-*.txt', + type: 'oneline', + }, + ], + }), + new Plugin('detect-clowns#v1.0.0'), + ]; + stepWithPlugins = new CommandStep('noop').plugins + .add(plugins[0]) + .plugins.add(plugins[1]); + }); + + createTest('add plugins', () => + new Pipeline('whatever').add(stepWithPlugins), + ); + + it('is possible to query existing plugins', () => { + expect(stepWithPlugins.plugins.filter(() => true)).toMatchObject( + plugins, + ); + }); + }); + + describe('soft_fail', () => { + createTest('boolean', () => [ + new Pipeline('whatever').add( + new CommandStep('noop').withSoftFail('*'), + ), + new Pipeline('whatever').add( + new CommandStep('noop').withSoftFail(true), + ), + ]); + + createTest('multiple', () => + new Pipeline('whatever').add( + new CommandStep('noop').withSoftFail(1).withSoftFail(-127), + ), + ); + + createTest('star', () => + new Pipeline('whatever').add( + new CommandStep('noop').withSoftFail(1).withSoftFail('*'), + ), + ); + }); + + describe('priority', () => { + createTest('default', () => [ + new Pipeline('whatever').add( + new CommandStep('noopImportant').withPriority(100), + new CommandStep('noop').withPriority(0), + new CommandStep('noopUnimportant').withPriority(-100), + ), + ]); + + it('throws if not an integer', () => { + expect(() => + new CommandStep('noop').withPriority(Infinity), + ).toThrow(); + expect(() => new CommandStep('noop').withPriority(1.234)).toThrow(); + }); + }); + + describe('skip', () => { + createTest('value', () => [ + new Pipeline('whatever').add(new CommandStep('noop').skip(false)), + new Pipeline('whatever').add( + new CommandStep('noop').skip(false).skip(true), + ), + new Pipeline('whatever').add( + new CommandStep('noop').skip('my reason'), + ), + ]); + + createTest('function', () => [ + new Pipeline('whatever').add( + new CommandStep('noop').skip(() => false), + ), + new Pipeline('whatever').add( + new CommandStep('noop').skip(() => true), + ), + new Pipeline('whatever').add( + new CommandStep('noop').skip(() => 'my reason'), + ), + ]); + }); + + createTest('retry', () => [ + new Pipeline('whatever').add( + new CommandStep('noop').retry.automatic(true), + ), + new Pipeline('whatever').add( + new CommandStep('noop').retry.automatic( + new Map([ + ['*', 2], + [255, 2], + ]), + ), + ), + new Pipeline('whatever').add( + new CommandStep('noop').retry.manual(false), + ), + new Pipeline('whatever').add( + new CommandStep('noop').retry.manual( + false, + false, + "Sorry, you can't retry a deployment", + ), + ), + new Pipeline('whatever').add( + new CommandStep('noop').retry.manual( + true, + false, + "Sorry, you can't retry a deployment", + ), + ), + new Pipeline('whatever').add( + new CommandStep('noop').retry.manual(true, true), + ), + new Pipeline('whatever').add( + new CommandStep('noop').retry + .automatic(true) + .retry.manual(true, true), + ), + new Pipeline('whatever').add( + new CommandStep('noop').retry.automatic(2), + ), + new Pipeline('whatever').add( + new CommandStep('noop').retry.automatic( + new Map([['*', 1]]), + ), + ), + ]); + + describe('retry', () => { + it('is possible to read automatic retries', () => { + expect( + new CommandStep('noop').retry + .automatic(true) + .retry.getAutomaticValue(), + ).toEqual(new Map([['*', 2]])); + expect( + new CommandStep('noop').retry + .automatic(5) + .retry.getAutomaticValue(), + ).toEqual(new Map([['*', 5]])); + expect( + new CommandStep('noop').retry + .automatic(new Map([[-1, 3]])) + .retry.getAutomaticValue(), + ).toEqual(new Map([[-1, 3]])); + }); + }); + + describe('edge cases', () => { + it('throws if key is empty', () => { + expect(() => new CommandStep('noop').withKey('')).toThrow(); + }); - createTest('parallelism', () => [ - new Pipeline('whatever').add( - new CommandStep('noop') - .withParallelism(100) - .withParallelism(123), - ), - new Pipeline('whatever').add( - new CommandStep('noop').withParallelism(1), - ), - ]); - - describe('plugins', () => { - let plugins: Plugin[]; - let stepWithPlugins: CommandStep; - - beforeEach(() => { - plugins = [ - new Plugin('bugcrowd/test-summary#v1.5.0', { - inputs: [ - { - label: ':htmllint: HTML lint', - artifact_path: 'web/target/htmllint-*.txt', - type: 'oneline', - }, - ], - }), - new Plugin('detect-clowns#v1.0.0'), - ]; - stepWithPlugins = new CommandStep('noop').plugins - .add(plugins[0]) - .plugins.add(plugins[1]); - }); - - createTest('add plugins', () => - new Pipeline('whatever').add(stepWithPlugins), - ); - - it('is possible to query existing plugins', () => { - expect( - stepWithPlugins.plugins.filter(() => true), - ).toMatchObject(plugins); - }); - }); - - describe('soft_fail', () => { - createTest('boolean', () => [ - new Pipeline('whatever').add( - new CommandStep('noop').withSoftFail('*'), - ), - new Pipeline('whatever').add( - new CommandStep('noop').withSoftFail(true), - ), - ]); - - createTest('multiple', () => - new Pipeline('whatever').add( - new CommandStep('noop') - .withSoftFail(1) - .withSoftFail(-127), - ), - ); - - createTest('star', () => - new Pipeline('whatever').add( - new CommandStep('noop') - .withSoftFail(1) - .withSoftFail('*'), - ), - ); - }); - - describe('priority', () => { - createTest('default', () => [ - new Pipeline('whatever').add( - new CommandStep('noopImportant').withPriority(100), - new CommandStep('noop').withPriority(0), - new CommandStep('noopUnimportant').withPriority(-100), - ), - ]); - - it('throws if not an integer', () => { - expect(() => - new CommandStep('noop').withPriority(Infinity), - ).toThrow(); - expect(() => - new CommandStep('noop').withPriority(1.234), - ).toThrow(); - }); - }); - - describe('skip', () => { - createTest('value', () => [ - new Pipeline('whatever').add( - new CommandStep('noop').skip(false), - ), - new Pipeline('whatever').add( - new CommandStep('noop').skip(false).skip(true), - ), - new Pipeline('whatever').add( - new CommandStep('noop').skip('my reason'), - ), - ]); - - createTest('function', () => [ - new Pipeline('whatever').add( - new CommandStep('noop').skip(() => false), - ), - new Pipeline('whatever').add( - new CommandStep('noop').skip(() => true), - ), - new Pipeline('whatever').add( - new CommandStep('noop').skip(() => 'my reason'), - ), - ]); - }); - - createTest('retry', () => [ - new Pipeline('whatever').add( - new CommandStep('noop').retry.automatic(true), - ), - new Pipeline('whatever').add( - new CommandStep('noop').retry.automatic( - new Map([ - ['*', 2], - [255, 2], - ]), - ), - ), - new Pipeline('whatever').add( - new CommandStep('noop').retry.manual(false), - ), - new Pipeline('whatever').add( - new CommandStep('noop').retry.manual( - false, - false, - "Sorry, you can't retry a deployment", - ), - ), - new Pipeline('whatever').add( - new CommandStep('noop').retry.manual( - true, - false, - "Sorry, you can't retry a deployment", - ), - ), - new Pipeline('whatever').add( - new CommandStep('noop').retry.manual(true, true), - ), - new Pipeline('whatever').add( - new CommandStep('noop').retry - .automatic(true) - .retry.manual(true, true), - ), - new Pipeline('whatever').add( - new CommandStep('noop').retry.automatic(2), - ), - new Pipeline('whatever').add( - new CommandStep('noop').retry.automatic( - new Map([['*', 1]]), - ), - ), - ]); - - describe('retry', () => { - it('is possible to read automatic retries', () => { - expect( - new CommandStep('noop').retry - .automatic(true) - .retry.getAutomaticValue(), - ).toEqual(new Map([['*', 2]])); - expect( - new CommandStep('noop').retry - .automatic(5) - .retry.getAutomaticValue(), - ).toEqual(new Map([['*', 5]])); - expect( - new CommandStep('noop').retry - .automatic(new Map([[-1, 3]])) - .retry.getAutomaticValue(), - ).toEqual(new Map([[-1, 3]])); - }); - }); - - describe('edge cases', () => { - it('throws if key is empty', () => { - expect(() => new CommandStep('noop').withKey('')).toThrow(); - }); - - it('throws if key is longer than 100 chars', () => { - expect(() => - new CommandStep('noop').withKey('a'.repeat(101)), - ).toThrow(); - }); - }); + it('throws if key is longer than 100 chars', () => { + expect(() => + new CommandStep('noop').withKey('a'.repeat(101)), + ).toThrow(); }); + }); }); + }); }); diff --git a/src/__tests__/conditional.spec.ts b/src/__tests__/conditional.spec.ts index b499063..be0b288 100644 --- a/src/__tests__/conditional.spec.ts +++ b/src/__tests__/conditional.spec.ts @@ -1,403 +1,367 @@ import { - CommandStep, - Conditional, - Pipeline, - Step, - ThingOrGenerator, + CommandStep, + Conditional, + Pipeline, + Step, + ThingOrGenerator, } from '../'; import { createTest, serializers } from './helpers'; class MyConditional extends Conditional { - constructor( - step: ThingOrGenerator, - private readonly accepted: ReturnType['accept']>, - ) { - super(step as any); - } - - accept(): ReturnType['accept']> { - return this.accepted; - } + constructor( + step: ThingOrGenerator, + private readonly accepted: ReturnType['accept']>, + ) { + super(step as any); + } + + accept(): ReturnType['accept']> { + return this.accepted; + } } describe('buildkite-graph', () => { - describe('Steps', () => { - describe('Command', () => { - createTest('step addition', () => [ - new Pipeline('whatever').add( - new MyConditional( - new CommandStep('yarn').add('yarn test'), - true, - ), - ), - new Pipeline('whatever').add( - new MyConditional( - () => new CommandStep('yarn').add('yarn test'), - true, - ), - ), - new Pipeline('whatever').add( - new MyConditional( - new CommandStep('yarn').add('yarn test'), - false, - ), - ), - ]); - createTest('async step addition', () => [ - new Pipeline('whatever').add( - new MyConditional( - new CommandStep('yarn').add('yarn test'), - Promise.resolve(true), - ), - ), - new Pipeline('whatever').add( - new MyConditional( - new CommandStep('yarn').add('yarn test'), - Promise.resolve(false), - ), - ), - ]); + describe('Steps', () => { + describe('Command', () => { + createTest('step addition', () => [ + new Pipeline('whatever').add( + new MyConditional(new CommandStep('yarn').add('yarn test'), true), + ), + new Pipeline('whatever').add( + new MyConditional( + () => new CommandStep('yarn').add('yarn test'), + true, + ), + ), + new Pipeline('whatever').add( + new MyConditional(new CommandStep('yarn').add('yarn test'), false), + ), + ]); + createTest('async step addition', () => [ + new Pipeline('whatever').add( + new MyConditional( + new CommandStep('yarn').add('yarn test'), + Promise.resolve(true), + ), + ), + new Pipeline('whatever').add( + new MyConditional( + new CommandStep('yarn').add('yarn test'), + Promise.resolve(false), + ), + ), + ]); + + createTest('async step creation', () => [ + new Pipeline('whatever').add( + new MyConditional( + Promise.resolve(new CommandStep('yarn').add('yarn test')), + true, + ), + ), + new Pipeline('whatever').add( + new MyConditional( + () => Promise.resolve(new CommandStep('yarn').add('yarn test')), + true, + ), + ), + ]); + + it('throws on accept rejection', async () => { + expect( + serializers.json.serialize( + new Pipeline('whatever').add( + new MyConditional( + new CommandStep('yarn').add('yarn test'), + Promise.reject(new Error('O noes!!!')), + ), + ), + ), + ).rejects.toThrowError(); + }); + + describe('Conditional dependencies', () => { + createTest('can be specified', () => { + const p = new Pipeline('x'); + + // even though the onditional is set to false, + // "a" will be added to the graph as "b" depends on it + const a = new MyConditional(new CommandStep('a'), false); + p.add(new CommandStep('b').dependsOn(a)); + + return p; + }); - createTest('async step creation', () => [ - new Pipeline('whatever').add( - new MyConditional( - Promise.resolve( - new CommandStep('yarn').add('yarn test'), - ), - true, - ), - ), - new Pipeline('whatever').add( - new MyConditional( - () => - Promise.resolve( - new CommandStep('yarn').add('yarn test'), - ), - true, - ), + describe('can be specified multiple times', () => { + createTest('as dependency', () => { + const p = new Pipeline('x'); + + // even though the onditional is set to false, + // "a" will be added to the graph as "b" depends on it + const a = new MyConditional(new CommandStep('a'), false); + p.add(new CommandStep('b').dependsOn(a)); + p.add(new CommandStep('c').dependsOn(a)); + + return p; + }); + it('but not in the pipeline', () => { + expect(() => { + const a = new MyConditional(new CommandStep('a'), false); + new Pipeline('x').add(a, a); + }).toThrow(); + }); + }); + + it('conditionals are only unwrapped once', () => { + const p = new Pipeline('x'); + + const gen = jest.fn(); + gen.mockReturnValueOnce(new CommandStep('a')); + gen.mockImplementation(() => { + throw new Error('only once!'); + }); + const a = new MyConditional(gen, false); + p.add(new CommandStep('b').dependsOn(a)); + + serializers.json.serialize(p); + }); + }); + + describe.only('isEffectOf', () => { + createTest( + 'will add steps if their effect dependency is accepted', + () => { + const acceptedTests = new MyConditional( + new CommandStep('run tests'), + true, + ); + const deployCoverage = new CommandStep( + 'deploy coverage', + ).isEffectOf(acceptedTests); + + return new Pipeline('x').add(acceptedTests, deployCoverage); + }, + ['structure'], + ); + + createTest( + 'will not add steps if effect dependency is rejected', + () => { + const acceptedTests = new MyConditional( + new CommandStep('run tests'), + false, + ); + const deployCoverage = new CommandStep( + 'deploy coverage', + ).isEffectOf(acceptedTests); + + return new Pipeline('x').add(acceptedTests, deployCoverage); + }, + ['structure'], + ); + + createTest( + 'will not add steps if any effect dependency is rejected', + () => { + const acceptedTests = new MyConditional( + new CommandStep('run tests 1'), + true, + ); + const rejectedTests = new MyConditional( + new CommandStep('run tests 2'), + false, + ); + const deployCoverage = new CommandStep( + 'deploy coverage', + ).isEffectOf(rejectedTests, acceptedTests); + + return new Pipeline('x').add( + acceptedTests, + rejectedTests, + deployCoverage, + ); + }, + ['structure'], + ); + + createTest( + 'effects of effects will be added if first effect dependency is accepted', + () => { + const acceptedTests = new MyConditional( + new CommandStep('run tests'), + true, + ); + const createCoverageStep = new CommandStep( + 'create coverage', + ).isEffectOf(acceptedTests); + const deployCoverage = new CommandStep( + 'deploy coverage', + ).isEffectOf(createCoverageStep); + + return new Pipeline('x').add( + acceptedTests, + createCoverageStep, + deployCoverage, + ); + }, + ['structure'], + ); + + createTest( + 'effects of effects will not be added if first effect dependency is rejected', + () => { + const acceptedTests = new MyConditional( + new CommandStep('run tests'), + false, + ); + const createCoverageStep = new CommandStep( + 'create coverage', + ).isEffectOf(acceptedTests); + const deployCoverage = new CommandStep( + 'deploy coverage', + ).isEffectOf(createCoverageStep); + + return new Pipeline('x').add( + acceptedTests, + createCoverageStep, + deployCoverage, + ); + }, + ['structure'], + ); + + createTest( + 'last call wins', + () => { + const acceptedTests = new MyConditional( + new CommandStep('run tests'), + false, + ); + const deployCoverage1 = new CommandStep('deploy coverage') + .isEffectOf(acceptedTests) + .dependsOn(acceptedTests); + const deployCoverage2 = new CommandStep('deploy coverage') + .dependsOn(acceptedTests) + .isEffectOf(acceptedTests); + + return [ + new Pipeline('x').add(deployCoverage1), + new Pipeline('x').add(deployCoverage2), + ]; + }, + ['structure'], + ); + + createTest( + 'dependsOn is used after isEffectOf', + () => { + const buildConditional = new MyConditional( + new CommandStep('build app'), + false, + ); + const tests = new MyConditional( + new CommandStep('run tests'), + false, + ); + + const deployApp = new CommandStep('deploy app') + .isEffectOf(buildConditional) + .dependsOn(tests); + + return new Pipeline('x').add(buildConditional, deployApp); + }, + ['structure'], + ); + + createTest( + 'effects of steps that are becoming part of the graph are exercised', + () => { + const buildConditional = new MyConditional( + new CommandStep('build app'), + false, + ); + const tests = new MyConditional( + () => + new CommandStep('run integration tests').dependsOn( + buildConditional, ), - ]); - - it('throws on accept rejection', async () => { - expect( - serializers.json.serialize( - new Pipeline('whatever').add( - new MyConditional( - new CommandStep('yarn').add('yarn test'), - Promise.reject(new Error('O noes!!!')), - ), + true, + ); + + const deployApp = new CommandStep('deploy app').isEffectOf( + buildConditional, + ); + + return new Pipeline('x').add(buildConditional, tests, deployApp); + }, + ['structure'], + ); + createTest( + 'later steps affect earlier effects', + () => { + const p = new Pipeline('x'); + const buildConditional = new MyConditional( + () => new CommandStep('build app'), + false, + ); + p.add(buildConditional); + + const deployApp = new CommandStep('deploy app').isEffectOf( + buildConditional, + ); + p.add(deployApp); + + const releaseApp = new CommandStep('release app').isEffectOf( + deployApp, + ); + p.add(releaseApp); + + const tests = new MyConditional( + () => + new Promise((resolve) => + setTimeout( + () => + resolve( + new CommandStep('run integration tests').dependsOn( + buildConditional, ), - ), - ).rejects.toThrowError(); - }); - - describe('Conditional dependencies', () => { - createTest('can be specified', () => { - const p = new Pipeline('x'); - - // even though the onditional is set to false, - // "a" will be added to the graph as "b" depends on it - const a = new MyConditional(new CommandStep('a'), false); - p.add(new CommandStep('b').dependsOn(a)); - - return p; - }); - - describe('can be specified multiple times', () => { - createTest('as dependency', () => { - const p = new Pipeline('x'); - - // even though the onditional is set to false, - // "a" will be added to the graph as "b" depends on it - const a = new MyConditional( - new CommandStep('a'), - false, - ); - p.add(new CommandStep('b').dependsOn(a)); - p.add(new CommandStep('c').dependsOn(a)); - - return p; - }); - it('but not in the pipeline', () => { - expect(() => { - const a = new MyConditional( - new CommandStep('a'), - false, - ); - new Pipeline('x').add(a, a); - }).toThrow(); - }); - }); - - it('conditionals are only unwrapped once', () => { - const p = new Pipeline('x'); - - const gen = jest.fn(); - gen.mockReturnValueOnce(new CommandStep('a')); - gen.mockImplementation(() => { - throw new Error('only once!'); - }); - const a = new MyConditional(gen, false); - p.add(new CommandStep('b').dependsOn(a)); - - serializers.json.serialize(p); - }); - }); - - describe.only('isEffectOf', () => { - createTest( - 'will add steps if their effect dependency is accepted', - () => { - const acceptedTests = new MyConditional( - new CommandStep('run tests'), - true, - ); - const deployCoverage = new CommandStep( - 'deploy coverage', - ).isEffectOf(acceptedTests); - - return new Pipeline('x').add( - acceptedTests, - deployCoverage, - ); - }, - ['structure'], - ); - - createTest( - 'will not add steps if effect dependency is rejected', - () => { - const acceptedTests = new MyConditional( - new CommandStep('run tests'), - false, - ); - const deployCoverage = new CommandStep( - 'deploy coverage', - ).isEffectOf(acceptedTests); - - return new Pipeline('x').add( - acceptedTests, - deployCoverage, - ); - }, - ['structure'], - ); - - createTest( - 'will not add steps if any effect dependency is rejected', - () => { - const acceptedTests = new MyConditional( - new CommandStep('run tests 1'), - true, - ); - const rejectedTests = new MyConditional( - new CommandStep('run tests 2'), - false, - ); - const deployCoverage = new CommandStep( - 'deploy coverage', - ).isEffectOf(rejectedTests, acceptedTests); - - return new Pipeline('x').add( - acceptedTests, - rejectedTests, - deployCoverage, - ); - }, - ['structure'], - ); - - createTest( - 'effects of effects will be added if first effect dependency is accepted', - () => { - const acceptedTests = new MyConditional( - new CommandStep('run tests'), - true, - ); - const createCoverageStep = new CommandStep( - 'create coverage', - ).isEffectOf(acceptedTests); - const deployCoverage = new CommandStep( - 'deploy coverage', - ).isEffectOf(createCoverageStep); - - return new Pipeline('x').add( - acceptedTests, - createCoverageStep, - deployCoverage, - ); - }, - ['structure'], - ); - - createTest( - 'effects of effects will not be added if first effect dependency is rejected', - () => { - const acceptedTests = new MyConditional( - new CommandStep('run tests'), - false, - ); - const createCoverageStep = new CommandStep( - 'create coverage', - ).isEffectOf(acceptedTests); - const deployCoverage = new CommandStep( - 'deploy coverage', - ).isEffectOf(createCoverageStep); - - return new Pipeline('x').add( - acceptedTests, - createCoverageStep, - deployCoverage, - ); - }, - ['structure'], - ); - - createTest( - 'last call wins', - () => { - const acceptedTests = new MyConditional( - new CommandStep('run tests'), - false, - ); - const deployCoverage1 = new CommandStep( - 'deploy coverage', - ) - .isEffectOf(acceptedTests) - .dependsOn(acceptedTests); - const deployCoverage2 = new CommandStep( - 'deploy coverage', - ) - .dependsOn(acceptedTests) - .isEffectOf(acceptedTests); - - return [ - new Pipeline('x').add(deployCoverage1), - new Pipeline('x').add(deployCoverage2), - ]; - }, - ['structure'], - ); - - createTest( - 'dependsOn is used after isEffectOf', - () => { - const buildConditional = new MyConditional( - new CommandStep('build app'), - false, - ); - const tests = new MyConditional( - new CommandStep('run tests'), - false, - ); - - const deployApp = new CommandStep('deploy app') - .isEffectOf(buildConditional) - .dependsOn(tests); - - return new Pipeline('x').add( - buildConditional, - deployApp, - ); - }, - ['structure'], - ); - - createTest( - 'effects of steps that are becoming part of the graph are exercised', - () => { - const buildConditional = new MyConditional( - new CommandStep('build app'), - false, - ); - const tests = new MyConditional( - () => - new CommandStep( - 'run integration tests', - ).dependsOn(buildConditional), - true, - ); - - const deployApp = new CommandStep( - 'deploy app', - ).isEffectOf(buildConditional); - - return new Pipeline('x').add( - buildConditional, - tests, - deployApp, - ); - }, - ['structure'], - ); - createTest( - 'later steps affect earlier effects', - () => { - const p = new Pipeline('x'); - const buildConditional = new MyConditional( - () => new CommandStep('build app'), - false, - ); - p.add(buildConditional); - - const deployApp = new CommandStep( - 'deploy app', - ).isEffectOf(buildConditional); - p.add(deployApp); - - const releaseApp = new CommandStep( - 'release app', - ).isEffectOf(deployApp); - p.add(releaseApp); - - const tests = new MyConditional( - () => - new Promise((resolve) => - setTimeout( - () => - resolve( - new CommandStep( - 'run integration tests', - ).dependsOn(buildConditional), - ), - 100, - ), - ), - true, - ); - p.add(tests); - - return p; - }, - ['structure'], - ); - - createTest( - 'effects and conditionals have correct depends_on', - () => { - const p = new Pipeline('x'); - const buildConditional = new MyConditional( - () => new CommandStep('build app').withKey('build'), - false, - ); - p.add(buildConditional); - - const deployStep = new CommandStep('deploy app') - .withKey('deploy') - .isEffectOf(buildConditional); - p.add(deployStep); - - const testsUsingBuildArtifact = new CommandStep( - 'ssr tests', - ) - .withKey('ssr-tests') - .dependsOn(buildConditional); - p.add(testsUsingBuildArtifact); - return p; - }, - ['yaml_depends_on'], - ); - }); - }); + ), + 100, + ), + ), + true, + ); + p.add(tests); + + return p; + }, + ['structure'], + ); + + createTest( + 'effects and conditionals have correct depends_on', + () => { + const p = new Pipeline('x'); + const buildConditional = new MyConditional( + () => new CommandStep('build app').withKey('build'), + false, + ); + p.add(buildConditional); + + const deployStep = new CommandStep('deploy app') + .withKey('deploy') + .isEffectOf(buildConditional); + p.add(deployStep); + + const testsUsingBuildArtifact = new CommandStep('ssr tests') + .withKey('ssr-tests') + .dependsOn(buildConditional); + p.add(testsUsingBuildArtifact); + return p; + }, + ['yaml_depends_on'], + ); + }); }); + }); }); diff --git a/src/__tests__/helpers.ts b/src/__tests__/helpers.ts index 6b70482..c1b62fb 100644 --- a/src/__tests__/helpers.ts +++ b/src/__tests__/helpers.ts @@ -1,62 +1,60 @@ import { - Pipeline, - Serializer, - serializers as predefinedSerializers, + Pipeline, + Serializer, + serializers as predefinedSerializers, } from '../'; import { resetUuidCounter } from './setup'; type SerializerType = - | 'json' - | 'json_depends_on' - | 'yaml' - | 'yaml_depends_on' - | 'dot' - | 'structure'; + | 'json' + | 'json_depends_on' + | 'yaml' + | 'yaml_depends_on' + | 'dot' + | 'structure'; export const serializers: Record> = { - json: new predefinedSerializers.JsonSerializer(), - json_depends_on: new predefinedSerializers.JsonSerializer({ - explicitDependencies: true, - }), - yaml: new predefinedSerializers.YamlSerializer(), - yaml_depends_on: new predefinedSerializers.YamlSerializer({ - explicitDependencies: true, - }), - dot: new predefinedSerializers.DotSerializer(), - structure: new predefinedSerializers.StructuralSerializer(), + json: new predefinedSerializers.JsonSerializer(), + json_depends_on: new predefinedSerializers.JsonSerializer({ + explicitDependencies: true, + }), + yaml: new predefinedSerializers.YamlSerializer(), + yaml_depends_on: new predefinedSerializers.YamlSerializer({ + explicitDependencies: true, + }), + dot: new predefinedSerializers.DotSerializer(), + structure: new predefinedSerializers.StructuralSerializer(), }; type PipelineGenerator = () => Pipeline | Pipeline[]; const defaultSerializerTypes: SerializerType[] = [ - 'json', - 'json_depends_on', - 'yaml', - 'yaml_depends_on', - 'dot', - 'structure', + 'json', + 'json_depends_on', + 'yaml', + 'yaml_depends_on', + 'dot', + 'structure', ]; export const createTest = ( - name: string, - gen: PipelineGenerator, - serializersToTest: SerializerType[] = defaultSerializerTypes, - describeFn = describe, + name: string, + gen: PipelineGenerator, + serializersToTest: SerializerType[] = defaultSerializerTypes, + describeFn = describe, ): void => - describeFn(name, () => { - test.each(serializersToTest)('%s', async (type) => { - resetUuidCounter(); - let entities = gen(); - if (!Array.isArray(entities)) { - entities = [entities]; - } - for (const entity of entities) { - expect( - await serializers[type].serialize(entity), - ).toMatchSnapshot(); - } - }); + describeFn(name, () => { + test.each(serializersToTest)('%s', async (type) => { + resetUuidCounter(); + let entities = gen(); + if (!Array.isArray(entities)) { + entities = [entities]; + } + for (const entity of entities) { + expect(await serializers[type].serialize(entity)).toMatchSnapshot(); + } }); + }); createTest.only = (name: string, gen: PipelineGenerator) => - createTest(name, gen, defaultSerializerTypes, describe.only); + createTest(name, gen, defaultSerializerTypes, describe.only); diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index e6cdee7..45f799e 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -3,50 +3,40 @@ import { createTest } from './helpers'; import { createComplex, createSimple } from './samples'; describe('buildkite-graph', () => { - describe('general serialization', () => { - createTest('simple', createSimple); - createTest('complex', createComplex); - - createTest( - 'JSON serializer can stringify', - () => new Pipeline('test'), - ['json'], - ); - - createTest( - 'Structural serializer can stringify', - () => createComplex(), - ['structure'], - ); + describe('general serialization', () => { + createTest('simple', createSimple); + createTest('complex', createComplex); + + createTest('JSON serializer can stringify', () => new Pipeline('test'), [ + 'json', + ]); + + createTest('Structural serializer can stringify', () => createComplex(), [ + 'structure', + ]); + }); + + createTest('missing transitive steps get added to the graph', () => { + const step1 = new CommandStep('yarn'); + const step2 = new CommandStep('yarn test').dependsOn(step1); + return new Pipeline('test').add(step2); + }); + + describe('Pipeline', () => { + it('returns a slug', () => { + expect(new Pipeline('A: B: c_d').slug()).toEqual('a-b-c-d'); + expect(new Pipeline('Web: E2E: page').slug()).toEqual('web-e2e-page'); }); - createTest('missing transitive steps get added to the graph', () => { - const step1 = new CommandStep('yarn'); - const step2 = new CommandStep('yarn test').dependsOn(step1); - return new Pipeline('test').add(step2); + it('only allows adding a step once', () => { + const step = new CommandStep(''); + expect(() => new Pipeline('test').add(step).add(step)).toThrowError(); }); - describe('Pipeline', () => { - it('returns a slug', () => { - expect(new Pipeline('A: B: c_d').slug()).toEqual('a-b-c-d'); - expect(new Pipeline('Web: E2E: page').slug()).toEqual( - 'web-e2e-page', - ); - }); - - it('only allows adding a step once', () => { - const step = new CommandStep(''); - expect(() => - new Pipeline('test').add(step).add(step), - ).toThrowError(); - }); - - createTest('env', () => [ - new Pipeline('whatever').env.set('COLOR', '1'), - ]); - - createTest('steps', () => [ - new Pipeline('whatever').add(new CommandStep('command')), - ]); - }); + createTest('env', () => [new Pipeline('whatever').env.set('COLOR', '1')]); + + createTest('steps', () => [ + new Pipeline('whatever').add(new CommandStep('command')), + ]); + }); }); diff --git a/src/__tests__/samples.ts b/src/__tests__/samples.ts index 6ab7d9f..01b739f 100644 --- a/src/__tests__/samples.ts +++ b/src/__tests__/samples.ts @@ -1,118 +1,118 @@ import { BlockStep, CommandStep, Pipeline, Plugin, TriggerStep } from '../'; export function createSimple(): Pipeline { - return new Pipeline('web-deploy').env - .set('USE_COLOR', '1') - .env.set('DEBUG', 'true') - .add(new CommandStep('buildkite/deploy_web.sh', 'Deploy')); + return new Pipeline('web-deploy').env + .set('USE_COLOR', '1') + .env.set('DEBUG', 'true') + .add(new CommandStep('buildkite/deploy_web.sh', 'Deploy')); } export function createComplex(): Pipeline { - const webDeploy = createSimple(); - const buildEditorStep = new CommandStep( - 'web/bin/buildkite/run_web_step.sh build editor', - 'Build Editor', - ); - const testEditorStep = new CommandStep( - 'web/bin/buildkite/run_web_step.sh test editor', - 'Test Editor', - ); + const webDeploy = createSimple(); + const buildEditorStep = new CommandStep( + 'web/bin/buildkite/run_web_step.sh build editor', + 'Build Editor', + ); + const testEditorStep = new CommandStep( + 'web/bin/buildkite/run_web_step.sh test editor', + 'Test Editor', + ); - const annotateFailuresStep = new CommandStep( - new Plugin('bugcrowd/test-summary#v1.5.0', { - inputs: [ - { - label: ':htmllint: HTML lint', - artifact_path: 'web/target/htmllint-*.txt', - type: 'oneline', - }, - ], - }), + const annotateFailuresStep = new CommandStep( + new Plugin('bugcrowd/test-summary#v1.5.0', { + inputs: [ + { + label: ':htmllint: HTML lint', + artifact_path: 'web/target/htmllint-*.txt', + type: 'oneline', + }, + ], + }), - 'Annotate failures', - ).plugins - .add(new Plugin('detect-clowns#v1.0.0')) - .alwaysExecute() - .dependsOn(testEditorStep); + 'Annotate failures', + ).plugins + .add(new Plugin('detect-clowns#v1.0.0')) + .alwaysExecute() + .dependsOn(testEditorStep); - const deployCoverageReportStep = new CommandStep( - 'web/bin/buildkite/run_web_step.sh deploy-report coverage editor', - 'Upload coverage', - ) - .alwaysExecute() - .dependsOn(testEditorStep); + const deployCoverageReportStep = new CommandStep( + 'web/bin/buildkite/run_web_step.sh deploy-report coverage editor', + 'Upload coverage', + ) + .alwaysExecute() + .dependsOn(testEditorStep); - const integrationTestStep = new CommandStep( - 'web/bin/buildkite/run_web_step.sh run-integration-tests local editor chrome', - 'Integration tests', - ) - .withParallelism(8) - .dependsOn(buildEditorStep); + const integrationTestStep = new CommandStep( + 'web/bin/buildkite/run_web_step.sh run-integration-tests local editor chrome', + 'Integration tests', + ) + .withParallelism(8) + .dependsOn(buildEditorStep); - const saucelabsIntegrationTestStep = new CommandStep( - 'web/bin/buildkite/run_web_step.sh run-integration-tests saucelabs editor safari', - ':saucelabs: Integration tests', - ) - .withParallelism(8) - .add(new Plugin('sauce-connect-plugin')) - .dependsOn(integrationTestStep); + const saucelabsIntegrationTestStep = new CommandStep( + 'web/bin/buildkite/run_web_step.sh run-integration-tests saucelabs editor safari', + ':saucelabs: Integration tests', + ) + .withParallelism(8) + .add(new Plugin('sauce-connect-plugin')) + .dependsOn(integrationTestStep); - const visregBaselineUpdateStep = new CommandStep( - 'web/bin/buildkite/run_web_step.sh run-visual-regression editor', - 'Visreg baseline update', - ).dependsOn(integrationTestStep); + const visregBaselineUpdateStep = new CommandStep( + 'web/bin/buildkite/run_web_step.sh run-visual-regression editor', + 'Visreg baseline update', + ).dependsOn(integrationTestStep); - const annotateCucumberFailuresStep = new CommandStep( - 'web/bin/buildkite/run_web_step.sh annotate-cucumber-failed-cases', - 'Annotate cucumber failures', - ) - .alwaysExecute() - .dependsOn(integrationTestStep) - .dependsOn(saucelabsIntegrationTestStep); + const annotateCucumberFailuresStep = new CommandStep( + 'web/bin/buildkite/run_web_step.sh annotate-cucumber-failed-cases', + 'Annotate cucumber failures', + ) + .alwaysExecute() + .dependsOn(integrationTestStep) + .dependsOn(saucelabsIntegrationTestStep); - const copyToDeployBucketStep = new CommandStep( - 'web/bin/buildkite/run_web_step.sh copy-to-deploy-bucket editor', - 'Copy to deploy bucket', - ).dependsOn(saucelabsIntegrationTestStep); + const copyToDeployBucketStep = new CommandStep( + 'web/bin/buildkite/run_web_step.sh copy-to-deploy-bucket editor', + 'Copy to deploy bucket', + ).dependsOn(saucelabsIntegrationTestStep); - const updateCheckpointStep = new CommandStep( - 'production/test/jobs/advance_branch.sh "checkpoint/web/green/editor"', - 'Update checkpoint', - ).dependsOn(copyToDeployBucketStep); + const updateCheckpointStep = new CommandStep( + 'production/test/jobs/advance_branch.sh "checkpoint/web/green/editor"', + 'Update checkpoint', + ).dependsOn(copyToDeployBucketStep); - const deployEditorToTechStep = new TriggerStep( - webDeploy, - 'Deploy to tech', - ).build.env - .set('FLAVOR', 'tech') - .build.env.set('RELEASE_PATH', 'some/path/') - .dependsOn(copyToDeployBucketStep); + const deployEditorToTechStep = new TriggerStep( + webDeploy, + 'Deploy to tech', + ).build.env + .set('FLAVOR', 'tech') + .build.env.set('RELEASE_PATH', 'some/path/') + .dependsOn(copyToDeployBucketStep); - const deployEditorToUserTestingStep = new TriggerStep( - webDeploy, - 'Deploy to usertesting', - ).build.env - .set('FLAVOR', 'usertesting') - .build.env.set('RELEASE_PATH', 'some/path/') - .dependsOn(copyToDeployBucketStep); + const deployEditorToUserTestingStep = new TriggerStep( + webDeploy, + 'Deploy to usertesting', + ).build.env + .set('FLAVOR', 'usertesting') + .build.env.set('RELEASE_PATH', 'some/path/') + .dependsOn(copyToDeployBucketStep); - const releaseStep = new BlockStep('Release editor').dependsOn( - updateCheckpointStep, - ); + const releaseStep = new BlockStep('Release editor').dependsOn( + updateCheckpointStep, + ); - const webBuildEditor = new Pipeline('web-build-editor') - .add(buildEditorStep) - .add(testEditorStep) - .add(annotateFailuresStep) - .add(deployCoverageReportStep) - .add(integrationTestStep) - .add(saucelabsIntegrationTestStep) - .add(visregBaselineUpdateStep) - .add(annotateCucumberFailuresStep) - .add(copyToDeployBucketStep) - .add(updateCheckpointStep) - .add(deployEditorToTechStep) - .add(deployEditorToUserTestingStep) - .add(releaseStep); - return webBuildEditor; + const webBuildEditor = new Pipeline('web-build-editor') + .add(buildEditorStep) + .add(testEditorStep) + .add(annotateFailuresStep) + .add(deployCoverageReportStep) + .add(integrationTestStep) + .add(saucelabsIntegrationTestStep) + .add(visregBaselineUpdateStep) + .add(annotateCucumberFailuresStep) + .add(copyToDeployBucketStep) + .add(updateCheckpointStep) + .add(deployEditorToTechStep) + .add(deployEditorToUserTestingStep) + .add(releaseStep); + return webBuildEditor; } diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index 9b4bb01..ba089d9 100644 --- a/src/__tests__/setup.ts +++ b/src/__tests__/setup.ts @@ -1,7 +1,7 @@ let value = 0; export function resetUuidCounter(): void { - value = 0; + value = 0; } jest.mock('uniqid', (): (() => string) => { - return () => `_${value++}`; + return () => `_${value++}`; }); diff --git a/src/__tests__/trigger.spec.ts b/src/__tests__/trigger.spec.ts index 5856de5..f147436 100644 --- a/src/__tests__/trigger.spec.ts +++ b/src/__tests__/trigger.spec.ts @@ -2,59 +2,55 @@ import { Pipeline, TriggerStep } from '../'; import { createTest } from './helpers'; describe('buildkite-graph', () => { - describe('Steps', () => { - describe('Trigger', () => { - createTest('can trigger another pipeline', () => [ - new Pipeline('whatever').add( - new TriggerStep(new Pipeline('another-build')), - ), - new Pipeline('whatever').add(new TriggerStep('another-build')), - ]); + describe('Steps', () => { + describe('Trigger', () => { + createTest('can trigger another pipeline', () => [ + new Pipeline('whatever').add( + new TriggerStep(new Pipeline('another-build')), + ), + new Pipeline('whatever').add(new TriggerStep('another-build')), + ]); - createTest('with a label', () => [ - new Pipeline('whatever').add( - new TriggerStep('another', 'Trigger another pipeline'), - ), - ]); + createTest('with a label', () => [ + new Pipeline('whatever').add( + new TriggerStep('another', 'Trigger another pipeline'), + ), + ]); - createTest('limit branches', () => [ - new Pipeline('whatever').add( - new TriggerStep('another').withBranch('master'), - ), - ]); - createTest('async', () => [ - new Pipeline('whatever').add( - new TriggerStep('another', 'Label', true), - ), - new Pipeline('whatever').add( - new TriggerStep('another', 'Label', true).async(false), - ), - new Pipeline('whatever').add( - new TriggerStep('another').async(true), - ), - ]); + createTest('limit branches', () => [ + new Pipeline('whatever').add( + new TriggerStep('another').withBranch('master'), + ), + ]); + createTest('async', () => [ + new Pipeline('whatever').add(new TriggerStep('another', 'Label', true)), + new Pipeline('whatever').add( + new TriggerStep('another', 'Label', true).async(false), + ), + new Pipeline('whatever').add(new TriggerStep('another').async(true)), + ]); - createTest('build', () => [ - new Pipeline('whatever').add( - new TriggerStep('another').build.env - .set('KEY', 'VALUE') - .build.env.set('ANOTHER_KEY', 'VALUE'), - ), - new Pipeline('whatever').add( - new TriggerStep('another').build.withBranch('rease-bla'), - ), - new Pipeline('whatever').add( - new TriggerStep('another').build.withCommit('c0ffee'), - ), - new Pipeline('whatever').add( - new TriggerStep('another').build.withMessage('My messge'), - ), - new Pipeline('whatever').add( - new TriggerStep('another').build.metadata - .set('release-version', '1.1') - .build.metadata.set('some-other', 'value'), - ), - ]); - }); + createTest('build', () => [ + new Pipeline('whatever').add( + new TriggerStep('another').build.env + .set('KEY', 'VALUE') + .build.env.set('ANOTHER_KEY', 'VALUE'), + ), + new Pipeline('whatever').add( + new TriggerStep('another').build.withBranch('rease-bla'), + ), + new Pipeline('whatever').add( + new TriggerStep('another').build.withCommit('c0ffee'), + ), + new Pipeline('whatever').add( + new TriggerStep('another').build.withMessage('My messge'), + ), + new Pipeline('whatever').add( + new TriggerStep('another').build.metadata + .set('release-version', '1.1') + .build.metadata.set('some-other', 'value'), + ), + ]); }); + }); }); diff --git a/src/base.ts b/src/base.ts index 78b7d22..978e908 100644 --- a/src/base.ts +++ b/src/base.ts @@ -1,9 +1,9 @@ import ow from 'ow'; import sortBy from 'lodash.sortby'; import { - PotentialStep, - Serializable, - ToJsonSerializationOptions, + PotentialStep, + Serializable, + ToJsonSerializationOptions, } from './index'; import uniqid from 'uniqid'; import { unwrapSteps } from './unwrapSteps'; @@ -15,176 +15,176 @@ export interface BaseStep {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Step extends BaseStep {} export abstract class Step implements BaseStep, Serializable { - /** - * A set of potential steps that are hard dependencies to the - * current step. Each of these will be marked as accepted in case - * it is a Conditional and added to the graph as a side-effect. - */ - public readonly dependencies: Set = new Set(); - - /** - * A set of potential steps the current step is an effect of. - * The current step will only be added if the potential step - * is accepted; the effect dependency will not be added to the - * graph automatically - */ - public readonly effectDependencies: Set = new Set(); - - private _key?: string; - - get key(): string { - this._key = this._key || uniqid(); - return this._key; + /** + * A set of potential steps that are hard dependencies to the + * current step. Each of these will be marked as accepted in case + * it is a Conditional and added to the graph as a side-effect. + */ + public readonly dependencies: Set = new Set(); + + /** + * A set of potential steps the current step is an effect of. + * The current step will only be added if the potential step + * is accepted; the effect dependency will not be added to the + * graph automatically + */ + public readonly effectDependencies: Set = new Set(); + + private _key?: string; + + get key(): string { + this._key = this._key || uniqid(); + return this._key; + } + + private _allowDependencyFailure = false; + allowDependencyFailure(allow = true): this { + this._allowDependencyFailure = allow; + return this; + } + + /** + * @deprecated + */ + withId(identifier: string): this { + /* istanbul ignore next */ + return this.withKey(identifier); + } + + withKey(identifier: string): this { + ow(identifier, ow.string.nonEmpty.maxLength(100)); + this._key = identifier; + return this; + } + + private assertEffectOrDependency(...steps: PotentialStep[]): void { + ow(steps, ow.array.ofType(ow.object.nonEmpty)); + if (steps.includes(this)) { + throw new Error('Self-references are not supported'); } - - private _allowDependencyFailure = false; - allowDependencyFailure(allow = true): this { - this._allowDependencyFailure = allow; - return this; - } - - /** - * @deprecated - */ - withId(identifier: string): this { - /* istanbul ignore next */ - return this.withKey(identifier); - } - - withKey(identifier: string): this { - ow(identifier, ow.string.nonEmpty.maxLength(100)); - this._key = identifier; - return this; - } - - private assertEffectOrDependency(...steps: PotentialStep[]): void { - ow(steps, ow.array.ofType(ow.object.nonEmpty)); - if (steps.includes(this)) { - throw new Error('Self-references are not supported'); - } + } + + /** + * This marks the given step or conditional as a dependency to the current + * step. + * In case the dependency is a conditional, then that conditional will + * always be added to the graph (e.g. the value of the accept function of that + * conditional will be trumped by the fact that the current step depends on it) + */ + dependsOn(...steps: PotentialStep[]): this { + this.assertEffectOrDependency(...steps); + // iterate in reverse so if dependencies are not added to the graph, yet + // they will be added in the order they are given as dependencies + for (let i = steps.length; i > 0; i--) { + const step = steps[i - 1]; + this.dependencies.add(step); + this.effectDependencies.delete(step); } - - /** - * This marks the given step or conditional as a dependency to the current - * step. - * In case the dependency is a conditional, then that conditional will - * always be added to the graph (e.g. the value of the accept function of that - * conditional will be trumped by the fact that the current step depends on it) - */ - dependsOn(...steps: PotentialStep[]): this { - this.assertEffectOrDependency(...steps); - // iterate in reverse so if dependencies are not added to the graph, yet - // they will be added in the order they are given as dependencies - for (let i = steps.length; i > 0; i--) { - const step = steps[i - 1]; - this.dependencies.add(step); - this.effectDependencies.delete(step); - } - return this; - } - - isEffectOf(...steps: PotentialStep[]): this { - this.assertEffectOrDependency(...steps); - steps.forEach((s) => { - this.effectDependencies.add(s); - this.dependencies.delete(s); - }); - return this; - } - - public always = false; - - alwaysExecute(): this { - this.always = true; - return this; - } - - async toJson( - opts: ToJsonSerializationOptions = { explicitDependencies: false }, - ): Promise> { - if (!opts.explicitDependencies) { - return {}; - } - const dependsOn = sortBy( - ( - await unwrapSteps( - [...this.dependencies, ...this.effectDependencies], - opts.cache, - ) - ).map((s) => ({ - step: s.key, - allow_failure: this._allowDependencyFailure - ? undefined - : this.always || undefined, - })), - 'step', - ); - return { - key: this.key, - depends_on: dependsOn.length ? dependsOn : undefined, - allow_dependency_failure: this._allowDependencyFailure || undefined, - }; + return this; + } + + isEffectOf(...steps: PotentialStep[]): this { + this.assertEffectOrDependency(...steps); + steps.forEach((s) => { + this.effectDependencies.add(s); + this.dependencies.delete(s); + }); + return this; + } + + public always = false; + + alwaysExecute(): this { + this.always = true; + return this; + } + + async toJson( + opts: ToJsonSerializationOptions = { explicitDependencies: false }, + ): Promise> { + if (!opts.explicitDependencies) { + return {}; } + const dependsOn = sortBy( + ( + await unwrapSteps( + [...this.dependencies, ...this.effectDependencies], + opts.cache, + ) + ).map((s) => ({ + step: s.key, + allow_failure: this._allowDependencyFailure + ? undefined + : this.always || undefined, + })), + 'step', + ); + return { + key: this.key, + depends_on: dependsOn.length ? dependsOn : undefined, + allow_dependency_failure: this._allowDependencyFailure || undefined, + }; + } } export class BranchLimitedStep extends Step { - private branches: Set = new Set(); - - withBranch(pattern: string): this { - ow(pattern, ow.string.nonEmpty); - this.branches.add(pattern); - return this; - } - async toJson( - opts: ToJsonSerializationOptions = { explicitDependencies: false }, - ): Promise> { - return { - branches: this.branches.size - ? [...this.branches].sort().join(' ') - : undefined, - ...(await super.toJson(opts)), - }; - } + private branches: Set = new Set(); + + withBranch(pattern: string): this { + ow(pattern, ow.string.nonEmpty); + this.branches.add(pattern); + return this; + } + async toJson( + opts: ToJsonSerializationOptions = { explicitDependencies: false }, + ): Promise> { + return { + branches: this.branches.size + ? [...this.branches].sort().join(' ') + : undefined, + ...(await super.toJson(opts)), + }; + } } export class LabeledStep extends BranchLimitedStep { - private _label?: string; - - get label(): string | undefined { - return this._label; - } - - withLabel(label: string): this { - this._label = label; - return this; - } - - async toJson( - opts: ToJsonSerializationOptions = { explicitDependencies: false }, - ): Promise> { - return { - label: this.label, - ...(await super.toJson(opts)), - }; - } + private _label?: string; + + get label(): string | undefined { + return this._label; + } + + withLabel(label: string): this { + this._label = label; + return this; + } + + async toJson( + opts: ToJsonSerializationOptions = { explicitDependencies: false }, + ): Promise> { + return { + label: this.label, + ...(await super.toJson(opts)), + }; + } } export abstract class Chainable { - constructor(protected readonly parent: T) { - this.parent = parent; - } + constructor(protected readonly parent: T) { + this.parent = parent; + } } export type ExitStatus = number | '*'; export const exitStatusPredicate = ow.any( - ow.string.equals('*'), - ow.number.integer, + ow.string.equals('*'), + ow.number.integer, ); export function mapToObject(m: Map): Record { - return Array.from(m).reduce>((acc, [key, value]) => { - acc[key] = value; - return acc; - }, {}); + return Array.from(m).reduce>((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {}); } diff --git a/src/conditional.ts b/src/conditional.ts index 5562174..0251bf3 100644 --- a/src/conditional.ts +++ b/src/conditional.ts @@ -4,17 +4,15 @@ export type Generator = () => T | Promise; export type ThingOrGenerator = T | Promise | Generator; export abstract class Conditional { - constructor(private readonly guarded: ThingOrGenerator) {} + constructor(private readonly guarded: ThingOrGenerator) {} - get(): T | Promise { - return typeof this.guarded === 'function' - ? this.guarded() - : this.guarded; - } + get(): T | Promise { + return typeof this.guarded === 'function' ? this.guarded() : this.guarded; + } - /** - * The step is accepted if the returned boolean is true or the promise resolves to true. - * The step is rejected if the returned boolean is false or the promise resolves to false. - */ - abstract accept(): boolean | Promise; + /** + * The step is accepted if the returned boolean is true or the promise resolves to true. + * The step is rejected if the returned boolean is false or the promise resolves to false. + */ + abstract accept(): boolean | Promise; } diff --git a/src/index.ts b/src/index.ts index fa184bf..c3f9a48 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,112 +21,112 @@ export { Plugin } from './steps/command/plugins'; export { TriggerStep } from './steps/trigger'; export const serializers = { - DotSerializer, - JsonSerializer, - StructuralSerializer, - YamlSerializer, + DotSerializer, + JsonSerializer, + StructuralSerializer, + YamlSerializer, }; export type SerializationOptions = { - /** - * Whether to use the new depends_on syntax which allows the serializer to serialize into a graph with dependencies instead of a list with wait steps. - * More details here: https://buildkite.com/docs/pipelines/dependencies#defining-explicit-dependencies - */ - explicitDependencies?: boolean; + /** + * Whether to use the new depends_on syntax which allows the serializer to serialize into a graph with dependencies instead of a list with wait steps. + * More details here: https://buildkite.com/docs/pipelines/dependencies#defining-explicit-dependencies + */ + explicitDependencies?: boolean; }; export type ToJsonSerializationOptions = - | { - explicitDependencies: true; - cache: StepCache; - } - | { explicitDependencies: false }; + | { + explicitDependencies: true; + cache: StepCache; + } + | { explicitDependencies: false }; type JSON = Record | JSON[]; export interface Serializable { - toJson(opts?: ToJsonSerializationOptions): Promise; + toJson(opts?: ToJsonSerializationOptions): Promise; } export type PotentialStep = Step | Conditional; export class Pipeline implements Serializable { - public readonly name: string; + public readonly name: string; - public readonly steps: PotentialStep[] = []; + public readonly steps: PotentialStep[] = []; - public readonly env: KeyValue; + public readonly env: KeyValue; - constructor(name: string) { - this.name = name; - this.env = new KeyValueImpl(this); - } + constructor(name: string) { + this.name = name; + this.env = new KeyValueImpl(this); + } - add(...step: PotentialStep[]): this { - step.forEach((s) => { - if (this.steps.includes(s)) { - throw new Error('Can not add the same step more than once'); - } - this.steps.push(s); - }); - return this; - } + add(...step: PotentialStep[]): this { + step.forEach((s) => { + if (this.steps.includes(s)) { + throw new Error('Can not add the same step more than once'); + } + this.steps.push(s); + }); + return this; + } - slug(): string { - return slugify(this.name, { - lowercase: true, - customReplacements: [['_', '-']], - decamelize: false, - }); - } + slug(): string { + return slugify(this.name, { + lowercase: true, + customReplacements: [['_', '-']], + decamelize: false, + }); + } - async toList( - opts: ToJsonSerializationOptions = { explicitDependencies: false }, - ): Promise<(WaitStep | Step)[]> { - if (opts.explicitDependencies) { - const sorted = await sortedSteps(this, opts.cache); - return sorted; - } + async toList( + opts: ToJsonSerializationOptions = { explicitDependencies: false }, + ): Promise<(WaitStep | Step)[]> { + if (opts.explicitDependencies) { + const sorted = await sortedSteps(this, opts.cache); + return sorted; + } - // eslint-disable-next-line @typescript-eslint/no-use-before-define - const stepsWithBlocks = await sortedWithBlocks(this); - const steps: (WaitStep | Step)[] = []; - let lastWait: WaitStep | undefined = undefined; - for (const s of stepsWithBlocks) { - if (s === null) { - lastWait = new WaitStep(); - steps.push(lastWait); - } else { - if (lastWait) { - if (s.always && !lastWait.continueOnFailure) { - lastWait.continueOnFailure = true; - } else if (lastWait.continueOnFailure && !s.always) { - lastWait = new WaitStep(); - steps.push(lastWait); - } - } - steps.push(s); - } + // eslint-disable-next-line @typescript-eslint/no-use-before-define + const stepsWithBlocks = await sortedWithBlocks(this); + const steps: (WaitStep | Step)[] = []; + let lastWait: WaitStep | undefined = undefined; + for (const s of stepsWithBlocks) { + if (s === null) { + lastWait = new WaitStep(); + steps.push(lastWait); + } else { + if (lastWait) { + if (s.always && !lastWait.continueOnFailure) { + lastWait.continueOnFailure = true; + } else if (lastWait.continueOnFailure && !s.always) { + lastWait = new WaitStep(); + steps.push(lastWait); + } } - return steps; + steps.push(s); + } } + return steps; + } - async toJson( - opts: SerializationOptions = { explicitDependencies: false }, - ): Promise> { - const newOpts: ToJsonSerializationOptions = opts.explicitDependencies - ? { - explicitDependencies: true, - cache: new Map(), - } - : { - explicitDependencies: false, - }; - - return { - env: await (this.env as KeyValueImpl).toJson(), - steps: await Promise.all( - (await this.toList(newOpts)).map((s) => s.toJson(newOpts)), - ), + async toJson( + opts: SerializationOptions = { explicitDependencies: false }, + ): Promise> { + const newOpts: ToJsonSerializationOptions = opts.explicitDependencies + ? { + explicitDependencies: true, + cache: new Map(), + } + : { + explicitDependencies: false, }; - } + + return { + env: await (this.env as KeyValueImpl).toJson(), + steps: await Promise.all( + (await this.toList(newOpts)).map((s) => s.toJson(newOpts)), + ), + }; + } } diff --git a/src/key_value.ts b/src/key_value.ts index 45ec9a5..e117282 100644 --- a/src/key_value.ts +++ b/src/key_value.ts @@ -3,23 +3,23 @@ import { Chainable, mapToObject } from './base'; import { Serializable } from './index'; export interface KeyValue { - set(name: string, value: string): T; + set(name: string, value: string): T; } export class KeyValueImpl - extends Chainable - implements KeyValue, Serializable + extends Chainable + implements KeyValue, Serializable { - public readonly vars: Map = new Map(); + public readonly vars: Map = new Map(); - set(name: string, value: string): T { - ow(name, ow.string.nonEmpty); - ow(value, ow.string.nonEmpty); - this.vars.set(name, value); - return this.parent; - } + set(name: string, value: string): T { + ow(name, ow.string.nonEmpty); + ow(value, ow.string.nonEmpty); + this.vars.set(name, value); + return this.parent; + } - async toJson(): Promise | undefined> { - return this.vars.size ? mapToObject(this.vars) : undefined; - } + async toJson(): Promise | undefined> { + return this.vars.size ? mapToObject(this.vars) : undefined; + } } diff --git a/src/serializers/dot.ts b/src/serializers/dot.ts index ce82d90..90bacbf 100644 --- a/src/serializers/dot.ts +++ b/src/serializers/dot.ts @@ -4,42 +4,39 @@ import { Pipeline, TriggerStep } from '../'; import { sortedWithBlocks } from '../sortedWithBlocks'; export class DotSerializer implements Serializer { - async serialize(e: Pipeline): Promise { - const allSteps = await sortedWithBlocks(e); - if (allSteps.length > 0) { - allSteps.unshift(null); - } + async serialize(e: Pipeline): Promise { + const allSteps = await sortedWithBlocks(e); + if (allSteps.length > 0) { + allSteps.unshift(null); + } - const graph = graphviz.digraph(`"${e.name}"`); - graph.set('compound', true); - let lastNode; - let i = 0; - let currentCluster: graphviz.Graph; - for (const step of allSteps) { - if (step === null) { - currentCluster = graph.addCluster(`cluster_${i++}`); - currentCluster.set('color', 'black'); - continue; - } - for (const dependency of step.dependencies) { - const edge = graph.addEdge( - dependency.toString(), - step.toString(), - ); - edge.set('ltail', `cluster_${i - 2}`); - edge.set('lhead', `cluster_${i - 1}`); - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - lastNode = currentCluster!.addNode(step.toString()); - lastNode.set('color', 'grey'); + const graph = graphviz.digraph(`"${e.name}"`); + graph.set('compound', true); + let lastNode; + let i = 0; + let currentCluster: graphviz.Graph; + for (const step of allSteps) { + if (step === null) { + currentCluster = graph.addCluster(`cluster_${i++}`); + currentCluster.set('color', 'black'); + continue; + } + for (const dependency of step.dependencies) { + const edge = graph.addEdge(dependency.toString(), step.toString()); + edge.set('ltail', `cluster_${i - 2}`); + edge.set('lhead', `cluster_${i - 1}`); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + lastNode = currentCluster!.addNode(step.toString()); + lastNode.set('color', 'grey'); - if (step instanceof TriggerStep) { - const triggered = graph.addNode(step.trigger); - triggered.set('shape', 'Msquare'); - const edge = graph.addEdge(lastNode, triggered); - edge.set('label', 'triggers'); - } - } - return graph.to_dot(); + if (step instanceof TriggerStep) { + const triggered = graph.addNode(step.trigger); + triggered.set('shape', 'Msquare'); + const edge = graph.addEdge(lastNode, triggered); + edge.set('label', 'triggers'); + } } + return graph.to_dot(); + } } diff --git a/src/serializers/index.ts b/src/serializers/index.ts index 19f70ab..93f6d5d 100644 --- a/src/serializers/index.ts +++ b/src/serializers/index.ts @@ -1,5 +1,5 @@ import { Pipeline } from '../'; export interface Serializer { - serialize(e: Pipeline): Promise; + serialize(e: Pipeline): Promise; } diff --git a/src/serializers/json.ts b/src/serializers/json.ts index 5d4e170..f5855b1 100644 --- a/src/serializers/json.ts +++ b/src/serializers/json.ts @@ -2,17 +2,17 @@ import { Pipeline, SerializationOptions } from '../'; import { Serializer } from '.'; type JsonSerializationOptions = { - stringify?: boolean; + stringify?: boolean; } & SerializationOptions; export class JsonSerializer - implements Serializer | string> + implements Serializer | string> { - constructor(private readonly opts: JsonSerializationOptions = {}) {} + constructor(private readonly opts: JsonSerializationOptions = {}) {} - async serialize(e: Pipeline): Promise | string> { - const serialized = JSON.stringify(await e.toJson(this.opts)); - // Workaround to get rid of undefined values - return this.opts.stringify ? serialized : JSON.parse(serialized); - } + async serialize(e: Pipeline): Promise | string> { + const serialized = JSON.stringify(await e.toJson(this.opts)); + // Workaround to get rid of undefined values + return this.opts.stringify ? serialized : JSON.parse(serialized); + } } diff --git a/src/serializers/structural.ts b/src/serializers/structural.ts index bc4e0a2..fd69dcd 100644 --- a/src/serializers/structural.ts +++ b/src/serializers/structural.ts @@ -2,8 +2,8 @@ import { Pipeline } from '../'; import { Serializer } from '.'; export class StructuralSerializer implements Serializer { - async serialize(p: Pipeline): Promise { - const steps = await p.toList(); - return steps.map((step) => `* ${step.toString()}`).join('\n'); - } + async serialize(p: Pipeline): Promise { + const steps = await p.toList(); + return steps.map((step) => `* ${step.toString()}`).join('\n'); + } } diff --git a/src/serializers/yaml.ts b/src/serializers/yaml.ts index 1861871..3f15f46 100644 --- a/src/serializers/yaml.ts +++ b/src/serializers/yaml.ts @@ -4,21 +4,19 @@ import { Serializer } from '.'; import { JsonSerializer } from './json'; export class YamlSerializer implements Serializer { - private readonly jsonSerializer: Serializer< - string | Record - >; + private readonly jsonSerializer: Serializer>; - constructor(opts: SerializationOptions = {}) { - this.jsonSerializer = new JsonSerializer({ ...opts, stringify: false }); - } + constructor(opts: SerializationOptions = {}) { + this.jsonSerializer = new JsonSerializer({ ...opts, stringify: false }); + } - async serialize(e: Pipeline): Promise { - return jsyaml.dump(await this.jsonSerializer.serialize(e), { - skipInvalid: true, - noRefs: true, - styles: { - '!!null': 'canonical', // dump null as ~ - }, - }); - } + async serialize(e: Pipeline): Promise { + return jsyaml.dump(await this.jsonSerializer.serialize(e), { + skipInvalid: true, + noRefs: true, + styles: { + '!!null': 'canonical', // dump null as ~ + }, + }); + } } diff --git a/src/sortedSteps.ts b/src/sortedSteps.ts index cbf6b9b..52f56fb 100644 --- a/src/sortedSteps.ts +++ b/src/sortedSteps.ts @@ -5,111 +5,106 @@ import { unwrapSteps, StepCache } from './unwrapSteps'; import { Conditional } from './conditional'; export async function sortedSteps( - e: Pipeline, - cache: StepCache, + e: Pipeline, + cache: StepCache, ): Promise { - const steps = await unwrapSteps(e.steps, cache); - const sortOp = new TopologicalSort( - new Map(steps.map((step) => [step, step])), - ); + const steps = await unwrapSteps(e.steps, cache); + const sortOp = new TopologicalSort( + new Map(steps.map((step) => [step, step])), + ); - async function getAndCacheDependency( - potentialDependency: PotentialStep, - ): Promise { - if (potentialDependency instanceof Conditional) { - if (cache.has(potentialDependency)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return cache.get(potentialDependency)!; - } else { - // in the case we have to unwrap the conditional we store it for later use - // otherwise the getter will be called many times, returning a new object each - // time which means evenm though multiple steps might depend on the same conditionals - // we would add a new step each time. Also, generating a step can be potentially expemnsive - // so we want to do this only once - const dependency = await potentialDependency.get(); - cache.set(potentialDependency, dependency); - return dependency; - } - } else { - return potentialDependency; - } + async function getAndCacheDependency( + potentialDependency: PotentialStep, + ): Promise { + if (potentialDependency instanceof Conditional) { + if (cache.has(potentialDependency)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return cache.get(potentialDependency)!; + } else { + // in the case we have to unwrap the conditional we store it for later use + // otherwise the getter will be called many times, returning a new object each + // time which means evenm though multiple steps might depend on the same conditionals + // we would add a new step each time. Also, generating a step can be potentially expemnsive + // so we want to do this only once + const dependency = await potentialDependency.get(); + cache.set(potentialDependency, dependency); + return dependency; + } + } else { + return potentialDependency; } - const inGraph = (s: Step): boolean => steps.indexOf(s) !== -1; - const addToGraph = (s: Step): void => { - if (!inGraph(s)) { - // a dependency has not been added to the graph explicitly, - // so we add it implicitly - sortOp.addNode(s, s); - steps.push(s); - // maybe we want to rather throw here? - // Unsure...there could be a strict mode where we: - // throw new Error(`Step not part of the graph: '${dependency}'`); + } + const inGraph = (s: Step): boolean => steps.indexOf(s) !== -1; + const addToGraph = (s: Step): void => { + if (!inGraph(s)) { + // a dependency has not been added to the graph explicitly, + // so we add it implicitly + sortOp.addNode(s, s); + steps.push(s); + // maybe we want to rather throw here? + // Unsure...there could be a strict mode where we: + // throw new Error(`Step not part of the graph: '${dependency}'`); + } + }; + + const removedEffects = new Set(); + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + const addDependency = (dependency: Step): void => { + addToGraph(dependency); + try { + sortOp.addEdge(dependency, step); + } catch (e) { + // edge was already added, that is fine + } + }; + const iterateAndAddEffect = async (s: Step): Promise => { + for (const potentialEffectDependency of s.effectDependencies) { + const dependency = await getAndCacheDependency( + potentialEffectDependency, + ); + if (potentialEffectDependency instanceof Conditional) { + // in case it is a conditional we are interested in whether it that one was accepted or not + if ( + (await potentialEffectDependency.accept()) || + inGraph(dependency) + ) { + // if it was accepted and it is part of the graph, add the dependency to the current step + addDependency(dependency); + s.dependsOn(potentialEffectDependency); + } else { + // remove the current step from the graph + removedEffects.add(s); + } + } else { + // the dependency is a step and it wasn't removed before; + // add ourselves to the graph if the step is part of the graph + if (inGraph(dependency) && !removedEffects.has(dependency)) { + addDependency(dependency); + s.dependsOn(potentialEffectDependency); + } else { + removedEffects.add(s); + } } + } }; - const removedEffects = new Set(); - for (let i = 0; i < steps.length; i++) { - const step = steps[i]; - const addDependency = (dependency: Step): void => { - addToGraph(dependency); - try { - sortOp.addEdge(dependency, step); - } catch (e) { - // edge was already added, that is fine - } - }; - const iterateAndAddEffect = async (s: Step): Promise => { - for (const potentialEffectDependency of s.effectDependencies) { - const dependency = await getAndCacheDependency( - potentialEffectDependency, - ); - if (potentialEffectDependency instanceof Conditional) { - // in case it is a conditional we are interested in whether it that one was accepted or not - if ( - (await potentialEffectDependency.accept()) || - inGraph(dependency) - ) { - // if it was accepted and it is part of the graph, add the dependency to the current step - addDependency(dependency); - s.dependsOn(potentialEffectDependency); - } else { - // remove the current step from the graph - removedEffects.add(s); - } - } else { - // the dependency is a step and it wasn't removed before; - // add ourselves to the graph if the step is part of the graph - if ( - inGraph(dependency) && - !removedEffects.has(dependency) - ) { - addDependency(dependency); - s.dependsOn(potentialEffectDependency); - } else { - removedEffects.add(s); - } - } - } - }; - - await iterateAndAddEffect(step); + await iterateAndAddEffect(step); - if (!removedEffects.has(step)) { - for (const potentialDependency of [...step.dependencies]) { - // when we depend on a conditional the acceptor of the conditional doesn't matter - // we need to always get it and add it to the graph - const dependency = await getAndCacheDependency( - potentialDependency, - ); - addDependency(dependency); - for (const removedEffectStep of [...removedEffects]) { - removedEffects.delete(removedEffectStep); - await iterateAndAddEffect(removedEffectStep); - } - } + if (!removedEffects.has(step)) { + for (const potentialDependency of [...step.dependencies]) { + // when we depend on a conditional the acceptor of the conditional doesn't matter + // we need to always get it and add it to the graph + const dependency = await getAndCacheDependency(potentialDependency); + addDependency(dependency); + for (const removedEffectStep of [...removedEffects]) { + removedEffects.delete(removedEffectStep); + await iterateAndAddEffect(removedEffectStep); } + } } - return Array.from(sortOp.sort().values()) - .map((i) => i.node) - .filter((s) => !removedEffects.has(s)); + } + return Array.from(sortOp.sort().values()) + .map((i) => i.node) + .filter((s) => !removedEffects.has(s)); } diff --git a/src/sortedWithBlocks.ts b/src/sortedWithBlocks.ts index 661bcc1..791f512 100644 --- a/src/sortedWithBlocks.ts +++ b/src/sortedWithBlocks.ts @@ -5,25 +5,24 @@ import { Conditional } from './conditional'; import { StepCache } from './unwrapSteps'; export async function sortedWithBlocks(e: Pipeline): Promise<(Step | null)[]> { - const cache: StepCache = new Map(); - const sorted = await sortedSteps(e, cache); - // null denotes a block - const allSteps: (Step | null)[] = []; - let lastWaitStep = -1; - for (const step of sorted) { - dep: for (const potentialDependency of step.dependencies) { - const dependency = - potentialDependency instanceof Conditional - ? cache.get(potentialDependency) || - (await potentialDependency.get()) - : potentialDependency; - const dependentStep = allSteps.indexOf(dependency); - if (dependentStep !== -1 && dependentStep > lastWaitStep) { - lastWaitStep = allSteps.push(null) - 1; - break dep; - } - } - allSteps.push(step); + const cache: StepCache = new Map(); + const sorted = await sortedSteps(e, cache); + // null denotes a block + const allSteps: (Step | null)[] = []; + let lastWaitStep = -1; + for (const step of sorted) { + dep: for (const potentialDependency of step.dependencies) { + const dependency = + potentialDependency instanceof Conditional + ? cache.get(potentialDependency) || (await potentialDependency.get()) + : potentialDependency; + const dependentStep = allSteps.indexOf(dependency); + if (dependentStep !== -1 && dependentStep > lastWaitStep) { + lastWaitStep = allSteps.push(null) - 1; + break dep; + } } - return allSteps; + allSteps.push(step); + } + return allSteps; } diff --git a/src/steps/block.ts b/src/steps/block.ts index a5f7d08..709e7d0 100644 --- a/src/steps/block.ts +++ b/src/steps/block.ts @@ -4,28 +4,28 @@ import { Fields, FieldsImpl } from './block/fields'; import { ToJsonSerializationOptions } from 'src'; export class BlockStep extends BranchLimitedStep { - private readonly title: string; - private readonly prompt?: string; - public readonly fields: Fields = new FieldsImpl(this); - constructor(title: string, prompt?: string) { - super(); - ow(title, ow.string.nonEmpty); - this.title = title; - this.prompt = prompt; - } + private readonly title: string; + private readonly prompt?: string; + public readonly fields: Fields = new FieldsImpl(this); + constructor(title: string, prompt?: string) { + super(); + ow(title, ow.string.nonEmpty); + this.title = title; + this.prompt = prompt; + } - toString(): string { - return `[block for '${this.title}']`; - } + toString(): string { + return `[block for '${this.title}']`; + } - async toJson( - opts: ToJsonSerializationOptions = { explicitDependencies: false }, - ): Promise> { - return { - ...(await super.toJson(opts)), - block: this.title, - prompt: this.prompt, - fields: await (this.fields as FieldsImpl).toJson(), - }; - } + async toJson( + opts: ToJsonSerializationOptions = { explicitDependencies: false }, + ): Promise> { + return { + ...(await super.toJson(opts)), + block: this.title, + prompt: this.prompt, + fields: await (this.fields as FieldsImpl).toJson(), + }; + } } diff --git a/src/steps/block/fields.ts b/src/steps/block/fields.ts index 65b73c6..701b2a7 100644 --- a/src/steps/block/fields.ts +++ b/src/steps/block/fields.ts @@ -3,128 +3,123 @@ import { Chainable } from '../../base'; import { Serializable } from '../../index'; abstract class Field implements Serializable { - constructor( - public readonly key: string, - public readonly hint?: string, - public readonly required = false, - ) { - ow(key, ow.string.nonEmpty); - ow(key, ow.string.matches(/[0-9a-z-\/]+/i)); - } + constructor( + public readonly key: string, + public readonly hint?: string, + public readonly required = false, + ) { + ow(key, ow.string.nonEmpty); + ow(key, ow.string.matches(/[0-9a-z-\/]+/i)); + } - async toJson(): Promise> { - return { - key: this.key, - hint: this.hint, - required: this.required ? undefined : false, - }; - } + async toJson(): Promise> { + return { + key: this.key, + hint: this.hint, + required: this.required ? undefined : false, + }; + } } export class TextField extends Field { - constructor( - key: string, - private readonly label: string, - hint?: string, - required = true, - private readonly defaultValue?: string, - ) { - super(key, hint, required); - } + constructor( + key: string, + private readonly label: string, + hint?: string, + required = true, + private readonly defaultValue?: string, + ) { + super(key, hint, required); + } - override async toJson(): Promise> { - return { - ...(await super.toJson()), - text: this.label, - default: this.defaultValue, - }; - } + override async toJson(): Promise> { + return { + ...(await super.toJson()), + text: this.label, + default: this.defaultValue, + }; + } } export class Option implements Serializable { - constructor( - private readonly label: string, - private readonly value: string, - ) { - ow(label, ow.string.nonEmpty); - ow(value, ow.string.nonEmpty); - } + constructor(private readonly label: string, private readonly value: string) { + ow(label, ow.string.nonEmpty); + ow(value, ow.string.nonEmpty); + } - async toJson(): Promise> { - return { - label: this.label, - value: this.value, - }; - } + async toJson(): Promise> { + return { + label: this.label, + value: this.value, + }; + } } export class SelectField extends Field { - private options: Option[] = []; + private options: Option[] = []; - constructor( - key: string, - label: string, - hint?: string, - required?: boolean, - multiple?: false, - defaultValue?: string, - ); - constructor( - key: string, - label: string, - hint?: string, - required?: boolean, - multiple?: true, - defaultValue?: string | string[], - ); - constructor( - key: string, - private readonly label: string, - hint?: string, - required = true, - private readonly multiple = false, - private readonly defaultValue?: string | string[], - ) { - super(key, hint, required); - } + constructor( + key: string, + label: string, + hint?: string, + required?: boolean, + multiple?: false, + defaultValue?: string, + ); + constructor( + key: string, + label: string, + hint?: string, + required?: boolean, + multiple?: true, + defaultValue?: string | string[], + ); + constructor( + key: string, + private readonly label: string, + hint?: string, + required = true, + private readonly multiple = false, + private readonly defaultValue?: string | string[], + ) { + super(key, hint, required); + } - addOption(option: Option): this { - this.options.push(option); - return this; - } + addOption(option: Option): this { + this.options.push(option); + return this; + } - override async toJson(): Promise> { - return { - ...(await super.toJson()), - options: await Promise.all(this.options.map((o) => o.toJson())), - select: this.label, - default: this.defaultValue, - multiple: this.multiple || undefined, - }; - } + override async toJson(): Promise> { + return { + ...(await super.toJson()), + options: await Promise.all(this.options.map((o) => o.toJson())), + select: this.label, + default: this.defaultValue, + multiple: this.multiple || undefined, + }; + } } export interface Fields { - add(field: Field): T; + add(field: Field): T; } export class FieldsImpl - extends Chainable - implements Fields, Serializable + extends Chainable + implements Fields, Serializable { - fields: Map = new Map(); - add(field: Field): T { - this.fields.set(field.key, field); - return this.parent; - } - hasFields(): boolean { - return this.fields.size > 0; - } + fields: Map = new Map(); + add(field: Field): T { + this.fields.set(field.key, field); + return this.parent; + } + hasFields(): boolean { + return this.fields.size > 0; + } - async toJson(): Promise[] | undefined> { - if (!this.hasFields()) { - return undefined; - } - return await Promise.all( - [...this.fields.values()].map((f) => f.toJson()), - ); + async toJson(): Promise[] | undefined> { + if (!this.hasFields()) { + return undefined; } + return await Promise.all([...this.fields.values()].map((f) => f.toJson())); + } } diff --git a/src/steps/command.ts b/src/steps/command.ts index a095105..399a54d 100644 --- a/src/steps/command.ts +++ b/src/steps/command.ts @@ -1,28 +1,28 @@ import ow from 'ow'; import { KeyValue, KeyValueImpl } from '../key_value'; import { - Plugin, - Plugins, - PluginsImpl, - transformPlugins, + Plugin, + Plugins, + PluginsImpl, + transformPlugins, } from './command/plugins'; import { - ExitStatus, - exitStatusPredicate, - LabeledStep, - mapToObject, + ExitStatus, + exitStatusPredicate, + LabeledStep, + mapToObject, } from '../base'; import { Retry, RetryImpl } from './command/retry'; import { ToJsonSerializationOptions } from 'src'; function assertTimeout(timeout: number): void { - ow(timeout, ow.number.integerOrInfinite.positive); + ow(timeout, ow.number.integerOrInfinite.positive); } function assertSkipValue(value: SkipValue): void { - if (typeof value === 'string') { - ow(value, ow.string.nonEmpty); - } + if (typeof value === 'string') { + ow(value, ow.string.nonEmpty); + } } type SkipValue = boolean | string; @@ -30,255 +30,255 @@ export type SkipFunction = () => SkipValue; const transformCommandKey: unique symbol = Symbol('transformCommand'); export class Command { - private static [transformCommandKey] = ( - value: Command[], - ): undefined | string | string[] => { - if (!value || value.length === 0) { - return undefined; - } - return value.length === 1 - ? value[0].serialize() - : value.map((c) => c.serialize()); - }; - - constructor(public command: string, public timeout: number = Infinity) { - ow(command, ow.string); - assertTimeout(timeout); + private static [transformCommandKey] = ( + value: Command[], + ): undefined | string | string[] => { + if (!value || value.length === 0) { + return undefined; } + return value.length === 1 + ? value[0].serialize() + : value.map((c) => c.serialize()); + }; - toString(): string { - return this.command; - } + constructor(public command: string, public timeout: number = Infinity) { + ow(command, ow.string); + assertTimeout(timeout); + } - protected serialize(): string { - return this.toString(); - } + toString(): string { + return this.command; + } + + protected serialize(): string { + return this.toString(); + } } type Agents = Map; const transformSoftFail = ( - value: Set, + value: Set, ): undefined | boolean | { exit_status: ExitStatus }[] => { - if (!value.size) { - return undefined; - } else if (value.has('*')) { - return true; - } else { - return [...value].map((s) => ({ - exit_status: s, - })); - } + if (!value.size) { + return undefined; + } else if (value.has('*')) { + return true; + } else { + return [...value].map((s) => ({ + exit_status: s, + })); + } }; const transformSkipValue = ( - value: SkipValue | SkipFunction, + value: SkipValue | SkipFunction, ): SkipValue | SkipFunction | undefined => { - if (typeof value === 'function') { - value = value(); - assertSkipValue(value); - } - return value || undefined; + if (typeof value === 'function') { + value = value(); + assertSkipValue(value); + } + return value || undefined; }; type ConcurrencyMethod = 'eager' | 'ordered'; export class CommandStep extends LabeledStep { - public readonly command: Command[] = []; - public readonly env: KeyValue; + public readonly command: Command[] = []; + public readonly env: KeyValue; - private _parallelism?: number; - private get parallelism(): number | undefined { - return this._parallelism === 1 ? undefined : this._parallelism; - } + private _parallelism?: number; + private get parallelism(): number | undefined { + return this._parallelism === 1 ? undefined : this._parallelism; + } - private concurrency?: number; - private concurrencyGroup?: string; - private _concurrencyMethod?: ConcurrencyMethod; - private get concurrencyMethod(): ConcurrencyMethod | undefined { - return this._concurrencyMethod !== 'ordered' - ? this._concurrencyMethod - : undefined; - } - private _artifactPaths: Set = new Set(); - private _agents: Agents = new Map(); - get agents(): Agents { - return this._agents; + private concurrency?: number; + private concurrencyGroup?: string; + private _concurrencyMethod?: ConcurrencyMethod; + private get concurrencyMethod(): ConcurrencyMethod | undefined { + return this._concurrencyMethod !== 'ordered' + ? this._concurrencyMethod + : undefined; + } + private _artifactPaths: Set = new Set(); + private _agents: Agents = new Map(); + get agents(): Agents { + return this._agents; + } + private _timeout?: number; + get timeout(): number | undefined { + if (this._timeout === Infinity || this._timeout === 0) { + return undefined; + } else if (this._timeout) { + return this._timeout; } - private _timeout?: number; - get timeout(): number | undefined { - if (this._timeout === Infinity || this._timeout === 0) { - return undefined; - } else if (this._timeout) { - return this._timeout; - } - if (this.command) { - const value = this.command.reduce((acc, command) => { - acc += command.timeout; - return acc; - }, 0); - if (value === Infinity || value === 0) { - return undefined; - } else if (value) { - return value; - } - } + if (this.command) { + const value = this.command.reduce((acc, command) => { + acc += command.timeout; + return acc; + }, 0); + if (value === Infinity || value === 0) { + return undefined; + } else if (value) { + return value; + } } - private _priority?: number; - get priority(): number | undefined { - if (this._priority === 0) { - return undefined; - } - return this._priority; + } + private _priority?: number; + get priority(): number | undefined { + if (this._priority === 0) { + return undefined; } + return this._priority; + } - public readonly plugins: Plugins = new PluginsImpl(this); - private _softFail: Set = new Set(); - private _skip?: SkipValue | SkipFunction; - public readonly retry: Retry = new RetryImpl(this); + public readonly plugins: Plugins = new PluginsImpl(this); + private _softFail: Set = new Set(); + private _skip?: SkipValue | SkipFunction; + public readonly retry: Retry = new RetryImpl(this); - constructor(plugin: Plugin, label?: string); - constructor(command: string, label?: string); - constructor(command: Command, label?: string); - constructor(commands: (string | Plugin | Command)[], label?: string); - constructor( - command: string | Plugin | Command | (string | Plugin | Command)[], - label?: string, - ) { - super(); - this.env = new KeyValueImpl(this); - if (label) { - this.withLabel(label); - } - if (Array.isArray(command)) { - ow(command, ow.array.minLength(1)); - for (const c of command) { - this.add(c); - } - } else { - this.add(command); - } + constructor(plugin: Plugin, label?: string); + constructor(command: string, label?: string); + constructor(command: Command, label?: string); + constructor(commands: (string | Plugin | Command)[], label?: string); + constructor( + command: string | Plugin | Command | (string | Plugin | Command)[], + label?: string, + ) { + super(); + this.env = new KeyValueImpl(this); + if (label) { + this.withLabel(label); } - - add(c: string | Plugin | Command): this { - if (c instanceof Plugin) { - this.plugins.add(c); - } else if (typeof c === 'string') { - this.command.push(new Command(c)); - } else if (c instanceof Command) { - this.command.push(c); - } - return this; + if (Array.isArray(command)) { + ow(command, ow.array.minLength(1)); + for (const c of command) { + this.add(c); + } + } else { + this.add(command); } + } - /** - * - * @param timeout for the step in seconds. - */ - withTimeout(timeout = Infinity): this { - assertTimeout(timeout); - this._timeout = timeout; - return this; + add(c: string | Plugin | Command): this { + if (c instanceof Plugin) { + this.plugins.add(c); + } else if (typeof c === 'string') { + this.command.push(new Command(c)); + } else if (c instanceof Command) { + this.command.push(c); } + return this; + } - /** - * See: https://github.com/buildkite/docs/pull/1087 - * - * @param priority the relative priority of this command step; defaults to 0 - */ - withPriority(priority = 0): this { - ow(priority, ow.number.integer); - this._priority = priority; - return this; - } + /** + * + * @param timeout for the step in seconds. + */ + withTimeout(timeout = Infinity): this { + assertTimeout(timeout); + this._timeout = timeout; + return this; + } - withParallelism(parallelism: number): this { - ow(parallelism, ow.number.integer.positive); - this._parallelism = parallelism; - return this; - } + /** + * See: https://github.com/buildkite/docs/pull/1087 + * + * @param priority the relative priority of this command step; defaults to 0 + */ + withPriority(priority = 0): this { + ow(priority, ow.number.integer); + this._priority = priority; + return this; + } - withAgent(key: string, value: string): this { - ow(key, ow.string.nonEmpty); - ow(value, ow.string.nonEmpty); - this._agents.set(key, value); - return this; - } + withParallelism(parallelism: number): this { + ow(parallelism, ow.number.integer.positive); + this._parallelism = parallelism; + return this; + } - withArtifactPath(...globs: string[]): this { - ow(globs, ow.array.ofType(ow.string.nonEmpty)); - for (const glob of globs) { - this._artifactPaths.add(glob); - } - return this; - } + withAgent(key: string, value: string): this { + ow(key, ow.string.nonEmpty); + ow(value, ow.string.nonEmpty); + this._agents.set(key, value); + return this; + } - withConcurrency(concurrency: number, group: string): this { - ow(concurrency, ow.number.integer.positive); - this.concurrency = concurrency; - this.concurrencyGroup = group; - return this; + withArtifactPath(...globs: string[]): this { + ow(globs, ow.array.ofType(ow.string.nonEmpty)); + for (const glob of globs) { + this._artifactPaths.add(glob); } + return this; + } - withConcurrencyMethod(method: ConcurrencyMethod): this { - ow(method, ow.string.oneOf(['eager', 'ordered'])); - this._concurrencyMethod = method; - return this; - } + withConcurrency(concurrency: number, group: string): this { + ow(concurrency, ow.number.integer.positive); + this.concurrency = concurrency; + this.concurrencyGroup = group; + return this; + } - withSoftFail(fail: ExitStatus | true): this { - if (fail !== true) { - ow(fail, exitStatusPredicate); - } - this._softFail.add(fail === true ? '*' : fail); - return this; - } - - skip(skip: SkipValue | SkipFunction): this { - if (typeof skip !== 'function') { - assertSkipValue(skip); - } + withConcurrencyMethod(method: ConcurrencyMethod): this { + ow(method, ow.string.oneOf(['eager', 'ordered'])); + this._concurrencyMethod = method; + return this; + } - this._skip = skip; - return this; + withSoftFail(fail: ExitStatus | true): this { + if (fail !== true) { + ow(fail, exitStatusPredicate); } + this._softFail.add(fail === true ? '*' : fail); + return this; + } - toString(): string { - return ( - (this.label || - (this.command - ? `<${this.command.join(' && ') || '(empty)'}>` - : this.plugins.toString())) + - (this.parallelism ? ` [x ${this.parallelism}]` : '') - ); + skip(skip: SkipValue | SkipFunction): this { + if (typeof skip !== 'function') { + assertSkipValue(skip); } - async toJson( - opts: ToJsonSerializationOptions = { explicitDependencies: false }, - ): Promise> { - // Need to pull out one of env/retry to get around a weird Typescript v4.0 bug. - // When both env and retry were specified inside the return object, - // the contents of retry were being copied to env. - const env = await (this.env as KeyValueImpl).toJson(); - const retry = await (this.retry as RetryImpl).toJson(); - return { - ...(await super.toJson(opts)), - command: Command[transformCommandKey](this.command), - priority: this.priority, - env, - parallelism: this.parallelism, - concurrency: this.concurrency, - concurrency_group: this.concurrencyGroup, - concurrency_method: this.concurrencyMethod, - artifact_paths: this._artifactPaths.size - ? Array.from(this._artifactPaths) - : undefined, - agents: this.agents.size ? mapToObject(this.agents) : undefined, - timeout_in_minutes: this.timeout, - plugins: transformPlugins(this.plugins as PluginsImpl), - soft_fail: transformSoftFail(this._softFail), - skip: this._skip ? transformSkipValue(this._skip) : undefined, - retry, - }; - } + this._skip = skip; + return this; + } + + toString(): string { + return ( + (this.label || + (this.command + ? `<${this.command.join(' && ') || '(empty)'}>` + : this.plugins.toString())) + + (this.parallelism ? ` [x ${this.parallelism}]` : '') + ); + } + + async toJson( + opts: ToJsonSerializationOptions = { explicitDependencies: false }, + ): Promise> { + // Need to pull out one of env/retry to get around a weird Typescript v4.0 bug. + // When both env and retry were specified inside the return object, + // the contents of retry were being copied to env. + const env = await (this.env as KeyValueImpl).toJson(); + const retry = await (this.retry as RetryImpl).toJson(); + return { + ...(await super.toJson(opts)), + command: Command[transformCommandKey](this.command), + priority: this.priority, + env, + parallelism: this.parallelism, + concurrency: this.concurrency, + concurrency_group: this.concurrencyGroup, + concurrency_method: this.concurrencyMethod, + artifact_paths: this._artifactPaths.size + ? Array.from(this._artifactPaths) + : undefined, + agents: this.agents.size ? mapToObject(this.agents) : undefined, + timeout_in_minutes: this.timeout, + plugins: transformPlugins(this.plugins as PluginsImpl), + soft_fail: transformSoftFail(this._softFail), + skip: this._skip ? transformSkipValue(this._skip) : undefined, + retry, + }; + } } diff --git a/src/steps/command/plugins.ts b/src/steps/command/plugins.ts index 58a3039..f016a02 100644 --- a/src/steps/command/plugins.ts +++ b/src/steps/command/plugins.ts @@ -3,45 +3,45 @@ import { Chainable } from '../../base'; type Configuration = Record; export class Plugin { - constructor( - public readonly pluginNameOrPath: string, - public readonly configuration?: Configuration, - ) { - ow(pluginNameOrPath, ow.string.not.empty); - } + constructor( + public readonly pluginNameOrPath: string, + public readonly configuration?: Configuration, + ) { + ow(pluginNameOrPath, ow.string.not.empty); + } } export interface Plugins { - add(plugin: Plugin): T; - /** Return a list of plugins that match the given predicate */ - filter: Plugin[]['filter']; + add(plugin: Plugin): T; + /** Return a list of plugins that match the given predicate */ + filter: Plugin[]['filter']; } export function transformPlugins( - value: PluginsImpl, + value: PluginsImpl, ): Record[] | undefined { - if (!value.plugins.length) { - return undefined; - } + if (!value.plugins.length) { + return undefined; + } - return value.plugins.map((plugin) => { - return { - [plugin.pluginNameOrPath]: plugin.configuration || null, - }; - }); + return value.plugins.map((plugin) => { + return { + [plugin.pluginNameOrPath]: plugin.configuration || null, + }; + }); } export class PluginsImpl extends Chainable implements Plugins { - public plugins: Plugin[] = []; + public plugins: Plugin[] = []; - add(plugin: Plugin): T { - this.plugins.push(plugin); - return this.parent; - } + add(plugin: Plugin): T { + this.plugins.push(plugin); + return this.parent; + } - filter( - ...args: Parameters['filter']> - ): ReturnType['filter']> { - return this.plugins.filter(...args); - } + filter( + ...args: Parameters['filter']> + ): ReturnType['filter']> { + return this.plugins.filter(...args); + } } diff --git a/src/steps/command/retry.ts b/src/steps/command/retry.ts index 6ec9a0f..1d4a71b 100644 --- a/src/steps/command/retry.ts +++ b/src/steps/command/retry.ts @@ -5,124 +5,122 @@ import { Serializable } from '../../index'; type Statuses = boolean | number | Map; export interface Retry { - automatic(statuses: Statuses): T; - manual(allowed: boolean, permitOnPassed?: boolean, reason?: string): T; - getAutomaticValue(): Map; + automatic(statuses: Statuses): T; + manual(allowed: boolean, permitOnPassed?: boolean, reason?: string): T; + getAutomaticValue(): Map; } class RetryManual implements Serializable { - allowed = true; - permitOnPassed = false; - reason?: string; + allowed = true; + permitOnPassed = false; + reason?: string; - hasValue(): boolean { - return !this.allowed || this.permitOnPassed; - } + hasValue(): boolean { + return !this.allowed || this.permitOnPassed; + } - async toJson(): Promise | undefined> { - if (!this.hasValue()) { - return undefined; - } - return { - allowed: this.allowed ? undefined : false, - permit_on_passed: this.permitOnPassed || undefined, - reason: this.reason || undefined, - }; + async toJson(): Promise | undefined> { + if (!this.hasValue()) { + return undefined; } + return { + allowed: this.allowed ? undefined : false, + permit_on_passed: this.permitOnPassed || undefined, + reason: this.reason || undefined, + }; + } } const transformAutomatic = ( - value: Statuses, + value: Statuses, ): - | undefined - | boolean - | { limit: number } - | { exit_status?: ExitStatus; limit: number }[] => { - if (!value) { - return undefined; - } - if (typeof value === 'boolean') { - return value; - } else if (typeof value === 'number') { - return { limit: value }; - } - if (value.size === 1 && value.has('*') && value.get('*')) { - return { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - limit: value.get('*')!, - }; - } else { - return [...value.entries()].map(([s, limit]) => ({ - exit_status: s, - limit, - })); - } + | undefined + | boolean + | { limit: number } + | { exit_status?: ExitStatus; limit: number }[] => { + if (!value) { + return undefined; + } + if (typeof value === 'boolean') { + return value; + } else if (typeof value === 'number') { + return { limit: value }; + } + if (value.size === 1 && value.has('*') && value.get('*')) { + return { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + limit: value.get('*')!, + }; + } else { + return [...value.entries()].map(([s, limit]) => ({ + exit_status: s, + limit, + })); + } }; export class RetryImpl - extends Chainable - implements Retry, Serializable + extends Chainable + implements Retry, Serializable { - async toJson(): Promise | undefined> { - if (!this.hasValue()) { - return undefined; - } - return { - manual: await this._manual.toJson(), - automatic: transformAutomatic(this._automatic), - }; - } - - private readonly _manual = new RetryManual(); - private _automatic: Statuses = false; - - hasValue(): boolean { - return !!(this._manual.hasValue() || this._automatic); + async toJson(): Promise | undefined> { + if (!this.hasValue()) { + return undefined; } - - getAutomaticValue(): Map { - switch (typeof this._automatic) { - case 'boolean': - return this._automatic ? new Map([['*', 2]]) : new Map(); - case 'number': - return new Map([['*', this._automatic]]); - default: - return this._automatic; - } + return { + manual: await this._manual.toJson(), + automatic: transformAutomatic(this._automatic), + }; + } + + private readonly _manual = new RetryManual(); + private _automatic: Statuses = false; + + hasValue(): boolean { + return !!(this._manual.hasValue() || this._automatic); + } + + getAutomaticValue(): Map { + switch (typeof this._automatic) { + case 'boolean': + return this._automatic ? new Map([['*', 2]]) : new Map(); + case 'number': + return new Map([['*', this._automatic]]); + default: + return this._automatic; } - - automatic(statuses: Statuses = true): T { - if (typeof statuses === 'object') { - ow(statuses, ow.map.nonEmpty); - ow(statuses, ow.map.valuesOfType(ow.number.integer.positive)); - ow(statuses, ow.map.valuesOfType(exitStatusPredicate as any)); // Fix predicate type - - this._automatic = - typeof this._automatic !== 'object' - ? new Map() - : this._automatic; - - for (const [exitStatus, limit] of statuses) { - this._automatic.set(exitStatus, limit); - } - } else if (typeof statuses === 'number') { - ow(statuses, ow.number.positive); - this._automatic = statuses; - } else { - this._automatic = statuses; - } - return this.parent; + } + + automatic(statuses: Statuses = true): T { + if (typeof statuses === 'object') { + ow(statuses, ow.map.nonEmpty); + ow(statuses, ow.map.valuesOfType(ow.number.integer.positive)); + ow(statuses, ow.map.valuesOfType(exitStatusPredicate as any)); // Fix predicate type + + this._automatic = + typeof this._automatic !== 'object' ? new Map() : this._automatic; + + for (const [exitStatus, limit] of statuses) { + this._automatic.set(exitStatus, limit); + } + } else if (typeof statuses === 'number') { + ow(statuses, ow.number.positive); + this._automatic = statuses; + } else { + this._automatic = statuses; } + return this.parent; + } - manual(allowed = true, permitOnPassed = false, reason?: string): T { - ow(allowed, ow.boolean); - ow(permitOnPassed, ow.boolean); - ow(reason, ow.any(ow.undefined, ow.string.nonEmpty)); + manual(allowed = true, permitOnPassed = false, reason?: string): T { + ow(allowed, ow.boolean); + ow(permitOnPassed, ow.boolean); + ow(reason, ow.any(ow.undefined, ow.string.nonEmpty)); - this._manual.allowed = allowed; - this._manual.permitOnPassed = permitOnPassed; - this._manual.reason = reason; + this._manual.allowed = allowed; + this._manual.permitOnPassed = permitOnPassed; + this._manual.reason = reason; - return this.parent; - } + return this.parent; + } } diff --git a/src/steps/trigger.ts b/src/steps/trigger.ts index d67e490..d36ad2b 100644 --- a/src/steps/trigger.ts +++ b/src/steps/trigger.ts @@ -3,43 +3,43 @@ import { LabeledStep } from '../base'; import { Build, BuildImpl } from './trigger/build'; export class TriggerStep extends LabeledStep implements Serializable { - get trigger(): string { - return this._trigger instanceof Pipeline - ? this._trigger.slug() - : this._trigger; - } + get trigger(): string { + return this._trigger instanceof Pipeline + ? this._trigger.slug() + : this._trigger; + } - private _async = false; + private _async = false; - public readonly build: Build = new BuildImpl(this); + public readonly build: Build = new BuildImpl(this); - constructor( - private readonly _trigger: Pipeline | string, - label?: string, - async = false, - ) { - super(); - if (label) { - this.withLabel(label); - } - this._async = async; - } - async(async: boolean): this { - this._async = async; - return this; - } - toString(): string { - return this.label || `[trigger ${this.trigger}]`; + constructor( + private readonly _trigger: Pipeline | string, + label?: string, + async = false, + ) { + super(); + if (label) { + this.withLabel(label); } + this._async = async; + } + async(async: boolean): this { + this._async = async; + return this; + } + toString(): string { + return this.label || `[trigger ${this.trigger}]`; + } - async toJson( - opts: ToJsonSerializationOptions = { explicitDependencies: false }, - ): Promise> { - return { - trigger: this.trigger, - ...(await super.toJson(opts)), - async: this._async || undefined, - build: await (this.build as BuildImpl).toJson(), - }; - } + async toJson( + opts: ToJsonSerializationOptions = { explicitDependencies: false }, + ): Promise> { + return { + trigger: this.trigger, + ...(await super.toJson(opts)), + async: this._async || undefined, + build: await (this.build as BuildImpl).toJson(), + }; + } } diff --git a/src/steps/trigger/build.ts b/src/steps/trigger/build.ts index f997af6..ad24c60 100644 --- a/src/steps/trigger/build.ts +++ b/src/steps/trigger/build.ts @@ -2,55 +2,55 @@ import { KeyValue, KeyValueImpl } from '../../key_value'; import { Serializable } from '../../index'; export interface Build { - env: KeyValue; - metadata: KeyValue; - withMessage(message: string): T; - withCommit(commit: string): T; - withBranch(branch: string): T; + env: KeyValue; + metadata: KeyValue; + withMessage(message: string): T; + withCommit(commit: string): T; + withBranch(branch: string): T; } export class BuildImpl implements Build, Serializable { - private _message?: string; - private _commit?: string; - private _branch?: string; - public readonly env: KeyValue; - public readonly metadata: KeyValue; - constructor(private readonly triggerStep: T) { - this.env = new KeyValueImpl(triggerStep); - this.metadata = new KeyValueImpl(triggerStep); - } - withMessage(message: string): T { - this._message = message; - return this.triggerStep; - } - withCommit(commit: string): T { - this._commit = commit; - return this.triggerStep; - } - withBranch(branch: string): T { - this._branch = branch; - return this.triggerStep; - } - hasData(): boolean { - return !!( - this._branch || - this._commit || - typeof this._message !== 'undefined' || - (this.env as KeyValueImpl).vars.size || - (this.metadata as KeyValueImpl).vars.size - ); - } + private _message?: string; + private _commit?: string; + private _branch?: string; + public readonly env: KeyValue; + public readonly metadata: KeyValue; + constructor(private readonly triggerStep: T) { + this.env = new KeyValueImpl(triggerStep); + this.metadata = new KeyValueImpl(triggerStep); + } + withMessage(message: string): T { + this._message = message; + return this.triggerStep; + } + withCommit(commit: string): T { + this._commit = commit; + return this.triggerStep; + } + withBranch(branch: string): T { + this._branch = branch; + return this.triggerStep; + } + hasData(): boolean { + return !!( + this._branch || + this._commit || + typeof this._message !== 'undefined' || + (this.env as KeyValueImpl).vars.size || + (this.metadata as KeyValueImpl).vars.size + ); + } - async toJson(): Promise | undefined> { - if (!this.hasData()) { - return undefined; - } - return { - message: this._message, - commit: this._commit, - branch: this._branch, - env: await (this.env as KeyValueImpl).toJson(), - meta_data: await (this.metadata as KeyValueImpl).toJson(), - }; + async toJson(): Promise | undefined> { + if (!this.hasData()) { + return undefined; } + return { + message: this._message, + commit: this._commit, + branch: this._branch, + env: await (this.env as KeyValueImpl).toJson(), + meta_data: await (this.metadata as KeyValueImpl).toJson(), + }; + } } diff --git a/src/steps/wait.ts b/src/steps/wait.ts index abd9889..f8f0410 100644 --- a/src/steps/wait.ts +++ b/src/steps/wait.ts @@ -2,19 +2,17 @@ import { BaseStep } from '../base'; import { Serializable } from '../index'; export class WaitStep implements BaseStep, Serializable { - constructor(public continueOnFailure = false) {} + constructor(public continueOnFailure = false) {} - toString(): string { - /* istanbul ignore next */ - return this.continueOnFailure - ? '[wait; continues on failure]' - : '[wait]'; - } + toString(): string { + /* istanbul ignore next */ + return this.continueOnFailure ? '[wait; continues on failure]' : '[wait]'; + } - async toJson(): Promise> { - return { - wait: null, - continue_on_failure: this.continueOnFailure || undefined, - }; - } + async toJson(): Promise> { + return { + wait: null, + continue_on_failure: this.continueOnFailure || undefined, + }; + } } diff --git a/src/unwrapSteps.ts b/src/unwrapSteps.ts index acd3b61..6fd1a32 100644 --- a/src/unwrapSteps.ts +++ b/src/unwrapSteps.ts @@ -4,23 +4,23 @@ import { Conditional } from './conditional'; export type StepCache = Map, Step>; export async function unwrapSteps( - steps: PotentialStep[], - cache: StepCache, + steps: PotentialStep[], + cache: StepCache, ): Promise { - const ret: Step[] = []; - for (const s of steps) { - if (s instanceof Conditional) { - if (cache.has(s)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - ret.push(cache.get(s)!); - } else if ((await s.accept()) === true) { - const cond = await s.get(); - cache.set(s, cond); - ret.push(cond); - } - } else { - ret.push(s); - } + const ret: Step[] = []; + for (const s of steps) { + if (s instanceof Conditional) { + if (cache.has(s)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ret.push(cache.get(s)!); + } else if ((await s.accept()) === true) { + const cond = await s.get(); + cache.set(s, cond); + ret.push(cond); + } + } else { + ret.push(s); } - return ret; + } + return ret; } diff --git a/tsconfig.json b/tsconfig.json index fa18db2..bc264b9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,67 +1,67 @@ { - "compilerOptions": { - /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ - "target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, - "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, - "lib": [ - "ES2015", - "dom" - ] /* Specify library files to be included in the compilation. */, - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - "declaration": true /* Generates corresponding '.d.ts' file. */, - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "dist" /* Redirect output structure to the directory. */, - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + "compilerOptions": { + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, + "module": "ESNext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + "lib": [ + "ES2015", + "dom" + ] /* Specify library files to be included in the compilation. */, + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true /* Generates corresponding '.d.ts' file. */, + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "dist" /* Redirect output structure to the directory. */, + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */, + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - /* Module Resolution Options */ - "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, - "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Module Resolution Options */ + "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, + "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - /* Experimental Options */ - "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - }, - "include": ["src"] + /* Experimental Options */ + "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "include": ["src"] }