diff --git a/.github/workflows/autogen/.eslintrc.yml b/.github/workflows/autogen/.eslintrc.yml new file mode 100644 index 000000000..4bad4ebe4 --- /dev/null +++ b/.github/workflows/autogen/.eslintrc.yml @@ -0,0 +1,14 @@ +env: + browser: true + es2021: true +extends: standard +parserOptions: + ecmaVersion: latest + sourceType: module +overrides: + - files: ["*.mjs"] + parserOptions: + ecmaVersion: latest + sourceType: module +rules: + "no-template-curly-in-string": "off" diff --git a/.github/workflows/autogen/README.md b/.github/workflows/autogen/README.md new file mode 100644 index 000000000..c75fe5a1b --- /dev/null +++ b/.github/workflows/autogen/README.md @@ -0,0 +1,42 @@ +# Solo autogen tool + +## Description + +The Solo autogen tool is used to add e2e test cases that need to be ran independently as their own job into the GitHub workflows and into the solo package.json + +## Usage + +from solo root directory: +```bash +cd .github/workflows/autogen +npm install +npm run autogen +``` + +Use git to detect file changes and validate that they are correct. + +The templates need to be maintained, you can either make changes directly to the templates and then run the tool, or make changes in both the workflow yaml files and the templates. Should the templates fall out of sync, then you can update the templates so that when autogen runs again, the git diff will better match. +```bash +template.flow-build-application.yaml +template.flow-pull-request-checks.yaml +template.zxc-code-analysis.yaml +template.zxc-env-vars.yaml + ``` +For new e2e test jobs update the `/.github/workflows/templates/config.yaml`, adding a new item to the tests object with a name and jestPostfix attribute. + +NOTE: IntelliJ copy/paste will alter the escape sequences, you might have to manually type it in, clone a line, or use an external text editor. + +e.g.: +```yaml + - name: Mirror Node + jestPostfix: --testRegex=\".*\\/e2e\\/commands\\/mirror_node\\.test\\.mjs\" + +``` + +## Development + +To run lint fix: +```bash +cd .github/workflows/autogen +eslint --fix . +``` diff --git a/.github/workflows/autogen/autogen.mjs b/.github/workflows/autogen/autogen.mjs new file mode 100755 index 000000000..a16d7818c --- /dev/null +++ b/.github/workflows/autogen/autogen.mjs @@ -0,0 +1,20 @@ +#!/usr/bin/env node +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +import * as fnm from './src/index.mjs' + +fnm.main(process.argv) diff --git a/.github/workflows/autogen/package-lock.json b/.github/workflows/autogen/package-lock.json new file mode 100644 index 000000000..7d5d3e2a2 --- /dev/null +++ b/.github/workflows/autogen/package-lock.json @@ -0,0 +1,1077 @@ +{ + "name": "add-e2e-test", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "add-e2e-test", + "version": "0.0.1", + "license": "Apache2.0", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "change-case": "^5.4.4", + "js-yaml": "^4.1.0" + }, + "bin": { + "autogen": "autogen.mjs" + }, + "devDependencies": { + "eslint": "^9.10.0" + }, + "engines": { + "node": ">=20.14.0", + "npm": ">=9.8.1" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz", + "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz", + "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/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, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "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 + }, + "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==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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 + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz", + "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.10.0", + "@eslint/plugin-kit": "^0.1.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "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" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/.github/workflows/autogen/package.json b/.github/workflows/autogen/package.json new file mode 100644 index 000000000..00b1ae76a --- /dev/null +++ b/.github/workflows/autogen/package.json @@ -0,0 +1,32 @@ +{ + "name": "add-e2e-test", + "version": "0.0.1", + "description": "uses templates to add e2e tests into the workflows and package.json", + "main": "src/index.mjs", + "type": "module", + "bin": { + "autogen": "autogen.mjs" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "autogen": "NODE_OPTIONS=--experimental-vm-modules node --no-deprecation autogen.mjs" + }, + "author": "Swirlds Labs", + "license": "Apache2.0", + "os": [ + "darwin", + "linux", + "win32" + ], + "engines": { + "node": ">=20.14.0", + "npm": ">=9.8.1" + }, + "devDependencies": { + "eslint": "^9.10.0" + }, + "dependencies": { + "change-case": "^5.4.4", + "js-yaml": "^4.1.0" + } +} diff --git a/.github/workflows/autogen/src/index.mjs b/.github/workflows/autogen/src/index.mjs new file mode 100644 index 000000000..8f2ebcd42 --- /dev/null +++ b/.github/workflows/autogen/src/index.mjs @@ -0,0 +1,339 @@ +/** + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the ""License""); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an ""AS IS"" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +'use strict' +import * as yaml from 'js-yaml' +import * as fs from 'node:fs' +import * as path from 'node:path' +import { fileURLToPath } from 'url' +import * as changeCase from 'change-case' + +export const AUTOGENERATE_E2E_TEST_JOBS = '# {AUTOGENERATE-E2E-TEST-JOBS}' +export const AUTOGENERATE_E2E_TEST_JOBS_2 = '# {AUTOGENERATE-E2E-TEST-JOBS-2}' +export const AUTOGENERATE_NEEDS = '# {AUTOGENERATE-NEEDS}' +export const AUTOGENERATE_WITH_SUBDIR = '# {AUTOGENERATE-WITH-SUBDIR}' +export const AUTOGENERATE_WITH_COVERAGE_REPORT = '# {AUTOGENERATE-WITH-COVERAGE-REPORT}' +export const AUTOGENERATE_JOB_OUTPUTS_SUB_DIRS = '# {AUTOGENERATE-JOB-OUTPUTS-SUB-DIRS}' +export const AUTOGENERATE_JOB_OUTPUTS_COVERAGE_REPORTS = '# {AUTOGENERATE-JOB-OUTPUTS-COVERAGE-REPORTS}' +export const AUTOGENERATE_WORKFLOW_OUTPUTS_SUB_DIRS = '# {AUTOGENERATE-WORKFLOW-OUTPUTS-SUB-DIRS}' +export const AUTOGENERATE_WORKFLOW_OUTPUTS_COVERAGE_REPORTS = '# {AUTOGENERATE-WORKFLOW-OUTPUTS-COVERAGE-REPORTS}' +export const AUTOGENERATE_INPUTS_SUB_DIRS = '# {AUTOGENERATE-INPUTS-SUB-DIRS}' +export const AUTOGENERATE_INPUTS_COVERAGE_REPORTS = '# {AUTOGENERATE-INPUTS-COVERAGE-REPORTS}' +export const AUTOGENERATE_DOWNLOAD_JOBS = '# {AUTOGENERATE-DOWNLOAD-JOBS}' + +/** + * @typedef {Object} Test + * @property {string} name + * @property {string} jestPostfix + */ + +/** + * @typedef {Object} Config + * @property {string} downloadArtifactAction + * @property {string} downloadArtifactActionComment + * @property {Test[]} tests + */ + +export function main () { + console.log('Begin autogen...') + + const __filename = fileURLToPath(import.meta.url) // get the resolved path to the file + const __dirname = path.dirname(__filename) // get the name of the directory + const outputDir = path.dirname(path.dirname(__dirname)) + const templateDir = path.join(outputDir, 'templates') + const configFile = path.join(templateDir, 'config.yaml') + const configData = fs.readFileSync(configFile, 'utf8') + const config = /** @type {Config} **/ yaml.load(configData) + + // generate the workflows with changes + buildWorkflows(outputDir, templateDir, config) + + // update the Solo package.json with changes + updatePackageJson(outputDir, config) + + console.log('...end autogen') +} + +/** + * Updates the Solo package.json by auto-generating the e2e test scripts based on + * the values in the config + * @param {string} outputDir + * @param {Config} config + */ +function updatePackageJson (outputDir, config) { + const packageJsonDir = path.dirname(path.dirname(outputDir)) + const packageJsonFile = path.join(packageJsonDir, 'package.json') + const inputData = fs.readFileSync(packageJsonFile, 'utf8') + const inputLines = inputData.split('\n') + const outputLines = [] + const generatedLines = [] + const firstMarker = '"test-e2e-all":' + const secondMarker = '"merge-clean":' + let skipNext = false + + inputLines.forEach(line => { + if (line.includes(firstMarker)) { + outputLines.push(line) + skipNext = true + const spacePrefix = line.substring(0, line.indexOf('"test-e2e')) + + config.tests.forEach(test => { + const formalNounName = test.name + const kebabCase = changeCase.kebabCase(formalNounName) + generatedLines.push(`${spacePrefix}"test-e2e-${kebabCase}": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E ${formalNounName} Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-${kebabCase}.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-${kebabCase}' ${test.jestPostfix}",`) + }) + + outputLines.push(...generatedLines) + } else if (line.includes(secondMarker)) { + outputLines.push(line) + skipNext = false + } else if (skipNext) { + // do nothing, we generate these lines after we see the firstMarker + } else { + outputLines.push(line) + } + }) + console.log(`outputFile: ${packageJsonFile}`) + fs.writeFileSync(packageJsonFile, outputLines.join('\n')) +} + +/** + * Autogenerate the GitHub workflows files with the entries needed to add the + * E2E test jobs + * @param {string} outputDir + * @param {string} templateDir + * @param {Config} config + */ +function buildWorkflows (outputDir, templateDir, config) { + const templates = [] + fs.readdirSync(templateDir).forEach(file => { + if (file.substring(0, 'template'.length) === 'template') { + templates.push(file) + } + }) + + templates.forEach(template => { + const templateFile = path.join(templateDir, template) + const templateData = fs.readFileSync(templateFile, 'utf8') + const templateLines = templateData.split('\n') + const outputFile = path.join(outputDir, template.substring('template.'.length)) + const outputLines = [] + console.log(`outputFile: ${outputFile}`) + + templateLines.forEach(line => { + const trimmedLine = line.trim() + + switch (trimmedLine) { + case AUTOGENERATE_E2E_TEST_JOBS: + case AUTOGENERATE_E2E_TEST_JOBS_2: + case AUTOGENERATE_WORKFLOW_OUTPUTS_SUB_DIRS: + case AUTOGENERATE_WORKFLOW_OUTPUTS_COVERAGE_REPORTS: + case AUTOGENERATE_INPUTS_SUB_DIRS: + case AUTOGENERATE_INPUTS_COVERAGE_REPORTS: + autogenerateYaml(line, config, outputLines, trimmedLine) + break + case AUTOGENERATE_NEEDS: + case AUTOGENERATE_WITH_SUBDIR: + case AUTOGENERATE_WITH_COVERAGE_REPORT: + case AUTOGENERATE_JOB_OUTPUTS_SUB_DIRS: + case AUTOGENERATE_JOB_OUTPUTS_COVERAGE_REPORTS: + autogenerateLine(line, config, outputLines, trimmedLine) + break + case AUTOGENERATE_DOWNLOAD_JOBS: + autogenerateLine(line, config, outputLines, trimmedLine) + outputLines.pop() // remove the extra new line character + break + default: + outputLines.push(line) + } + }) + + fs.writeFileSync(outputFile, outputLines.join('\n')) + }) +} + +/** + * Generates the YAML for the provided templateKey + * @param {string} line + * @param {Config} config + * @param {string[]} outputLines + * @param {string} templateKey + */ +export function autogenerateYaml (line, config, outputLines, templateKey) { + const spacePrefix = line.substring(0, + line.indexOf(templateKey)) + let suppressEmptyLines = false + + config.tests.forEach(test => { + const outputYaml = {} + + switch (templateKey) { + case AUTOGENERATE_E2E_TEST_JOBS: + case AUTOGENERATE_E2E_TEST_JOBS_2: + generateTestJobs(test, templateKey, outputYaml) + break + default: + generateOutputs(test, templateKey, outputYaml) + suppressEmptyLines = true + } + + const yamlLines = yaml.dump(outputYaml, { lineWidth: -1, quotingType: '"' }).split('\n') + + yamlLines.forEach(function (line) { + line = line.replaceAll('¡', '"') + if (/^\s*$/.test(line)) { + if (!suppressEmptyLines) { + outputLines.push(line) + } + } else { + outputLines.push(`${spacePrefix}${line}`) + } + }) + }) + + if (!suppressEmptyLines) { + outputLines.pop() // remove the extra new line character + } +} + +/** + * Generates the output lines for the provided templateKey + * @param {Test} test + * @param {string} templateKey + * @param {Object} outputYaml + */ +export function generateOutputs (test, templateKey, outputYaml) { + const formalNounName = test.name + const kebabCase = changeCase.kebabCase(formalNounName) + const snakeCase = changeCase.snakeCase(formalNounName) + let outputKey + const outputValue = {} + + switch (templateKey) { + case AUTOGENERATE_WORKFLOW_OUTPUTS_SUB_DIRS: + outputKey = `e2e-${kebabCase}-test-subdir` + outputValue.description = `¡E2E ${formalNounName} Test Subdirectory¡` + outputValue.value = '${{ jobs.env-vars.outputs.e2e_' + snakeCase + '_test_subdir }}' + break + case AUTOGENERATE_WORKFLOW_OUTPUTS_COVERAGE_REPORTS: + outputKey = `e2e-${kebabCase}-coverage-report` + outputValue.description = `¡E2E ${formalNounName} Tests Coverage Report¡` + outputValue.value = '${{ jobs.env-vars.outputs.e2e_' + snakeCase + + '_coverage_report }}' + break + case AUTOGENERATE_INPUTS_SUB_DIRS: + outputKey = `e2e-${kebabCase}-test-subdir` + outputValue.description = `¡E2E ${formalNounName} Test Subdirectory:¡` + outputValue.type = 'string' + outputValue.required = false + outputValue.default = `¡e2e-${kebabCase}¡` + break + case AUTOGENERATE_INPUTS_COVERAGE_REPORTS: + outputKey = `e2e-${kebabCase}-coverage-report` + outputValue.description = `¡E2E ${formalNounName} Coverage Report:¡` + outputValue.type = 'string' + outputValue.required = false + outputValue.default = `¡E2E ${formalNounName} Tests Coverage Report¡` + } + + outputYaml[outputKey] = outputValue +} + +/** + * Generates the test jobs for the provided templateKey + * @param {Test} test + * @param {string} templateKey + * @param {Object} outputYaml + */ +export function generateTestJobs (test, templateKey, outputYaml) { + const formalNounName = test.name + const kebabCase = changeCase.kebabCase(formalNounName) + const testJobKey = `e2e-${kebabCase}-tests` + const testJobValue = {} + testJobValue.name = 'E2E Tests' + + if (templateKey === AUTOGENERATE_E2E_TEST_JOBS) { + testJobValue.if = '${{ github.event_name == \'push\' || github.event.inputs.enable-e2e-tests == \'true\' }}' + } else { + testJobValue.if = '${{ !cancelled() && always() }}' + } + + testJobValue.uses = './.github/workflows/zxc-e2e-test.yaml' + testJobValue.needs = ['env-vars', 'code-style'] + testJobValue.with = { + 'custom-job-label': formalNounName, + 'npm-test-script': 'test-${{ needs.env-vars.outputs.e2e-' + + kebabCase + '-test-subdir }}', + 'coverage-subdirectory': '${{ needs.env-vars.outputs.e2e-' + + kebabCase + '-test-subdir }}', + 'coverage-report-name': '${{ needs.env-vars.outputs.e2e-' + + kebabCase + '-coverage-report }}' + } + + outputYaml[testJobKey] = testJobValue +} + +/** + * Generates the output line for the provided templateKey + * @param {string} line + * @param {Config} config + * @param {string[]} outputLines + * @param {string} templateKey + */ +export function autogenerateLine (line, config, outputLines, templateKey) { + const spacePrefix = line.substring(0, + line.indexOf(templateKey)) + + config.tests.forEach(test => { + const formalNounName = test.name + const kebabCase = changeCase.kebabCase(formalNounName) + const snakeCase = changeCase.snakeCase(formalNounName) + let namePart + let namePart2 + + switch (templateKey) { + case AUTOGENERATE_WITH_SUBDIR: + namePart = `e2e-${kebabCase}-test` + outputLines.push(spacePrefix + namePart + '-subdir: ${{ needs.env-vars.outputs.' + namePart + '-subdir }}') + break + case AUTOGENERATE_WITH_COVERAGE_REPORT: + namePart = `e2e-${kebabCase}` + outputLines.push(spacePrefix + namePart + '-coverage-report: ${{ needs.env-vars.outputs.' + namePart + '-coverage-report }}') + break + case AUTOGENERATE_NEEDS: + namePart = `e2e-${kebabCase}-tests` + outputLines.push(`${spacePrefix}- ${namePart}`) + break + case AUTOGENERATE_JOB_OUTPUTS_SUB_DIRS: + namePart = `e2e_${snakeCase}_test_subdir` + namePart2 = `e2e-${kebabCase}` + outputLines.push(`${spacePrefix}${namePart}: ${namePart2}`) + break + case AUTOGENERATE_JOB_OUTPUTS_COVERAGE_REPORTS: + namePart = `e2e_${snakeCase}_coverage_report` + outputLines.push(`${spacePrefix}${namePart}: "E2E ${formalNounName} Tests Coverage Report"`) + break + case AUTOGENERATE_DOWNLOAD_JOBS: + outputLines.push(`${spacePrefix}- name: Download E2E ${formalNounName} Coverage Report`) + outputLines.push(`${spacePrefix} uses: ${config.downloadArtifactAction} # ${config.downloadArtifactActionComment}`) + outputLines.push(spacePrefix + ' if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }}') + outputLines.push(`${spacePrefix} with:`) + outputLines.push(spacePrefix + ' name: ${{ inputs.e2e-' + kebabCase + '-coverage-report }}') + outputLines.push(spacePrefix + ' path: \'coverage/${{ inputs.e2e-' + kebabCase + '-test-subdir }}\'') + outputLines.push('') + } + }) +} diff --git a/.github/workflows/flow-build-application.yaml b/.github/workflows/flow-build-application.yaml index 384b8ff88..ba309424f 100644 --- a/.github/workflows/flow-build-application.yaml +++ b/.github/workflows/flow-build-application.yaml @@ -64,7 +64,7 @@ jobs: with: custom-job-label: Standard - e2e-tests: + e2e-standard-tests: name: E2E Tests if: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} uses: ./.github/workflows/zxc-e2e-test.yaml @@ -73,9 +73,9 @@ jobs: - code-style with: custom-job-label: Standard - npm-test-script: test-${{ needs.env-vars.outputs.e2e-test-subdir }} - coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-test-subdir }} - coverage-report-name: ${{ needs.env-vars.outputs.e2e-coverage-report }} + npm-test-script: test-${{ needs.env-vars.outputs.e2e-standard-test-subdir }} + coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-standard-test-subdir }} + coverage-report-name: ${{ needs.env-vars.outputs.e2e-standard-coverage-report }} e2e-mirror-node-tests: name: E2E Tests @@ -118,6 +118,7 @@ jobs: e2e-node-local-build-tests: name: E2E Tests + if: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -130,6 +131,7 @@ jobs: e2e-node-add-tests: name: E2E Tests + if: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -142,6 +144,7 @@ jobs: e2e-node-update-tests: name: E2E Tests + if: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -154,6 +157,7 @@ jobs: e2e-node-delete-tests: name: E2E Tests + if: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -183,7 +187,7 @@ jobs: needs: - env-vars - unit-tests - - e2e-tests + - e2e-standard-tests - e2e-mirror-node-tests - e2e-node-pem-stop-tests - e2e-node-pem-kill-tests @@ -199,7 +203,7 @@ jobs: enable-codecov-analysis: true enable-codacy-coverage: true enable-e2e-coverage-report: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} - e2e-test-subdir: ${{ needs.env-vars.outputs.e2e-test-subdir }} + e2e-standard-test-subdir: ${{ needs.env-vars.outputs.e2e-standard-test-subdir }} e2e-mirror-node-test-subdir: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} e2e-node-pem-stop-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} e2e-node-pem-kill-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-kill-test-subdir }} @@ -208,7 +212,7 @@ jobs: e2e-node-update-test-subdir: ${{ needs.env-vars.outputs.e2e-node-update-test-subdir }} e2e-node-delete-test-subdir: ${{ needs.env-vars.outputs.e2e-node-delete-test-subdir }} e2e-relay-test-subdir: ${{ needs.env-vars.outputs.e2e-relay-test-subdir }} - e2e-coverage-report: ${{ needs.env-vars.outputs.e2e-coverage-report }} + e2e-standard-coverage-report: ${{ needs.env-vars.outputs.e2e-standard-coverage-report }} e2e-mirror-node-coverage-report: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} e2e-node-pem-stop-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} e2e-node-pem-kill-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-kill-coverage-report }} diff --git a/.github/workflows/flow-pull-request-checks.yaml b/.github/workflows/flow-pull-request-checks.yaml index 51a39c30f..ac9625690 100644 --- a/.github/workflows/flow-pull-request-checks.yaml +++ b/.github/workflows/flow-pull-request-checks.yaml @@ -52,17 +52,18 @@ jobs: with: custom-job-label: Standard - e2e-tests: + e2e-standard-tests: name: E2E Tests + if: ${{ !cancelled() && always() }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars - code-style with: custom-job-label: Standard - npm-test-script: test-${{ needs.env-vars.outputs.e2e-test-subdir }} - coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-test-subdir }} - coverage-report-name: ${{ needs.env-vars.outputs.e2e-coverage-report }} + npm-test-script: test-${{ needs.env-vars.outputs.e2e-standard-test-subdir }} + coverage-subdirectory: ${{ needs.env-vars.outputs.e2e-standard-test-subdir }} + coverage-report-name: ${{ needs.env-vars.outputs.e2e-standard-coverage-report }} e2e-mirror-node-tests: name: E2E Tests @@ -79,6 +80,7 @@ jobs: e2e-node-pem-stop-tests: name: E2E Tests + if: ${{ !cancelled() && always() }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -91,6 +93,7 @@ jobs: e2e-node-pem-kill-tests: name: E2E Tests + if: ${{ !cancelled() && always() }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -103,6 +106,7 @@ jobs: e2e-node-local-build-tests: name: E2E Tests + if: ${{ !cancelled() && always() }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -115,6 +119,7 @@ jobs: e2e-node-add-tests: name: E2E Tests + if: ${{ !cancelled() && always() }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -127,6 +132,7 @@ jobs: e2e-node-update-tests: name: E2E Tests + if: ${{ !cancelled() && always() }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -139,6 +145,7 @@ jobs: e2e-node-delete-tests: name: E2E Tests + if: ${{ !cancelled() && always() }} uses: ./.github/workflows/zxc-e2e-test.yaml needs: - env-vars @@ -168,7 +175,7 @@ jobs: needs: - env-vars - unit-tests - - e2e-tests + - e2e-standard-tests - e2e-mirror-node-tests - e2e-node-pem-stop-tests - e2e-node-pem-kill-tests @@ -182,7 +189,7 @@ jobs: custom-job-label: Standard enable-codecov-analysis: true enable-e2e-coverage-report: true - e2e-test-subdir: ${{ needs.env-vars.outputs.e2e-test-subdir }} + e2e-standard-test-subdir: ${{ needs.env-vars.outputs.e2e-standard-test-subdir }} e2e-mirror-node-test-subdir: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} e2e-node-pem-stop-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} e2e-node-pem-kill-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-kill-test-subdir }} @@ -191,7 +198,7 @@ jobs: e2e-node-update-test-subdir: ${{ needs.env-vars.outputs.e2e-node-update-test-subdir }} e2e-node-delete-test-subdir: ${{ needs.env-vars.outputs.e2e-node-delete-test-subdir }} e2e-relay-test-subdir: ${{ needs.env-vars.outputs.e2e-relay-test-subdir }} - e2e-coverage-report: ${{ needs.env-vars.outputs.e2e-coverage-report }} + e2e-standard-coverage-report: ${{ needs.env-vars.outputs.e2e-standard-coverage-report }} e2e-mirror-node-coverage-report: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} e2e-node-pem-stop-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} e2e-node-pem-kill-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-kill-coverage-report }} @@ -209,7 +216,7 @@ jobs: needs: - env-vars - unit-tests - - e2e-tests + - e2e-standard-tests - e2e-mirror-node-tests - e2e-node-pem-stop-tests - e2e-node-pem-kill-tests @@ -223,7 +230,7 @@ jobs: custom-job-label: Coverage enable-codacy-coverage: true enable-e2e-coverage-report: true - e2e-test-subdir: ${{ needs.env-vars.outputs.e2e-test-subdir }} + e2e-standard-test-subdir: ${{ needs.env-vars.outputs.e2e-standard-test-subdir }} e2e-mirror-node-test-subdir: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} e2e-node-pem-stop-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} e2e-node-pem-kill-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-kill-test-subdir }} @@ -232,7 +239,7 @@ jobs: e2e-node-update-test-subdir: ${{ needs.env-vars.outputs.e2e-node-update-test-subdir }} e2e-node-delete-test-subdir: ${{ needs.env-vars.outputs.e2e-node-delete-test-subdir }} e2e-relay-test-subdir: ${{ needs.env-vars.outputs.e2e-relay-test-subdir }} - e2e-coverage-report: ${{ needs.env-vars.outputs.e2e-coverage-report }} + e2e-standard-coverage-report: ${{ needs.env-vars.outputs.e2e-standard-coverage-report }} e2e-mirror-node-coverage-report: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} e2e-node-pem-stop-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} e2e-node-pem-kill-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-kill-coverage-report }} @@ -243,43 +250,3 @@ jobs: e2e-relay-coverage-report: ${{ needs.env-vars.outputs.e2e-relay-coverage-report }} secrets: codacy-project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - -# snyk: -# name: Snyk Scan -# uses: ./.github/workflows/zxc-code-analysis.yaml -# needs: -# - env-vars -# - unit-tests -# - e2e-tests -# - e2e-mirror-node-tests -# - e2e-node-pem-stop-tests -# - e2e-node-pem-kill-tests -# - e2e-node-local-build-tests -# - e2e-node-add-tests -# - e2e-node-update-tests -# - e2e-node-delete-tests -# - e2e-relay-tests -# if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name && github.actor != 'dependabot[bot]' }} -# with: -# custom-job-label: Standard -# enable-snyk-scan: true -# e2e-test-subdir: ${{ needs.env-vars.outputs.e2e-test-subdir }} -# e2e-mirror-node-test-subdir: ${{ needs.env-vars.outputs.e2e-mirror-node-test-subdir }} -# e2e-node-pem-stop-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-stop-test-subdir }} -# e2e-node-pem-kill-test-subdir: ${{ needs.env-vars.outputs.e2e-node-pem-kill-test-subdir }} -# e2e-node-local-build-test-subdir: ${{ needs.env-vars.outputs.e2e-node-local-build-test-subdir }} -# e2e-node-add-test-subdir: ${{ needs.env-vars.outputs.e2e-node-add-test-subdir }} -# e2e-node-update-test-subdir: ${{ needs.env-vars.outputs.e2e-node-update-test-subdir }} -# e2e-node-delete-test-subdir: ${{ needs.env-vars.outputs.e2e-node-delete-test-subdir }} -# e2e-relay-test-subdir: ${{ needs.env-vars.outputs.e2e-relay-test-subdir }} -# e2e-coverage-report: ${{ needs.env-vars.outputs.e2e-coverage-report }} -# e2e-mirror-node-coverage-report: ${{ needs.env-vars.outputs.e2e-mirror-node-coverage-report }} -# e2e-node-pem-stop-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-stop-coverage-report }} -# e2e-node-pem-kill-coverage-report: ${{ needs.env-vars.outputs.e2e-node-pem-kill-coverage-report }} -# e2e-node-local-build-coverage-report: ${{ needs.env-vars.outputs.e2e-node-local-build-coverage-report }} -# e2e-node-add-coverage-report: ${{ needs.env-vars.outputs.e2e-node-add-coverage-report }} -# e2e-node-update-coverage-report: ${{ needs.env-vars.outputs.e2e-node-update-coverage-report }} -# e2e-node-delete-coverage-report: ${{ needs.env-vars.outputs.e2e-node-delete-coverage-report }} -# e2e-relay-coverage-report: ${{ needs.env-vars.outputs.e2e-relay-coverage-report }} -# secrets: -# snyk-token: ${{ secrets.SNYK_TOKEN }} diff --git a/.github/workflows/templates/config.yaml b/.github/workflows/templates/config.yaml new file mode 100644 index 000000000..d1ecdb8b2 --- /dev/null +++ b/.github/workflows/templates/config.yaml @@ -0,0 +1,23 @@ +downloadArtifactAction: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 +downloadArtifactActionComment: v4.1.8 +tests: + # name: capitalized noun form + # testFilePrefix: node-update # for node-update.test.mjs + - name: Standard + jestPostfix: --testPathIgnorePatterns=\".*/unit/.*\" --testPathIgnorePatterns=\".*/e2e/commands/mirror_node.*\" --testPathIgnorePatterns=\".*/e2e/commands/node.*\" --testPathIgnorePatterns=\".*/e2e/commands/relay.*\" + - name: Mirror Node + jestPostfix: --testRegex=\".*\\/e2e\\/commands\\/mirror_node\\.test\\.mjs\" + - name: Node PEM Stop + jestPostfix: --testRegex=\".*\\/e2e\\/commands\\/node_pem_stop\\.test\\.mjs\" + - name: Node PEM Kill + jestPostfix: --testRegex=\".*\\/e2e\\/commands\\/node_pem_kill\\.test\\.mjs\" + - name: Node Local Build + jestPostfix: --testRegex=\".*\\/e2e\\/commands\\/node_local.*\\.test\\.mjs\" + - name: Node Add + jestPostfix: --testRegex=\".*\\/e2e\\/commands\\/node_add.*\\.test\\.mjs\" + - name: Node Update + jestPostfix: --testRegex=\".*\\/e2e\\/commands\\/node_update.*\\.test\\.mjs\" + - name: Node Delete + jestPostfix: --testRegex=\".*\\/e2e\\/commands\\/node_delete.*\\.test\\.mjs\" + - name: Relay + jestPostfix: --testRegex=\".*\\/e2e\\/commands\\/relay\\.test\\.mjs\" diff --git a/.github/workflows/templates/template.flow-build-application.yaml b/.github/workflows/templates/template.flow-build-application.yaml new file mode 100644 index 000000000..51c76dee3 --- /dev/null +++ b/.github/workflows/templates/template.flow-build-application.yaml @@ -0,0 +1,88 @@ +## +# Copyright (C) 2022-2023 Hedera Hashgraph, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +name: "Build Application" +on: + workflow_dispatch: + inputs: + enable-unit-tests: + description: "Unit Testing Enabled" + type: boolean + required: false + default: true + enable-e2e-tests: + description: "E2E Testing Enabled" + type: boolean + required: false + default: false + enable-snyk-scan: + description: "Snyk Scan Enabled" + type: boolean + required: false + default: false + push: + branches: + - main + - 'release/*' + +defaults: + run: + shell: bash + +jobs: + env-vars: + name: Set Environment Variables + uses: ./.github/workflows/zxc-env-vars.yaml + with: + custom-job-label: Set Environment Variables + + code-style: + name: Code Style + uses: ./.github/workflows/zxc-code-style.yaml + with: + custom-job-label: Standard + + unit-tests: + name: Unit Tests + uses: ./.github/workflows/zxc-unit-test.yaml + if: ${{ github.event_name == 'push' || github.event.inputs.enable-unit-tests == 'true' }} + needs: + - code-style + with: + custom-job-label: Standard + + # {AUTOGENERATE-E2E-TEST-JOBS} + + analyze: + name: Analyze + uses: ./.github/workflows/zxc-code-analysis.yaml + needs: + - env-vars + - unit-tests + # {AUTOGENERATE-NEEDS} + if: ${{ (github.event_name == 'push' || github.event.inputs.enable-unit-tests == 'true' || github.event.inputs.enable-e2e-tests == 'true') && !failure() && !cancelled() }} + with: + custom-job-label: Source Code + #enable-snyk-scan: ${{ github.event_name == 'push' || github.event.inputs.enable-snyk-scan == 'true' }} + enable-codecov-analysis: true + enable-codacy-coverage: true + enable-e2e-coverage-report: ${{ github.event_name == 'push' || github.event.inputs.enable-e2e-tests == 'true' }} + # {AUTOGENERATE-WITH-SUBDIR} + # {AUTOGENERATE-WITH-COVERAGE-REPORT} + secrets: + snyk-token: ${{ secrets.SNYK_TOKEN }} + codecov-token: ${{ secrets.CODECOV_TOKEN }} + codacy-project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} diff --git a/.github/workflows/templates/template.flow-pull-request-checks.yaml b/.github/workflows/templates/template.flow-pull-request-checks.yaml new file mode 100644 index 000000000..0c4cb25be --- /dev/null +++ b/.github/workflows/templates/template.flow-pull-request-checks.yaml @@ -0,0 +1,89 @@ +## +# Copyright (C) 2023-2024 Hedera Hashgraph, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +name: "PR Checks" +on: + workflow_dispatch: + pull_request: + types: + - opened + - reopened + - synchronize + +defaults: + run: + shell: bash + +concurrency: + group: pr-checks-${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + env-vars: + name: Set Environment Variables + uses: ./.github/workflows/zxc-env-vars.yaml + with: + custom-job-label: Set Environment Variables + + code-style: + name: Code Style + uses: ./.github/workflows/zxc-code-style.yaml + with: + custom-job-label: Standard + + unit-tests: + name: Unit Tests + uses: ./.github/workflows/zxc-unit-test.yaml + needs: + - code-style + with: + custom-job-label: Standard + + # {AUTOGENERATE-E2E-TEST-JOBS-2} + + codecov: + name: CodeCov + uses: ./.github/workflows/zxc-code-analysis.yaml + needs: + - env-vars + - unit-tests + # {AUTOGENERATE-NEEDS} + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + with: + custom-job-label: Standard + enable-codecov-analysis: true + enable-e2e-coverage-report: true + # {AUTOGENERATE-WITH-SUBDIR} + # {AUTOGENERATE-WITH-COVERAGE-REPORT} + secrets: + codecov-token: ${{ secrets.CODECOV_TOKEN }} + + codacy-coverage: + name: Codacy + uses: ./.github/workflows/zxc-code-analysis.yaml + needs: + - env-vars + - unit-tests + # {AUTOGENERATE-NEEDS} + if: ${{ github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name }} + with: + custom-job-label: Coverage + enable-codacy-coverage: true + enable-e2e-coverage-report: true + # {AUTOGENERATE-WITH-SUBDIR} + # {AUTOGENERATE-WITH-COVERAGE-REPORT} + secrets: + codacy-project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} diff --git a/.github/workflows/templates/template.zxc-code-analysis.yaml b/.github/workflows/templates/template.zxc-code-analysis.yaml new file mode 100644 index 000000000..7f7a0a762 --- /dev/null +++ b/.github/workflows/templates/template.zxc-code-analysis.yaml @@ -0,0 +1,185 @@ +## +# Copyright (C) 2023-2024 Hedera Hashgraph, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +name: "ZXC: Code Analysis" +# The purpose of this reusable workflow is to perform static code analysis and code coverage reporting. +# This reusable component is called by the following workflows: +# - .github/workflows/flow-pull-request-checks.yaml +# - .github/workflows/flow-build-application.yaml +# +# This workflow is only run if the pull request is coming from the original repository and not a fork. + +on: + workflow_call: + inputs: + enable-codecov-analysis: + description: "CodeCov Analysis Enabled" + type: boolean + required: false + default: false + enable-codacy-coverage: + description: "Codacy Coverage Enabled" + type: boolean + required: false + default: false + enable-e2e-coverage-report: + description: "E2E Coverage Report Enabled" + type: boolean + required: false + default: false + enable-snyk-scan: + description: "Snyk Scan Enabled" + type: boolean + required: false + default: false + node-version: + description: "NodeJS Version:" + type: string + required: false + default: "20.14.0" + custom-job-label: + description: "Custom Job Label:" + type: string + required: false + default: "Analyze" + # {AUTOGENERATE-INPUTS-SUB-DIRS} + # {AUTOGENERATE-INPUTS-COVERAGE-REPORTS} + secrets: + snyk-token: + description: "The Snyk access token is used by Snyk to analyze the code for vulnerabilities " + required: false + codecov-token: + description: "The CodeCov access token is used by CodeCov.io to analyze the code coverage " + required: false + codacy-project-token: + description: "The Codacy project token used to report code coverage." + required: false + +defaults: + run: + shell: bash + +permissions: + contents: read + actions: read + pull-requests: write + checks: write + statuses: write + +jobs: + analyze: + name: ${{ inputs.custom-job-label || 'Analyze' }} + runs-on: solo-linux-medium + steps: + - name: Checkout Code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + ref: ${{ github.event.workflow_run.head_branch }} + fetch-depth: ${{ inputs.enable-sonar-analysis && '0' || '' }} + + - name: Setup Node with Retry + uses: Wandalen/wretry.action@6feedb7dedadeb826de0f45ff482b53b379a7844 # v3.5.0 + with: + action: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + with: | + node-version: ${{ inputs.node-version }} + cache: npm + attempt_limit: 3 + attempt_delay: 5000 + + - name: Download Unit Test Coverage Report + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && !cancelled() && !failure() }} + with: + name: Unit Test Coverage Report + path: 'coverage/unit' + + # {AUTOGENERATE-DOWNLOAD-JOBS} + + - name: Publish To Codecov + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 + if: ${{ inputs.enable-codecov-analysis && !cancelled() && !failure() }} + env: + CODECOV_TOKEN: ${{ secrets.codecov-token }} + with: + verbose: true + directory: 'coverage' + + - name: Publish to Codacy + env: + CODACY_PROJECT_TOKEN: ${{ secrets.codacy-project-token }} + if: ${{ inputs.enable-codacy-coverage && !cancelled() && !failure() }} + run: bash <(curl -Ls https://coverage.codacy.com/get.sh) report -l Javascript $(find . -name 'lcov.info' -printf '-r %p ') + + - name: Setup Snyk + env: + SNYK_TOKEN: ${{ secrets.snyk-token }} + if: ${{ inputs.enable-snyk-scan && !cancelled() && !failure() }} + run: npm install -g snyk snyk-to-html @wcj/html-to-markdown-cli + + - name: Snyk Scan + id: snyk + env: + SNYK_TOKEN: ${{ secrets.snyk-token }} + if: ${{ inputs.enable-snyk-scan && !cancelled() && !failure() }} + run: snyk test --org=release-engineering-N6EoZVZn3jw4qNuVkiG5Qs --all-projects --severity-threshold=high --json-file-output=snyk-test.json + + - name: Snyk Code + id: snyk-code + env: + SNYK_TOKEN: ${{ secrets.snyk-token }} + if: ${{ inputs.enable-snyk-scan && !cancelled() && !failure() }} + run: snyk code test --org=release-engineering-N6EoZVZn3jw4qNuVkiG5Qs --severity-threshold=high --json-file-output=snyk-code.json + + - name: Publish Snyk Results + if: ${{ inputs.enable-snyk-scan && !cancelled() && !failure() }} + run: | + if [[ -f "snyk-test.json" && -n "$(cat snyk-test.json | tr -d '[:space:]')" ]]; then + snyk-to-html -i snyk-test.json -o snyk-test.html --summary + html-to-markdown snyk-test.html -o snyk + cat snyk/snyk-test.html.md >> $GITHUB_STEP_SUMMARY + fi + + - name: Publish Snyk Code Results + if: ${{ inputs.enable-snyk-scan && !cancelled() && !failure() }} + run: | + if [[ -f "snyk-code.json" && -n "$(cat snyk-code.json | tr -d '[:space:]')" ]]; then + snyk-to-html -i snyk-code.json -o snyk-code.html --summary + html-to-markdown snyk-code.html -o snyk + cat snyk/snyk-code.html.md >> $GITHUB_STEP_SUMMARY + fi + + - name: Check Snyk Files + if: ${{ always() }} + run: | + echo "::group::Snyk File List" + ls -lah snyk* || true + echo "::endgroup::" + echo "::group::Snyk Test Contents" + cat snyk-test.json || true + echo "::endgroup::" + echo "::group::Snyk Code Contents" + cat snyk-code.json || true + echo "::endgroup::" + + - name: Publish Snyk Reports + uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + if: ${{ inputs.enable-snyk-scan && !cancelled() && !failure() }} + with: + name: Snyk Reports + path: | + snyk-*.html + snyk-*.json diff --git a/.github/workflows/templates/template.zxc-env-vars.yaml b/.github/workflows/templates/template.zxc-env-vars.yaml new file mode 100644 index 000000000..d2b3b27c8 --- /dev/null +++ b/.github/workflows/templates/template.zxc-env-vars.yaml @@ -0,0 +1,44 @@ +## +# Copyright (C) 2023-2024 Hedera Hashgraph, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +name: "ZXC: Environment Variables" +# The purpose of this reusable workflow is to provide environment variables for use in re-usable workflows. + +on: + workflow_call: + inputs: + custom-job-label: + description: "Custom Job Label:" + type: string + required: false + default: "Set Environment Variables" + outputs: + # {AUTOGENERATE-WORKFLOW-OUTPUTS-SUB-DIRS} + # {AUTOGENERATE-WORKFLOW-OUTPUTS-COVERAGE-REPORTS} + +defaults: + run: + shell: bash + +jobs: + env-vars: + name: ${{ inputs.custom-job-label || 'Set Environment Variables' }} + runs-on: solo-linux-medium + outputs: + # {AUTOGENERATE-JOB-OUTPUTS-SUB-DIRS} + # {AUTOGENERATE-JOB-OUTPUTS-COVERAGE-REPORTS} + steps: + - run: echo "Exposing environment variables to reusable workflows" diff --git a/.github/workflows/zxc-code-analysis.yaml b/.github/workflows/zxc-code-analysis.yaml index 6957d3b61..b7e5c3871 100644 --- a/.github/workflows/zxc-code-analysis.yaml +++ b/.github/workflows/zxc-code-analysis.yaml @@ -55,11 +55,11 @@ on: type: string required: false default: "Analyze" - e2e-test-subdir: - description: "E2E Test Subdirectory:" + e2e-standard-test-subdir: + description: "E2E Standard Test Subdirectory:" type: string required: false - default: "e2e" + default: "e2e-standard" e2e-mirror-node-test-subdir: description: "E2E Mirror Node Test Subdirectory:" type: string @@ -100,11 +100,11 @@ on: type: string required: false default: "e2e-relay" - e2e-coverage-report: - description: "E2E Coverage Report:" + e2e-standard-coverage-report: + description: "E2E Standard Coverage Report:" type: string required: false - default: "E2E Tests Coverage Report" + default: "E2E Standard Tests Coverage Report" e2e-mirror-node-coverage-report: description: "E2E Mirror Node Coverage Report:" type: string @@ -195,12 +195,12 @@ jobs: name: Unit Test Coverage Report path: 'coverage/unit' - - name: Download E2E Coverage Report + - name: Download E2E Standard Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} with: - name: ${{ inputs.e2e-coverage-report }} - path: 'coverage/${{ inputs.e2e-test-subdir }}' + name: ${{ inputs.e2e-standard-coverage-report }} + path: 'coverage/${{ inputs.e2e-standard-test-subdir }}' - name: Download E2E Mirror Node Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 @@ -223,41 +223,41 @@ jobs: name: ${{ inputs.e2e-node-pem-kill-coverage-report }} path: 'coverage/${{ inputs.e2e-node-pem-kill-test-subdir }}' - - name: Download E2E Relay Coverage Report - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 - if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} - with: - name: ${{ inputs.e2e-relay-coverage-report }} - path: 'coverage/${{ inputs.e2e-relay-test-subdir }}' - - - name: Download E2E Local Build Coverage Report + - name: Download E2E Node Local Build Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} with: name: ${{ inputs.e2e-node-local-build-coverage-report }} path: 'coverage/${{ inputs.e2e-node-local-build-test-subdir }}' - - name: Download E2E Add Coverage Report + - name: Download E2E Node Add Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} with: name: ${{ inputs.e2e-node-add-coverage-report }} path: 'coverage/${{ inputs.e2e-node-add-test-subdir }}' - - name: Download E2E Update Coverage Report + - name: Download E2E Node Update Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} with: - name: ${{ inputs.e2e-node-udpate-coverage-report }} - path: 'coverage/${{ inputs.e2e-udpate-add-test-subdir }}' + name: ${{ inputs.e2e-node-update-coverage-report }} + path: 'coverage/${{ inputs.e2e-node-update-test-subdir }}' - - name: Download E2E Delete Coverage Report + - name: Download E2E Node Delete Coverage Report uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} with: name: ${{ inputs.e2e-node-delete-coverage-report }} path: 'coverage/${{ inputs.e2e-node-delete-test-subdir }}' + - name: Download E2E Relay Coverage Report + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + if: ${{ (inputs.enable-codecov-analysis || inputs.enable-codacy-coverage) && inputs.enable-e2e-coverage-report && !cancelled() && !failure() }} + with: + name: ${{ inputs.e2e-relay-coverage-report }} + path: 'coverage/${{ inputs.e2e-relay-test-subdir }}' + - name: Publish To Codecov uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 if: ${{ inputs.enable-codecov-analysis && !cancelled() && !failure() }} diff --git a/.github/workflows/zxc-e2e-test.yaml b/.github/workflows/zxc-e2e-test.yaml index 3ecbc6e18..3fa873793 100644 --- a/.github/workflows/zxc-e2e-test.yaml +++ b/.github/workflows/zxc-e2e-test.yaml @@ -37,7 +37,7 @@ on: description: "NPM Test Run Script:" type: string required: false - default: "test-e2e" + default: "test-e2e-standard" coverage-subdirectory: description: "Coverage Report Subdirectory:" type: string @@ -163,7 +163,7 @@ jobs: if: ${{ runner.os == 'linux' && (inputs.npm-test-script == 'test-e2e-node-local-build' || inputs.npm-test-script == 'test-e2e-node-add') && !cancelled() && !failure() }} run: | cd .. - git clone https://github.com/hashgraph/hedera-services.git --branch v0.53.2 + git clone https://github.com/hashgraph/hedera-services.git --depth 1 --branch v0.54.0-alpha.4 cd hedera-services ls -ltr ${{ env.CG_EXEC }} ./gradlew assemble --stacktrace --info diff --git a/.github/workflows/zxc-env-vars.yaml b/.github/workflows/zxc-env-vars.yaml index bae0e1a6a..a839b1558 100644 --- a/.github/workflows/zxc-env-vars.yaml +++ b/.github/workflows/zxc-env-vars.yaml @@ -26,9 +26,9 @@ on: required: false default: "Set Environment Variables" outputs: - e2e-test-subdir: - description: "E2E Test Subdirectory" - value: ${{ jobs.env-vars.outputs.e2e_test_subdir }} + e2e-standard-test-subdir: + description: "E2E Standard Test Subdirectory" + value: ${{ jobs.env-vars.outputs.e2e_standard_test_subdir }} e2e-mirror-node-test-subdir: description: "E2E Mirror Node Test Subdirectory" value: ${{ jobs.env-vars.outputs.e2e_mirror_node_test_subdir }} @@ -53,9 +53,9 @@ on: e2e-relay-test-subdir: description: "E2E Relay Test Subdirectory" value: ${{ jobs.env-vars.outputs.e2e_relay_test_subdir }} - e2e-coverage-report: - description: "E2E Tests Coverage Report" - value: ${{ jobs.env-vars.outputs.e2e_coverage_report }} + e2e-standard-coverage-report: + description: "E2E Standard Tests Coverage Report" + value: ${{ jobs.env-vars.outputs.e2e_standard_coverage_report }} e2e-mirror-node-coverage-report: description: "E2E Mirror Node Tests Coverage Report" value: ${{ jobs.env-vars.outputs.e2e_mirror_node_coverage_report }} @@ -90,7 +90,7 @@ jobs: name: ${{ inputs.custom-job-label || 'Set Environment Variables' }} runs-on: solo-linux-medium outputs: - e2e_test_subdir: e2e + e2e_standard_test_subdir: e2e-standard e2e_mirror_node_test_subdir: e2e-mirror-node e2e_node_pem_stop_test_subdir: e2e-node-pem-stop e2e_node_pem_kill_test_subdir: e2e-node-pem-kill @@ -99,7 +99,7 @@ jobs: e2e_node_update_test_subdir: e2e-node-update e2e_node_delete_test_subdir: e2e-node-delete e2e_relay_test_subdir: e2e-relay - e2e_coverage_report: "E2E Tests Coverage Report" + e2e_standard_coverage_report: "E2E Standard Tests Coverage Report" e2e_mirror_node_coverage_report: "E2E Mirror Node Tests Coverage Report" e2e_node_pem_stop_coverage_report: "E2E Node PEM Stop Tests Coverage Report" e2e_node_pem_kill_coverage_report: "E2E Node PEM Kill Tests Coverage Report" diff --git a/DEV.md b/DEV.md index f85260e29..d940483f7 100644 --- a/DEV.md +++ b/DEV.md @@ -22,11 +22,11 @@ Below we describe how you can set up local environment and contribute to `solo`. * In order to run E2E test, we need to set up cluster and install the chart. * Run `./test/e2e/setup-e2e.sh` - * Run `npm run test-e2e` + * Run `npm run test-e2e-standard`, NOTE: this excludes some E2E tests that have their own command * Tests are run in random order. The random seed value is shown as message such as: `Using timestamp seed 1711414247085 for random test order` -* If you like to rerun tests with the same seed, use environment variable `RANDOM_SEED=` with `npm run test-e2e` command. - * Example: `RANDOM_SEED=20 npm run test-e2e`, +* If you like to rerun tests with the same seed, use environment variable `RANDOM_SEED=` with `npm run test-e2e-standard` command. + * Example: `RANDOM_SEED=20 npm run test-e2e-standard`, and you should see an output like: `Using preset seed 20 for random test order` diff --git a/README.md b/README.md index 3e98fa9f9..650d4c2f4 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ You can now use your cluster with: kubectl cluster-info --context kind-solo -Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/docs/user/quick-start/ +Have a nice day! 👋 ``` You may now view pods in your cluster using `k9s -A` as below: @@ -174,9 +174,9 @@ Kubernetes Namespace : solo ✔ Gossip pem key for node: node3 ✔ Generate gossip keys ✔ Backup old files -✔ TLS key for node: node2 ✔ TLS key for node: node3 ✔ TLS key for node: node1 +✔ TLS key for node: node2 ✔ Generate gRPC TLS keys ✔ Finalize ``` @@ -235,11 +235,11 @@ Kubernetes Namespace : solo ✔ Prepare staging directory ✔ Copy TLS keys ✔ Copy Gossip keys +✔ Node: node3 +✔ Copy Gossip keys ✔ Node: node2 ✔ Copy Gossip keys ✔ Node: node1 -✔ Copy Gossip keys -✔ Node: node3 ✔ Copy node keys to secrets ✔ Install chart 'fullstack-deployment' ✔ Check Node: node1 @@ -247,11 +247,11 @@ Kubernetes Namespace : solo ✔ Check Node: node3 ✔ Check node pods are running ✔ Check Envoy Proxy for: node1 -✔ Check Envoy Proxy for: node2 -✔ Check HAProxy for: node2 -✔ Check HAProxy for: node3 ✔ Check Envoy Proxy for: node3 ✔ Check HAProxy for: node1 +✔ Check HAProxy for: node3 +✔ Check Envoy Proxy for: node2 +✔ Check HAProxy for: node2 ✔ Check proxy pods are running ✔ Check MinIO ✔ Check auxiliary pods are ready @@ -276,19 +276,19 @@ Kubernetes Namespace : solo ********************************************************************************** ✔ Initialize ✔ Check network pod: node3 -✔ Check network pod: node2 ✔ Check network pod: node1 +✔ Check network pod: node2 ✔ Identify network pods +✔ Update node: node1 [ platformVersion = v0.42.5 ] ✔ Update node: node3 [ platformVersion = v0.42.5 ] ✔ Update node: node2 [ platformVersion = v0.42.5 ] -✔ Update node: node1 [ platformVersion = v0.42.5 ] ✔ Fetch platform software into network nodes ✔ Set file permissions -✔ Node: node1 +✔ Node: node2 ✔ Set file permissions ✔ Node: node3 ✔ Set file permissions -✔ Node: node2 +✔ Node: node1 ✔ Setup network nodes ``` @@ -309,13 +309,13 @@ Kubernetes Cluster : kind-solo Kubernetes Namespace : solo ********************************************************************************** ✔ Initialize -✔ Check network pod: node2 ✔ Check network pod: node1 +✔ Check network pod: node2 ✔ Check network pod: node3 ✔ Identify network pods +✔ Start node: node1 ✔ Start node: node3 ✔ Start node: node2 -✔ Start node: node1 ✔ Starting nodes *********************************** ERROR ***************************************** Error starting node: node 'node1' is not ACTIVE [ attempt = 100/100 ] @@ -492,9 +492,9 @@ Kubernetes Namespace : solo ✔ Gossip pem key for node: node3 ✔ Generate gossip keys ✔ Backup old files +✔ TLS key for node: node2 ✔ TLS key for node: node3 ✔ TLS key for node: node1 -✔ TLS key for node: node2 ✔ Generate gRPC TLS keys ✔ Finalize ``` diff --git a/package.json b/package.json index e9df5800f..c8ee7d9cc 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,14 @@ "scripts": { "test": "cross-env NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME=\"Unit Tests\" jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/unit' --testPathIgnorePatterns=\".*/e2e/.*\"", "test-e2e-all": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E All Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e' --testPathIgnorePatterns=\".*/unit/.*\"", - "test-e2e": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e' --testPathIgnorePatterns=\".*/unit/.*\" --testPathIgnorePatterns=\".*/e2e/commands/mirror_node.*\" --testPathIgnorePatterns=\".*/e2e/commands/node.*\" --testPathIgnorePatterns=\".*/e2e/commands/relay.*\"", + "test-e2e-standard": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Standard Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-standard.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-standard' --testPathIgnorePatterns=\".*/unit/.*\" --testPathIgnorePatterns=\".*/e2e/commands/mirror_node.*\" --testPathIgnorePatterns=\".*/e2e/commands/node.*\" --testPathIgnorePatterns=\".*/e2e/commands/relay.*\"", "test-e2e-mirror-node": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Mirror Node Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-mirror-node.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-mirror-node' --testRegex=\".*\\/e2e\\/commands\\/mirror_node\\.test\\.mjs\"", "test-e2e-node-pem-stop": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node PEM Stop Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-pem-stop.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-pem-stop' --testRegex=\".*\\/e2e\\/commands\\/node_pem_stop\\.test\\.mjs\"", "test-e2e-node-pem-kill": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node PEM Kill Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-pem-kill.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-pem-kill' --testRegex=\".*\\/e2e\\/commands\\/node_pem_kill\\.test\\.mjs\"", - "test-e2e-node-local-build": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Local Custom Build' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-local-build.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-local-build' --testRegex=\".*\\/e2e\\/commands\\/node_local.*\\.test\\.mjs\"", - "test-e2e-node-add": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Add' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-add.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-add' --testRegex=\".*\\/e2e\\/commands\\/node_add.*\\.test\\.mjs\"", - "test-e2e-node-update": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Update' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-update.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-update' --testRegex=\".*\\/e2e\\/commands\\/node_update.*\\.test\\.mjs\"", - "test-e2e-node-delete": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Delete' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-delete.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-delete' --testRegex=\".*\\/e2e\\/commands\\/node_delete.*\\.test\\.mjs\"", + "test-e2e-node-local-build": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Local Build Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-local-build.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-local-build' --testRegex=\".*\\/e2e\\/commands\\/node_local.*\\.test\\.mjs\"", + "test-e2e-node-add": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Add Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-add.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-add' --testRegex=\".*\\/e2e\\/commands\\/node_add.*\\.test\\.mjs\"", + "test-e2e-node-update": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Update Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-update.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-update' --testRegex=\".*\\/e2e\\/commands\\/node_update.*\\.test\\.mjs\"", + "test-e2e-node-delete": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Node Delete Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-node-delete.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-node-delete' --testRegex=\".*\\/e2e\\/commands\\/node_delete.*\\.test\\.mjs\"", "test-e2e-relay": "NODE_OPTIONS=--experimental-vm-modules JEST_SUITE_NAME='Jest E2E Relay Tests' JEST_JUNIT_OUTPUT_NAME='junit-e2e-relay.xml' jest --runInBand --detectOpenHandles --forceExit --coverage --coverageDirectory='coverage/e2e-relay' --testRegex=\".*\\/e2e\\/commands\\/relay\\.test\\.mjs\"", "merge-clean": "rm -rf .nyc_output && mkdir .nyc_output && rm -rf coverage/lcov-report && rm -rf coverage/solo && rm coverage/*.*", "merge-e2e": "nyc merge ./coverage/e2e/ .nyc_output/coverage.json", diff --git a/src/core/constants.mjs b/src/core/constants.mjs index b742c90da..e87309cdb 100644 --- a/src/core/constants.mjs +++ b/src/core/constants.mjs @@ -93,6 +93,7 @@ export const POD_CONDITION_READY = 'Ready' export const POD_CONDITION_POD_SCHEDULED = 'PodScheduled' export const POD_CONDITION_STATUS_TRUE = 'True' +export const K8_COPY_FROM_RETRY_TIMES = process.env.K8_COPY_FROM_RETRY_TIMES || 5 /** * Listr related * @return a object that defines the default color options diff --git a/src/core/k8.mjs b/src/core/k8.mjs index f52676972..07ad6a5c9 100644 --- a/src/core/k8.mjs +++ b/src/core/k8.mjs @@ -28,6 +28,7 @@ import { v4 as uuid4 } from 'uuid' import { V1ObjectMeta, V1Secret } from '@kubernetes/client-node' import { sleep } from './helpers.mjs' import { constants } from './index.mjs' +import * as stream from 'node:stream' /** * A kubernetes API wrapper class providing custom functionalities required by solo @@ -342,7 +343,7 @@ export class K8 { * @param {string} containerName * @param {string} destPath - path inside the container * @param {number} [timeout] - timeout in ms - * @returns {Promise<{owner: string, size: number, modifiedAt: string, name: string, directory: boolean, group: string}>} + * @returns {Promise<{owner: string, size: number, modifiedAt: string, name: string, directory: boolean, group: string}[]>} * array of directory entries, custom object */ async listDir (podName, containerName, destPath, timeout = 5000) { @@ -571,61 +572,137 @@ export class K8 { const self = this return new Promise((resolve, reject) => { const execInstance = new k8s.Exec(this.kubeConfig) - const command = ['tar', 'cf', '-', '-C', srcDir, srcFile] - const writerStream = fs.createWriteStream(tmpFile) - const errStream = new sb.WritableStreamBuffer() + const command = ['cat', `${srcDir}/${srcFile}`] + const outputFileStream = fs.createWriteStream(tmpFile) + const outputPassthroughStream = new stream.PassThrough({ highWaterMark: 10 * 1024 * 1024 }) + const errStream = new stream.PassThrough() + let additionalErrorMessageDetail = '' + + // Use pipe() to automatically handle backpressure between streams + outputPassthroughStream.pipe(outputFileStream) + + outputPassthroughStream.on('data', (chunk) => { + this.logger.debug(`received chunk size=${chunk.length}`) + const canWrite = outputFileStream.write(chunk) // Write chunk to file and check if buffer is full + + if (!canWrite) { + console.log(`Buffer is full, pausing data stream... for copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) + outputPassthroughStream.pause() // Pause the data stream if buffer is full + } + }) + + outputFileStream.on('drain', () => { + outputPassthroughStream.resume() + this.logger.debug(`stream drained, resume write for copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) + }) execInstance.exec( namespace, podName, containerName, command, - writerStream, + outputFileStream, errStream, null, false, - async ({ status }) => { - writerStream.close() - if (status === 'Failure' || errStream.size()) { + ({ status }) => { + if (status === 'Failure') { self._deleteTempFile(tmpFile) + const errorMessage = `tar command failed with status Failure while copying from ${podName}:${srcDir}/${srcFile} to ${destPath}` + this.logger.error(errorMessage) + return reject(new FullstackTestingError(errorMessage)) } + this.logger.debug(`copyFrom.callback(status)=${status}`) }) .then(conn => { - conn.on('close', async (code, reason) => { + conn.on('error', (e) => { + self._deleteTempFile(tmpFile) + return reject(new FullstackTestingError( + `failed copying from ${podName}:${srcDir}/${srcFile} to ${destPath} because of connection error: ${e.message}`, e)) + }) + + conn.on('close', (code, reason) => { + this.logger.debug(`connection closed copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) if (code !== 1000) { // code 1000 is the success code - return reject(new FullstackTestingError(`failed to copy because of error (${code}): ${reason}`)) + const errorMessage = `failed copying from ${podName}:${srcDir}/${srcFile} to ${destPath} because of error (${code}): ${reason}` + this.logger.error(errorMessage) + return reject(new FullstackTestingError(errorMessage)) } - try { - // extract the downloaded file - await tar.x({ - file: tmpFile, - cwd: destDir - }) + outputFileStream.end() + outputFileStream.close(() => { + this.logger.debug(`finished closing writerStream copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) - self._deleteTempFile(tmpFile) + try { + fs.copyFileSync(tmpFile, destPath) - const stat = fs.statSync(destPath) - if (stat && stat.size === srcFileSize) { - return resolve(true) - } - } catch (e) { - return reject(new FullstackTestingError(`failed to extract file: ${destPath}`, e)) - } + self._deleteTempFile(tmpFile) - return reject(new FullstackTestingError(`failed to download file completely: ${destPath}`)) - }) + const stat = fs.statSync(destPath) + let rejection + if (stat && stat.size === srcFileSize) { + this.logger.info(`Finished successfully copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) + } else { + rejection = true + if (!stat) { + additionalErrorMessageDetail = ', statSync returned no file status for the destination file' + } else { + additionalErrorMessageDetail = `, stat.size=${stat.size} != srcFileSize=${srcFileSize}` + } + } - conn.on('error', (e) => { - self._deleteTempFile(tmpFile) - return reject(new FullstackTestingError( - `failed to copy file ${destPath} because of connection error: ${e.message}`, e)) + if (rejection) { + const errorMessage = `failed copying from ${podName}:${srcDir}/${srcFile} to ${destPath} to download file completely: ${destPath}${additionalErrorMessageDetail}` + this.logger.error(errorMessage) + return reject(new FullstackTestingError(errorMessage)) + } else { + return resolve(true) + } + } catch (e) { + const errorMessage = `failed to complete copying from ${podName}:${srcDir}/${srcFile} to ${destPath} to extract file: ${destPath}` + this.logger.error(errorMessage, e) + return reject(new FullstackTestingError(errorMessage, e)) + } + }) }) }) + + errStream.on('data', (data) => { + const errorMessage = `error encountered copying from ${podName}:${srcDir}/${srcFile} to ${destPath}, error: ${data.toString()}` + this.logger.error(errorMessage) + return reject(new FullstackTestingError(errorMessage)) + }) + + outputFileStream.on('close', () => { + this.logger.debug(`finished copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) + }) + + outputFileStream.on('error', (err) => { + const errorMessage = `writerStream error encountered copying from ${podName}:${srcDir}/${srcFile} to ${destPath}, err: ${err.toString()}` + this.logger.error(errorMessage, err) + return reject(new FullstackTestingError(errorMessage, err)) + }) + + outputFileStream.on('end', () => { + this.logger.debug(`writerStream has ended for copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) + }) + + outputPassthroughStream.on('end', () => { + this.logger.debug(`writerPassthroughStream has ended for copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) + }) + + outputFileStream.on('finish', () => { + this.logger.debug(`stopping copy, writerStream has finished for copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) + }) + + outputPassthroughStream.on('finish', () => { + this.logger.debug(`stopping copy, writerPassthroughStream has finished for copying from ${podName}:${srcDir}/${srcFile} to ${destPath}`) + }) }) } catch (e) { - throw new FullstackTestingError( - `failed to download file from ${podName}:${containerName} [${srcPath} -> ${destDir}]: ${e.message}`, e) + const errorMessage = `failed to download file from ${podName}:${containerName} [${srcPath} -> ${destDir}]: ${e.message}` + this.logger.error(errorMessage, e) + throw new FullstackTestingError(errorMessage, e) } } diff --git a/test/data/build-v0.54.0-alpha.4.zip b/test/data/build-v0.54.0-alpha.4.zip new file mode 100644 index 000000000..3e5660cb6 Binary files /dev/null and b/test/data/build-v0.54.0-alpha.4.zip differ diff --git a/test/e2e/core/k8_e2e.test.mjs b/test/e2e/core/k8_e2e.test.mjs index fb364f26a..0622dc6fd 100644 --- a/test/e2e/core/k8_e2e.test.mjs +++ b/test/e2e/core/k8_e2e.test.mjs @@ -14,7 +14,6 @@ * limitations under the License. * */ -import { afterAll, beforeAll, describe, expect, it } from '@jest/globals' import fs from 'fs' import net from 'net' import os from 'os' @@ -38,6 +37,7 @@ import { V1ServiceSpec, V1VolumeResourceRequirements } from '@kubernetes/client-node' +import crypto from 'crypto' const defaultTimeout = 120000 @@ -173,25 +173,40 @@ describe('K8', () => { await expect(k8.hasDir(podName, containerName, '/tmp')).resolves.toBeTruthy() }, defaultTimeout) - it('should be able to copy a file to and from a container', async () => { - const pods = await k8.waitForPodReady([`app=${podLabelValue}`], 1, 20) - expect(pods.length).toStrictEqual(1) - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8-')) - const destDir = '/tmp' - const srcPath = 'test/data/pem/keys/a-private-node0.pem' - const destPath = `${destDir}/a-private-node0.pem` + describe.each([ + { localFilePath: 'test/data/pem/keys/a-private-node0.pem' }, + { localFilePath: 'test/data/build-v0.54.0-alpha.4.zip' } + ])('test copyTo and copyFrom', (input) => { + it('should be able to copy a file to and from a container', async () => { + const pods = await k8.waitForPodReady([`app=${podLabelValue}`], 1, 20) + expect(pods.length).toStrictEqual(1) + const localTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'k8-test')) + const remoteTmpDir = '/tmp' + const localFilePath = input.localFilePath + const fileName = path.basename(localFilePath) + const remoteFilePath = `${remoteTmpDir}/${fileName}` + const originalFileData = fs.readFileSync(localFilePath) + const originalFileHash = crypto.createHash('sha384').update(originalFileData).digest('hex') + const originalStat = fs.statSync(localFilePath) - // upload the file - await expect(k8.copyTo(podName, containerName, srcPath, destDir)).resolves.toBeTruthy() + // upload the file + await expect(k8.copyTo(podName, containerName, localFilePath, remoteTmpDir)).resolves.toBeTruthy() - // download the same file - await expect(k8.copyFrom(podName, containerName, destPath, tmpDir)).resolves.toBeTruthy() + // download the same file + await expect(k8.copyFrom(podName, containerName, remoteFilePath, localTmpDir)).resolves.toBeTruthy() + const downloadedFilePath = path.join(localTmpDir, fileName) + const downloadedFileData = fs.readFileSync(downloadedFilePath) + const downloadedFileHash = crypto.createHash('sha384').update(downloadedFileData).digest('hex') + const downloadedStat = fs.statSync(downloadedFilePath) - // rm file inside the container - await expect(k8.execContainer(podName, containerName, ['rm', '-f', destPath])).resolves + expect(downloadedStat.size, 'downloaded file size should match original file size').toEqual(originalStat.size) + expect(downloadedFileHash, 'downloaded file hash should match original file hash').toEqual(originalFileHash) + // rm file inside the container + await expect(k8.execContainer(podName, containerName, ['rm', '-f', remoteFilePath])).resolves - fs.rmdirSync(tmpDir, { recursive: true }) - }, defaultTimeout) + fs.rmdirSync(localTmpDir, { recursive: true }) + }, defaultTimeout) + }) it('should be able to port forward gossip port', (done) => { const podName = Templates.renderNetworkPodName('node1') diff --git a/test/e2e/setup-e2e.sh b/test/e2e/setup-e2e.sh index 0ae643dfa..89a7420a2 100755 --- a/test/e2e/setup-e2e.sh +++ b/test/e2e/setup-e2e.sh @@ -21,3 +21,4 @@ kind create cluster -n "${SOLO_CLUSTER_NAME}" --image "${KIND_IMAGE}" || exit 1 solo init --namespace "${SOLO_NAMESPACE}" -i node1 -s "${SOLO_CLUSTER_SETUP_NAMESPACE}" -d "${SOLO_FST_CHARTS_DIR}" --dev || exit 1 # cache args for subsequent commands solo cluster setup || exit 1 helm list --all-namespaces +sleep 10 # give time for fullstack-setup to finish deploying diff --git a/test/test_util.js b/test/test_util.js index 22f962061..59311b582 100644 --- a/test/test_util.js +++ b/test/test_util.js @@ -54,7 +54,7 @@ import { AccountCommand } from '../src/commands/account.mjs' export const testLogger = logging.NewLogger('debug', true) export const TEST_CLUSTER = 'solo-e2e' -export const HEDERA_PLATFORM_VERSION_TAG = 'v0.53.2' +export const HEDERA_PLATFORM_VERSION_TAG = 'v0.54.0-alpha.4' export function getTestCacheDir (testName) { const baseDir = 'test/data/tmp' diff --git a/version.mjs b/version.mjs index 2d6298b7b..34a14e4ae 100644 --- a/version.mjs +++ b/version.mjs @@ -22,4 +22,4 @@ export const JAVA_VERSION = '21.0.1+12' export const HELM_VERSION = 'v3.14.2' export const FST_CHART_VERSION = 'v0.30.0' -export const HEDERA_PLATFORM_VERSION = 'v0.53.2' +export const HEDERA_PLATFORM_VERSION = 'v0.54.0-alpha.4'