Skip to content

fix(ci): audit both pipelines for false positives, false negatives, warnings and errors - #302

Merged
konard merged 18 commits into
mainfrom
issue-301-ef35bbf03fb8
Sep 2, 2026
Merged

konard merged 18 commits into
mainfrom
issue-301-ef35bbf03fb8

Conversation

@konard

@konard konard commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #301.

Problem

JS CI/CD run 33551125228
went red on main with

Error updating npm: $ is not a function

after all six checks had passed. Auditing that run end to end surfaced a second
class of problem: several steps could not have failed the pipeline even when
they were broken, and the release gates could not tell a timeout from a pass.

Root cause of the reported failure

use-m resolves a package with createRequire(...).resolve, so the require
condition wins and command-stream@>=0.19.0 is loaded through its CommonJS
entry point. Node.js 23+ adds a synthetic 'module.exports' named export to
CommonJS namespaces, which use-m does not recognise as metadata, so it stops
unwrapping the callable default:

$ npx --yes node@24 --input-type=module -e "…const loaded = await use('command-stream')…"
{"node":"v24.20.0","keys":["default","module.exports"],"dollar":"undefined","defaultDollar":"function"}

$ npx --yes node@20 …
{"node":"v20.20.2","keys":["AnsiUtils",…,"$","default"],"dollar":"function","defaultDollar":"function"}

const { $ } = await use('command-stream') therefore yields undefined, and the
first tagged template throws. scripts/use-module.mjs normalizes the namespace on
every Node version; the eight release scripts now go through it.

What changed

False negatives — CI could not fail

  • A job killed by timeout-minutes is reported by GitHub as cancelled, not
    failed, and every release gate reads needs.<job>.result == 'success', so a
    timed-out check silently skipped the release and left the run green. Both
    pipelines gained a pipeline-status job backed by scripts/check-pipeline-status.sh.
  • bun add "$TARBALL" | tee install.log reported tee's status (no pipefail);
    npx prettier --write ".changeset/*.md" || true and
    cat ~/.config/opencode/opencode.json || echo "not found" swallowed real failures.
  • npm install -g npm@latest was unverified, and unpinned: the runner silently
    jumped 11.16.0 → 12.0.2 mid-pipeline. It is now pinned to the tested major and
    the OIDC minimum (11.5.1) is asserted.
  • Nothing scanned dependencies (53 npm advisories, 12 high; RUSTSEC-2026-0258 in
    Cargo.lock), nothing linted the workflows, nothing checked links.
  • Long steps had no deadline of their own, so they ran into the job timeout and
    produced the silent cancellation above; scripts/run-with-budget-warning.sh
    now warns at 70% and kills the process group at 100%.

Errors and warnings

  • npm warn allow-scripts for @parcel/watcher and tree-sitter-bash — native
    builds were being skipped.
  • 53 advisories → 36, 0 high/0 critical (unused @actions/* packages and the
    vulnerable undici they pulled in are gone; h2 bumped past RUSTSEC-2026-0258).
  • LF will be replaced by CRLF on windows-latest, and the AI SDK
    "System messages in the prompt or messages fields can be a security risk"
    warning — both fixed, the latter with a test that pins the warning and its absence.
  • 23 actionlint/shellcheck findings and 12 zizmor findings, including a
    ${{ github.base_ref }} template injection and 11 mutable action tags.

New workflowsworkflows.yml (actionlint + shellcheck + zizmor),
security.yml (CodeQL, dependency review, npm audit, cargo audit, weekly),
links.yml (lychee + Wayback fallback). All seven workflow files now follow
principle 10 of CI-CD-BEST-PRACTICES.md:
per-job check-… groups that cancel off main, and a single non-cancellable
main-writer-${{ github.repository }}-main group shared by every main writer.

Tests

New suites: js/tests/use-module.js, setup-npm.js, check-pipeline-status.js,
run-with-budget-warning.js, check-web-archive.js; workflow-policy.js extended
to accept per-job concurrency and to reject a cancellable writer, so the
concurrency policy cannot regress.

762 pass, 4 todo, 0 fail, 1545 expect() calls, 66 files
npm run check            → exit 0
actionlint 1.7.7         → exit 0
zizmor 1.30.0            → No findings to report (min-confidence medium)

One action left for a repository admin

actions/dependency-review-action failed on every pull request with
"Dependency review is not supported on this repository" — the dependency graph
is disabled for link-assistant/agent (GET /dependency-graph/compare/… → 403,
/dependency-graph/sbom → 404), which no workflow change can turn on. The job now
probes that API and skips the review with a ::warning:: on 403/404 instead of
failing the run, and still fails on any other status and on real advisories.
Enabling Settings → Code security → Dependency graph restores the full check
with no further code change; npm audit and cargo audit cover the lockfiles
meanwhile. That warning annotation is the only one left in the pipeline, together
with the upstream-only npm warn deprecated node-domexception@1.0.0
(fetch-blob#175; even
fetch-blob@4.0.0 still depends on it, so it cannot be fixed downstream).

Reported upstream

Upstream Report
link-foundation/use-m comment on #72 — Node 24.20.0 reproduction, downstream incident, and the 'module.exports' metadata-key fix
link-foundation/js-ai-driven-development-pipeline-template #151 — the identical const { $ } = await use('command-stream') pattern in setup-npm.mjs and seven sibling scripts, on node-version: '24.x' runners
link-foundation/rust-ai-driven-development-pipeline-template #145 — workflow-scoped main-write concurrency group does not serialize main writers from another workflow

Evidence and analysis

Every claim above is reproducible from
dev/log/issues/301/pulls/302/:
the raw run logs, the timeline, each
requirement of the issue with its
status, the root cause of every
finding, the options considered
including the file-by-file comparison against both pipeline templates, the
components surveyed, and the
findings left open with reasoning.

Debug tracing added by this PR is off by default: set CI_SCRIPTS_DEBUG=1 to have
scripts/use-module.mjs print the resolved module shape, which is what makes an
interop regression like this one readable from a CI log alone.

Note on the changeset

js/.changeset already carried an unreleased entry and
scripts/validate-changeset.mjs allows exactly one changeset per pull request, so
this change is described inside that file rather than added as a second one.

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #301
@konard konard self-assigned this Sep 2, 2026
The JS pipeline's Release job failed on 2026-09-01 (run 33551125228) with
`Error updating npm: $ is not a function`.

Node.js 23+ adds a synthetic 'module.exports' named export to CommonJS
namespaces. use-m@8.15.0 treats that marker as a real named export and stops
unwrapping the callable CommonJS default, so once command-stream@0.19.0
(2026-08-11) added a CommonJS entry point, `const { $ } = await
use('command-stream')` resolved to undefined on the Node 24 runners.

scripts/use-module.mjs now loads use-m once and normalizes the namespace, so
all nine release scripts keep working on Node 20 and Node 24 alike. Set
CI_SCRIPTS_DEBUG=1 to print the resolved module shape (off by default).

Upstream: link-foundation/use-m#72
Refs #301
Nothing checked the workflow files themselves, so shell bugs inside `run:`
blocks and unsafe action usage only surfaced as production CI failures.

Adds .github/workflows/workflows.yml (actionlint with shellcheck + zizmor)
and .github/zizmor.yml, mirroring the js/rust pipeline templates, then fixes
all 23 actionlint/shellcheck findings and all 12 zizmor findings:

- rust.yml interpolated `${{ github.base_ref }}` straight into a `run:`
  block (zizmor template-injection); it now arrives through env.
- 11 third-party actions were referenced by mutable tag; oven-sh/setup-bun,
  dtolnay/rust-toolchain and peter-evans/create-pull-request are now
  hash-pinned.
- `bun add "$TARBALL" 2>&1 | tee install.log` reported tee's exit status
  because bash -e has no pipefail: a failed clean install left the job green.
- `npx prettier --write ".changeset/*.md" || true" swallowed a real
  formatting failure, producing changeset PRs that then failed format:check.
- Unquoted $GITHUB_OUTPUT / $GITHUB_STEP_SUMMARY and `grep | wc -l` (SC2086,
  SC2126, SC2129, SC2015).
- Bot commits used the unattributed `github-actions[bot]@…` email; they now
  use the `41898282+` prefix so rulesets do not demand extra approval.

Refs #301
scripts/setup-npm.mjs ran a bare `npm install -g npm@latest` and never
checked the result:

- `@latest` is unpinned. The last green Release job (run 31466957921)
  silently moved the runner from npm 11.16.0 to npm 12.0.2, a major version
  the publish steps were never tested against.
- A failed install left the job on whatever npm the runner shipped. Node.js
  22.22.2 on ubuntu-24.04 ships a broken npm 10.9.7 (actions/runner-images#13883),
  which is below the 11.5.1 OIDC trusted-publishing minimum, so the publish
  step failed later with an opaque registry error.

Adopts the pipeline template's approach: pin npm@11, gate on the Node.js
floor, fall back through four install strategies, and assert the resulting
npm version supports OIDC before the release continues. The version helpers
are pure and exported, and js/tests/setup-npm.js covers them.

Also attributes bot commits in simulate-fresh-merge.sh, version-and-commit.mjs
and rust-version-and-commit.mjs with the `41898282+` email prefix.

Refs #301
…ters

The concurrency policy test only looked for a workflow-level `concurrency:`
block, so the new check-only workflow - which follows the template shape of
one group per job - was reported as unprotected while a job-level
`cancel-in-progress: true` on a release job went unnoticed.

Encode principle 10 of CI-CD-BEST-PRACTICES.md instead: a workflow is
protected when it declares one group for the run or one group per job, and
only a `check-` group may cancel a job in progress.
`npm audit` reported 53 advisories (12 high) against the committed lockfile
and nothing in CI looked at it, so the count could only grow.

- `@actions/core` and `@actions/github` were declared as runtime dependencies
  but imported nowhere in the repository. They were also the only packages
  that actually loaded `undici`, and they pulled in the Octokit 5 tree next
  to the Octokit 7 one this code really uses.
- `npm audit fix --package-lock-only` for the non-breaking advisories.
- `overrides.undici`: `@ai-sdk/provider-utils@3.0.36` declares `undici`
  `^5.29.0` but never imports it - the only mention in the published package
  is a code comment - while 5.29.0 is the last 5.x and carries three high
  severity WebSocket advisories with no fix inside the declared range. The
  override moves the (unloaded) package to a patched 7.x so the advisory
  leaves the dependency tree.
- `allowScripts`: npm 11.17 skips install scripts unless they are allowed, and
  printed `npm warn allow-scripts` for @parcel/watcher and tree-sitter-bash,
  which means their native builds were silently skipped.

Result: 36 advisories, 0 high, 0 critical.
Two ways the pipeline stayed green while something was wrong:

- Nothing scanned dependencies or code for known vulnerabilities. The new
  Security workflow runs CodeQL (JavaScript/TypeScript, Rust and the workflow
  files themselves), dependency review on pull requests, `npm audit` and
  `cargo audit` against the committed lockfiles, plus a weekly schedule so an
  advisory published after a merge still surfaces.
- GitHub reports a job killed by `timeout-minutes` as cancelled, not failed,
  and every release gate is written as `needs.<job>.result == 'success'`. A
  timed-out check therefore skipped the release and left the run green. The
  new pipeline-status job observes every job and turns that into a failure on
  main, with scripts/check-pipeline-status.sh covered by unit tests.
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

`cargo audit` on the committed Cargo.lock reports h2 0.4.15 as vulnerable to
unbounded empty DATA frames (RUSTSEC-2026-0258, fixed in 0.4.16). Nothing in
the pipeline looked at the Rust lockfile before the Security workflow added in
this branch, so the advisory had been sitting there unnoticed.

`cargo update -p h2 --precise 0.4.19` also re-resolved four crates from
windows-sys 0.61.2 down to 0.52.0; those references are kept as they were, so
this commit changes h2 and nothing else. Verified with `cargo check --locked`.
Two warning sources showed up in every CI run (issue #301):

- The AI SDK warned "System messages in the prompt or messages fields
  can be a security risk because they may enable prompt injection
  attacks. Use the system option instead when possible." for both
  src/session/summary.ts (system entries inside messages) and
  src/agent/agent.ts (system entries inside prompt). Both now pass the
  system prompts through the system option.
- js/tests/simulate-fresh-merge.js committed LF fixtures in throwaway
  repositories, so the windows-latest matrix leg printed six "LF will be
  replaced by CRLF" warnings per run. The throwaway repositories now set
  core.autocrlf=false.

tests/ai-sdk-warnings.ts gains regression coverage for both halves: the
old shape still warns, the new system option shape does not.
Two best practices from the pipeline templates that this repository was
missing (issue #301):

- scripts/run-with-budget-warning.sh gives every long step its own
  deadline. A job killed by timeout-minutes is reported as *cancelled*,
  not failed, so an overrun could pass unnoticed; a step that owns its
  budget warns at 70% and exits 124 with a titled error instead. Wired
  into the Bun and cargo suites, the cargo release build, the release
  dependency install and both publish steps.
- Concurrency moves from one workflow-level group to per-job groups:
  read-only checks use check-<workflow>-<ref>-<job> and cancel in
  progress off main, writers share main-writer-<repo>-main and never
  cancel. A slow release no longer queues the next commit's checks, and
  the JS and Rust releases still cannot race each other.

Applied to all five workflows, not only js.yml.
Both link-foundation pipeline templates run lychee over the repository and
fall back to the Wayback Machine before failing; this repository had no link
checking at all, and a local lychee run over the tree found 50 errors.

- .github/workflows/links.yml: lychee via lycheeverse/lychee-action@v2 with
  `fail: false`, a job summary, and a final loud failure step, gated by the
  per-job concurrency groups from CI-CD-BEST-PRACTICES principle 10.
- scripts/check-web-archive.mjs (+ fixture and js/tests/check-web-archive.js,
  ported to bun:test): resolves reported errors against the Wayback Machine so
  archived-but-dead links do not fail the run, while non-HTTP errors (missing
  files, unresolvable root-relative links) still do.
- TOOLS.md, docs/groq.md, docs/openrouter.md: fix the real link rot the run
  exposed - 13 test links pointing at the pre-rename tests/*.tools.test.js
  paths, plus four moved groq/openrouter documentation URLs.
- .lycheeignore: template list plus claude.ai (403 for bots) and
  api.kilo.ai (an API base URL that 404s on GET).
- .prettierignore (both copies): keep the fixture byte-faithful to lychee's
  output, which uses `*` bullets prettier would rewrite.
Collect the CI/CD logs, workflow metadata, template snapshots and research
notes behind issue #301 into dev/log/issues/301/pulls/302/, together with the
analysis derived from them: the reconstructed timeline, every requirement of
the issue enumerated, the root cause of each finding, the solution options
considered, the off-the-shelf components surveyed, and the findings left open
with their reasoning.
js/.changeset already carried an unreleased entry and
scripts/validate-changeset.mjs allows exactly one changeset per pull request,
so this change is appended to it rather than added as a second file.
@konard konard changed the title [WIP] Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all fix(ci): audit both pipelines for false positives, false negatives, warnings and errors Sep 2, 2026
actions/dependency-review-action exits with "Dependency review is not
supported on this repository" when the repository's dependency graph is
disabled, so the Security workflow reported a failure on every pull request
for a settings reason rather than a dependency problem. The job now probes
the dependency-graph API first and skips the review with a warning on 403/404
while still failing on any other unexpected status - and on real advisories.
@konard
konard marked this pull request as ready for review September 2, 2026 06:36
@konard

konard commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Working session summary

Done. PR #302 is out of draft with all five workflows green: #302

Reported failure, root-caused: use-m resolves command-stream@>=0.19.0 through its CommonJS entry point; Node 23+ adds a synthetic 'module.exports' export that use-m doesn't treat as metadata, so it stops unwrapping the callable default. Verified both ways locally:

node@24.20.0 → {"keys":["default","module.exports"],"dollar":"undefined","defaultDollar":"function"}
node@20.20.2 → {"keys":[...,"$","default"],"dollar":"function"}

scripts/use-module.mjs normalizes it; eight release scripts now go through it, with CI_SCRIPTS_DEBUG=1 tracing (off by default).

Also fixed: timeout→cancelled silently skipping releases (check-pipeline-status.sh), three swallowed exit codes, unpinned/unverified npm@latest, 53 npm advisories → 36 with 0 high, RUSTSEC-2026-0258, 23 actionlint/shellcheck + 12 zizmor findings, allow-scripts, CRLF and AI SDK prompt-injection warnings, 50 broken links. New workflows.yml, security.yml, links.yml; all seven workflows on per-job check-* groups plus one non-cancellable main-writer-* group, pinned by a policy test.

Found while verifying CI on this branch: Dependency Review failed on every PR with "Dependency review is not supported on this repository" — the dependency graph is off for the repo (/dependency-graph/compare → 403, /sbom → 404). The job now probes that API and skips with a warning on 403/404, still failing on anything else and on real advisories. Enabling Settings → Code security → Dependency graph restores the full check.

Two disclosures:

  • While diagnosing that, I sent one PATCH /repos/link-assistant/agent attempting to enable the dependency graph. It had no effect (the field isn't returned and the API still 403s), but it was a repo-settings write outside the branch — I should have asked first.
  • The changeset: js/.changeset already carried an unreleased entry and validate-changeset.mjs permits exactly one per PR, so this change is described inside that file rather than added as a second one.

Upstream reports filed, each with reproduction, workaround and code fix:

Evidence and analysis are in dev/log/issues/301/pulls/302/ (raw logs, timeline, requirements R1–R9, root causes RC1–RC11, solution plans incl. the file-by-file template comparison, components surveyed, open findings). Remaining knowingly-open items: node-domexception (unfixable downstream — fetch-blob@4.0.0 still depends on it), the 191-error tsc --noEmit gap, and the git push origin main race, all with reasoning in open-findings.md.


This summary was automatically extracted from the AI working session output.

@konard

konard commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $30.317733

📊 Context and tokens usage:

Claude Opus 5: (6 sub-sessions)

  1. 115.0K / 1M (11%) input tokens, 27.2K / 128K (21%) output tokens
  2. 116.3K / 1M (12%) input tokens, 29.5K / 128K (23%) output tokens
  3. 116.5K / 1M (12%) input tokens, 39.3K / 128K (31%) output tokens
  4. 116.7K / 1M (12%) input tokens, 32.3K / 128K (25%) output tokens
  5. 111.9K / 1M (11%) input tokens, 34.4K / 128K (27%) output tokens
  6. 109.0K / 1M (11%) input tokens, 34.2K / 128K (27%) output tokens

Total: (15.4K new + 606.5K cache writes + 35.4M cache reads) input tokens, 258.8K output tokens, $30.317733 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: medium (~15999 tokens)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (11023KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
konard merged commit efcd5c6 into main Sep 2, 2026
29 checks passed
@konard

konard commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

🎉 Auto-merged

This pull request has been automatically merged by hive-mind.

  • All CI checks have passed

Auto-merged by hive-mind with --auto-merge flag

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all

2 participants