diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dc1e54..b89892b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,17 +129,19 @@ jobs: # the scan is lenient — planting one `new Function(` in dist makes it # exit 1. # - # SCOPE, STATED SO A GREEN RUN IS NOT READ AS MORE THAN IT IS. This + # THE TARGET IS THE PUBLISHED SURFACE, NOT `dist` (DS §2, v2.71.0). This # package's `files` list ships BOTH `src` and `dist`, and its default - # export is `"." : "./src/index.js"` — so a consumer doing - # `import … from 'markdownwizard-tools'` gets the SOURCE, and only the - # `./iife` subpath gets what this scan covers. src/ scans clean today - # (all 8 files, exit 0), so nothing is hiding there; whether §2's target - # should widen for a package whose default export is source is a - # standards question, raised in tgwab-standards#173 rather than decided - # here. Widening this to `dist src` is a one-word change and is green. + # export is `"." : "./src/index.js"` — so a consumer writing + # `import … from 'markdownwizard-tools'` gets the SOURCE, and `dist` + # alone covered only the `./iife` subpath. That was a control aimed at + # the path most consumers do not use. + # + # `scripts/no-eval.sh` is re-vendored in the same change because it HAD + # TO BE: the previous copy read only "$1", so `dist src` would have + # scanned dist and dropped src silently — the widened gate would have + # looked widened and covered exactly what it did before. - name: No runtime code generation in built output (§2) - run: ./scripts/no-eval.sh dist + run: ./scripts/no-eval.sh dist src # Full tree, deliberately not --omit=dev (DS §15): the dev half is the # build toolchain, and what it writes into dist/ is what ships. Excluding it diff --git a/scripts/no-eval.sh b/scripts/no-eval.sh index de07e4e..7d3fff6 100755 --- a/scripts/no-eval.sh +++ b/scripts/no-eval.sh @@ -15,14 +15,40 @@ # The CSP MUST NOT be loosened instead. set -euo pipefail -TARGET="${1:-dist}" +# TARGETS: every path this package actually ships (DS §2). Defaults to `dist`, +# which is what every caller passed before this took more than one. +# +# TWO DEFECTS THIS REPLACED, BOTH OF WHICH REPORTED GREEN OVER NOTHING: +# +# 1. It read only "$1". `no-eval.sh dist src` set TARGET=dist and DROPPED src +# silently — measured 2026-09-07 by planting `new Function(` in src/ and +# running all three forms: `src` alone exits 1, `dist` alone exits 0, and +# `dist src` printed "eval-free: OK (dist)" and exited 0. A gate widened +# that way looks widened, reports green, and scans exactly what it did +# before. +# +# 2. A target that did not exist reported "eval-free: OK" and exited 0, +# because the `|| true` below swallows grep's exit-2. So a build that never +# ran read as a clean scan. Latent rather than live — all 11 adopters run +# this after a build step — but it is the same failure class as (1), and a +# missing target is now a hard error. +if [ "$#" -eq 0 ]; then set -- dist; fi + +missing=() +for t in "$@"; do [ -e "$t" ] || missing+=("$t"); done +if [ "${#missing[@]}" -gt 0 ]; then + printf '::error::no-eval target not found: %s\n' "${missing[*]}" + printf 'A missing target is a failure, not a pass: it used to print "eval-free: OK" and exit 0, so a build that never ran read as a clean scan.\n' + exit 1 +fi + ALLOW=".eval-allowlist" PATTERN='(^|[^A-Za-z0-9_$])eval[[:space:]]*\(|new[[:space:]]+Function[[:space:]]*\(|(^|[^A-Za-z0-9_$.])Function[[:space:]]*\([[:space:]]*["'"'"'`]|set(Timeout|Interval|Immediate)[[:space:]]*\([[:space:]]*["'"'"'`]' hits="$(grep -nEr "$PATTERN" \ --include='*.js' --include='*.mjs' --include='*.cjs' --include='*.html' \ - "$TARGET" || true)" + "$@" || true)" if [ -s "$ALLOW" ]; then hits="$(printf '%s\n' "$hits" \ @@ -30,9 +56,9 @@ if [ -s "$ALLOW" ]; then fi if [ -n "$hits" ]; then - printf '::error::runtime code-generation found in %s (CSP has no %s)\n' "$TARGET" "'unsafe-eval'" + printf '::error::runtime code-generation found in %s (CSP has no %s)\n' "$*" "'unsafe-eval'" printf '%s\n' "$hits" exit 1 fi -printf 'eval-free: OK (%s)\n' "$TARGET" +printf 'eval-free: OK (%s)\n' "$*"