Skip to content

release: v0.10.6 — Restores background-daemon mode in the pre-built b… #185

release: v0.10.6 — Restores background-daemon mode in the pre-built b…

release: v0.10.6 — Restores background-daemon mode in the pre-built b… #185

Workflow file for this run

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 }}