From 026026e52156735f0a620b25b0d494e20e5e1145 Mon Sep 17 00:00:00 2001 From: Ron Netzer Date: Tue, 1 Mar 2022 10:04:39 +0200 Subject: [PATCH] feat: migrate to composite actions (#27) remove git utils and use nrwl/nx-set-shas action instead closes #21, closes #22 --- .github/workflows/code-scanning.yml | 2 + .github/workflows/main.yml | 15 ++- CHANGELOG.md | 6 -- README.md | 3 +- .../@actions/artifact.ts | 0 .../__mocks__ => __mocks__}/@actions/cache.ts | 2 +- .../__mocks__ => __mocks__}/@actions/core.ts | 1 + __mocks__/@actions/exec.ts | 1 + .../@actions/github.ts | 0 __mocks__/@actions/glob.ts | 10 ++ __mocks__/@actions/io.ts | 4 + .../utils/__mocks__ => __mocks__}/which.ts | 0 jest.preset.js | 2 + package-lock.json | 94 ++++++++++++++----- package.json | 9 +- packages/nx-affected-matrix/action.yml | 68 ++++++++++++++ packages/nx-affected-matrix/jest.config.js | 1 - packages/nx-affected-matrix/project.json | 32 +++---- packages/nx-affected-matrix/src/app/inputs.ts | 11 ++- .../src/app/nx-affected-matrix.spec.ts | 42 ++++----- .../src/app/nx-affected-matrix.ts | 44 +++++---- .../nx-affected-matrix/src/assets/.gitkeep | 0 .../nx-affected-matrix/src/assets/action.yml | 38 -------- .../src/environments/environment.prod.ts | 3 - .../src/environments/environment.ts | 3 - packages/nx-affected-matrix/src/main.ts | 9 +- packages/nx-affected-matrix/tsconfig.app.json | 3 +- .../__mocks__/@actions/core.ts | 15 --- .../{src/assets => }/action.yml | 25 ++++- packages/nx-distributed-task/jest.config.js | 1 - packages/nx-distributed-task/project.json | 32 +++---- .../nx-distributed-task/src/app/inputs.ts | 24 ++--- .../src/app/nx-distributed-task.spec.ts | 63 +++++-------- .../src/app/nx-distributed-task.ts | 88 +++++++++-------- .../nx-distributed-task/src/assets/.gitkeep | 0 .../src/environments/environment.prod.ts | 3 - .../src/environments/environment.ts | 3 - packages/nx-distributed-task/src/main.ts | 17 +++- .../nx-distributed-task/tsconfig.app.json | 3 +- packages/utils/__mocks__/@actions/core.ts | 15 --- packages/utils/__mocks__/@actions/exec.ts | 1 - packages/utils/__mocks__/@actions/glob.ts | 9 -- packages/utils/jest.config.js | 1 - packages/utils/src/index.ts | 8 -- packages/utils/src/lib/__mocks__/artifact.ts | 1 + packages/utils/src/lib/__mocks__/logger.ts | 28 ++++-- packages/utils/src/lib/__mocks__/nx.ts | 5 + packages/utils/src/lib/artifact.spec.ts | 28 +++--- packages/utils/src/lib/artifact.ts | 24 ++--- packages/utils/src/lib/cache.spec.ts | 45 ++++----- packages/utils/src/lib/cache.ts | 67 +++++++------ packages/utils/src/lib/exec.spec.ts | 16 +++- packages/utils/src/lib/exec.ts | 17 ++-- packages/utils/src/lib/fs.ts | 11 +-- packages/utils/src/lib/git.spec.ts | 58 ------------ packages/utils/src/lib/git.ts | 32 ------- packages/utils/src/lib/inputs.spec.ts | 18 ++-- packages/utils/src/lib/inputs.ts | 38 +++++--- packages/utils/src/lib/logger.spec.ts | 61 ++++++++++++ packages/utils/src/lib/logger.ts | 70 +++++++++++--- packages/utils/src/lib/npm.ts | 5 + packages/utils/src/lib/nx.spec.ts | 44 ++++++--- packages/utils/src/lib/nx.ts | 39 ++++---- packages/utils/src/lib/set-env.spec.ts | 17 ++++ packages/utils/src/lib/set-env.ts | 7 ++ tsconfig.base.json | 2 +- 66 files changed, 753 insertions(+), 591 deletions(-) rename {packages/utils/__mocks__ => __mocks__}/@actions/artifact.ts (100%) rename {packages/utils/__mocks__ => __mocks__}/@actions/cache.ts (63%) rename {packages/nx-affected-matrix/__mocks__ => __mocks__}/@actions/core.ts (90%) create mode 100644 __mocks__/@actions/exec.ts rename {packages/utils/__mocks__ => __mocks__}/@actions/github.ts (100%) create mode 100644 __mocks__/@actions/glob.ts create mode 100644 __mocks__/@actions/io.ts rename {packages/utils/__mocks__ => __mocks__}/which.ts (100%) create mode 100644 packages/nx-affected-matrix/action.yml delete mode 100644 packages/nx-affected-matrix/src/assets/.gitkeep delete mode 100644 packages/nx-affected-matrix/src/assets/action.yml delete mode 100644 packages/nx-affected-matrix/src/environments/environment.prod.ts delete mode 100644 packages/nx-affected-matrix/src/environments/environment.ts delete mode 100644 packages/nx-distributed-task/__mocks__/@actions/core.ts rename packages/nx-distributed-task/{src/assets => }/action.yml (60%) delete mode 100644 packages/nx-distributed-task/src/assets/.gitkeep delete mode 100644 packages/nx-distributed-task/src/environments/environment.prod.ts delete mode 100644 packages/nx-distributed-task/src/environments/environment.ts delete mode 100644 packages/utils/__mocks__/@actions/core.ts delete mode 100644 packages/utils/__mocks__/@actions/exec.ts delete mode 100644 packages/utils/__mocks__/@actions/glob.ts delete mode 100644 packages/utils/src/index.ts create mode 100644 packages/utils/src/lib/__mocks__/artifact.ts create mode 100644 packages/utils/src/lib/__mocks__/nx.ts delete mode 100644 packages/utils/src/lib/git.spec.ts delete mode 100644 packages/utils/src/lib/git.ts create mode 100644 packages/utils/src/lib/logger.spec.ts create mode 100644 packages/utils/src/lib/npm.ts create mode 100644 packages/utils/src/lib/set-env.spec.ts create mode 100644 packages/utils/src/lib/set-env.ts diff --git a/.github/workflows/code-scanning.yml b/.github/workflows/code-scanning.yml index bce906d1..c5e877f4 100644 --- a/.github/workflows/code-scanning.yml +++ b/.github/workflows/code-scanning.yml @@ -5,6 +5,8 @@ on: branches: - main pull_request: + types: + - ready_for_review schedule: - cron: 0 0 * * 1 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 298cd501..86b80ee4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -26,9 +26,10 @@ jobs: id: affected with: targets: 'test,build' + maxDistribution: 1 execute: - name: Run ${{ matrix.target }} ${{ matrix.distribution }} + name: Run ${{ matrix.target }} if: ${{ fromJSON(needs.setup.outputs.hasChanges) }} runs-on: ubuntu-latest needs: [ setup ] @@ -52,7 +53,6 @@ jobs: id: execute with: target: ${{ matrix.target }} - distribution: ${{ matrix.distribution }} projects: ${{ matrix.projects }} nxCloud: true @@ -71,10 +71,7 @@ jobs: hasChanges: ${{ steps.affected.outputs.hasChanges || needs.setup.outputs.hasChanges }} matrix: ${{ steps.affected.outputs.matrix || needs.setup.outputs.matrix }} steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 + - uses: actions/checkout@v2 - uses: e-square-io/.github/.github/actions/npm-ci@main @@ -88,12 +85,12 @@ jobs: uses: ./dist/packages/nx-affected-matrix id: affected with: - targets: 'build' + targets: build maxDistribution: 1 debug: true e2e-execute: - name: 'E2E nx-distributed-task [${{ matrix.target }} (${{ matrix.distribution }})]' + name: E2E nx-distributed-task [${{ matrix.target }} (${{ matrix.distribution }})] if: ${{ fromJSON(needs.e2e-distribute.outputs.hasChanges) && (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && contains(needs.setup.outputs.matrix, 'nx-distributed-task') }} needs: [setup,e2e-distribute] runs-on: ubuntu-latest @@ -182,7 +179,7 @@ jobs: if: ${{ contains(needs.setup.outputs.matrix, matrix.package) }} uses: e-square-io/.github/.github/actions/push-to-repo@main with: - path: 'dist/packages/${{ matrix.package }}' + path: dist/packages/${{ matrix.package }} github-token: ${{ needs.release.outputs.github-token }} version: ${{ needs.release.outputs.version }} repository: ${{ matrix.package }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e0180616..a05f5c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,3 @@ -## [2.1.5](https://github.com/e-square-io/nx-github-actions/compare/v2.1.4...v2.1.5) (2022-02-22) - -### Build System - -- update nx packages (#26) ([75aec2c](https://github.com/e-square-io/nx-github-actions/commit/75aec2cad472aa19729c15af1c11b6f3cd1936cf)), closes [#26](https://github.com/e-square-io/nx-github-actions/issues/26) - ## [2.1.4](https://github.com/e-square-io/nx-github-actions/compare/v2.1.3...v2.1.4) (2022-02-19) ### Continuous Integration diff --git a/README.md b/README.md index 70bc563b..a5044ebf 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # NX Github Actions - -[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/e-square-io/nx-github-actions/Main%20Workflow?style=flat-square)](https://github.com/e-square-io/nx-github-actions/actions/workflows/main.yml) +[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/e-square-io/nx-github-actions/Main%20Workflow/main?event=push&logo=github&style=flat-square)](https://github.com/e-square-io/nx-github-actions/actions/workflows/main.yml) [![Codecov](https://img.shields.io/codecov/c/github/e-square-io/nx-github-actions?logo=codecov&style=flat-square&token=PVPVUJAD1X)](https://app.codecov.io/gh/e-square-io/nx-github-actions) [![PRs](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](.github/PULL_REQUEST_TEMPLATE.md) [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) diff --git a/packages/utils/__mocks__/@actions/artifact.ts b/__mocks__/@actions/artifact.ts similarity index 100% rename from packages/utils/__mocks__/@actions/artifact.ts rename to __mocks__/@actions/artifact.ts diff --git a/packages/utils/__mocks__/@actions/cache.ts b/__mocks__/@actions/cache.ts similarity index 63% rename from packages/utils/__mocks__/@actions/cache.ts rename to __mocks__/@actions/cache.ts index a40d4b61..18dbb31e 100644 --- a/packages/utils/__mocks__/@actions/cache.ts +++ b/__mocks__/@actions/cache.ts @@ -2,4 +2,4 @@ export const restoreCache = jest.fn().mockResolvedValue('test'); export const saveCache = jest.fn().mockResolvedValue('test'); -export { ReserveCacheError } from '@actions/cache'; +export const { ReserveCacheError } = jest.requireActual('@actions/cache'); diff --git a/packages/nx-affected-matrix/__mocks__/@actions/core.ts b/__mocks__/@actions/core.ts similarity index 90% rename from packages/nx-affected-matrix/__mocks__/@actions/core.ts rename to __mocks__/@actions/core.ts index 2cc9a18e..e886c88c 100644 --- a/packages/nx-affected-matrix/__mocks__/@actions/core.ts +++ b/__mocks__/@actions/core.ts @@ -1,5 +1,6 @@ module.exports = { ...jest.requireActual('@actions/core'), + isDebug: jest.fn().mockReturnValue(false), setFailed: jest.fn(), saveState: jest.fn(), setOutput: jest.fn(), diff --git a/__mocks__/@actions/exec.ts b/__mocks__/@actions/exec.ts new file mode 100644 index 00000000..e2290aaf --- /dev/null +++ b/__mocks__/@actions/exec.ts @@ -0,0 +1 @@ +export const exec = jest.fn().mockResolvedValue(0); diff --git a/packages/utils/__mocks__/@actions/github.ts b/__mocks__/@actions/github.ts similarity index 100% rename from packages/utils/__mocks__/@actions/github.ts rename to __mocks__/@actions/github.ts diff --git a/__mocks__/@actions/glob.ts b/__mocks__/@actions/glob.ts new file mode 100644 index 00000000..a3b05a68 --- /dev/null +++ b/__mocks__/@actions/glob.ts @@ -0,0 +1,10 @@ +const create = jest.fn(() => + Promise.resolve({ + glob: jest.fn().mockResolvedValue([]), + }) +); + +module.exports = { + ...jest.requireActual('@actions/glob'), + create, +}; diff --git a/__mocks__/@actions/io.ts b/__mocks__/@actions/io.ts new file mode 100644 index 00000000..4fe74f95 --- /dev/null +++ b/__mocks__/@actions/io.ts @@ -0,0 +1,4 @@ +module.exports = { + ...jest.requireActual('@actions/io'), + which: jest.fn(), +}; diff --git a/packages/utils/__mocks__/which.ts b/__mocks__/which.ts similarity index 100% rename from packages/utils/__mocks__/which.ts rename to __mocks__/which.ts diff --git a/jest.preset.js b/jest.preset.js index 7b3ce7c5..787ddbbc 100644 --- a/jest.preset.js +++ b/jest.preset.js @@ -2,6 +2,8 @@ const nxPreset = require('@nrwl/jest/preset'); module.exports = { ...nxPreset, + roots: ['', `${__dirname}/__mocks__`], clearMocks: true, coverageReporters: ['json-summary', 'text', 'lcovonly'], + testEnvironment: 'node', }; diff --git a/package-lock.json b/package-lock.json index ede74eed..157dd6ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,24 @@ { "name": "@e-square/nx-github-actions", - "version": "2.1.5", + "version": "2.1.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@e-square/nx-github-actions", - "version": "2.1.5", + "version": "2.1.4", "license": "MIT", "dependencies": { "@actions/artifact": "^0.6.1", "@actions/cache": "^1.0.8", + "tslib": "^2.0.0" + }, + "devDependencies": { "@actions/core": "^1.6.0", "@actions/exec": "^1.1.0", "@actions/github": "^5.0.0", "@actions/glob": "^0.2.0", "@actions/io": "^1.1.1", - "tslib": "^2.0.0", - "which": "^2.0.2" - }, - "devDependencies": { "@commitlint/cli": "^12.1.4", "@commitlint/config-angular": "^12.1.4", "@commitlint/config-conventional": "^12.1.4", @@ -136,6 +135,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.0.tgz", "integrity": "sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==", + "dev": true, "dependencies": { "@actions/http-client": "^1.0.11", "@octokit/core": "^3.4.0", @@ -147,6 +147,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.2.0.tgz", "integrity": "sha512-mqE2a7I66kxcvsdwxs/filQwZsq25IfktMaviGfDB51v6Q3bvxnV7mFsZnvYtLhqGZbPxwBnH8AD3UYaOWb//w==", + "dev": true, "dependencies": { "@actions/core": "^1.2.6", "minimatch": "^3.0.4" @@ -2255,6 +2256,7 @@ "version": "2.4.5", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz", "integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==", + "dev": true, "dependencies": { "@octokit/types": "^6.0.3" } @@ -2263,6 +2265,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.4.0.tgz", "integrity": "sha512-6/vlKPP8NF17cgYXqucdshWqmMZGXkuvtcrWCgU5NOI0Pl2GjlmZyWgBMrU8zJ3v2MJlM6++CiB45VKYmhiWWg==", + "dev": true, "dependencies": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -2277,6 +2280,7 @@ "version": "6.0.11", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.11.tgz", "integrity": "sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==", + "dev": true, "dependencies": { "@octokit/types": "^6.0.3", "is-plain-object": "^5.0.0", @@ -2287,6 +2291,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2295,6 +2300,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.2.tgz", "integrity": "sha512-WmsIR1OzOr/3IqfG9JIczI8gMJUMzzyx5j0XXQ4YihHtKlQc+u35VpVoOXhlKAlaBntvry1WpAzPl/a+s3n89Q==", + "dev": true, "dependencies": { "@octokit/request": "^5.3.0", "@octokit/types": "^6.0.3", @@ -2304,12 +2310,14 @@ "node_modules/@octokit/openapi-types": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-7.2.1.tgz", - "integrity": "sha512-IHQJpLciwzwDvciLxiFj3IEV5VYn7lSVcj5cu0jbTwMfK4IG6/g8SPrVp3Le1VRzIiYSRcBzm1dA7vgWelYP3Q==" + "integrity": "sha512-IHQJpLciwzwDvciLxiFj3IEV5VYn7lSVcj5cu0jbTwMfK4IG6/g8SPrVp3Le1VRzIiYSRcBzm1dA7vgWelYP3Q==", + "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { "version": "2.13.3", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.13.3.tgz", "integrity": "sha512-46lptzM9lTeSmIBt/sVP/FLSTPGx6DCzAdSX3PfeJ3mTf4h9sGC26WpaQzMEq/Z44cOcmx8VsOhO+uEgE3cjYg==", + "dev": true, "dependencies": { "@octokit/types": "^6.11.0" }, @@ -2321,6 +2329,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.3.0.tgz", "integrity": "sha512-+4NAJ9WzPN5gW6W1H1tblw/DesTD6PaWuU5+/x3Pa7nyK6qjR6cn4jwkJtzxVPTgtzryOFLX+5vs7MROFswp5Q==", + "dev": true, "dependencies": { "@octokit/types": "^6.16.0", "deprecation": "^2.3.1" @@ -2333,6 +2342,7 @@ "version": "5.4.15", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.15.tgz", "integrity": "sha512-6UnZfZzLwNhdLRreOtTkT9n57ZwulCve8q3IT/Z477vThu6snfdkBuhxnChpOKNGxcQ71ow561Qoa6uqLdPtag==", + "dev": true, "dependencies": { "@octokit/endpoint": "^6.0.1", "@octokit/request-error": "^2.0.0", @@ -2346,6 +2356,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.5.tgz", "integrity": "sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==", + "dev": true, "dependencies": { "@octokit/types": "^6.0.3", "deprecation": "^2.0.0", @@ -2356,6 +2367,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -2364,6 +2376,7 @@ "version": "6.16.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.16.0.tgz", "integrity": "sha512-EktqSNq8EKXE82a7Vw33ozOEhFXIRik+rZHJTHAgVZRm/p2K5r5ecn5fVpRkLCm3CAVFwchRvt3yvtmfbt2LCQ==", + "dev": true, "dependencies": { "@octokit/openapi-types": "^7.2.0" } @@ -3770,7 +3783,8 @@ "node_modules/before-after-hook": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.1.tgz", - "integrity": "sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw==" + "integrity": "sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw==", + "dev": true }, "node_modules/binary-extensions": { "version": "2.2.0", @@ -4458,7 +4472,8 @@ "node_modules/deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true }, "node_modules/detect-newline": { "version": "3.1.0", @@ -6059,7 +6074,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "node_modules/isobject": { "version": "3.0.1", @@ -9652,7 +9668,8 @@ "node_modules/universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true }, "node_modules/universalify": { "version": "2.0.0", @@ -9888,6 +9905,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -10169,6 +10187,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@actions/github/-/github-5.0.0.tgz", "integrity": "sha512-QvE9eAAfEsS+yOOk0cylLBIO/d6WyWIOvsxxzdrPFaud39G6BOkUwScXZn1iBzQzHyu9SBkkLSWlohDWdsasAQ==", + "dev": true, "requires": { "@actions/http-client": "^1.0.11", "@octokit/core": "^3.4.0", @@ -10180,6 +10199,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.2.0.tgz", "integrity": "sha512-mqE2a7I66kxcvsdwxs/filQwZsq25IfktMaviGfDB51v6Q3bvxnV7mFsZnvYtLhqGZbPxwBnH8AD3UYaOWb//w==", + "dev": true, "requires": { "@actions/core": "^1.2.6", "minimatch": "^3.0.4" @@ -11857,6 +11877,7 @@ "version": "2.4.5", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.5.tgz", "integrity": "sha512-BpGYsPgJt05M7/L/5FoE1PiAbdxXFZkX/3kDYcsvd1v6UhlnE5e96dTDr0ezX/EFwciQxf3cNV0loipsURU+WA==", + "dev": true, "requires": { "@octokit/types": "^6.0.3" } @@ -11865,6 +11886,7 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.4.0.tgz", "integrity": "sha512-6/vlKPP8NF17cgYXqucdshWqmMZGXkuvtcrWCgU5NOI0Pl2GjlmZyWgBMrU8zJ3v2MJlM6++CiB45VKYmhiWWg==", + "dev": true, "requires": { "@octokit/auth-token": "^2.4.4", "@octokit/graphql": "^4.5.8", @@ -11879,6 +11901,7 @@ "version": "6.0.11", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.11.tgz", "integrity": "sha512-fUIPpx+pZyoLW4GCs3yMnlj2LfoXTWDUVPTC4V3MUEKZm48W+XYpeWSZCv+vYF1ZABUm2CqnDVf1sFtIYrj7KQ==", + "dev": true, "requires": { "@octokit/types": "^6.0.3", "is-plain-object": "^5.0.0", @@ -11888,7 +11911,8 @@ "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true } } }, @@ -11896,6 +11920,7 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.6.2.tgz", "integrity": "sha512-WmsIR1OzOr/3IqfG9JIczI8gMJUMzzyx5j0XXQ4YihHtKlQc+u35VpVoOXhlKAlaBntvry1WpAzPl/a+s3n89Q==", + "dev": true, "requires": { "@octokit/request": "^5.3.0", "@octokit/types": "^6.0.3", @@ -11905,12 +11930,14 @@ "@octokit/openapi-types": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-7.2.1.tgz", - "integrity": "sha512-IHQJpLciwzwDvciLxiFj3IEV5VYn7lSVcj5cu0jbTwMfK4IG6/g8SPrVp3Le1VRzIiYSRcBzm1dA7vgWelYP3Q==" + "integrity": "sha512-IHQJpLciwzwDvciLxiFj3IEV5VYn7lSVcj5cu0jbTwMfK4IG6/g8SPrVp3Le1VRzIiYSRcBzm1dA7vgWelYP3Q==", + "dev": true }, "@octokit/plugin-paginate-rest": { "version": "2.13.3", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.13.3.tgz", "integrity": "sha512-46lptzM9lTeSmIBt/sVP/FLSTPGx6DCzAdSX3PfeJ3mTf4h9sGC26WpaQzMEq/Z44cOcmx8VsOhO+uEgE3cjYg==", + "dev": true, "requires": { "@octokit/types": "^6.11.0" } @@ -11919,6 +11946,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.3.0.tgz", "integrity": "sha512-+4NAJ9WzPN5gW6W1H1tblw/DesTD6PaWuU5+/x3Pa7nyK6qjR6cn4jwkJtzxVPTgtzryOFLX+5vs7MROFswp5Q==", + "dev": true, "requires": { "@octokit/types": "^6.16.0", "deprecation": "^2.3.1" @@ -11928,6 +11956,7 @@ "version": "5.4.15", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.15.tgz", "integrity": "sha512-6UnZfZzLwNhdLRreOtTkT9n57ZwulCve8q3IT/Z477vThu6snfdkBuhxnChpOKNGxcQ71ow561Qoa6uqLdPtag==", + "dev": true, "requires": { "@octokit/endpoint": "^6.0.1", "@octokit/request-error": "^2.0.0", @@ -11940,7 +11969,8 @@ "is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true } } }, @@ -11948,6 +11978,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.5.tgz", "integrity": "sha512-T/2wcCFyM7SkXzNoyVNWjyVlUwBvW3igM3Btr/eKYiPmucXTtkxt2RBsf6gn3LTzaLSLTQtNmvg+dGsOxQrjZg==", + "dev": true, "requires": { "@octokit/types": "^6.0.3", "deprecation": "^2.0.0", @@ -11958,6 +11989,7 @@ "version": "6.16.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.16.0.tgz", "integrity": "sha512-EktqSNq8EKXE82a7Vw33ozOEhFXIRik+rZHJTHAgVZRm/p2K5r5ecn5fVpRkLCm3CAVFwchRvt3yvtmfbt2LCQ==", + "dev": true, "requires": { "@octokit/openapi-types": "^7.2.0" } @@ -12691,13 +12723,15 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true + "dev": true, + "requires": {} }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "7.2.0", @@ -12740,7 +12774,8 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true + "dev": true, + "requires": {} }, "all-contributors-cli": { "version": "6.20.0", @@ -12987,7 +13022,8 @@ "before-after-hook": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.1.tgz", - "integrity": "sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw==" + "integrity": "sha512-/6FKxSTWoJdbsLDF8tdIjaRiFXiE6UHsEHE3OPI/cwPURCVi1ukP0gmLn7XWEiFk5TcwQjjY5PWsU+j+tgXgmw==", + "dev": true }, "binary-extensions": { "version": "2.2.0", @@ -13507,7 +13543,8 @@ "deprecation": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true }, "detect-newline": { "version": "3.1.0", @@ -13811,7 +13848,8 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true + "dev": true, + "requires": {} }, "eslint-scope": { "version": "5.1.1", @@ -14689,7 +14727,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "isobject": { "version": "3.0.1", @@ -15332,7 +15371,8 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "dev": true, + "requires": {} }, "jest-regex-util": { "version": "27.5.1", @@ -16747,7 +16787,8 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/rxjs-for-await/-/rxjs-for-await-0.0.2.tgz", "integrity": "sha512-IJ8R/ZCFMHOcDIqoABs82jal00VrZx8Xkgfe7TOKoaRPAW5nH/VFlG23bXpeGdrmtqI9UobFPgUKgCuFc7Lncw==", - "dev": true + "dev": true, + "requires": {} }, "safe-buffer": { "version": "5.1.2", @@ -17417,7 +17458,8 @@ "universal-user-agent": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true }, "universalify": { "version": "2.0.0", @@ -17607,6 +17649,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } @@ -17661,7 +17704,8 @@ "version": "7.5.1", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.1.tgz", "integrity": "sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==", - "dev": true + "dev": true, + "requires": {} }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index f0b7fd83..3faa7191 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@e-square/nx-github-actions", - "version": "2.1.5", + "version": "2.1.4", "license": "MIT", "scripts": { "nx": "nx", @@ -39,15 +39,14 @@ "dependencies": { "@actions/artifact": "^0.6.1", "@actions/cache": "^1.0.8", + "tslib": "^2.0.0" + }, + "devDependencies": { "@actions/core": "^1.6.0", "@actions/exec": "^1.1.0", "@actions/github": "^5.0.0", "@actions/glob": "^0.2.0", "@actions/io": "^1.1.1", - "tslib": "^2.0.0", - "which": "^2.0.2" - }, - "devDependencies": { "@commitlint/cli": "^12.1.4", "@commitlint/config-angular": "^12.1.4", "@commitlint/config-conventional": "^12.1.4", diff --git a/packages/nx-affected-matrix/action.yml b/packages/nx-affected-matrix/action.yml new file mode 100644 index 00000000..ccaeefc7 --- /dev/null +++ b/packages/nx-affected-matrix/action.yml @@ -0,0 +1,68 @@ +name: 'Nx affected matrix' +branding: + icon: grid + color: white +author: 'e-square.io' +description: "Calculates changes in NX workspace and outputs a JSON that can be used as GitHub's job matrix" +inputs: + targets: + description: 'comma-delimited targets to run' + required: true + default: 'test,build' + args: + description: 'space-delimited args to add to nx command execution' + required: false + maxDistribution: + description: "Maximum distribution of jobs per target" + required: false + default: '3' + maxParallel: + description: "Maximum distribution of jobs per target" + deprecationMessage: "Deprecated. Use maxDistribution instead" + required: false + default: '3' + workingDirectory: + description: "Path to the Nx workspace, needed if not the repository root" + required: false + main-branch: + description: "The 'main' branch of your repository (the base branch which you target with PRs)." + required: false + default: main + workflow-id: + description: "The ID of the github action workflow to check for successful run or the name of the file name containing the workflow." + required: false + debug: + description: "Enable Debug mode" + required: false + default: 'false' +outputs: + matrix: + description: "The matrix that should be used in next job's matrix strategy" + value: ${{ steps.calculate.outputs.matrix }} + hasChanges: + description: "Returns true when there are changes, can be used to skip next steps" + value: ${{ steps.calculate.outputs.hasChanges }} +runs: + using: 'composite' + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + clean: false + + - name: Derive appropriate SHAs for base and head for `nx affected` commands + uses: nrwl/nx-set-shas@v2 + with: + main-branch-name: ${{ inputs.main-branch }} + workflow-id: ${{ inputs.workflow-id }} + + - name: Calculate affected + id: calculate + uses: actions/github-script@v6 + with: + script: | + const { default: setEnv } = require('${{ github.action_path }}/set-env.js'); + const { default: main } = require('${{ github.action_path }}/main.js'); + + setEnv(${{ toJSON(inputs) }}, process); + await main(context, core, exec, io, require); diff --git a/packages/nx-affected-matrix/jest.config.js b/packages/nx-affected-matrix/jest.config.js index 15e2599a..d9207645 100644 --- a/packages/nx-affected-matrix/jest.config.js +++ b/packages/nx-affected-matrix/jest.config.js @@ -6,7 +6,6 @@ module.exports = { tsconfig: '/tsconfig.spec.json', }, }, - testEnvironment: 'node', transform: { '^.+\\.[tj]s$': 'ts-jest', }, diff --git a/packages/nx-affected-matrix/project.json b/packages/nx-affected-matrix/project.json index 9bba948b..acca58d2 100644 --- a/packages/nx-affected-matrix/project.json +++ b/packages/nx-affected-matrix/project.json @@ -10,33 +10,25 @@ "outputPath": "dist/packages/nx-affected-matrix", "main": "packages/nx-affected-matrix/src/main.ts", "tsConfig": "packages/nx-affected-matrix/tsconfig.app.json", - "externalDependencies": "none", + "externalDependencies": ["@actions/core", "@actions/glob", "@actions/exec", "@actions/github", "@actions/io"], + "additionalEntryPoints": [ + { + "entryName": "set-env", + "entryPath": "packages/utils/src/lib/set-env.ts" + } + ], "generatePackageJson": true, + "optimization": true, + "extractLicenses": false, + "sourceMap": false, + "inspect": false, "assets": [ - { - "input": "packages/nx-affected-matrix/src/assets", - "glob": "*", - "output": "." - }, { "input": "packages/nx-affected-matrix", - "glob": "README.md", + "glob": "*.@(yml|md)", "output": "." } ] - }, - "configurations": { - "production": { - "optimization": true, - "extractLicenses": true, - "inspect": false, - "fileReplacements": [ - { - "replace": "packages/nx-affected-matrix/src/environments/environment.ts", - "with": "packages/nx-affected-matrix/src/environments/environment.prod.ts" - } - ] - } } }, "serve": { diff --git a/packages/nx-affected-matrix/src/app/inputs.ts b/packages/nx-affected-matrix/src/app/inputs.ts index b9682dc1..e30193c9 100644 --- a/packages/nx-affected-matrix/src/app/inputs.ts +++ b/packages/nx-affected-matrix/src/app/inputs.ts @@ -1,4 +1,5 @@ -import { BaseInputs, getBaseInputs, getMaxDistribution, getStringArrayInput } from '@e-square/utils'; +import type * as Core from '@actions/core'; +import { BaseInputs, getBaseInputs, getMaxDistribution, getStringArrayInput } from '@e-square/utils/inputs'; export interface Inputs extends BaseInputs { targets: string[]; @@ -7,12 +8,12 @@ export interface Inputs extends BaseInputs { maxParallel?: number; } -export function getInputs(): Inputs { - const targets = getStringArrayInput('targets', ','); +export function getInputs(core: typeof Core): Inputs { + const targets = getStringArrayInput(core, 'targets', ','); return { - ...getBaseInputs(), + ...getBaseInputs(core), targets, - maxDistribution: getMaxDistribution(targets), + maxDistribution: getMaxDistribution(core, targets), }; } diff --git a/packages/nx-affected-matrix/src/app/nx-affected-matrix.spec.ts b/packages/nx-affected-matrix/src/app/nx-affected-matrix.spec.ts index e6485fee..1e3c58d8 100644 --- a/packages/nx-affected-matrix/src/app/nx-affected-matrix.spec.ts +++ b/packages/nx-affected-matrix/src/app/nx-affected-matrix.spec.ts @@ -1,14 +1,15 @@ -import { chunkify, generateAffectedMatrix, main } from './nx-affected-matrix'; +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as io from '@actions/io'; +import { Exec } from '@e-square/utils/exec'; +import { assertNxInstalled, nxPrintAffected } from '@e-square/utils/nx'; -jest.mock('@e-square/utils', () => ({ - ...(jest.requireActual('@e-square/utils') as any), - retrieveGitBoundaries: jest.fn().mockResolvedValue(['1', '2']), - assertNxInstalled: jest.fn().mockResolvedValue(true), - nxPrintAffected: jest.fn().mockResolvedValue(['project1', 'project2', 'project3', 'project4']), -})); +import { chunkify, generateAffectedMatrix, main } from './nx-affected-matrix'; +import { context } from '@actions/github'; -import { assertNxInstalled, Exec, nxPrintAffected } from '@e-square/utils'; -import * as core from '@actions/core'; +jest.mock('@e-square/utils/nx'); +jest.mock('@e-square/utils/logger'); +jest.mock('@e-square/utils/cache'); describe('nxAffectedMatrix', () => { describe('chunkify', () => { @@ -31,7 +32,7 @@ describe('nxAffectedMatrix', () => { maxDistribution: { test1: 1, test2: 2 }, args: [], }, - new Exec() + new Exec(exec.exec) ) ).resolves.toEqual({ include: [ @@ -56,7 +57,6 @@ describe('nxAffectedMatrix', () => { }); describe('main', () => { - let setOutput; beforeEach(() => { const env = { INPUT_TARGETS: 'test,build', @@ -68,16 +68,15 @@ describe('nxAffectedMatrix', () => { }; process.env = { ...process.env, ...env }; - setOutput = jest.spyOn(core, 'setOutput'); }); it('should output the generated matrix and if there are changes', async () => { - await main(); + await main(context, core, exec, io); expect(assertNxInstalled).toHaveBeenCalled(); expect(nxPrintAffected).toHaveBeenCalled(); - expect(setOutput).toHaveBeenCalledTimes(2); - expect(setOutput).toHaveBeenNthCalledWith(1, 'matrix', { + expect(core.setOutput).toHaveBeenCalledTimes(2); + expect(core.setOutput).toHaveBeenNthCalledWith(1, 'matrix', { include: [ { distribution: 1, projects: 'project1,project2', target: 'test' }, { distribution: 2, projects: 'project3,project4', target: 'test' }, @@ -85,13 +84,13 @@ describe('nxAffectedMatrix', () => { { distribution: 2, projects: 'project3,project4', target: 'build' }, ], }); - expect(setOutput).toHaveBeenNthCalledWith(2, 'hasChanges', true); + expect(core.setOutput).toHaveBeenNthCalledWith(2, 'hasChanges', true); process.env.INPUT_MAXDISTRIBUTION = '{"test": 2, "build": 1}'; - await main(); + await main(context, core, exec, io); - expect(setOutput).toHaveBeenNthCalledWith(3, 'matrix', { + expect(core.setOutput).toHaveBeenNthCalledWith(3, 'matrix', { include: [ { distribution: 1, projects: 'project1,project2', target: 'test' }, { distribution: 2, projects: 'project3,project4', target: 'test' }, @@ -101,11 +100,10 @@ describe('nxAffectedMatrix', () => { }); it('should set job as failed if any unhandled error occurs', async () => { - (assertNxInstalled as jest.Mock).mockRejectedValue('test'); - const spy = jest.spyOn(core, 'setFailed'); - await main(); + (nxPrintAffected as jest.Mock).mockRejectedValue('test'); + await main(context, core, exec, io); - expect(spy).toHaveBeenCalledWith('test'); + expect(core.setFailed).toHaveBeenCalledWith('test'); }); }); }); diff --git a/packages/nx-affected-matrix/src/app/nx-affected-matrix.ts b/packages/nx-affected-matrix/src/app/nx-affected-matrix.ts index 8ac14f98..cef553b7 100644 --- a/packages/nx-affected-matrix/src/app/nx-affected-matrix.ts +++ b/packages/nx-affected-matrix/src/app/nx-affected-matrix.ts @@ -1,6 +1,12 @@ -import { setFailed, setOutput } from '@actions/core'; +import type * as Core from '@actions/core'; +import type * as _Exec from '@actions/exec'; +import * as Io from '@actions/io'; +import type { context as Context } from '@actions/github'; + +import { Exec } from '@e-square/utils/exec'; +import { assertNxInstalled, nxPrintAffected } from '@e-square/utils/nx'; +import { debug, group, success } from '@e-square/utils/logger'; -import { Exec, retrieveGitBoundaries, nxPrintAffected, assertNxInstalled, logger } from '@e-square/utils'; import { getInputs, Inputs } from './inputs'; interface NxAffectedTarget { @@ -40,17 +46,15 @@ export function generateAffectedMatrix( { targets, maxDistribution, args = [] }: Pick, exec: Exec ): Promise { - return logger.group(`⚙️ Generating affected matrix for ${targets}`, async () => { + return group(`⚙️ Generating affected matrix for ${targets}`, async () => { const matrix: NxAffectedMatrix = { include: [], }; - const [base, head] = await retrieveGitBoundaries(exec); - for (const target of targets) { - exec.withArgs(`--base=${base}`, `--head=${head}`, ...args); + exec.withArgs(...args); - logger.debug(`Calculating affected for "${target}" target`); + debug(`Calculating affected for "${target}" target`); const projects = await nxPrintAffected(target, exec); const affectedTargets: NxAffectedTarget[] = chunkify(projects, maxDistribution[target]) @@ -66,25 +70,29 @@ export function generateAffectedMatrix( } } - logger.debug(`matrix: ${matrix}`); - logger.success(`Generated affected matrix`); + debug(`matrix: ${matrix}`); + success(`Generated affected matrix`); return matrix; }); } -export async function main(): Promise { - const inputs = getInputs(); - +export async function main( + context: typeof Context, + core: typeof Core, + exec: typeof _Exec, + io: typeof Io, + require? +): Promise { try { - await assertNxInstalled(); + const parsedInputs = getInputs(core); - const exec = new Exec(); - const matrix = await generateAffectedMatrix(inputs, exec); + await assertNxInstalled(new Exec(exec.exec)); + const matrix = await generateAffectedMatrix(parsedInputs, new Exec(exec.exec)); - setOutput('matrix', matrix); - setOutput('hasChanges', !!matrix.include.find((target) => target.projects.length)); + core.setOutput('matrix', matrix); + core.setOutput('hasChanges', !!matrix.include.find((target) => target.projects.length)); } catch (e) { - setFailed(e); + core.setFailed(e); } } diff --git a/packages/nx-affected-matrix/src/assets/.gitkeep b/packages/nx-affected-matrix/src/assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/nx-affected-matrix/src/assets/action.yml b/packages/nx-affected-matrix/src/assets/action.yml deleted file mode 100644 index be87c42d..00000000 --- a/packages/nx-affected-matrix/src/assets/action.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: 'Nx affected matrix' -branding: - icon: grid - color: white -author: 'e-square.io' -description: "Calculates changes in NX workspace and outputs a JSON that can be used as GitHub's job matrix" -inputs: - targets: - description: 'comma-delimited targets to run' - required: true - default: 'test,build' - args: - description: 'space-delimited args to add to nx command execution' - required: false - maxDistribution: - description: "Maximum distribution of jobs per target" - required: false - default: '3' - maxParallel: - description: "Maximum distribution of jobs per target" - deprecationMessage: "Deprecated. Use maxDistribution instead" - required: false - default: '3' - workingDirectory: - description: "Path to the Nx workspace, needed if not the repository root" - required: false - debug: - description: "Enable Debug mode" - required: false - default: 'false' -outputs: - matrix: - description: "The matrix that should be used in next job's matrix strategy" - hasChanges: - description: "Returns true when there are changes, can be used to skip next steps" -runs: - using: 'node16' - main: 'main.js' diff --git a/packages/nx-affected-matrix/src/environments/environment.prod.ts b/packages/nx-affected-matrix/src/environments/environment.prod.ts deleted file mode 100644 index c9669790..00000000 --- a/packages/nx-affected-matrix/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true, -}; diff --git a/packages/nx-affected-matrix/src/environments/environment.ts b/packages/nx-affected-matrix/src/environments/environment.ts deleted file mode 100644 index a20cfe55..00000000 --- a/packages/nx-affected-matrix/src/environments/environment.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: false, -}; diff --git a/packages/nx-affected-matrix/src/main.ts b/packages/nx-affected-matrix/src/main.ts index 39123d45..9d7ab0b3 100644 --- a/packages/nx-affected-matrix/src/main.ts +++ b/packages/nx-affected-matrix/src/main.ts @@ -1,3 +1,10 @@ +import type * as Core from '@actions/core'; +import type * as Exec from '@actions/exec'; +import type { context as Context } from '@actions/github'; +import type * as Io from '@actions/io'; + import { main } from './app/nx-affected-matrix'; -void main(); +export default async function (context: typeof Context, core: typeof Core, exec: typeof Exec, io: typeof Io, require?) { + await main(context, core, exec, io, require); +} diff --git a/packages/nx-affected-matrix/tsconfig.app.json b/packages/nx-affected-matrix/tsconfig.app.json index 82810560..0301aa1f 100644 --- a/packages/nx-affected-matrix/tsconfig.app.json +++ b/packages/nx-affected-matrix/tsconfig.app.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", - "types": ["node"] + "target": "ES2021", + "types": ["ES2021", "node"] }, "exclude": ["**/*.spec.ts", "**/*.test.ts", "__mocks__/**/*", "**/*/__mocks__/**/*"], "include": ["**/*.ts"] diff --git a/packages/nx-distributed-task/__mocks__/@actions/core.ts b/packages/nx-distributed-task/__mocks__/@actions/core.ts deleted file mode 100644 index 2cc9a18e..00000000 --- a/packages/nx-distributed-task/__mocks__/@actions/core.ts +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - ...jest.requireActual('@actions/core'), - setFailed: jest.fn(), - saveState: jest.fn(), - setOutput: jest.fn(), - getState: jest.fn().mockImplementation((key) => process.env[`STATE_${key}`]), - info: jest.fn(), - error: jest.fn(), - warning: jest.fn(), - notice: jest.fn(), - debug: jest.fn(), - group: jest.fn().mockImplementation(async (title, cb) => await cb()), - startGroup: jest.fn(), - endGroup: jest.fn(), -}; diff --git a/packages/nx-distributed-task/src/assets/action.yml b/packages/nx-distributed-task/action.yml similarity index 60% rename from packages/nx-distributed-task/src/assets/action.yml rename to packages/nx-distributed-task/action.yml index a04c5a4b..3ec41534 100644 --- a/packages/nx-distributed-task/src/assets/action.yml +++ b/packages/nx-distributed-task/action.yml @@ -42,6 +42,25 @@ inputs: required: false default: 'false' runs: - using: 'node16' - main: 'main.js' - post: 'main.js' + using: 'composite' + steps: + - name: Execute target + uses: actions/github-script@v6 + with: + script: | + const { default: setEnv } = require('${{ github.action_path }}/set-env.js'); + const { default: main } = require('${{ github.action_path }}/main.js'); + + setEnv(${{ toJSON(inputs) }}, process); + await main(context, core, exec, glob, io, require); + + - name: Post Execute target + if: ${{ !fromJSON(inputs.nxCloud) }} + uses: actions/github-script@v6 + with: + script: | + const { default: setEnv } = require('${{ github.action_path }}/set-env.js'); + const { default: main } = require('${{ github.action_path }}/main.js'); + + setEnv(${{ toJSON(inputs) }}, process); + await main(context, core, exec, glob, io, require); diff --git a/packages/nx-distributed-task/jest.config.js b/packages/nx-distributed-task/jest.config.js index fa06a9ce..446c6214 100644 --- a/packages/nx-distributed-task/jest.config.js +++ b/packages/nx-distributed-task/jest.config.js @@ -6,7 +6,6 @@ module.exports = { tsconfig: '/tsconfig.spec.json', }, }, - testEnvironment: 'node', transform: { '^.+\\.[tj]s$': 'ts-jest', }, diff --git a/packages/nx-distributed-task/project.json b/packages/nx-distributed-task/project.json index e24c4b93..eb2e072a 100644 --- a/packages/nx-distributed-task/project.json +++ b/packages/nx-distributed-task/project.json @@ -10,33 +10,25 @@ "outputPath": "dist/packages/nx-distributed-task", "main": "packages/nx-distributed-task/src/main.ts", "tsConfig": "packages/nx-distributed-task/tsconfig.app.json", - "externalDependencies": "none", + "externalDependencies": ["@actions/core", "@actions/glob", "@actions/exec", "@actions/github"], + "additionalEntryPoints": [ + { + "entryName": "set-env", + "entryPath": "packages/utils/src/lib/set-env.ts" + } + ], "generatePackageJson": true, + "optimization": true, + "extractLicenses": false, + "sourceMap": false, + "inspect": false, "assets": [ - { - "input": "packages/nx-distributed-task/src/assets", - "glob": "*", - "output": "." - }, { "input": "packages/nx-distributed-task", - "glob": "README.md", + "glob": "*.@(yml|md)", "output": "." } ] - }, - "configurations": { - "production": { - "optimization": true, - "extractLicenses": true, - "inspect": false, - "fileReplacements": [ - { - "replace": "packages/nx-distributed-task/src/environments/environment.ts", - "with": "packages/nx-distributed-task/src/environments/environment.prod.ts" - } - ] - } } }, "serve": { diff --git a/packages/nx-distributed-task/src/app/inputs.ts b/packages/nx-distributed-task/src/app/inputs.ts index c64e48ac..6a52d8a4 100644 --- a/packages/nx-distributed-task/src/app/inputs.ts +++ b/packages/nx-distributed-task/src/app/inputs.ts @@ -1,6 +1,6 @@ -import { getBooleanInput, getInput } from '@actions/core'; +import type * as Core from '@actions/core'; -import { BaseInputs, getBaseInputs, getStringArrayInput, getMaxDistribution } from '@e-square/utils'; +import { BaseInputs, getBaseInputs, getMaxDistribution, getStringArrayInput } from '@e-square/utils/inputs'; export interface Inputs extends BaseInputs { target: string; @@ -11,20 +11,20 @@ export interface Inputs extends BaseInputs { nxCloud: boolean; } -function getDistribution(): number { - return parseInt(getInput('distribution') || getInput('bucket') || '0'); +function getDistribution(core: typeof Core): number { + return parseInt(core.getInput('distribution') || core.getInput('bucket') || '0'); } -export function getInputs(): Inputs { - const target = getInput('target', { required: true }); +export function getInputs(core: typeof Core): Inputs { + const target = core.getInput('target', { required: true }); return { - ...getBaseInputs(), + ...getBaseInputs(core), target, - distribution: getDistribution(), - projects: getStringArrayInput('projects', ','), - maxParallel: getMaxDistribution(target, 'maxParallel')[target], - nxCloud: getBooleanInput('nxCloud'), - uploadOutputs: getBooleanInput('uploadOutputs'), + distribution: getDistribution(core), + projects: getStringArrayInput(core, 'projects', ','), + maxParallel: getMaxDistribution(core, target, 'maxParallel')[target], + nxCloud: core.getBooleanInput('nxCloud'), + uploadOutputs: core.getBooleanInput('uploadOutputs'), }; } diff --git a/packages/nx-distributed-task/src/app/nx-distributed-task.spec.ts b/packages/nx-distributed-task/src/app/nx-distributed-task.spec.ts index 6150e76a..0e4241f4 100644 --- a/packages/nx-distributed-task/src/app/nx-distributed-task.spec.ts +++ b/packages/nx-distributed-task/src/app/nx-distributed-task.spec.ts @@ -1,22 +1,20 @@ -import { logger } from '../../../utils/src/lib/__mocks__/logger'; +import * as core from '@actions/core'; +import * as exec from '@actions/exec'; +import * as glob from '@actions/glob'; +import * as io from '@actions/io'; -jest.mock('@e-square/utils', () => ({ - ...(jest.requireActual('@e-square/utils') as any), - assertNxInstalled: jest.fn().mockResolvedValue(true), - nxRunMany: jest.fn().mockResolvedValue(true), - getWorkspaceProjects: jest.fn(), - getProjectOutputs: jest.fn().mockReturnValue('dist/test'), - uploadArtifact: jest.fn().mockResolvedValue('test'), - nxPrintAffected: jest.fn().mockResolvedValue(['project1', 'project2', 'project3', 'project4']), - logger, -})); +import { context } from '@actions/github'; +import { restoreCache, saveCache } from '@actions/cache'; -jest.mock('@actions/cache'); +import { PRIMARY_KEY } from '@e-square/utils/cache'; +import { uploadArtifact } from '@e-square/utils/artifact'; +import { assertNxInstalled, nxRunMany } from '@e-square/utils/nx'; -import { restoreCache, saveCache } from '@actions/cache'; -import { assertNxInstalled, nxRunMany, PRIMARY_KEY, uploadArtifact } from '@e-square/utils'; import { main } from './nx-distributed-task'; -import * as core from '@actions/core'; + +jest.mock('@e-square/utils/nx'); +jest.mock('@e-square/utils/artifact'); +jest.mock('@e-square/utils/logger'); describe('nxDistributedTask', () => { beforeEach(() => { @@ -33,23 +31,27 @@ describe('nxDistributedTask', () => { }); it('should run nx target for projects', async () => { - await main(); + process.env = { ...process.env, [`STATE_${PRIMARY_KEY}`]: 'test' }; + + await main(context, core, exec, glob, io); expect(assertNxInstalled).toHaveBeenCalled(); expect(restoreCache).toHaveBeenCalled(); + expect(saveCache).toHaveBeenCalled(); expect(nxRunMany).toHaveBeenCalledWith( + context, 'test', expect.any(Object), expect.objectContaining({ args: ['--projects=project1,project2'] }) ); expect(uploadArtifact).toHaveBeenCalledTimes(2); - expect(uploadArtifact).toHaveBeenNthCalledWith(1, 'test', 'dist/test'); + expect(uploadArtifact).toHaveBeenNthCalledWith(1, glob, 'test', 'dist/test'); }); it('should exit if no projects to run', async () => { process.env['INPUT_PROJECTS'] = undefined; - await main(); + await main(context, core, exec, glob, io); expect(assertNxInstalled).not.toHaveBeenCalled(); }); @@ -57,7 +59,7 @@ describe('nxDistributedTask', () => { it('should not upload artifacts', async () => { process.env['INPUT_UPLOADOUTPUTS'] = 'false'; - await main(); + await main(context, core, exec, glob, io); expect(assertNxInstalled).toHaveBeenCalled(); expect(uploadArtifact).not.toHaveBeenCalled(); @@ -65,27 +67,8 @@ describe('nxDistributedTask', () => { it('should set job as failed if any unhandled error occurs', async () => { (assertNxInstalled as jest.Mock).mockRejectedValue('test'); - const spy = jest.spyOn(core, 'setFailed'); - await main(); - - expect(spy).toHaveBeenCalledWith('test'); - }); - - describe('post job', () => { - it('should save cache if key is available', async () => { - process.env = { ...process.env, [`STATE_isPostJob`]: 'true', [`STATE_${PRIMARY_KEY}`]: 'test' }; - - (saveCache as jest.Mock).mockClear(); - (assertNxInstalled as jest.Mock).mockClear(); - (nxRunMany as jest.Mock).mockClear(); - (uploadArtifact as jest.Mock).mockClear(); - - await main(); + await main(context, core, exec, glob, io); - expect(saveCache).toHaveBeenCalled(); - expect(assertNxInstalled).not.toHaveBeenCalled(); - expect(nxRunMany).not.toHaveBeenCalled(); - expect(uploadArtifact).not.toHaveBeenCalled(); - }); + expect(core.setFailed).toHaveBeenCalledWith('test'); }); }); diff --git a/packages/nx-distributed-task/src/app/nx-distributed-task.ts b/packages/nx-distributed-task/src/app/nx-distributed-task.ts index 0b61b87d..5bb99aa7 100644 --- a/packages/nx-distributed-task/src/app/nx-distributed-task.ts +++ b/packages/nx-distributed-task/src/app/nx-distributed-task.ts @@ -1,71 +1,79 @@ -import { getState, saveState, setFailed } from '@actions/core'; +import type * as Core from '@actions/core'; +import type * as _Exec from '@actions/exec'; +import type * as Glob from '@actions/glob'; +import type * as Io from '@actions/io'; +import type { context as Context } from '@actions/github'; -import { - assertNxInstalled, - Exec, - getProjectOutputs, - getWorkspaceProjects, - nxRunMany, - restoreNxCache, - saveNxCache, - uploadArtifact, - logger, -} from '@e-square/utils'; -import { getInputs, Inputs } from './inputs'; +import { uploadArtifact } from '@e-square/utils/artifact'; +import { restoreNxCache, saveNxCache } from '@e-square/utils/cache'; +import { Exec } from '@e-square/utils/exec'; +import { group, info } from '@e-square/utils/logger'; +import { assertNxInstalled, getProjectOutputs, getWorkspaceProjects, nxRunMany } from '@e-square/utils/nx'; -const IS_POST_JOB = 'isPostJob'; +import { getInputs, Inputs } from './inputs'; -function uploadProjectsOutputs(inputs: Inputs): Promise { +function uploadProjectsOutputs(glob: typeof Glob, inputs: Inputs): Promise { if (!inputs.uploadOutputs) return; - return logger.group('⬆️ Uploading artifacts', async () => { + return group('⬆️ Uploading artifacts', async () => { const projects = getWorkspaceProjects(); const artifactName = inputs.target; await Promise.all( inputs.projects.map((project) => - uploadArtifact(artifactName, getProjectOutputs(projects, project, inputs.target)) + uploadArtifact(glob, artifactName, getProjectOutputs(projects, project, inputs.target)) ) ); }); } -function runNxTask(inputs: Inputs): Promise { - return logger.group('🏃 Running NX target', async () => { - const exec = new Exec(); - exec.withArgs(`--projects=${inputs.projects}`); - await nxRunMany(inputs.target, inputs, exec); +function runNxTask(context: typeof Context, exec: typeof _Exec, inputs: Inputs): Promise { + return group('🏃 Running NX target', async () => { + const _exec = new Exec(exec.exec); + _exec.withArgs(`--projects=${inputs.projects}`); + await nxRunMany(context, inputs.target, inputs, _exec); }); } -function restoreCache({ target, distribution, nxCloud }: Inputs): Promise { +function restoreCache( + context: typeof Context, + glob: typeof Glob, + core: typeof Core, + { target, distribution, nxCloud }: Inputs +): Promise { if (nxCloud) return; - return logger.group('🚀 Retrieving NX cache', () => restoreNxCache(target, distribution)); + return group('🚀 Retrieving NX cache', () => restoreNxCache(context, glob, core, target, distribution)); } -export async function main(): Promise { - /* post-job execution */ - if (getState(IS_POST_JOB) === 'true') { - await saveNxCache(); - return; - } +function saveCache(core: typeof Core, { nxCloud }: Inputs): Promise { + if (nxCloud) return; + + return group('🚀 Saving NX cache', () => saveNxCache(core)); +} - const inputs = getInputs(); +export async function main( + context: typeof Context, + core: typeof Core, + exec: typeof _Exec, + glob: typeof Glob, + io: typeof Io, + require? +): Promise { + const parsedInputs = getInputs(core); - if (inputs.projects.length === 0) { - logger.info('There are no projects to run, completing'); + if (parsedInputs.projects.length === 0) { + info('There are no projects to run, completing'); return; } try { - await assertNxInstalled(); - await restoreCache(inputs); - await runNxTask(inputs); - await uploadProjectsOutputs(inputs); + await assertNxInstalled(new Exec(exec.exec)); + await restoreCache(context, glob, core, parsedInputs); + await runNxTask(context, exec, parsedInputs); + await uploadProjectsOutputs(glob, parsedInputs); + await saveCache(core, parsedInputs); } catch (e) { - setFailed(e); + core.setFailed(e); } - - saveState(IS_POST_JOB, true); } diff --git a/packages/nx-distributed-task/src/assets/.gitkeep b/packages/nx-distributed-task/src/assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/nx-distributed-task/src/environments/environment.prod.ts b/packages/nx-distributed-task/src/environments/environment.prod.ts deleted file mode 100644 index c9669790..00000000 --- a/packages/nx-distributed-task/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true, -}; diff --git a/packages/nx-distributed-task/src/environments/environment.ts b/packages/nx-distributed-task/src/environments/environment.ts deleted file mode 100644 index a20cfe55..00000000 --- a/packages/nx-distributed-task/src/environments/environment.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: false, -}; diff --git a/packages/nx-distributed-task/src/main.ts b/packages/nx-distributed-task/src/main.ts index 05985f3c..fbe51beb 100644 --- a/packages/nx-distributed-task/src/main.ts +++ b/packages/nx-distributed-task/src/main.ts @@ -1,3 +1,18 @@ +import type * as Core from '@actions/core'; +import type * as Exec from '@actions/exec'; +import type * as Glob from '@actions/glob'; +import type * as Io from '@actions/io'; +import type { context as Context } from '@actions/github'; + import { main } from './app/nx-distributed-task'; -void main(); +export default async function ( + context: typeof Context, + core: typeof Core, + exec: typeof Exec, + glob: typeof Glob, + io: typeof Io, + require? +) { + await main(context, core, exec, glob, io, require); +} diff --git a/packages/nx-distributed-task/tsconfig.app.json b/packages/nx-distributed-task/tsconfig.app.json index 82810560..0301aa1f 100644 --- a/packages/nx-distributed-task/tsconfig.app.json +++ b/packages/nx-distributed-task/tsconfig.app.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", - "types": ["node"] + "target": "ES2021", + "types": ["ES2021", "node"] }, "exclude": ["**/*.spec.ts", "**/*.test.ts", "__mocks__/**/*", "**/*/__mocks__/**/*"], "include": ["**/*.ts"] diff --git a/packages/utils/__mocks__/@actions/core.ts b/packages/utils/__mocks__/@actions/core.ts deleted file mode 100644 index 2cc9a18e..00000000 --- a/packages/utils/__mocks__/@actions/core.ts +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - ...jest.requireActual('@actions/core'), - setFailed: jest.fn(), - saveState: jest.fn(), - setOutput: jest.fn(), - getState: jest.fn().mockImplementation((key) => process.env[`STATE_${key}`]), - info: jest.fn(), - error: jest.fn(), - warning: jest.fn(), - notice: jest.fn(), - debug: jest.fn(), - group: jest.fn().mockImplementation(async (title, cb) => await cb()), - startGroup: jest.fn(), - endGroup: jest.fn(), -}; diff --git a/packages/utils/__mocks__/@actions/exec.ts b/packages/utils/__mocks__/@actions/exec.ts deleted file mode 100644 index 01587455..00000000 --- a/packages/utils/__mocks__/@actions/exec.ts +++ /dev/null @@ -1 +0,0 @@ -export const exec = jest.fn().mockResolvedValue(true); diff --git a/packages/utils/__mocks__/@actions/glob.ts b/packages/utils/__mocks__/@actions/glob.ts deleted file mode 100644 index c2d73481..00000000 --- a/packages/utils/__mocks__/@actions/glob.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { hashFiles } from '@actions/glob'; - -export { hashFiles }; - -export const create = jest.fn(() => - Promise.resolve({ - glob: jest.fn().mockResolvedValue([]), - }) -); diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js index cb43042b..485cb7b4 100644 --- a/packages/utils/jest.config.js +++ b/packages/utils/jest.config.js @@ -11,5 +11,4 @@ module.exports = { }, moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], coverageDirectory: '../../coverage/packages/utils', - testEnvironment: 'node', }; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts deleted file mode 100644 index bb2a3d18..00000000 --- a/packages/utils/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './lib/artifact'; -export * from './lib/cache'; -export * from './lib/exec'; -export * from './lib/logger'; -export * from './lib/inputs'; -export * from './lib/fs'; -export * from './lib/nx'; -export * from './lib/git'; diff --git a/packages/utils/src/lib/__mocks__/artifact.ts b/packages/utils/src/lib/__mocks__/artifact.ts new file mode 100644 index 00000000..1c0fc7a3 --- /dev/null +++ b/packages/utils/src/lib/__mocks__/artifact.ts @@ -0,0 +1 @@ +export const uploadArtifact = jest.fn().mockResolvedValue('test'); diff --git a/packages/utils/src/lib/__mocks__/logger.ts b/packages/utils/src/lib/__mocks__/logger.ts index f7bd7831..37992b63 100644 --- a/packages/utils/src/lib/__mocks__/logger.ts +++ b/packages/utils/src/lib/__mocks__/logger.ts @@ -1,11 +1,19 @@ -export const logger = { - ...(jest.requireActual('../logger') as any), - log: jest.fn(), - info: jest.fn(), - success: jest.fn(), - debug: jest.fn(), - warning: jest.fn(), - error: jest.fn(), - notice: jest.fn(), - group: jest.fn().mockImplementation(async (title, cb) => await cb()), +const _logger = { + _debugMode: jest.fn().mockReturnValue(false), + set debugMode(value: boolean) { + this._debugMode.mockReturnValue(value); + }, + get debugMode() { + return this._debugMode(); + }, }; + +export const logger = jest.fn().mockReturnValue(_logger); +export const log = jest.fn(); +export const info = jest.fn(); +export const success = jest.fn(); +export const debug = jest.fn(); +export const warning = jest.fn(); +export const error = jest.fn(); +export const notice = jest.fn(); +export const group = jest.fn().mockImplementation(async (title, cb) => await cb()); diff --git a/packages/utils/src/lib/__mocks__/nx.ts b/packages/utils/src/lib/__mocks__/nx.ts new file mode 100644 index 00000000..b89f7052 --- /dev/null +++ b/packages/utils/src/lib/__mocks__/nx.ts @@ -0,0 +1,5 @@ +export const assertNxInstalled = jest.fn().mockResolvedValue(true); +export const nxRunMany = jest.fn().mockResolvedValue(true); +export const getWorkspaceProjects = jest.fn(); +export const getProjectOutputs = jest.fn().mockReturnValue('dist/test'); +export const nxPrintAffected = jest.fn().mockResolvedValue(['project1', 'project2', 'project3', 'project4']); diff --git a/packages/utils/src/lib/artifact.spec.ts b/packages/utils/src/lib/artifact.spec.ts index a508be80..9db01668 100644 --- a/packages/utils/src/lib/artifact.spec.ts +++ b/packages/utils/src/lib/artifact.spec.ts @@ -1,27 +1,27 @@ -import { uploadArtifact } from './artifact'; -import { create } from '@actions/glob'; import { create as aCreate } from '@actions/artifact'; +import * as glob from '@actions/glob'; -jest.mock('./logger'); +import { uploadArtifact } from './artifact'; +import { logger, warning } from './logger'; -import { logger } from './logger'; +jest.mock('./logger'); describe('artifact', () => { beforeEach(() => { - (create as jest.Mock).mockResolvedValue({ glob: jest.fn().mockResolvedValue(['dis/test1', 'dist/test2']) }); + (glob.create as jest.Mock).mockResolvedValue({ glob: jest.fn().mockResolvedValue(['dis/test1', 'dist/test2']) }); }); it('should not upload if no paths', async () => { - await expect(uploadArtifact('test', [])).resolves.toBeUndefined(); + await expect(uploadArtifact(glob, 'test', [])).resolves.toBeUndefined(); }); it('should not upload if not found files', async () => { - (create as jest.Mock).mockResolvedValue({ glob: jest.fn().mockResolvedValue([]) }); - await expect(uploadArtifact('test', ['dist'])).resolves.toBeUndefined(); + (glob.create as jest.Mock).mockResolvedValue({ glob: jest.fn().mockResolvedValue([]) }); + await expect(uploadArtifact(glob, 'test', ['dist'])).resolves.toBeUndefined(); }); it('should upload files', async () => { - await expect(uploadArtifact('test', ['dist'])).resolves.toBe('test'); + await expect(uploadArtifact(glob, 'test', ['dist'])).resolves.toBe('test'); }); it('should skip upload if debug mode is on', async () => { @@ -30,12 +30,12 @@ describe('artifact', () => { uploadArtifact: uploadSpy, })); - logger.debugMode = true; + logger().debugMode = true; - await expect(uploadArtifact('test', ['dist'])).resolves.toBeUndefined(); + await expect(uploadArtifact(glob, 'test', ['dist'])).resolves.toBeUndefined(); expect(uploadSpy).not.toHaveBeenCalled(); - logger.debugMode = false; + logger().debugMode = false; }); it('should fail uploading silently', async () => { @@ -43,8 +43,8 @@ describe('artifact', () => { uploadArtifact: jest.fn().mockRejectedValue('test'), })); - await uploadArtifact('test', ['dist']); + await uploadArtifact(glob, 'test', ['dist']); - expect(logger.warning).toHaveBeenCalled(); + expect(warning).toHaveBeenCalled(); }); }); diff --git a/packages/utils/src/lib/artifact.ts b/packages/utils/src/lib/artifact.ts index f7b1296f..63afded2 100644 --- a/packages/utils/src/lib/artifact.ts +++ b/packages/utils/src/lib/artifact.ts @@ -1,35 +1,35 @@ import { create as artifactClient } from '@actions/artifact'; -import { create as globClient } from '@actions/glob'; +import type * as Glob from '@actions/glob'; -import { logger } from './logger'; +import { debug, info, logger, success, warning } from './logger'; -export async function uploadArtifact(name: string, paths: string[]): Promise { +export async function uploadArtifact(glob: typeof Glob, name: string, paths: string[]): Promise { if (paths.length === 0) return; const globPaths = paths.map((path) => `${path}/*`).join('\n'); - logger.debug(`Upload paths: ${globPaths}`); + debug(`Upload paths: ${globPaths}`); - const files = await globClient(globPaths).then((glob) => glob.glob()); + const files = await glob.create(globPaths).then((glob) => glob.glob()); if (!files.length) { - logger.info(`Couldn't find files to upload in ${paths.join(', ')}`); + info(`Couldn't find files to upload in ${paths.join(', ')}`); return; } - logger.debug(`Found ${files.length} files to upload`); + debug(`Found ${files.length} files to upload`); - if (logger.debugMode) { - logger.debug(`Debug mode is on, skipping uploading artifacts`); + if (logger().debugMode) { + debug(`Debug mode is on, skipping uploading artifacts`); return; } try { const { failedItems, artifactName, size } = await artifactClient().uploadArtifact(name, files, process.cwd()); - logger.debug(`name: ${artifactName}, size: ${size}, failedItems: ${failedItems.join(', ')}`); + debug(`name: ${artifactName}, size: ${size}, failedItems: ${failedItems.join(', ')}`); - logger.success(`Successfully uploaded ${artifactName}`); + success(`Successfully uploaded ${artifactName}`); return artifactName; } catch (e) { - logger.warning(e); + warning(e); } } diff --git a/packages/utils/src/lib/cache.spec.ts b/packages/utils/src/lib/cache.spec.ts index 07ae3917..9a3dd3f5 100644 --- a/packages/utils/src/lib/cache.spec.ts +++ b/packages/utils/src/lib/cache.spec.ts @@ -1,11 +1,15 @@ -import { NX_CACHE_PATH, restoreNxCache, saveNxCache, PRIMARY_KEY, CACHE_KEY } from './cache'; import { resolve } from 'path'; import { tree } from './fs'; + import { saveCache, restoreCache, ReserveCacheError } from '@actions/cache'; +import * as core from '@actions/core'; +import * as glob from '@actions/glob'; +import { context } from '@actions/github'; -jest.mock('./logger'); +import { NX_CACHE_PATH, restoreNxCache, saveNxCache, PRIMARY_KEY, CACHE_KEY } from './cache'; +import { info, logger, warning } from './logger'; -import { logger } from './logger'; +jest.mock('./logger'); describe('cache', () => { beforeAll(() => { @@ -19,7 +23,7 @@ describe('cache', () => { describe('restoreNxCache', () => { it('should restore cache with primary key and restoreKeys', async () => { - await restoreNxCache('test', 2); + await restoreNxCache(context, glob, core, 'test', 2); expect(restoreCache).toHaveBeenCalledWith( [resolve(NX_CACHE_PATH)], expect.stringContaining('test-2'), @@ -29,26 +33,26 @@ describe('cache', () => { it('should fail silently', async () => { (restoreCache as jest.Mock).mockRejectedValueOnce(''); - await restoreNxCache('test', 2); + await restoreNxCache(context, glob, core, 'test', 2); - expect(logger.warning).toHaveBeenCalledWith(''); + expect(warning).toHaveBeenCalledWith(''); }); it('should report cache miss', async () => { (restoreCache as jest.Mock).mockResolvedValueOnce(''); - await restoreNxCache('test', 2); + await restoreNxCache(context, glob, core, 'test', 2); - expect(logger.info).toHaveBeenCalledWith('Cache miss'); + expect(info).toHaveBeenCalledWith('Cache miss'); }); it('should not restore cache if in debug mode', async () => { - logger.debugMode = true; + logger(core).debugMode = true; - await restoreNxCache('test', 2); + await restoreNxCache(context, glob, core, 'test', 2); expect(restoreCache).not.toHaveBeenCalled(); - logger.debugMode = false; + logger().debugMode = false; }); }); @@ -58,34 +62,34 @@ describe('cache', () => { }); it('should save cache with primary key', async () => { - await saveNxCache(); + await saveNxCache(core); expect(saveCache).toHaveBeenCalledWith([resolve(NX_CACHE_PATH)], 'test'); }); it('should fail silently for ReserveCacheError', async () => { (saveCache as jest.Mock).mockRejectedValueOnce(new ReserveCacheError('test')); - await expect(saveNxCache()).resolves.toBeUndefined(); + await expect(saveNxCache(core)).resolves.toBeUndefined(); }); it('should fail for not ReserveCacheError', async () => { (saveCache as jest.Mock).mockRejectedValueOnce(new Error('test')); - await expect(saveNxCache()).rejects.toThrowError('test'); + await expect(saveNxCache(core)).rejects.toThrowError('test'); }); it('should not save cache if in debug mode', async () => { - logger.debugMode = true; + logger(core).debugMode = true; - await saveNxCache(); + await saveNxCache(core); expect(saveCache).not.toHaveBeenCalled(); - logger.debugMode = false; + logger().debugMode = false; }); it('should not save if no primary key is provided', async () => { process.env[`STATE_${PRIMARY_KEY}`] = ''; - await saveNxCache(); + await saveNxCache(core); expect(saveCache).not.toHaveBeenCalled(); }); @@ -93,9 +97,8 @@ describe('cache', () => { it('should not save if cache hit occurred on primary key', async () => { process.env[`STATE_${CACHE_KEY}`] = 'test'; - await saveNxCache(); - - expect(saveCache).not.toHaveBeenCalled(); + await saveNxCache(core); }); + expect(saveCache).not.toHaveBeenCalled(); }); }); diff --git a/packages/utils/src/lib/cache.ts b/packages/utils/src/lib/cache.ts index f93ee3e5..f8320eec 100644 --- a/packages/utils/src/lib/cache.ts +++ b/packages/utils/src/lib/cache.ts @@ -1,10 +1,10 @@ import { ReserveCacheError, restoreCache, saveCache } from '@actions/cache'; -import { getState, saveState } from '@actions/core'; -import { context } from '@actions/github'; -import { hashFiles } from '@actions/glob'; +import type * as Core from '@actions/core'; +import type * as Glob from '@actions/glob'; +import type { context as Context } from '@actions/github'; import { tree } from './fs'; -import { logger } from './logger'; +import { debug, info, logger, success, warning } from './logger'; export const NX_CACHE_PATH = 'node_modules/.cache/nx'; export const CACHE_KEY = 'CACHE_KEY'; @@ -20,7 +20,12 @@ function isExactKeyMatch(key: string, cacheKey?: string): boolean { ); } -async function getCacheKeys(target: string, distribution: number): Promise<[primary: string, restoreKeys: string[]]> { +async function getCacheKeys( + context: typeof Context, + glob: typeof Glob, + target: string, + distribution: number +): Promise<[primary: string, restoreKeys: string[]]> { const keyParts = []; const restoreKeys = []; @@ -29,7 +34,7 @@ async function getCacheKeys(target: string, distribution: number): Promise<[prim const lockFile = tree.getLockFilePath(); if (lockFile) { - keyParts.push(await hashFiles(lockFile)); + keyParts.push(await glob.hashFiles(lockFile)); addRestoreKey(); } @@ -51,67 +56,73 @@ async function getCacheKeys(target: string, distribution: number): Promise<[prim keyParts.push(context.payload.pull_request.number.toString()); } - logger.debug(`primary key is: ${keyParts.join('-')}`); - logger.debug(`restore keys are: ${restoreKeys.join(' | ')}`); + debug(`primary key is: ${keyParts.join('-')}`); + debug(`restore keys are: ${restoreKeys.join(' | ')}`); return [keyParts.join('-'), restoreKeys]; } -export async function restoreNxCache(target: string, distribution: number): Promise { - if (logger.debugMode) { - logger.debug(`Debug mode is on, skipping restoring cache`); +export async function restoreNxCache( + context: typeof Context, + glob: typeof Glob, + core: typeof Core, + target: string, + distribution: number +): Promise { + if (logger().debugMode) { + debug(`Debug mode is on, skipping restoring cache`); return; } - const [primaryKey, restoreKeys] = await getCacheKeys(target, distribution); + const [primaryKey, restoreKeys] = await getCacheKeys(context, glob, target, distribution); - saveState(PRIMARY_KEY, primaryKey); - logger.debug(`Restoring NX cache for ${primaryKey}`); + core.saveState(PRIMARY_KEY, primaryKey); + debug(`Restoring NX cache for ${primaryKey}`); try { const key = await restoreCache([tree.resolve(NX_CACHE_PATH)], primaryKey, restoreKeys); if (key) { - logger.success(`Cache hit: ${key}`); + success(`Cache hit: ${key}`); - saveState(CACHE_KEY, key); + core.saveState(CACHE_KEY, key); } else { - logger.info(`Cache miss`); + info(`Cache miss`); } } catch (e) { - logger.warning(e); + warning(e); } } -export async function saveNxCache(): Promise { - if (logger.debugMode) { - logger.debug(`Debug mode is on, skipping saving cache`); +export async function saveNxCache(core: typeof Core): Promise { + if (logger().debugMode) { + debug(`Debug mode is on, skipping saving cache`); return; } - const cacheKey = getState(CACHE_KEY); - const primaryKey = getState(PRIMARY_KEY); + const cacheKey = core.getState(CACHE_KEY); + const primaryKey = core.getState(PRIMARY_KEY); if (!primaryKey) { - logger.info(`Couldn't find the primary key, skipping saving cache`); + info(`Couldn't find the primary key, skipping saving cache`); return; } if (isExactKeyMatch(primaryKey, cacheKey)) { - logger.info(`Cache hit occurred on the primary key ${cacheKey}, not saving cache.`); + info(`Cache hit occurred on the primary key ${cacheKey}, not saving cache.`); return; } - logger.debug(`Saving NX cache to ${primaryKey}`); + debug(`Saving NX cache to ${primaryKey}`); try { await saveCache([tree.resolve(NX_CACHE_PATH)], primaryKey); - logger.success(`Successfully saved cache to ${primaryKey}`); + success(`Successfully saved cache to ${primaryKey}`); } catch (err) { // don't throw an error if cache already exists, which may happen due to concurrency if (err instanceof ReserveCacheError) { - logger.warning(err); + warning(err); return; } // otherwise re-throw diff --git a/packages/utils/src/lib/exec.spec.ts b/packages/utils/src/lib/exec.spec.ts index b6f7169e..c5ce3dc0 100644 --- a/packages/utils/src/lib/exec.spec.ts +++ b/packages/utils/src/lib/exec.spec.ts @@ -1,17 +1,29 @@ import { Exec } from './exec'; + +jest.mock('@actions/exec'); +jest.mock('./logger'); + import { exec as ghExec } from '@actions/exec'; describe('exec', () => { let exec: Exec; beforeEach(() => { - exec = new Exec(); + exec = new Exec(ghExec); exec.withCommand('test'); jest.spyOn(exec, 'build'); }); + it('should be possible to create new instance from another one', async () => { + const newExec = new Exec(exec); + + await newExec.withCommand('test').build()(); + + expect(ghExec).toHaveBeenCalledWith('test', expect.anything(), expect.anything()); + }); + it('should add command', async () => { await exec.build()(); expect(ghExec).toHaveBeenCalledWith('test', expect.anything(), expect.anything()); @@ -32,7 +44,7 @@ describe('exec', () => { }); it('should throw if no command is specified', async () => { - exec = new Exec(); + exec = new Exec(ghExec); try { exec.build(); diff --git a/packages/utils/src/lib/exec.ts b/packages/utils/src/lib/exec.ts index 20cfb924..eaa5db63 100644 --- a/packages/utils/src/lib/exec.ts +++ b/packages/utils/src/lib/exec.ts @@ -1,7 +1,6 @@ -import { setFailed } from '@actions/core'; -import { exec, ExecOptions } from '@actions/exec'; +import type { ExecOptions, exec as __exec } from '@actions/exec'; -import { logger } from './logger'; +import { debug } from './logger'; export type ExecWrapper = (args?: string[], options?: ExecOptions) => Promise; @@ -10,6 +9,12 @@ export class Exec { private options: ExecOptions = {}; private args: string[] = []; + private get exec(): typeof __exec { + return this._exec instanceof Exec ? this._exec.exec : this._exec; + } + + constructor(private _exec: typeof __exec | Exec) {} + build(): ExecWrapper { const command = this.command; const coercedArgs = [...this.args]; @@ -36,10 +41,10 @@ export class Exec { }, }; - logger.debug(`Running command ${command} - args: ${finalArgs.join(' ')}", options: ${finalOpts}`); + debug(`Running command ${command} - args: ${finalArgs.join(' ')}", options: ${finalOpts}`); - return exec(command, finalArgs, finalOpts).then((code) => { - if (code !== 0) setFailed(stderr); + return this.exec(command, finalArgs, finalOpts).then((code) => { + if (code !== 0) throw stderr; return stdout; }); }; diff --git a/packages/utils/src/lib/fs.ts b/packages/utils/src/lib/fs.ts index 6a123319..ce50beef 100644 --- a/packages/utils/src/lib/fs.ts +++ b/packages/utils/src/lib/fs.ts @@ -1,10 +1,9 @@ -import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, chmodSync } from 'fs'; +import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, chmodSync, renameSync, rmSync } from 'fs'; import { resolve } from 'path'; -import { cp, rmRF } from '@actions/io'; import type { Tree } from '@nrwl/devkit'; -import { logger } from './logger'; +import { info } from './logger'; export class GHTree implements Tree { readonly root: string; @@ -21,7 +20,7 @@ export class GHTree implements Tree { } delete(filePath: string): void { - void rmRF(this.resolve(filePath)); + void rmSync(this.resolve(filePath), { recursive: true }); } exists(filePath: string): boolean { @@ -43,7 +42,7 @@ export class GHTree implements Tree { } async rename(from: string, to: string): Promise { - await cp(this.resolve(from), resolve(this.root, to)); + renameSync(this.resolve(from), resolve(this.root, to)); } write(filePath: string, content: Buffer | string): void { @@ -58,7 +57,7 @@ export class GHTree implements Tree { const lockFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'pnpm-lock.yml']; const lockFile = lockFiles.find((file) => this.exists(file)); if (!lockFile) { - logger.info(`Couldn't find any lock file`); + info(`Couldn't find any lock file`); return; } return this.resolve(lockFile); diff --git a/packages/utils/src/lib/git.spec.ts b/packages/utils/src/lib/git.spec.ts deleted file mode 100644 index bec0c6e7..00000000 --- a/packages/utils/src/lib/git.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { retrieveGitBoundaries, retrieveGitSHA } from './git'; -import { Exec } from './exec'; -import { context } from '@actions/github'; -import * as core from '@actions/core'; - -jest.mock('./logger'); - -describe('git', () => { - let exec: Exec; - let buildSpy: jest.SpyInstance; - let execWrapper: jest.Mock; - - beforeEach(() => { - exec = new Exec(); - execWrapper = jest.fn().mockReturnValue(Promise.resolve('')); - buildSpy = jest.spyOn(exec, 'build').mockReturnValue(execWrapper); - jest.spyOn(exec, 'withCommand'); - jest.spyOn(exec, 'withArgs'); - jest.spyOn(exec, 'withOptions'); - }); - - describe('retrieveGitSHA', () => { - it('should get SHA', async () => { - buildSpy.mockRestore(); - await expect(retrieveGitSHA(exec.withCommand('git rev-parse').build(), 'HEAD')).resolves.toBeDefined(); - }); - - it('should format result', async () => { - execWrapper.mockReturnValueOnce(Promise.resolve(`with\nline\nbreaks`)); - - await expect(retrieveGitSHA(exec.build(), '')).resolves.toEqual('withlinebreaks'); - }); - }); - - describe('retrieveGitBoundaries', () => { - it('should fail pipeline if throws', async () => { - jest.spyOn(context, 'eventName', 'get').mockReturnValueOnce('push'); - execWrapper.mockReturnValueOnce(Promise.reject()); - - await retrieveGitBoundaries(exec); - expect(core.setFailed).toHaveBeenCalled(); - }); - - it('should use git to get base & head SHA', async () => { - jest.spyOn(context, 'eventName', 'get').mockReturnValueOnce('push'); - - await expect(retrieveGitBoundaries(exec)).resolves.toEqual(['', '']); - expect(execWrapper).toHaveBeenNthCalledWith(1, ['HEAD~1']); - expect(execWrapper).toHaveBeenNthCalledWith(2, ['HEAD']); - }); - - it('should use PR payload to get base & head SHA', async () => { - await expect(retrieveGitBoundaries(exec)).resolves.toEqual(['0', '0']); - expect(buildSpy).not.toHaveBeenCalled(); - expect(execWrapper).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/utils/src/lib/git.ts b/packages/utils/src/lib/git.ts deleted file mode 100644 index 752fdbcd..00000000 --- a/packages/utils/src/lib/git.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { setFailed } from '@actions/core'; -import { context } from '@actions/github'; - -import type { Exec, ExecWrapper } from './exec'; -import { logger } from './logger'; - -export async function retrieveGitSHA(exec: ExecWrapper, rev: string): Promise { - return exec([rev]).then((res) => res.replace(/(\r\n|\n|\r)/gm, '')); -} - -export function retrieveGitBoundaries(exec: Exec): Promise { - const boundaries = []; - return logger.group('🔀 Setting Git boundaries', async () => { - if (context.eventName === 'pull_request') { - const prPayload = context.payload.pull_request; - boundaries.push(prPayload.base.sha.toString(), prPayload.head.sha.toString()); - } else { - const wrapper = exec.withCommand('git rev-parse').build(); - - try { - boundaries.push(...(await Promise.all([retrieveGitSHA(wrapper, 'HEAD~1'), retrieveGitSHA(wrapper, 'HEAD')]))); - } catch (e) { - setFailed(e); - } - } - - logger.debug(`Base SHA: ${boundaries[0]}`); - logger.debug(`Head SHA: ${boundaries[1]}`); - - return boundaries; - }); -} diff --git a/packages/utils/src/lib/inputs.spec.ts b/packages/utils/src/lib/inputs.spec.ts index ece934ce..b17bad39 100644 --- a/packages/utils/src/lib/inputs.spec.ts +++ b/packages/utils/src/lib/inputs.spec.ts @@ -1,8 +1,10 @@ -jest.mock('./fs'); - import * as core from '@actions/core'; + import { getMaxDistribution, getStringArrayInput } from './inputs'; +jest.mock('./fs'); +jest.mock('./logger'); + describe('inputs', () => { let spy: jest.SpyInstance; @@ -16,32 +18,32 @@ describe('inputs', () => { }); it('should return an array of strings from a string input', () => { - expect(getStringArrayInput('test', ',')).toEqual(['test1', 'test2', 'test3']); + expect(getStringArrayInput(core, 'test', ',')).toEqual(['test1', 'test2', 'test3']); }); }); describe('getMaxDistribution', () => { it('should return an object of targets distribution from a string input', () => { spy.mockReturnValueOnce(2); - expect(getMaxDistribution('test')).toEqual({ test: 2 }); + expect(getMaxDistribution(core, 'test')).toEqual({ test: 2 }); }); it('should return an object of targets distribution from a JSON object', () => { spy.mockReturnValueOnce('{ "test1": 1, "test2": 2 }'); - expect(getMaxDistribution(['test1', 'test2'])).toEqual({ test1: 1, test2: 2 }); + expect(getMaxDistribution(core, ['test1', 'test2'])).toEqual({ test1: 1, test2: 2 }); }); it('should return an object of targets distribution from a JSON array', () => { spy.mockReturnValueOnce('[1,2]'); - expect(getMaxDistribution(['test1', 'test2'])).toEqual({ test1: 1, test2: 2 }); + expect(getMaxDistribution(core, ['test1', 'test2'])).toEqual({ test1: 1, test2: 2 }); }); it('should return an object of targets distribution from a partial JSON object/array', () => { spy.mockReturnValueOnce('{ "test1": 1 }'); - expect(getMaxDistribution(['test1', 'test2'])).toEqual({ test1: 1, test2: 3 }); + expect(getMaxDistribution(core, ['test1', 'test2'])).toEqual({ test1: 1, test2: 3 }); spy.mockReturnValueOnce('[1]'); - expect(getMaxDistribution(['test1', 'test2'])).toEqual({ test1: 1, test2: 3 }); + expect(getMaxDistribution(core, ['test1', 'test2'])).toEqual({ test1: 1, test2: 3 }); }); }); }); diff --git a/packages/utils/src/lib/inputs.ts b/packages/utils/src/lib/inputs.ts index a22784d1..97159416 100644 --- a/packages/utils/src/lib/inputs.ts +++ b/packages/utils/src/lib/inputs.ts @@ -1,6 +1,6 @@ -import { getBooleanInput, getInput, InputOptions } from '@actions/core'; +import type * as Core from '@actions/core'; -import { logger } from './logger'; +import { log, logger, warning } from './logger'; export interface BaseInputs { args: string[]; @@ -8,14 +8,24 @@ export interface BaseInputs { workingDirectory: string; } -export function getStringArrayInput(name: string, separator = ' ', options?: InputOptions): string[] { - return getInput(name, options) +export function getStringArrayInput( + core: typeof Core, + name: string, + separator = ' ', + options?: Core.InputOptions +): string[] { + return core + .getInput(name, options) .split(separator) .filter((value) => value.length > 0); } -export function getMaxDistribution(targets: string | string[], name?: string): Record { - const value = name ? getInput(name) : getInput('maxDistribution') || getInput('maxParallel'); +export function getMaxDistribution( + core: typeof Core, + targets: string | string[], + name?: string +): Record { + const value = name ? core.getInput(name) : core.getInput('maxDistribution') || core.getInput('maxParallel'); const coercedTargets = [].concat(targets); const maybeNumberValue = parseInt(value); @@ -23,7 +33,7 @@ export function getMaxDistribution(targets: string | string[], name?: string): R coercedTargets.reduce((acc, curr, idx) => { let targetVal = typeof source === 'object' ? (Array.isArray(source) ? source[idx] : source[curr]) : source; if (targetVal === null || targetVal === undefined || isNaN(targetVal) || targetVal <= 0) { - logger.warning( + warning( new Error( `Received invalid value for ${name} input: '${targetVal}' for target '${curr}', using the default instead` ) @@ -39,26 +49,26 @@ export function getMaxDistribution(targets: string | string[], name?: string): R try { return reduceTargetsDistribution(JSON.parse(value)); } catch { - logger.warning(new Error(`Couldn't parse '${value}' as a valid JSON object, using default value for ${name}`)); + warning(new Error(`Couldn't parse '${value}' as a valid JSON object, using default value for ${name}`)); } } return reduceTargetsDistribution(maybeNumberValue); } -export function getBaseInputs(): BaseInputs { - const debug = getBooleanInput('debug'); - const workingDirectory = getInput('workingDirectory'); +export function getBaseInputs(core: typeof Core): BaseInputs { + const debug = core.getBooleanInput('debug'); + const workingDirectory = core.getInput('workingDirectory'); - logger.debugMode = debug; + logger(core).debugMode = debug; if (workingDirectory?.length > 0) { - logger.log(`🏃 Working in custom directory: ${workingDirectory}`); + log(`🏃 Working in custom directory: ${workingDirectory}`); process.chdir(workingDirectory); } return { - args: getStringArrayInput('args'), + args: getStringArrayInput(core, 'args'), debug, workingDirectory, }; diff --git a/packages/utils/src/lib/logger.spec.ts b/packages/utils/src/lib/logger.spec.ts new file mode 100644 index 00000000..a96e9448 --- /dev/null +++ b/packages/utils/src/lib/logger.spec.ts @@ -0,0 +1,61 @@ +import * as core from '@actions/core'; + +import { debug, error, group, info, initializeLogger, log, logger, notice, success, warning } from './logger'; + +const expectLogging = ( + method: (arg: string) => void, + spy: (arg: string) => void, + expectedPrefix: string, + ...expectedParams: any +) => { + (spy as jest.Mock).mockClear(); + + method('test'); + expect(spy).toHaveBeenCalledWith(`${expectedPrefix ? `${expectedPrefix} ` : ''}test`, ...expectedParams); +}; + +describe('logger', () => { + let instance: ReturnType; + + beforeEach(() => { + initializeLogger(core); + + instance = logger(); + }); + + describe('use the underlying core package', () => { + it('should use info', () => { + expectLogging(log, core.info, ''); + expectLogging(info, core.info, '❕'); + expectLogging(success, core.info, '✅'); + + instance.debugMode = true; + expectLogging(debug, core.info, '🐞'); + }); + + it('should use debug', () => { + (core.isDebug as jest.Mock).mockReturnValueOnce(true); + + expectLogging(debug, core.debug, '🐞'); + }); + + it('should use notice', () => { + expectLogging(notice, core.notice, '', undefined); + }); + + it('should use warning', () => { + expectLogging(warning, core.warning, '', undefined); + }); + + it('should use error', () => { + expectLogging(error, core.error, '', undefined); + }); + + it('should use group', async () => { + const cb = jest.fn(); + await group('test', cb); + + expect(core.group).toHaveBeenCalledWith('test', cb); + }); + }); +}); diff --git a/packages/utils/src/lib/logger.ts b/packages/utils/src/lib/logger.ts index 9db8be09..e13a642a 100644 --- a/packages/utils/src/lib/logger.ts +++ b/packages/utils/src/lib/logger.ts @@ -1,4 +1,6 @@ -import { info, debug, warning, error, notice, AnnotationProperties, group, isDebug } from '@actions/core'; +import type * as Core from '@actions/core'; + +let _logger: Logger; class Logger { private _debugMode = false; @@ -9,8 +11,10 @@ class Logger { return this._debugMode; } + constructor(private core: typeof Core) {} + log(message: string) { - info(message); + this.core.info(message); } info(message: string) { @@ -22,25 +26,67 @@ class Logger { } debug(message: string) { - if (this.debugMode && !isDebug()) this.log(`🐞 ${message}`); - debug(`🐞 ${message}`); + if (this.debugMode && !this.core.isDebug()) this.log(`🐞 ${message}`); + this.core.debug(`🐞 ${message}`); } - notice(message: string | Error, properties?: AnnotationProperties) { - notice(message, properties); + notice(message: string | Error, properties?: Core.AnnotationProperties) { + this.core.notice(message, properties); } - warning(message: string | Error, properties?: AnnotationProperties) { - warning(message, properties); + warning(message: string | Error, properties?: Core.AnnotationProperties) { + this.core.warning(message, properties); } - error(message: string | Error, properties?: AnnotationProperties) { - error(message, properties); + error(message: string | Error, properties?: Core.AnnotationProperties) { + this.core.error(message, properties); } async group(name: string, cb: () => Promise): Promise { - return await group(name, cb); + return await this.core.group(name, cb); } } -export const logger = new Logger(); +export function initializeLogger(core: typeof Core) { + _logger = new Logger(core); +} + +export const logger = (core?: typeof Core) => { + if (!_logger) { + if (!core) throw 'logger is not initialized'; + initializeLogger(core); + } + return _logger; +}; + +export function log(message: string) { + logger().log(message); +} + +export function info(message: string) { + logger().info(message); +} + +export function success(message: string) { + logger().success(message); +} + +export function debug(message: string) { + logger().debug(message); +} + +export function notice(message: string | Error, properties?: Core.AnnotationProperties) { + logger().notice(message, properties); +} + +export function warning(message: string | Error, properties?: Core.AnnotationProperties) { + logger().warning(message, properties); +} + +export function error(message: string | Error, properties?: Core.AnnotationProperties) { + logger().error(message, properties); +} + +export async function group(name: string, cb: () => Promise): Promise { + return await logger().group(name, cb); +} diff --git a/packages/utils/src/lib/npm.ts b/packages/utils/src/lib/npm.ts new file mode 100644 index 00000000..21b364a9 --- /dev/null +++ b/packages/utils/src/lib/npm.ts @@ -0,0 +1,5 @@ +import { Exec } from './exec'; + +export function getNpmVersion(exec: Exec): Promise { + return new Exec(exec).withCommand('npm -v').build()(); +} diff --git a/packages/utils/src/lib/nx.spec.ts b/packages/utils/src/lib/nx.spec.ts index 797598c3..d5780bef 100644 --- a/packages/utils/src/lib/nx.spec.ts +++ b/packages/utils/src/lib/nx.spec.ts @@ -1,31 +1,37 @@ import { workspaceMock, mergedWorkspaceMock } from './__mocks__/fs'; -jest.mock('./fs'); -jest.mock('./logger'); - import { assertNxInstalled, getProjectOutputs, getWorkspaceProjects, - NX_BIN_PATH, nxCommand, nxPrintAffected, nxRunMany, } from './nx'; -import * as which from 'which'; +import * as npm from './npm'; import { tree } from './fs'; import { Exec } from './exec'; +import { context } from '@actions/github'; + +jest.mock('./fs'); +jest.mock('./logger'); +jest.mock('@actions/github'); describe('nx', () => { describe('assertNxInstalled', () => { it('should fail to assert and throw error', async () => { - (which as jest.Mock).mockReturnValueOnce(Promise.reject()); - await expect(assertNxInstalled()).rejects.toThrow("Couldn't find Nx binary, Have you run npm/yarn install?"); + const exec = new Exec(jest.fn().mockResolvedValueOnce(0)); + await expect(assertNxInstalled(exec)).rejects.toThrow("Couldn't find Nx binary, Have you run npm/yarn install?"); }); it('should success assertion', async () => { - (which as jest.Mock).mockReturnValueOnce(Promise.resolve()); - await expect(assertNxInstalled()).resolves.toBeUndefined(); + const exec = new Exec( + jest.fn().mockImplementation(async (_, __, opts) => { + opts.listeners.stdout('test'); + return Promise.resolve(0); + }) + ); + await expect(assertNxInstalled(exec)).resolves.toBeUndefined(); }); }); @@ -91,30 +97,40 @@ describe('nx', () => { let exec: Exec; beforeEach(() => { - exec = new Exec(); + exec = new Exec(jest.fn()); jest.spyOn(exec, 'build').mockReturnValue(() => Promise.resolve('')); jest.spyOn(exec, 'withCommand'); jest.spyOn(exec, 'withArgs'); jest.spyOn(exec, 'withOptions'); + jest.spyOn(npm, 'getNpmVersion').mockResolvedValue('6.8.0'); }); it('should call nxCommand', async () => { await expect(nxCommand('test', 'build', exec, [])).resolves.toBe(''); - expect(exec.withCommand).toHaveBeenCalledWith(`${NX_BIN_PATH} test`); + expect(exec.withCommand).toHaveBeenCalledWith(`npx -p @nrwl/cli nx test`); expect(exec.withArgs).toHaveBeenCalledWith('--target=build'); + + jest.spyOn(npm, 'getNpmVersion').mockResolvedValueOnce('7.0.0'); + await expect(nxCommand('test', 'build', exec, [])).resolves.toBe(''); + expect(exec.withCommand).toHaveBeenCalledWith(`npx --no -p @nrwl/cli nx test`); }); it('should call nxPrintAffected', async () => { await expect(nxPrintAffected('test', exec)).resolves.toEqual(['']); - expect(exec.withCommand).toHaveBeenCalledWith(`${NX_BIN_PATH} print-affected`); + expect(exec.withCommand).toHaveBeenCalledWith(`npx -p @nrwl/cli nx print-affected`); expect(exec.withArgs).toHaveBeenCalledWith('--target=test', '--select=tasks.target.project'); }); it('should call nxRunMany', async () => { await expect( - nxRunMany('test', { args: [], debug: false, workingDirectory: '', nxCloud: true, maxParallel: 3 }, exec) + nxRunMany( + context, + 'test', + { args: [], debug: false, workingDirectory: '', nxCloud: true, maxParallel: 3 }, + exec + ) ).resolves.toBe(''); - expect(exec.withCommand).toHaveBeenCalledWith(`${NX_BIN_PATH} run-many`); + expect(exec.withCommand).toHaveBeenCalledWith(`npx -p @nrwl/cli nx run-many`); expect(exec.withArgs).toHaveBeenCalledWith('--target=test', '--scan', '--parallel', '--maxParallel=3'); expect(exec.withOptions).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/packages/utils/src/lib/nx.ts b/packages/utils/src/lib/nx.ts index 4d6125fc..43db447a 100644 --- a/packages/utils/src/lib/nx.ts +++ b/packages/utils/src/lib/nx.ts @@ -1,14 +1,11 @@ -import * as which from 'which'; - -import { context } from '@actions/github'; +import type { context as Context } from '@actions/github'; import type { ProjectConfiguration } from '@nrwl/devkit'; import { Exec } from './exec'; import { BaseInputs } from './inputs'; import { tree } from './fs'; -import { logger } from './logger'; - -export const NX_BIN_PATH = 'node_modules/.bin/nx'; +import { debug, logger, warning } from './logger'; +import { getNpmVersion } from './npm'; export interface WorkspaceJsonConfiguration { projects: Record; @@ -18,7 +15,7 @@ export type WorkspaceProjects = Record; export function getWorkspaceProjects(): WorkspaceProjects { const workspaceFile = tree.exists('angular.json') ? 'angular.json' : 'workspace.json'; - logger.debug(`Found ${workspaceFile} as nx workspace`); + debug(`Found ${workspaceFile} as nx workspace`); const workspaceContent: WorkspaceJsonConfiguration = JSON.parse( tree @@ -57,7 +54,7 @@ export function getProjectOutputs(projects: WorkspaceProjects, project: string, const [scope, prop] = path.replace(/[{}]/g, '').split('.'); if (!projectTarget?.[scope]?.[prop]) { - logger.warning( + warning( new Error( `Couldn't find output value for ${project}. full path: project.${project}.targets.${target}.${scope}.${prop}` ) @@ -69,24 +66,25 @@ export function getProjectOutputs(projects: WorkspaceProjects, project: string, }; const resolvedOutputs = outputs.map(replaceExpressions); - logger.debug(`Found ${resolvedOutputs} as outputs for ${target}`); + debug(`Found ${resolvedOutputs} as outputs for ${target}`); return resolvedOutputs; } -export async function assertNxInstalled() { - try { - logger.debug(`Checking existence of nx`); +export async function assertNxInstalled(exec: Exec) { + debug(`Checking existence of nx`); - await which(NX_BIN_PATH); - } catch { - throw new Error("Couldn't find Nx binary, Have you run npm/yarn install?"); - } + const path = await exec.withCommand('npm ls @nrwl/cli -p --depth 1').build()(); + debug(`NX bin path: ${path}`); + + if (!path) throw new Error("Couldn't find Nx binary, Have you run npm/yarn install?"); } export async function nxCommand(command: string, target: string, exec: Exec, args: string[]): Promise { + const npxVersion = (await getNpmVersion(exec)).split('.'); + const wrapper = exec - .withCommand(`${NX_BIN_PATH} ${command}`) + .withCommand(`npx ${+npxVersion[0] > 6 ? '--no ' : ''}-p @nrwl/cli nx ${command}`) .withArgs(`--target=${target}`, ...args) .build(); @@ -95,12 +93,13 @@ export async function nxCommand(command: string, target: string, exec: Exec, arg export async function nxPrintAffected(target: string, exec: Exec): Promise { const projects = (await nxCommand('print-affected', target, exec, ['--select=tasks.target.project'])).trim(); - logger.debug(`Affected project for ${target}: ${projects}`); + debug(`Affected project for ${target}: ${projects}`); return projects.split(', '); } export async function nxRunMany( + context: typeof Context, target: string, inputs: BaseInputs & { nxCloud?: boolean; maxParallel?: number }, exec: Exec @@ -121,8 +120,8 @@ export async function nxRunMany( args.push('--parallel', `--maxParallel=${inputs.maxParallel || 3}`); - if (logger.debugMode) { - logger.debug(`Debug mode is on, skipping target execution`); + if (logger().debugMode) { + debug(`Debug mode is on, skipping target execution`); return Promise.resolve('[DEBUG MODE] skipping execution'); } diff --git a/packages/utils/src/lib/set-env.spec.ts b/packages/utils/src/lib/set-env.spec.ts new file mode 100644 index 00000000..23d2f7bd --- /dev/null +++ b/packages/utils/src/lib/set-env.spec.ts @@ -0,0 +1,17 @@ +import { setInputsInEnv } from './set-env'; + +describe('set-env', () => { + it('should parse inputs and set the in process env', () => { + const inputs = { + test: 'test', + }; + + const _process: Partial = { + env: { prevVar: 'test' }, + }; + + setInputsInEnv(inputs, _process as typeof process); + + expect(_process.env).toEqual({ prevVar: 'test', INPUT_TEST: 'test' }); + }); +}); diff --git a/packages/utils/src/lib/set-env.ts b/packages/utils/src/lib/set-env.ts new file mode 100644 index 00000000..cf64b03a --- /dev/null +++ b/packages/utils/src/lib/set-env.ts @@ -0,0 +1,7 @@ +export function setInputsInEnv(inputs: Record, _process: typeof process): void { + for (const k in inputs) { + _process.env[`INPUT_${k.toUpperCase()}`] = inputs[k]; + } +} + +export default setInputsInEnv; diff --git a/tsconfig.base.json b/tsconfig.base.json index 9e9b67f9..f7bb1da6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,7 +15,7 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { - "@e-square/utils": ["packages/utils/src/index.ts"] + "@e-square/utils/*": ["packages/utils/src/lib/*"] } }, "exclude": ["node_modules", "tmp"]