diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a5377ae --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,34 @@ +name: Continuous Integration + +on: + pull_request: + branches: + - main + push: + branches: + - main + +permissions: + contents: read + +jobs: + test-action: + name: GitHub Actions Test + runs-on: local-test + + steps: + - name: Checkout + id: checkout + uses: https://gitea.joylink.club/actions/checkout@v4 + + - name: Test Local Action + id: test-action + uses: ./ + with: + version: 'v18.19.1' + cache: 'npm' + cache-dependency-path: 'package-lock.json' + + - name: Print Output + id: output + run: echo "${{ steps.test-action.outputs.version }}" diff --git a/.github/linters/.eslintrc.yml b/.github/linters/.eslintrc.yml index f452aba..23eb375 100644 --- a/.github/linters/.eslintrc.yml +++ b/.github/linters/.eslintrc.yml @@ -36,6 +36,9 @@ extends: rules: { + 'prefer-const': 'off', + 'no-shadow': 'off', + 'github/array-foreach': 'off', 'camelcase': 'off', 'eslint-comments/no-use': 'off', 'eslint-comments/no-unused-disable': 'off', diff --git a/action.yml b/action.yml index 101186a..dfd3877 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,7 @@ -name: 'The name of your action here' -description: 'Provide a description here' -author: 'Your name or organization here' +name: 'Setup Node.js env' +description: + 'Setup a Node.js environment by downloading and adding it to the PATH.' +author: 'walker-sheng' # Add your action's branding here. This will appear on the GitHub Marketplace. branding: @@ -9,16 +10,32 @@ branding: # Define your inputs here. inputs: - milliseconds: - description: 'Your input description here' + version: + description: + 'Version Spec of the version to use. Examples: 12.x, 10.15.1, >=10.15.0.' + cache: + description: + 'Used to specify a package manager for caching in the default directory. + Supported values: npm, yarn, pnpm.' required: true - default: '1000' + default: 'npm' + cache-dependency-path: + description: + 'Used to specify the path to a dependency file: package-lock.json, + yarn.lock, etc. Supports wildcards or a list of file names for caching + multiple dependencies.' + required: true + default: 'package-lock.json' # Define your outputs here. outputs: - time: - description: 'Your output description here' + cache-hit: + description: 'A boolean value to indicate if a cache was hit.' + version: + description: 'The installed node version.' runs: using: node20 main: dist/index.js + post: 'dist/cache-save/index.js' + post-if: success() diff --git a/dist/cache-save/index.js b/dist/cache-save/index.js new file mode 100644 index 0000000..934b1f8 Binary files /dev/null and b/dist/cache-save/index.js differ diff --git a/dist/index.js b/dist/index.js index 875f5c4..ebcabe7 100644 Binary files a/dist/index.js and b/dist/index.js differ diff --git a/dist/index.js.map b/dist/index.js.map deleted file mode 100644 index 4c7214d..0000000 Binary files a/dist/index.js.map and /dev/null differ diff --git a/dist/licenses.txt b/dist/licenses.txt deleted file mode 100644 index a8fa3a8..0000000 Binary files a/dist/licenses.txt and /dev/null differ diff --git a/dist/sourcemap-register.js b/dist/sourcemap-register.js deleted file mode 100644 index 466141d..0000000 Binary files a/dist/sourcemap-register.js and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 32726fe..97f55f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,12 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@actions/core": "^1.10.1" + "@actions/cache": "^3.2.4", + "@actions/core": "^1.10.1", + "@actions/glob": "^0.4.0", + "@actions/http-client": "^2.2.1", + "@actions/io": "^1.1.3", + "@actions/tool-cache": "^2.0.1" }, "devDependencies": { "@types/jest": "^29.5.12", @@ -42,6 +47,69 @@ "node": ">=0.10.0" } }, + "node_modules/@actions/cache": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@actions/cache/-/cache-3.2.4.tgz", + "integrity": "sha512-RuHnwfcDagtX+37s0ZWy7clbOfnZ7AlDJQ7k/9rzt2W4Gnwde3fa/qjSjVuz4vLcLIpc7fUob27CMrqiWZytYA==", + "dependencies": { + "@actions/core": "^1.10.0", + "@actions/exec": "^1.0.1", + "@actions/glob": "^0.1.0", + "@actions/http-client": "^2.1.1", + "@actions/io": "^1.0.1", + "@azure/abort-controller": "^1.1.0", + "@azure/ms-rest-js": "^2.6.0", + "@azure/storage-blob": "^12.13.0", + "semver": "^6.3.1", + "uuid": "^3.3.3" + } + }, + "node_modules/@actions/cache/node_modules/@actions/glob": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.1.2.tgz", + "integrity": "sha512-SclLR7Ia5sEqjkJTPs7Sd86maMDw43p769YxBOxvPvEWuPEhpAnBsQfENOpXjFYMmhCqd127bmf+YdvJqVqR4A==", + "dependencies": { + "@actions/core": "^1.2.6", + "minimatch": "^3.0.4" + } + }, + "node_modules/@actions/cache/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@actions/cache/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@actions/cache/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@actions/cache/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/@actions/core": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.1.tgz", @@ -51,15 +119,87 @@ "uuid": "^8.3.2" } }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/glob": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.4.0.tgz", + "integrity": "sha512-+eKIGFhsFa4EBwaf/GMyzCdWrXWymGXfFmZU3FHQvYS8mPcHtTtZONbkcqqUMzw9mJ/pImEBFET1JNifhqGsAQ==", + "dependencies": { + "@actions/core": "^1.9.1", + "minimatch": "^3.0.4" + } + }, + "node_modules/@actions/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@actions/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@actions/http-client": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz", - "integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.1.tgz", + "integrity": "sha512-KhC/cZsq7f8I4LfZSJKgCvEwfkE8o1538VoBeoGzokVLLnbFDEAdFD3UhoMklxo2un9NJVBdANOresx7vTHlHw==", "dependencies": { "tunnel": "^0.0.6", "undici": "^5.25.4" } }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, + "node_modules/@actions/tool-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-2.0.1.tgz", + "integrity": "sha512-iPU+mNwrbA8jodY8eyo/0S/QqCKDajiR8OxWTnSk/SnYg0sj8Hp4QcUEVC1YFpHWXtrfbQrE13Jz4k4HXJQKcA==", + "dependencies": { + "@actions/core": "^1.2.6", + "@actions/exec": "^1.0.0", + "@actions/http-client": "^2.0.1", + "@actions/io": "^1.1.1", + "semver": "^6.1.0", + "uuid": "^3.3.2" + } + }, + "node_modules/@actions/tool-cache/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@actions/tool-cache/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -73,6 +213,198 @@ "node": ">=6.0.0" } }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.0.tgz", + "integrity": "sha512-OuDVn9z2LjyYbpu6e7crEwSipa62jX7/ObV/pmXQfnOG8cHwm363jYtg3FSX3GB1V7jsIKri1zgq7mfXkFk/qw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.0.tgz", + "integrity": "sha512-SYtcG13aiV7znycu6plCClWUzD9BBtfnsbIxT89nkkRvQRB4n0kuZyJJvJ7hqdKOn7x7YoGKZ9lVStLJpLnOFw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-http": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz", + "integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/core-util": "^1.1.1", + "@azure/logger": "^1.0.0", + "@types/node-fetch": "^2.5.0", + "@types/tunnel": "^0.0.3", + "form-data": "^4.0.0", + "node-fetch": "^2.6.7", + "process": "^0.11.10", + "tslib": "^2.2.0", + "tunnel": "^0.0.6", + "uuid": "^8.3.0", + "xml2js": "^0.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@azure/core-http/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@azure/core-lro": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.7.0.tgz", + "integrity": "sha512-oj7d8vWEvOREIByH1+BnoiFwszzdE7OXUEd6UTv+cmx5HvjBBlkVezm3uZgpXWaxDj5ATL/k89+UMeGx1Ou9TQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.2.0", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-lro/node_modules/@azure/abort-controller": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.0.tgz", + "integrity": "sha512-SYtcG13aiV7znycu6plCClWUzD9BBtfnsbIxT89nkkRvQRB4n0kuZyJJvJ7hqdKOn7x7YoGKZ9lVStLJpLnOFw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-paging": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@azure/core-paging/-/core-paging-1.6.0.tgz", + "integrity": "sha512-W8eRv7MVFx/jbbYfcRT5+pGnZ9St/P1UvOi+63vxPwuQ3y+xj+wqWTGxpkXUETv3szsqGu0msdxVtjszCeB4zA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.0.0-preview.13", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.0-preview.13.tgz", + "integrity": "sha512-KxDlhXyMlh2Jhj2ykX6vNEU0Vou4nHr025KoSEiz7cS3BNiHNaZcdECk/DmLkEB0as5T7b/TpRcehJ5yV6NeXQ==", + "dependencies": { + "@opentelemetry/api": "^1.0.1", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.8.0.tgz", + "integrity": "sha512-w8NrGnrlGDF7fj36PBnJhGXDK2Y3kpTOgL7Ksb5snEHXq/3EAbKYOp1yqme0yWCUlSDq5rjqvxSBAJmsqYac3w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.0.tgz", + "integrity": "sha512-SYtcG13aiV7znycu6plCClWUzD9BBtfnsbIxT89nkkRvQRB4n0kuZyJJvJ7hqdKOn7x7YoGKZ9lVStLJpLnOFw==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.0.tgz", + "integrity": "sha512-BnfkfzVEsrgbVCtqq0RYRMePSH2lL/cgUUR5sYRF4yNN10zJZq/cODz0r89k3ykY83MqeM3twR292a3YBNgC3w==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/ms-rest-js": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@azure/ms-rest-js/-/ms-rest-js-2.7.0.tgz", + "integrity": "sha512-ngbzWbqF+NmztDOpLBVDxYM+XLcUj7nKhxGbSU9WtIsXfRB//cf2ZbAG5HkOrhU9/wd/ORRB6lM/d69RKVjiyA==", + "dependencies": { + "@azure/core-auth": "^1.1.4", + "abort-controller": "^3.0.0", + "form-data": "^2.5.0", + "node-fetch": "^2.6.7", + "tslib": "^1.10.0", + "tunnel": "0.0.6", + "uuid": "^8.3.2", + "xml2js": "^0.5.0" + } + }, + "node_modules/@azure/ms-rest-js/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/@azure/storage-blob": { + "version": "12.17.0", + "resolved": "https://registry.npmjs.org/@azure/storage-blob/-/storage-blob-12.17.0.tgz", + "integrity": "sha512-sM4vpsCpcCApagRW5UIjQNlNylo02my2opgp0Emi8x888hZUvJ3dN69Oq20cEGXkMUWnoCrBaB0zyS3yeB87sQ==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-http": "^3.0.0", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.1.1", + "@azure/core-tracing": "1.0.0-preview.13", + "@azure/logger": "^1.0.0", + "events": "^3.0.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", @@ -1350,6 +1682,14 @@ "node": ">= 8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -1486,11 +1826,32 @@ "version": "20.11.28", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.28.tgz", "integrity": "sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/@types/node-fetch/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -1503,6 +1864,14 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -1723,6 +2092,17 @@ "ncc": "dist/ncc/cli.js" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -1994,6 +2374,11 @@ "has-symbols": "^1.0.3" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2146,8 +2531,7 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/brace-expansion": { "version": "2.0.1", @@ -2380,6 +2764,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -2392,8 +2787,7 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/convert-source-map": { "version": "2.0.0", @@ -2522,6 +2916,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3496,6 +3898,22 @@ "node": ">=0.10.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3683,6 +4101,19 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5542,6 +5973,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5596,6 +6046,25 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6178,6 +6647,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -6441,6 +6918,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -6847,6 +7329,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", @@ -6938,8 +7425,7 @@ "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -7118,8 +7604,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/update-browserslist-db": { "version": "1.0.13", @@ -7215,6 +7700,20 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7342,6 +7841,26 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 9d2f69b..db4da43 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,11 @@ "format:write": "npx prettier --write .", "format:check": "npx prettier --check .", "lint": "npx eslint . -c ./.github/linters/.eslintrc.yml", - "package": "npx ncc build src/index.ts -o dist --source-map --license licenses.txt", + "package": "npx ncc build src/index.ts -o dist && npx ncc build -o dist/cache-save src/cache-save.ts", "package:watch": "npm run package -- --watch", "test": "npx jest", - "all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package" + "all": "npm run format:write && npm run lint && npm run package", + "all-all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package" }, "license": "MIT", "jest": { @@ -66,7 +67,12 @@ ] }, "dependencies": { - "@actions/core": "^1.10.1" + "@actions/cache": "^3.2.4", + "@actions/core": "^1.10.1", + "@actions/glob": "^0.4.0", + "@actions/http-client": "^2.2.1", + "@actions/io": "^1.1.3", + "@actions/tool-cache": "^2.0.1" }, "devDependencies": { "@types/jest": "^29.5.12", diff --git a/src/cache-restore.ts b/src/cache-restore.ts new file mode 100644 index 0000000..1ccf041 --- /dev/null +++ b/src/cache-restore.ts @@ -0,0 +1,86 @@ +import * as cache from '@actions/cache' +import * as core from '@actions/core' +import * as glob from '@actions/glob' +import path from 'path' +import fs from 'fs' + +import { State } from './constants' +import { + getCacheDirectories, + getPackageManagerInfo, + repoHasYarnBerryManagedDependencies, + PackageManagerInfo +} from './cache-utils' + +export const restoreCache = async ( + packageManager: string, + cacheDependencyPath: string +): Promise => { + const packageManagerInfo = await getPackageManagerInfo(packageManager) + if (!packageManagerInfo) { + throw new Error(`Caching for '${packageManager}' is not supported`) + } + const platform = process.env.RUNNER_OS + + const cachePaths = await getCacheDirectories( + packageManagerInfo, + cacheDependencyPath + ) + core.saveState(State.CachePaths, cachePaths) + const lockFilePath = cacheDependencyPath + ? cacheDependencyPath + : findLockFile(packageManagerInfo) + const fileHash = await glob.hashFiles(lockFilePath) + + if (!fileHash) { + throw new Error( + 'Some specified paths were not resolved, unable to cache dependencies.' + ) + } + + const keyPrefix = `node-cache-${platform}-${packageManager}` + const primaryKey = `${keyPrefix}-${fileHash}` + core.debug(`primary key is ${primaryKey}`) + + core.saveState(State.CachePrimaryKey, primaryKey) + + const isManagedByYarnBerry = await repoHasYarnBerryManagedDependencies( + packageManagerInfo, + cacheDependencyPath + ) + let cacheKey: string | undefined + if (isManagedByYarnBerry) { + core.info( + 'All dependencies are managed locally by yarn3, the previous cache can be used' + ) + cacheKey = await cache.restoreCache(cachePaths, primaryKey, [keyPrefix]) + } else { + cacheKey = await cache.restoreCache(cachePaths, primaryKey) + } + + core.setOutput('cache-hit', Boolean(cacheKey)) + + if (!cacheKey) { + core.info(`${packageManager} cache is not found`) + return + } + + core.saveState(State.CacheMatchedKey, cacheKey) + core.info(`Cache restored from key: ${cacheKey}`) +} + +const findLockFile = (packageManager: PackageManagerInfo): string => { + const lockFiles = packageManager.lockFilePatterns + const workspace = process.env.GITHUB_WORKSPACE! + + const rootContent = fs.readdirSync(workspace) + + const lockFile = lockFiles.find(item => rootContent.includes(item)) + if (!lockFile) { + throw new Error( + `Dependencies lock file is not found in ${workspace}. Supported file patterns: ${lockFiles.toString()}` + ) + } + + return path.join(workspace, lockFile) +} diff --git a/src/cache-save.ts b/src/cache-save.ts new file mode 100644 index 0000000..cc8ad6f --- /dev/null +++ b/src/cache-save.ts @@ -0,0 +1,71 @@ +import * as core from '@actions/core' +import * as cache from '@actions/cache' + +import { State } from './constants' +import { getPackageManagerInfo } from './cache-utils' + +// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in +// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to +// throw an uncaught exception. Instead of failing this action, just warn. +process.on('uncaughtException', e => { + const warningPrefix = '[warning]' + core.info(`${warningPrefix}${e.message}`) +}) + +// Added early exit to resolve issue with slow post action step: +export async function run(earlyExit?: boolean): Promise { + try { + const cacheLock = core.getState(State.CachePackageManager) + + if (cacheLock) { + await cachePackages(cacheLock) + + if (earlyExit) { + process.exit(0) + } + } else { + core.debug(`Caching for '${cacheLock}' is not supported`) + } + } catch (error) { + core.setFailed((error as Error).message) + } +} + +const cachePackages = async (packageManager: string): Promise => { + const state = core.getState(State.CacheMatchedKey) + const primaryKey = core.getState(State.CachePrimaryKey) + const cachePaths = JSON.parse( + core.getState(State.CachePaths) || '[]' + ) as string[] + + const packageManagerInfo = await getPackageManagerInfo(packageManager) + if (!packageManagerInfo) { + core.debug(`Caching for '${packageManager}' is not supported`) + return + } + + if (!cachePaths.length) { + // TODO: core.getInput has a bug - it can return undefined despite its definition (tests only?) + // export declare function getInput(name: string, options?: InputOptions): string; + const cacheDependencyPath = core.getInput('cache-dependency-path') || '' + throw new Error( + `Cache folder paths are not retrieved for ${packageManager} with cache-dependency-path = ${cacheDependencyPath}` + ) + } + + if (primaryKey === state) { + core.info( + `Cache hit occurred on the primary key ${primaryKey}, not saving cache.` + ) + return + } + + const cacheId = await cache.saveCache(cachePaths, primaryKey) + if (cacheId === -1) { + return + } + + core.info(`Cache saved with the key: ${primaryKey}`) +} + +run(true) diff --git a/src/cache-utils.ts b/src/cache-utils.ts new file mode 100644 index 0000000..151c428 --- /dev/null +++ b/src/cache-utils.ts @@ -0,0 +1,316 @@ +import * as core from '@actions/core' +import * as exec from '@actions/exec' +import * as cache from '@actions/cache' +import * as glob from '@actions/glob' +import path from 'path' +import fs from 'fs' +import { unique } from './util' + +export interface PackageManagerInfo { + name: string + lockFilePatterns: string[] + getCacheFolderPath: (projectDir?: string) => Promise +} + +interface SupportedPackageManagers { + npm: PackageManagerInfo + pnpm: PackageManagerInfo + yarn: PackageManagerInfo +} +export const supportedPackageManagers: SupportedPackageManagers = { + npm: { + name: 'npm', + lockFilePatterns: ['package-lock.json', 'npm-shrinkwrap.json', 'yarn.lock'], + getCacheFolderPath: async () => + getCommandOutputNotEmpty( + 'npm config get cache', + 'Could not get npm cache folder path' + ) + }, + pnpm: { + name: 'pnpm', + lockFilePatterns: ['pnpm-lock.yaml'], + getCacheFolderPath: async () => + getCommandOutputNotEmpty( + 'pnpm store path --silent', + 'Could not get pnpm cache folder path' + ) + }, + yarn: { + name: 'yarn', + lockFilePatterns: ['yarn.lock'], + getCacheFolderPath: async projectDir => { + const yarnVersion = await getCommandOutputNotEmpty( + `yarn --version`, + 'Could not retrieve version of yarn', + projectDir + ) + + core.debug( + `Consumed yarn version is ${yarnVersion} (working dir: "${ + projectDir || '' + }")` + ) + + const stdOut = yarnVersion.startsWith('1.') + ? await getCommandOutput('yarn cache dir', projectDir) + : await getCommandOutput('yarn config get cacheFolder', projectDir) + + if (!stdOut) { + throw new Error( + `Could not get yarn cache folder path for ${projectDir}` + ) + } + return stdOut + } + } +} + +export const getCommandOutput = async ( + toolCommand: string, + cwd?: string +): Promise => { + let { stdout, stderr, exitCode } = await exec.getExecOutput( + toolCommand, + undefined, + { ignoreReturnCode: true, ...(cwd && { cwd }) } + ) + + if (exitCode) { + stderr = !stderr.trim() + ? `The '${toolCommand}' command failed with exit code: ${exitCode}` + : stderr + throw new Error(stderr) + } + + return stdout.trim() +} + +export const getCommandOutputNotEmpty = async ( + toolCommand: string, + error: string, + cwd?: string +): Promise => { + const stdOut = getCommandOutput(toolCommand, cwd) + if (!stdOut) { + throw new Error(error) + } + return stdOut +} + +export const getPackageManagerInfo = async ( + packageManager: string +): Promise => { + if (packageManager === 'npm') { + return supportedPackageManagers.npm + } else if (packageManager === 'pnpm') { + return supportedPackageManagers.pnpm + } else if (packageManager === 'yarn') { + return supportedPackageManagers.yarn + } else { + return null + } +} + +/** + * getProjectDirectoriesFromCacheDependencyPath is called twice during `restoreCache` + * - first through `getCacheDirectories` + * - second from `repoHasYarn3ManagedCache` + * + * it contains expensive IO operation and thus should be memoized + */ + +let projectDirectoriesMemoized: string[] | null = null +/** + * unit test must reset memoized variables + */ +export const resetProjectDirectoriesMemoized = (): null => + (projectDirectoriesMemoized = null) +/** + * Expands (converts) the string input `cache-dependency-path` to list of directories that + * may be project roots + * @param cacheDependencyPath - either a single string or multiline string with possible glob patterns + * expected to be the result of `core.getInput('cache-dependency-path')` + * @return list of directories and possible + */ +const getProjectDirectoriesFromCacheDependencyPath = async ( + cacheDependencyPath: string +): Promise => { + if (projectDirectoriesMemoized !== null) { + return projectDirectoriesMemoized + } + + const globber = await glob.create(cacheDependencyPath) + const cacheDependenciesPaths = await globber.glob() + + const existingDirectories: string[] = cacheDependenciesPaths + .map(dirName => path.dirname(dirName)) + .filter(unique()) + .map(dirName => fs.realpathSync(dirName)) + .filter(directory => fs.lstatSync(directory).isDirectory()) + + if (!existingDirectories.length) + core.warning( + `No existing directories found containing cache-dependency-path="${cacheDependencyPath}"` + ) + + projectDirectoriesMemoized = existingDirectories + return existingDirectories +} + +/** + * Finds the cache directories configured for the repo if cache-dependency-path is not empty + * @param packageManagerInfo - an object having getCacheFolderPath method specific to given PM + * @param cacheDependencyPath - either a single string or multiline string with possible glob patterns + * expected to be the result of `core.getInput('cache-dependency-path')` + * @return list of files on which the cache depends + */ +const getCacheDirectoriesFromCacheDependencyPath = async ( + packageManagerInfo: PackageManagerInfo, + cacheDependencyPath: string +): Promise => { + const projectDirectories = + await getProjectDirectoriesFromCacheDependencyPath(cacheDependencyPath) + const cacheFoldersPaths = await Promise.all( + projectDirectories.map(async projectDirectory => { + const cacheFolderPath = + await packageManagerInfo.getCacheFolderPath(projectDirectory) + core.debug( + `${packageManagerInfo.name}'s cache folder "${cacheFolderPath}" configured for the directory "${projectDirectory}"` + ) + return cacheFolderPath + }) + ) + // uniq in order to do not cache the same directories twice + return cacheFoldersPaths.filter(unique()) +} + +/** + * Finds the cache directories configured for the repo ignoring cache-dependency-path + * @param packageManagerInfo - an object having getCacheFolderPath method specific to given PM + * @return list of files on which the cache depends + */ +const getCacheDirectoriesForRootProject = async ( + packageManagerInfo: PackageManagerInfo +): Promise => { + const cacheFolderPath = await packageManagerInfo.getCacheFolderPath() + core.debug( + `${packageManagerInfo.name}'s cache folder "${cacheFolderPath}" configured for the root directory` + ) + return [cacheFolderPath] +} + +/** + * A function to find the cache directories configured for the repo + * currently it handles only the case of PM=yarn && cacheDependencyPath is not empty + * @param packageManagerInfo - an object having getCacheFolderPath method specific to given PM + * @param cacheDependencyPath - either a single string or multiline string with possible glob patterns + * expected to be the result of `core.getInput('cache-dependency-path')` + * @return list of files on which the cache depends + */ +export const getCacheDirectories = async ( + packageManagerInfo: PackageManagerInfo, + cacheDependencyPath: string +): Promise => { + // For yarn, if cacheDependencyPath is set, ask information about cache folders in each project + // folder satisfied by cacheDependencyPath https://github.com/actions/setup-node/issues/488 + if (packageManagerInfo.name === 'yarn' && cacheDependencyPath) { + return getCacheDirectoriesFromCacheDependencyPath( + packageManagerInfo, + cacheDependencyPath + ) + } + return getCacheDirectoriesForRootProject(packageManagerInfo) +} + +/** + * A function to check if the directory is a yarn project configured to manage + * obsolete dependencies in the local cache + * @param directory - a path to the folder + * @return - true if the directory's project is yarn managed + * - if there's .yarn/cache folder do not mess with the dependencies kept in the repo, return false + * - global cache is not managed by yarn @see https://yarnpkg.com/features/offline-cache, return false + * - if local cache is not explicitly enabled (not yarn3), return false + * - return true otherwise + */ +const projectHasYarnBerryManagedDependencies = async ( + directory: string +): Promise => { + const workDir = directory || process.env.GITHUB_WORKSPACE || '.' + core.debug(`check if "${workDir}" has locally managed yarn3 dependencies`) + + // if .yarn/cache directory exists the cache is managed by version control system + const yarnCacheFile = path.join(workDir, '.yarn', 'cache') + if ( + fs.existsSync(yarnCacheFile) && + fs.lstatSync(yarnCacheFile).isDirectory() + ) { + core.debug( + `"${workDir}" has .yarn/cache - dependencies are kept in the repository` + ) + return Promise.resolve(false) + } + + // NOTE: yarn1 returns 'undefined' with return code = 0 + const enableGlobalCache = await getCommandOutput( + 'yarn config get enableGlobalCache', + workDir + ) + // only local cache is not managed by yarn + const managed = enableGlobalCache.includes('false') + if (managed) { + core.debug(`"${workDir}" dependencies are managed by yarn 3 locally`) + return true + } else { + core.debug(`"${workDir}" dependencies are not managed by yarn 3 locally`) + return false + } +} + +/** + * A function to report the repo contains Yarn managed projects + * @param packageManagerInfo - used to make sure current package manager is yarn + * @param cacheDependencyPath - either a single string or multiline string with possible glob patterns + * expected to be the result of `core.getInput('cache-dependency-path')` + * @return - true if all project directories configured to be Yarn managed + */ +export const repoHasYarnBerryManagedDependencies = async ( + packageManagerInfo: PackageManagerInfo, + cacheDependencyPath: string +): Promise => { + if (packageManagerInfo.name !== 'yarn') return false + + const yarnDirs = cacheDependencyPath + ? await getProjectDirectoriesFromCacheDependencyPath(cacheDependencyPath) + : [''] + + const isManagedList = await Promise.all( + yarnDirs.map(projectHasYarnBerryManagedDependencies) + ) + + return isManagedList.every(Boolean) +} + +export function isGhes(): boolean { + const ghUrl = new URL( + process.env['GITHUB_SERVER_URL'] || 'https://github.com' + ) + return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM' +} + +export function isCacheFeatureAvailable(): boolean { + if (cache.isFeatureAvailable()) return true + + if (isGhes()) { + core.warning( + 'Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.' + ) + return false + } + + core.warning( + 'The runner was not able to contact the cache service. Caching will be skipped' + ) + + return false +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..1a31827 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,16 @@ +export enum LockType { + Npm = 'npm', + Pnpm = 'pnpm', + Yarn = 'yarn' +} + +export enum State { + CachePackageManager = 'SETUP_NODE_CACHE_PACKAGE_MANAGER', + CachePrimaryKey = 'CACHE_KEY', + CacheMatchedKey = 'CACHE_RESULT', + CachePaths = 'CACHE_PATHS' +} + +export enum Outputs { + CacheHit = 'cache-hit' +} diff --git a/src/install.ts b/src/install.ts new file mode 100644 index 0000000..b6e9c7f --- /dev/null +++ b/src/install.ts @@ -0,0 +1,47 @@ +import * as core from '@actions/core' +import * as tc from '@actions/tool-cache' +import os from 'os' + +const BinBaseUrl = 'https://nodejs.org/dist/' +const DestDir = '/denv' + +export function getFileName(version: string, arch = os.arch()): string { + return `node-${version}-${os.platform()}-${arch}.tar.gz` +} + +export function getRootPath(version: string): string { + return `${DestDir}/node-${version}` +} + +export async function getInstalledPath( + version: string, + fileName: string +): Promise { + const downloadUrl = `${BinBaseUrl}${version}/${fileName}` + core.info(`Downloading from ${downloadUrl}`) + const downloadPath = await tc.downloadTool(downloadUrl) + core.info(`Extracting from ${downloadPath}`) + const extPath = await extractArchive(downloadPath, version) + core.info(`Successfully extracted to ${extPath}`) + return extPath +} + +async function extractArchive( + archivePath: string, + version: string +): Promise { + const platform = os.platform() + let extPath: string + + if (platform === 'win32') { + extPath = await tc.extractZip(archivePath, getRootPath(version)) + } else { + extPath = await tc.extractTar(archivePath, getRootPath(version), [ + 'xz', + '--strip', + '1' + ]) + } + + return extPath +} diff --git a/src/main.ts b/src/main.ts index c804f90..b78a65e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,12 @@ import * as core from '@actions/core' -import { wait } from './wait' +import * as io from '@actions/io' +import * as ac from '@actions/cache' +import os from 'os' +import path from 'path' +import cp from 'child_process' +import { getFileName, getRootPath, getInstalledPath } from './install' +import { restoreCache } from './cache-restore' +import { State } from './constants' /** * The main function for the action. @@ -7,20 +14,59 @@ import { wait } from './wait' */ export async function run(): Promise { try { - const ms: string = core.getInput('milliseconds') - + // 获取输入的version + const version: string = resolveVersionInput() // Debug logs are only output if the `ACTIONS_STEP_DEBUG` secret is true - core.debug(`Waiting ${ms} milliseconds ...`) + core.debug(`The input version is: ${version}`) + // 获取输入的architecture + let arch = core.getInput('architecture') + if (!arch) { + arch = os.arch() + } + if (arch === 'x64') { + arch = 'amd64' + } + // 构建文件名 + const dlgfName = getFileName(version, arch) + let installedPath = getRootPath(version) + // 尝试从缓存中恢复 + const rcr = await ac.restoreCache([installedPath], dlgfName) + if (!rcr) { + core.info(`Cache not found for input key: ${dlgfName}`) + // 缓存中没有找到,下载解压安装 + installedPath = await getInstalledPath(version, dlgfName) + core.info(`Success Installed to ${installedPath}`) + // 保存到缓存 + await ac.saveCache([installedPath], dlgfName) + } - // Log the current timestamp, wait, then log the new timestamp - core.debug(new Date().toTimeString()) - await wait(parseInt(ms, 10)) - core.debug(new Date().toTimeString()) + // 将bin目录添加到PATH + const binPath = path.join(installedPath, 'bin') + core.addPath(binPath) + // 测试输出版本 + const bp = await io.which('node') + const execVersion = (cp.execSync(`${bp} -v`) || '').toString() + core.info(`version cmd result is: ${execVersion}`) - // Set outputs for other workflow steps to use - core.setOutput('time', new Date().toTimeString()) + // 尝试从缓存中恢复依赖包 + const cache = core.getInput('cache') + core.saveState(State.CachePackageManager, cache) + const cacheDependencyPath = core.getInput('cache-dependency-path') + await restoreCache(cache, cacheDependencyPath) + + // 设置输出参数 + core.setOutput('version', execVersion) } catch (error) { // Fail the workflow run if an error occurs if (error instanceof Error) core.setFailed(error.message) } } + +function resolveVersionInput(): string { + const version = core.getInput('version') + + if (version) { + return version + } + throw new Error(`没有指定版本`) +} diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..1981577 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,116 @@ +import * as core from '@actions/core' +import * as exec from '@actions/exec' +import * as io from '@actions/io' + +import fs from 'fs' +import path from 'path' + +export function getNodeVersionFromFile(versionFilePath: string): string | null { + if (!fs.existsSync(versionFilePath)) { + throw new Error( + `The specified node version file at: ${versionFilePath} does not exist` + ) + } + + const contents = fs.readFileSync(versionFilePath, 'utf8') + + // Try parsing the file as an NPM `package.json` file. + try { + const manifest = JSON.parse(contents) + + // Presume package.json file. + if (typeof manifest === 'object' && !!manifest) { + // Support Volta. + // See https://docs.volta.sh/guide/understanding#managing-your-project + if (manifest.volta?.node) { + return manifest.volta.node + } + + if (manifest.engines?.node) { + return manifest.engines.node + } + + // Support Volta workspaces. + // See https://docs.volta.sh/advanced/workspaces + if (manifest.volta?.extends) { + const extendedFilePath = path.resolve( + path.dirname(versionFilePath), + manifest.volta.extends + ) + core.info(`Resolving node version from ${extendedFilePath}`) + return getNodeVersionFromFile(extendedFilePath) + } + + // If contents are an object, we parsed JSON + // this can happen if node-version-file is a package.json + // yet contains no volta.node or engines.node + // + // If node-version file is _not_ JSON, control flow + // will not have reached these lines. + // + // And because we've reached here, we know the contents + // *are* JSON, so no further string parsing makes sense. + return null + } + } catch { + core.info('Node version file is not JSON file') + } + + const found = contents.match(/^(?:node(js)?\s+)?v?(?[^\s]+)$/m) + return found?.groups?.version ?? contents.trim() +} + +export async function printEnvDetailsAndSetOutput(): Promise { + core.startGroup('Environment details') + const promises = ['node', 'npm', 'yarn'].map(async tool => { + const pathTool = await io.which(tool, false) + const output = pathTool ? await getToolVersion(tool, ['--version']) : '' + + return { tool, output } + }) + + const tools = await Promise.all(promises) + tools.forEach(({ tool, output }) => { + if (tool === 'node') { + core.setOutput(`${tool}-version`, output) + } + core.info(`${tool}: ${output}`) + }) + + core.endGroup() +} + +async function getToolVersion( + tool: string, + options: string[] +): Promise { + try { + const { stdout, stderr, exitCode } = await exec.getExecOutput( + tool, + options, + { + ignoreReturnCode: true, + silent: true + } + ) + + if (exitCode > 0) { + core.info(`[warning]${stderr}`) + return '' + } + + return stdout.trim() + } catch (err) { + return '' + } +} + +type uniqueFn = (value: unknown) => boolean +export const unique = (): uniqueFn => { + const encountered = new Set() + return (value: unknown): boolean => { + if (encountered.has(value)) return false + encountered.add(value) + return true + } +} diff --git a/src/wait.ts b/src/wait.ts deleted file mode 100644 index 0ddf692..0000000 --- a/src/wait.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Wait for a number of milliseconds. - * @param milliseconds The number of milliseconds to wait. - * @returns {Promise} Resolves with 'done!' after the wait is over. - */ -export async function wait(milliseconds: number): Promise { - return new Promise(resolve => { - if (isNaN(milliseconds)) { - throw new Error('milliseconds not a number') - } - - setTimeout(() => resolve('done!'), milliseconds) - }) -}