diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c9b4630..b08dbfd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,7 +28,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' cache: 'npm' cache-dependency-path: client-vscode/package-lock.json diff --git a/.github/workflows/npm-audit-fix.yml b/.github/workflows/npm-audit-fix.yml new file mode 100644 index 0000000..2b8ab79 --- /dev/null +++ b/.github/workflows/npm-audit-fix.yml @@ -0,0 +1,176 @@ +name: npm audit fix + +on: + schedule: + - cron: '0 6 * * 2' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: npm-audit-fix + cancel-in-progress: false + +jobs: + # Runs updated third-party code (npm ci, tsc), so it gets no write access and no persisted credentials. + audit-fix: + runs-on: ubuntu-latest + outputs: + fixed: ${{ steps.after.outputs.fixed }} + defaults: + run: + working-directory: client-vscode + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: client-vscode/package-lock.json + + - name: Install dependencies + run: npm ci --ignore-scripts + + - name: Audit before fix + id: before + run: | + f="$RUNNER_TEMP/audit-before.json" + npm audit --json > "$f" || true + if ! jq -e '.metadata.vulnerabilities.total | numbers' "$f" > /dev/null 2>&1; then + echo "::error::npm audit did not return a valid report" + cat "$f" + exit 1 + fi + echo "count=$(jq '.metadata.vulnerabilities.total' "$f")" >> "$GITHUB_OUTPUT" + + # Without --force: only in-range updates, so no breaking major bumps of direct deps. + # Exits non-zero whenever vulnerabilities remain, so only real npm errors are surfaced. + - name: Apply npm audit fix + if: steps.before.outputs.count != '0' + run: | + log="$RUNNER_TEMP/audit-fix.log" + if ! npm audit fix --ignore-scripts > "$log" 2>&1; then + if grep -qE '^npm (ERR!|error)' "$log"; then + echo "::warning::npm audit fix reported errors; see log below" + fi + fi + cat "$log" + + - name: Audit after fix + id: after + if: steps.before.outputs.count != '0' + env: + BEFORE: ${{ steps.before.outputs.count }} + run: | + f="$RUNNER_TEMP/audit-after.json" + npm audit --json > "$f" || true + if ! jq -e '.metadata.vulnerabilities.total | numbers' "$f" > /dev/null 2>&1; then + echo "::error::npm audit did not return a valid report after fix" + cat "$f" + exit 1 + fi + after=$(jq '.metadata.vulnerabilities.total' "$f") + if [ "$after" -lt "$BEFORE" ] && ! git diff --quiet -- package.json package-lock.json; then + echo "fixed=true" >> "$GITHUB_OUTPUT" + else + echo "fixed=false" >> "$GITHUB_OUTPUT" + fi + + - name: Verify fixed tree installs and compiles + if: steps.after.outputs.fixed == 'true' + run: | + npm ci --ignore-scripts + npm run compile + + - name: Build PR body and patch + if: steps.after.outputs.fixed == 'true' + run: | + out="$RUNNER_TEMP/audit-fix-out" + mkdir -p "$out" + git diff -- package.json package-lock.json > "$out/fix.patch" + { + echo "Automated \`npm audit fix\` for vulnerable dependencies that Dependabot did not update." + echo + echo "### Fixed" + jq -r --slurpfile after "$RUNNER_TEMP/audit-after.json" ' + ($after[0].vulnerabilities // {}) as $a + | (.vulnerabilities // {}) | to_entries[] + | select(.key as $k | $a | has($k) | not) + | ([.value.via[] | objects | .title] | unique | join("; ")) as $t + | "- `\(.key)` (\(.value.severity)): " + + (if $t == "" then "via " + ([.value.via[] | strings] | unique | join(", ")) else $t end) + ' "$RUNNER_TEMP/audit-before.json" | grep . || echo "_None_" + echo + echo "### Still vulnerable (needs a major bump or \`overrides\`)" + jq -r ' + (.vulnerabilities // {}) | to_entries[] + | "- `\(.key)` (\(.value.severity)) via \([.value.via[] | if type == "string" then . else .name end] | unique | join(", "))" + ' "$RUNNER_TEMP/audit-after.json" | grep . || echo "_None_" + echo + echo "### Changes" + echo '```' + git diff --stat -- package.json package-lock.json + echo '```' + } > "$out/pr-body.md" + cat "$out/pr-body.md" >> "$GITHUB_STEP_SUMMARY" + + - name: Upload fix + if: steps.after.outputs.fixed == 'true' + uses: actions/upload-artifact@v4 + with: + name: audit-fix + path: ${{ runner.temp }}/audit-fix-out/ + retention-days: 1 + + - name: Report unfixable vulnerabilities + if: steps.before.outputs.count != '0' && steps.after.outputs.fixed != 'true' + run: | + { + echo "### npm audit: vulnerabilities with no in-range fix" + echo '```' + npm audit || true + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + # Never runs project code; only applies the verified patch and opens the PR. + # AUDIT_FIX_TOKEN (not GITHUB_TOKEN) so the PR triggers build.yml. + create-pr: + needs: audit-fix + if: needs.audit-fix.outputs.fixed == 'true' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Download fix + uses: actions/download-artifact@v4 + with: + name: audit-fix + path: ${{ runner.temp }}/audit-fix + + - name: Apply patch + run: git apply "$RUNNER_TEMP/audit-fix/fix.patch" + + - name: Create pull request + uses: peter-evans/create-pull-request@v7 + with: + token: ${{ secrets.AUDIT_FIX_TOKEN }} + branch: automation/npm-audit-fix + delete-branch: true + add-paths: | + client-vscode/package.json + client-vscode/package-lock.json + commit-message: Fix vulnerable npm dependencies via npm audit fix + title: Fix vulnerable npm dependencies (npm audit fix) + body-path: ${{ runner.temp }}/audit-fix/pr-body.md + labels: dependencies, security diff --git a/client-vscode/package-lock.json b/client-vscode/package-lock.json index 815fe98..3cc2987 100644 --- a/client-vscode/package-lock.json +++ b/client-vscode/package-lock.json @@ -304,9 +304,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -324,9 +321,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -344,9 +338,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -364,9 +355,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -384,9 +372,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1859,10 +1844,11 @@ } }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1"