release: v0.10.6 — Restores background-daemon mode in the pre-built b… #185
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| # Least privilege by default: the lint job runs untrusted PR code, so it | |
| # gets read-only. The PR-comment write is granted narrowly on the separate | |
| # `comment` job below, which never checks out PR code. | |
| permissions: | |
| contents: read | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| # Expose the rendered summary + verdict so the separate `comment` job | |
| # (which holds the PR-comment write scope) can post it, without this | |
| # untrusted-code job ever holding write. | |
| outputs: | |
| summary_body: ${{ steps.summary.outputs.body }} | |
| all_ok: ${{ steps.summary.outputs.all_ok }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Node.js | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '>=24.18.0' | |
| - name: Install lint tools | |
| # Install eslint + eslint-plugin-security from the committed | |
| # package-lock.json (reproducible, lockfile-pinned) rather than | |
| # resolving them ad-hoc at run time. | |
| run: npm ci | |
| # ── Vendor freshness ─────────────────────────────────────── | |
| # Fails CI when the pinned blamejs in vendor/MANIFEST.json doesn't | |
| # match the latest upstream release. Loud signal so a stale vendored | |
| # package doesn't silently miss security fixes from blamejs. | |
| - name: Check vendored blamejs is current | |
| id: vendor_freshness | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: node scripts/check-blamejs-version.js | |
| # ── ESLint ───────────────────────────────────────────────── | |
| - name: Run ESLint | |
| id: eslint | |
| run: | | |
| cat > eslint.ci.config.js << 'ESLINT_CONFIG' | |
| var security = require("eslint-plugin-security"); | |
| module.exports = [ | |
| { | |
| ignores: [ | |
| "tests/", "node_modules/**", "vendor/**", "build/**", | |
| "eslint.ci.config.js", | |
| ], | |
| }, | |
| { | |
| files: ["**/*.js"], | |
| plugins: { security: security }, | |
| linterOptions: { reportUnusedDisableDirectives: "off" }, | |
| languageOptions: { | |
| ecmaVersion: 2022, | |
| sourceType: "commonjs", | |
| globals: { | |
| require: "readonly", module: "readonly", exports: "readonly", | |
| __dirname: "readonly", __filename: "readonly", | |
| process: "readonly", console: "readonly", Buffer: "readonly", | |
| setTimeout: "readonly", setInterval: "readonly", setImmediate: "readonly", | |
| clearTimeout: "readonly", clearInterval: "readonly", | |
| URL: "readonly", URLSearchParams: "readonly", | |
| global: "readonly", crypto: "readonly", | |
| TextEncoder: "readonly", TextDecoder: "readonly", | |
| Atomics: "readonly", SharedArrayBuffer: "readonly", Int32Array: "readonly", | |
| }, | |
| }, | |
| rules: { | |
| // Built-in | |
| "no-unused-vars": ["warn", { argsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }], | |
| "no-console": "off", | |
| "no-eval": "error", | |
| "no-implied-eval": "error", | |
| "no-new-func": "error", | |
| "no-self-compare": "error", | |
| "no-constructor-return": "error", | |
| "no-new-wrappers": "error", | |
| "no-throw-literal": "error", | |
| // Security plugin | |
| "security/detect-eval-with-expression": "error", | |
| "security/detect-child-process": "warn", | |
| "security/detect-unsafe-regex": "error", | |
| "security/detect-buffer-noassert": "error", | |
| "security/detect-new-buffer": "error", | |
| "security/detect-possible-timing-attacks": "warn", | |
| "security/detect-pseudoRandomBytes": "warn", | |
| "security/detect-object-injection": "off", | |
| "security/detect-non-literal-fs-filename": "off", | |
| "security/detect-non-literal-require": "off", | |
| "security/detect-non-literal-regexp": "off", | |
| }, | |
| }, | |
| ]; | |
| ESLINT_CONFIG | |
| set +e | |
| OUTPUT=$(npx eslint --config eslint.ci.config.js . 2>&1) | |
| EXIT_CODE=$? | |
| set -e | |
| echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "output<<ESLINT_EOF" | |
| echo "$OUTPUT" | |
| echo "ESLINT_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "::error::ESLint found issues" | |
| fi | |
| exit $EXIT_CODE | |
| # ── Codebase-patterns (static-shape gates) ───────────────── | |
| # 38 grep-style detectors covering bug classes surfaced in the | |
| # upstream framework's release history plus the operator-facing | |
| # doc gates (docs-leak-vocab, current-version-stamp, | |
| # docs-secret-shape) that interface with the structured | |
| # release-notes pipeline. | |
| - name: Run codebase-patterns | |
| if: always() | |
| id: codebase_patterns | |
| run: | | |
| set +e | |
| OUTPUT=$(node --test scripts/test-codebase-patterns.js 2>&1) | |
| EXIT_CODE=$? | |
| set -e | |
| printf '%s\n' "$OUTPUT" | |
| { | |
| echo "output<<CBP_EOF" | |
| printf '%s\n' "$OUTPUT" | |
| echo "CBP_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "::error::codebase-patterns detector(s) tripped" | |
| fi | |
| exit $EXIT_CODE | |
| # ── Changelog drift gate ─────────────────────────────────── | |
| # Refuses to pass if (a) the current VERSION's release-notes | |
| # JSON is missing, or (b) CHANGELOG.md drifts from the JSON | |
| # tree. The release workflow re-runs the same render at tag | |
| # time; this gate surfaces drift in main / PR instead of late. | |
| - name: Run changelog gate | |
| if: always() | |
| id: changelog_gate | |
| run: | | |
| set +e | |
| OUTPUT=$(node scripts/check-changelog-extract.js 2>&1) | |
| EXIT_CODE=$? | |
| set -e | |
| printf '%s\n' "$OUTPUT" | |
| { | |
| echo "output<<CHG_EOF" | |
| printf '%s\n' "$OUTPUT" | |
| echo "CHG_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "::error::changelog drift gate failed" | |
| fi | |
| exit $EXIT_CODE | |
| # ── Vendored transitive SBOM projection drift gate ───────── | |
| # packages.blamejs.transitive in vendor/MANIFEST.json is a | |
| # mechanical projection of blamejs's own authoritative vendor | |
| # manifest, populated at the vendor step and consumed by the | |
| # release SBOM. Refuses to pass if the projection drifts from the | |
| # vendored tree (a blamejs refresh that skipped vendor-hash, or a | |
| # hand-edit), so a stale transitive surface can't reach the SBOM. | |
| - name: Run transitive SBOM projection gate | |
| if: always() | |
| id: transitive_gate | |
| run: | | |
| set +e | |
| OUTPUT=$(node scripts/project-transitive-manifest.js --check 2>&1) | |
| EXIT_CODE=$? | |
| set -e | |
| printf '%s\n' "$OUTPUT" | |
| { | |
| echo "output<<TRANS_EOF" | |
| printf '%s\n' "$OUTPUT" | |
| echo "TRANS_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "::error::transitive SBOM projection drift gate failed" | |
| fi | |
| exit $EXIT_CODE | |
| # ── Operator-facing doc version drift gate ───────────────── | |
| # The hand-typed version references in README / SECURITY / | |
| # RELEASING (the vendored blamejs version, its minor line, and | |
| # the Node floor) must match their source of truth | |
| # (vendor/MANIFEST.json + package.json engines). Refuses to pass | |
| # if a vendor bump left a stale version stranded in the prose. | |
| - name: Run doc version drift gate | |
| if: always() | |
| id: doc_version_gate | |
| run: | | |
| set +e | |
| OUTPUT=$(node scripts/check-doc-versions.js --check 2>&1) | |
| EXIT_CODE=$? | |
| set -e | |
| printf '%s\n' "$OUTPUT" | |
| { | |
| echo "output<<DOCVER_EOF" | |
| printf '%s\n' "$OUTPUT" | |
| echo "DOCVER_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "::error::operator-facing doc version drift gate failed" | |
| fi | |
| exit $EXIT_CODE | |
| # ── Hadolint (Dockerfile) ────────────────────────────────── | |
| # Guarded: only runs when a Dockerfile is present. Will activate | |
| # automatically once a Dockerfile is added for containerized builds. | |
| - name: Check for Dockerfile | |
| if: always() | |
| id: dockerfile_check | |
| run: | | |
| if [ -f Dockerfile ]; then | |
| echo "present=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "present=false" >> "$GITHUB_OUTPUT" | |
| echo "No Dockerfile found — skipping Hadolint" | |
| fi | |
| - name: Run Hadolint | |
| if: always() && steps.dockerfile_check.outputs.present == 'true' | |
| id: hadolint | |
| uses: hadolint/hadolint-action@2332a7b74a6de0dda2e2221d575162eba76ba5e5 # v3.3.0 | |
| with: | |
| dockerfile: Dockerfile | |
| failure-threshold: error | |
| - name: Capture Hadolint result | |
| if: always() | |
| id: hadolint_result | |
| env: | |
| DOCKERFILE_PRESENT: ${{ steps.dockerfile_check.outputs.present }} | |
| HADOLINT_RESULTS: ${{ steps.hadolint.outputs.results }} | |
| run: | | |
| if [ "$DOCKERFILE_PRESENT" != "true" ]; then | |
| echo "exit_code=0" >> "$GITHUB_OUTPUT" | |
| echo "status=skipped" >> "$GITHUB_OUTPUT" | |
| echo "output=" >> "$GITHUB_OUTPUT" | |
| elif [ -z "$HADOLINT_RESULTS" ]; then | |
| echo "exit_code=0" >> "$GITHUB_OUTPUT" | |
| echo "status=pass" >> "$GITHUB_OUTPUT" | |
| echo "output=" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "exit_code=1" >> "$GITHUB_OUTPUT" | |
| echo "status=fail" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "output<<HADOLINT_EOF" | |
| printf '%s\n' "$HADOLINT_RESULTS" | |
| echo "HADOLINT_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| fi | |
| # ── ShellCheck ───────────────────────────────────────────── | |
| - name: Run ShellCheck | |
| if: always() | |
| id: shellcheck | |
| run: | | |
| SCRIPTS=$(find . -name '*.sh' -not -path './node_modules/*' -not -path './tests/*' -not -path './build/*' -not -path './vendor/*' | sort) | |
| if [ -z "$SCRIPTS" ]; then | |
| echo "No shell scripts found — skipping" | |
| echo "exit_code=0" >> "$GITHUB_OUTPUT" | |
| echo "output=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Checking: $SCRIPTS" | |
| set +e | |
| OUTPUT=$(echo "$SCRIPTS" | xargs shellcheck --severity=warning --format=gcc 2>&1) | |
| EXIT_CODE=$? | |
| set -e | |
| echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "output<<SHELLCHECK_EOF" | |
| echo "$OUTPUT" | |
| echo "SHELLCHECK_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| if [ $EXIT_CODE -ne 0 ]; then | |
| echo "::error::ShellCheck found issues" | |
| fi | |
| exit $EXIT_CODE | |
| # ── Summary + PR comment ─────────────────────────────────── | |
| - name: Build summary | |
| if: always() | |
| id: summary | |
| env: | |
| ESLINT_OUTPUT: ${{ steps.eslint.outputs.output }} | |
| CBP_OUTPUT: ${{ steps.codebase_patterns.outputs.output }} | |
| CHG_OUTPUT: ${{ steps.changelog_gate.outputs.output }} | |
| HADOLINT_OUTPUT: ${{ steps.hadolint_result.outputs.output }} | |
| HADOLINT_STATUS: ${{ steps.hadolint_result.outputs.status }} | |
| SHELLCHECK_OUTPUT: ${{ steps.shellcheck.outputs.output }} | |
| ESLINT_OUTCOME_OK: ${{ steps.eslint.outcome == 'success' }} | |
| CBP_OUTCOME_OK: ${{ steps.codebase_patterns.outcome == 'success' }} | |
| CHG_OUTCOME_OK: ${{ steps.changelog_gate.outcome == 'success' }} | |
| SHELLCHECK_OUTCOME_OK: ${{ steps.shellcheck.outcome == 'success' }} | |
| run: | | |
| ESLINT_OK="$ESLINT_OUTCOME_OK" | |
| CBP_OK="$CBP_OUTCOME_OK" | |
| CHG_OK="$CHG_OUTCOME_OK" | |
| SHELLCHECK_OK="$SHELLCHECK_OUTCOME_OK" | |
| PASS="✅" | |
| FAIL="❌" | |
| SKIP="⏭️" | |
| ESLINT_ICON=$( [ "$ESLINT_OK" = "true" ] && echo "$PASS" || echo "$FAIL" ) | |
| CBP_ICON=$( [ "$CBP_OK" = "true" ] && echo "$PASS" || echo "$FAIL" ) | |
| CHG_ICON=$( [ "$CHG_OK" = "true" ] && echo "$PASS" || echo "$FAIL" ) | |
| SHELLCHECK_ICON=$( [ "$SHELLCHECK_OK" = "true" ] && echo "$PASS" || echo "$FAIL" ) | |
| if [ "$HADOLINT_STATUS" = "skipped" ]; then | |
| HADOLINT_ICON="$SKIP" | |
| HADOLINT_OK=true | |
| elif [ "$HADOLINT_STATUS" = "pass" ]; then | |
| HADOLINT_ICON="$PASS" | |
| HADOLINT_OK=true | |
| else | |
| HADOLINT_ICON="$FAIL" | |
| HADOLINT_OK=false | |
| fi | |
| ALL_OK=true | |
| [ "$ESLINT_OK" = "true" ] && [ "$CBP_OK" = "true" ] && [ "$CHG_OK" = "true" ] && [ "$HADOLINT_OK" = "true" ] && [ "$SHELLCHECK_OK" = "true" ] || ALL_OK=false | |
| { | |
| echo "body<<SUMMARY_EOF" | |
| if [ "$ALL_OK" = "true" ]; then | |
| echo "🎉 Linting finished with no errors or warnings 🎉" | |
| echo "" | |
| fi | |
| echo "| Check | Status |" | |
| echo "|-------|--------|" | |
| echo "| ESLint (JS + security) | $ESLINT_ICON |" | |
| echo "| Codebase-patterns (static-shape gates) | $CBP_ICON |" | |
| echo "| Changelog drift gate | $CHG_ICON |" | |
| echo "| Hadolint (Dockerfile) | $HADOLINT_ICON |" | |
| echo "| ShellCheck (shell scripts) | $SHELLCHECK_ICON |" | |
| if [ "$ESLINT_OK" != "true" ] && [ -n "$ESLINT_OUTPUT" ]; then | |
| echo "" | |
| echo "<details><summary>ESLint output</summary>" | |
| echo "" | |
| echo '```' | |
| printf '%s\n' "$ESLINT_OUTPUT" | |
| echo '```' | |
| echo "</details>" | |
| fi | |
| if [ "$CBP_OK" != "true" ] && [ -n "$CBP_OUTPUT" ]; then | |
| echo "" | |
| echo "<details><summary>Codebase-patterns output</summary>" | |
| echo "" | |
| echo '```' | |
| printf '%s\n' "$CBP_OUTPUT" | |
| echo '```' | |
| echo "</details>" | |
| fi | |
| if [ "$CHG_OK" != "true" ] && [ -n "$CHG_OUTPUT" ]; then | |
| echo "" | |
| echo "<details><summary>Changelog gate output</summary>" | |
| echo "" | |
| echo '```' | |
| printf '%s\n' "$CHG_OUTPUT" | |
| echo '```' | |
| echo "</details>" | |
| fi | |
| if [ "$HADOLINT_STATUS" = "fail" ] && [ -n "$HADOLINT_OUTPUT" ]; then | |
| echo "" | |
| echo "<details><summary>Hadolint output</summary>" | |
| echo "" | |
| echo '```' | |
| printf '%s\n' "$HADOLINT_OUTPUT" | |
| echo '```' | |
| echo "</details>" | |
| fi | |
| if [ "$SHELLCHECK_OK" != "true" ] && [ -n "$SHELLCHECK_OUTPUT" ]; then | |
| echo "" | |
| echo "<details><summary>ShellCheck output</summary>" | |
| echo "" | |
| echo '```' | |
| printf '%s\n' "$SHELLCHECK_OUTPUT" | |
| echo '```' | |
| echo "</details>" | |
| fi | |
| echo "SUMMARY_EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "all_ok=$ALL_OK" >> "$GITHUB_OUTPUT" | |
| # Step summary | |
| echo "### Lint Results" >> "$GITHUB_STEP_SUMMARY" | |
| echo "" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Check | Status |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| ESLint | $ESLINT_ICON |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Codebase-patterns | $CBP_ICON |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Changelog gate | $CHG_ICON |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| Hadolint | $HADOLINT_ICON |" >> "$GITHUB_STEP_SUMMARY" | |
| echo "| ShellCheck | $SHELLCHECK_ICON |" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Fail if any linter failed | |
| if: always() | |
| env: | |
| SUMMARY_ALL_OK: ${{ steps.summary.outputs.all_ok }} | |
| run: | | |
| if [ "$SUMMARY_ALL_OK" != "true" ]; then | |
| echo "One or more linters failed" | |
| exit 1 | |
| fi | |
| # PR-comment posting is isolated in its own job so pull-requests:write | |
| # never applies to the lint job that runs untrusted PR code, and this job | |
| # never checks out that code. Guarded to same-repo PRs: a fork PR gets a | |
| # read-only GITHUB_TOKEN, so a comment write would 403 and turn the check | |
| # red even when every linter passed. Skipping it there keeps fork-PR CI | |
| # honest — the required `lint` job is the sole verdict. | |
| comment: | |
| needs: lint | |
| if: ${{ always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Comment on PR | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const body = process.env.PR_BODY; | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => | |
| c.user.type === 'Bot' && c.body.includes('Linting') | |
| ); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body, | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| } | |
| env: | |
| PR_BODY: ${{ needs.lint.outputs.summary_body }} |