diff --git a/.github/workflows/BuildAndDeploy.yml b/.github/workflows/BuildAndDeploy.yml index daa03260b8..6323859920 100644 --- a/.github/workflows/BuildAndDeploy.yml +++ b/.github/workflows/BuildAndDeploy.yml @@ -2,9 +2,9 @@ name: Build and Deploy on: push: - branches: [ "*" ] + branches: ["*"] pull_request: - branches: [ "main" ] + branches: ["main"] workflow_dispatch: jobs: @@ -16,7 +16,7 @@ jobs: - name: Run ShellCheck uses: ludeeus/action-shellcheck@master with: - scandir: './ops' + scandir: "./ops" fossa-check: runs-on: ubuntu-latest @@ -62,19 +62,19 @@ jobs: - name: Run Trivy vulnerability scanner in repo mode uses: aquasecurity/trivy-action@master with: - scan-type: 'fs' + scan-type: "fs" ignore-unfixed: true - exit-code: '1' - severity: 'CRITICAL' + exit-code: "1" + severity: "CRITICAL" trivyignores: .trivyignore - name: Run Trivy vulnerability scanner in IaC mode uses: aquasecurity/trivy-action@master with: - scan-type: 'config' - exit-code: '1' + scan-type: "config" + exit-code: "1" ignore-unfixed: true - severity: 'CRITICAL' + severity: "CRITICAL" trivyignores: .trivyignore backend-check: runs-on: ubuntu-latest @@ -140,6 +140,8 @@ jobs: npm install -g pnpm pnpm install --no-frozen-lockfile pnpm lint + - name: Audit for vulnerabilities + run: pnpm dlx audit-ci@^6 --critical --report-type full - name: Testing and coverage run: | pnpm coverage @@ -241,11 +243,11 @@ jobs: uses: aquasecurity/trivy-action@master with: image-ref: ${{ steps.login-ecr.outputs.registry }}/heartbeat_backend:latest - format: 'table' - exit-code: '1' + format: "table" + exit-code: "1" ignore-unfixed: true - severity: 'CRITICAL,HIGH' - trivyignores: '.trivyignore' + severity: "CRITICAL,HIGH" + trivyignores: ".trivyignore" # - name: Push for Backend # env: # REGISTRY: ${{ steps.login-ecr.outputs.registry }} @@ -290,11 +292,11 @@ jobs: uses: aquasecurity/trivy-action@master with: image-ref: ${{ steps.login-ecr.outputs.registry }}/heartbeat_frontend:latest - format: 'table' - exit-code: '1' + format: "table" + exit-code: "1" ignore-unfixed: true - severity: 'CRITICAL,HIGH' - trivyignores: '.trivyignore' + severity: "CRITICAL,HIGH" + trivyignores: ".trivyignore" # - name: Push for Frontend # env: @@ -341,11 +343,11 @@ jobs: uses: aquasecurity/trivy-action@master with: image-ref: ${{ steps.login-ecr.outputs.registry }}/heartbeat_stub:latest - format: 'table' - exit-code: '1' + format: "table" + exit-code: "1" ignore-unfixed: true - severity: 'CRITICAL,HIGH' - trivyignores: '.trivyignore' + severity: "CRITICAL,HIGH" + trivyignores: ".trivyignore" # - name: Push for MockServer # if: ${{ contains(github.event.head_commit.message, '[stub]') }} @@ -517,4 +519,3 @@ jobs: # docker pull ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_HOST }}/heartbeat_backend:$IMAGE_TAG # docker pull ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_HOST }}/heartbeat_frontend:$IMAGE_TAG # docker-compose up -d frontend - diff --git a/frontend/.husky/pre-push b/frontend/.husky/pre-push index 83a4f62be8..86e410b793 100755 --- a/frontend/.husky/pre-push +++ b/frontend/.husky/pre-push @@ -1,5 +1,5 @@ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" -cd frontend && npm run coverage +cd frontend && npm run audit && npm run coverage cd ../backend && ./gradlew clean check diff --git a/frontend/.license-compliance.json b/frontend/.license-compliance.json new file mode 100644 index 0000000000..85a2f992bc --- /dev/null +++ b/frontend/.license-compliance.json @@ -0,0 +1,17 @@ +{ + "allowed": [ + "UNKNOWN", + "Unlicense", + "MIT", + "ISC", + "0BSD", + "BSD-2-Clause", + "BSD-3-Clause", + "Apache-2.0", + "Python-2.0", + "CC-BY-4.0", + "CC-BY-3.0", + "WTFPL", + "CC0-1.0" + ] +} diff --git a/frontend/audit-ci.jsonc b/frontend/audit-ci.jsonc new file mode 100644 index 0000000000..1e35f8d006 --- /dev/null +++ b/frontend/audit-ci.jsonc @@ -0,0 +1,6 @@ +{ + // $schema provides code completion hints to IDEs. + "$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json", + "moderate": true, + "allowlist": ["@adobe/css-tools"] +} diff --git a/frontend/package.json b/frontend/package.json index 639a3c204f..d01213c4be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,12 +11,13 @@ "preview": "vite preview", "lint": "eslint -c .eslintrc.json ./ && prettier --check ./", "fix": "eslint -c .eslintrc.json --fix && npx prettier --write . --ignore-unknown", + "audit": "npx audit-ci@^6 --config ./audit-ci.jsonc", "test": "jest", "coverage": "jest --env=jsdom --watchAll=false --coverage", "e2e:open": "TZ='PRC' cypress open", "e2e": "TZ='PRC' cypress run --spec cypress/", "prepare": "cd .. && husky install frontend/.husky", - "license-compliance": "license-compliance -r detailed --allow='Unlicense;MIT;ISC;0BSD;BSD-2-Clause;BSD-3-Clause;Apache-2.0;Python-2.0;CC-BY-4.0;CC-BY-3.0;WTFPL;CC0-1.0'" + "license-compliance": "license-compliance -r detailed" }, "lint-staged": { "**/*": [ @@ -55,6 +56,7 @@ "@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/parser": "^5.57.1", "@vitejs/plugin-react-swc": "^3.2.0", + "audit-ci": "^6.6.1", "autoprefixer": "^10.4.14", "cypress": "^13.6.0", "eslint": "^8.37.0", @@ -70,7 +72,7 @@ "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.5.0", - "license-compliance": "^1.2.5", + "license-compliance": "^2.0.1", "lint-staged": "^13.2.0", "msw": "^1.3.2", "prettier": "2.8.7", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 910f16711d..1a67f9c786 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -91,6 +91,9 @@ devDependencies: '@vitejs/plugin-react-swc': specifier: ^3.2.0 version: 3.3.2(vite@5.0.5) + audit-ci: + specifier: ^6.6.1 + version: 6.6.1 autoprefixer: specifier: ^10.4.14 version: 10.4.15(postcss@8.4.32) @@ -137,8 +140,8 @@ devDependencies: specifier: ^29.5.0 version: 29.6.3 license-compliance: - specifier: ^1.2.5 - version: 1.2.5 + specifier: ^2.0.1 + version: 2.0.1(typescript@5.1.6) lint-staged: specifier: ^13.2.0 version: 13.3.0 @@ -3126,6 +3129,7 @@ packages: /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + dev: false /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} @@ -3386,6 +3390,14 @@ packages: dev: true optional: true + /JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + dependencies: + jsonparse: 1.3.1 + through: 2.3.8 + dev: true + /abab@2.0.6: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} dev: true @@ -3671,6 +3683,21 @@ packages: engines: {node: '>= 4.0.0'} dev: true + /audit-ci@6.6.1: + resolution: {integrity: sha512-zqZEoYfEC4QwX5yBkDNa0h7YhZC63HWtKtP19BVq+RS0dxRBInfmHogxe4VUeOzoADQjuTLZUI7zp3Pjyl+a5g==} + engines: {node: '>=12.9.0'} + hasBin: true + dependencies: + JSONStream: 1.3.5 + cross-spawn: 7.0.3 + escape-string-regexp: 4.0.0 + event-stream: 4.0.1 + jju: 1.4.0 + readline-transform: 1.0.0 + semver: 7.5.4 + yargs: 17.7.2 + dev: true + /autoprefixer@10.4.15(postcss@8.4.32): resolution: {integrity: sha512-KCuPB8ZCIqFdA4HwKXsvz7j6gvSDNhDP7WnUjBleRkKjPdvCmHFuQ77ocavI8FT6NdvlBnE2UFr2H4Mycn8Vew==} engines: {node: ^10 || ^12 || >=14} @@ -4180,6 +4207,11 @@ packages: engines: {node: '>=16'} dev: true + /commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + dev: true + /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: true @@ -4189,11 +4221,6 @@ packages: engines: {node: '>= 6'} dev: true - /commander@9.4.0: - resolution: {integrity: sha512-sRPT+umqkz90UA8M1yqYfnHlZA7fF6nSphDtxeywPZ49ysjxDQybzk13CL+mXekDRG92skbcqCLVovuCusNmFw==} - engines: {node: ^12.20.0 || >=14} - dev: true - /common-tags@1.8.2: resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} engines: {node: '>=4.0.0'} @@ -4225,8 +4252,8 @@ packages: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} dev: true - /cosmiconfig@7.0.1: - resolution: {integrity: sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==} + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} dependencies: '@types/parse-json': 4.0.0 @@ -4234,18 +4261,23 @@ packages: parse-json: 5.2.0 path-type: 4.0.0 yaml: 1.10.2 - dev: true + dev: false - /cosmiconfig@7.1.0: - resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} - engines: {node: '>=10'} + /cosmiconfig@8.3.6(typescript@5.1.6): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true dependencies: - '@types/parse-json': 4.0.0 import-fresh: 3.3.0 + js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - yaml: 1.10.2 - dev: false + typescript: 5.1.6 + dev: true /create-jest@29.7.0(@types/node@18.17.8): resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} @@ -4532,6 +4564,10 @@ packages: webidl-conversions: 7.0.0 dev: true + /duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + dev: true + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -5161,6 +5197,18 @@ packages: engines: {node: '>=0.10.0'} dev: true + /event-stream@4.0.1: + resolution: {integrity: sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==} + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.0.7 + pause-stream: 0.0.11 + split: 1.0.1 + stream-combiner: 0.2.2 + through: 2.3.8 + dev: true + /eventemitter2@6.4.7: resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} dev: true @@ -5441,6 +5489,10 @@ packages: resolution: {integrity: sha512-/KxoyCnPM0GwYI4NN0Iag38Tqt+od3/mLuguepLgCAKPn0ZhC544nssAW0tG2/00zXEYl9W+7hwAIpLHo6Oc7Q==} dev: true + /from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + dev: true + /fs-extra@9.1.0: resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} engines: {node: '>=10'} @@ -6777,8 +6829,12 @@ packages: - ts-node dev: true - /joi@17.6.1: - resolution: {integrity: sha512-Hl7/iBklIX345OCM1TiFSCZRVaAOLDGlWCp0Df2vWYgBgjkezaR7Kvm3joBciBHQjZj5sxXs859r6eqsRSlG8w==} + /jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + dev: true + + /joi@17.11.0: + resolution: {integrity: sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==} dependencies: '@hapi/hoek': 9.3.0 '@hapi/topo': 5.1.0 @@ -6910,6 +6966,11 @@ packages: graceful-fs: 4.2.11 dev: true + /jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + dev: true + /jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} @@ -6958,22 +7019,23 @@ packages: type-check: 0.4.0 dev: true - /license-compliance@1.2.5: - resolution: {integrity: sha512-vvtnMsWRQmd1RSBroKZwXSIMQ5cGScyBZg/WxMlFLtWHIfhjZ3Unmcgz6klw4TZV94Jire9VTcGEEOAFsRIsZQ==} - engines: {node: '>=12.13.0'} + /license-compliance@2.0.1(typescript@5.1.6): + resolution: {integrity: sha512-c6w74uKnDgvbW3opy8NFMeX1pgWsHl8dLOu7Bc85s9eurXYCzF5x+Sj7gvZJIjtsYu3vQ7aYYpjc23ru+4onPA==} + engines: {node: '>=14.17.0'} hasBin: true dependencies: chalk: 4.1.2 - commander: 9.4.0 - cosmiconfig: 7.0.1 + commander: 11.1.0 + cosmiconfig: 8.3.6(typescript@5.1.6) debug: 4.3.4(supports-color@8.1.1) - joi: 17.6.1 + joi: 17.11.0 spdx-expression-parse: 3.0.1 spdx-satisfies: 5.0.1 - tslib: 2.4.0 + tslib: 2.6.2 xmlbuilder: 15.1.1 transitivePeerDependencies: - supports-color + - typescript dev: true /lilconfig@2.1.0: @@ -7158,6 +7220,10 @@ packages: tmpl: 1.0.5 dev: true + /map-stream@0.0.7: + resolution: {integrity: sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==} + dev: true + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -7571,6 +7637,12 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + /pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + dependencies: + through: 2.3.8 + dev: true + /pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} dev: true @@ -7865,6 +7937,11 @@ packages: picomatch: 2.3.1 dev: true + /readline-transform@1.0.0: + resolution: {integrity: sha512-7KA6+N9IGat52d83dvxnApAWN+MtVb1MiVuMR/cf1O4kYsJG+g/Aav0AHcHKsb6StinayfPLne0+fMX2sOzAKg==} + engines: {node: '>=6'} + dev: true + /redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -8346,6 +8423,12 @@ packages: spdx-ranges: 2.1.1 dev: true + /split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + dependencies: + through: 2.3.8 + dev: true + /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true @@ -8380,6 +8463,13 @@ packages: internal-slot: 1.0.5 dev: true + /stream-combiner@0.2.2: + resolution: {integrity: sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==} + dependencies: + duplexer: 0.1.2 + through: 2.3.8 + dev: true + /strict-event-emitter@0.2.8: resolution: {integrity: sha512-KDf/ujU8Zud3YaLtMCcTI4xkZlZVIYxTLr+XIULexP+77EEVWixeXroLUXQXiVtH4XH2W7jr/3PT1v3zBuvc3A==} dependencies: @@ -8746,10 +8836,6 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true - /tslib@2.4.0: - resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==} - dev: true - /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: true @@ -9422,6 +9508,7 @@ packages: /yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + dev: false /yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} diff --git a/ops/check.sh b/ops/check.sh index 38c4db9d28..c4ab89db64 100755 --- a/ops/check.sh +++ b/ops/check.sh @@ -54,6 +54,7 @@ backend_check() { frontend_check(){ cd frontend + pnpm dlx audit-ci@^6 --config ./audit-ci.jsonc pnpm install --no-frozen-lockfile pnpm lint pnpm coverage