Skip to content

ci: fix every false positive, false negative, warning and error in the pipeline - #200

Merged
konard merged 34 commits into
mainfrom
issue-199-32c07917fc87
Sep 5, 2026
Merged

ci: fix every false positive, false negative, warning and error in the pipeline#200
konard merged 34 commits into
mainfrom
issue-199-32c07917fc87

Conversation

@konard

@konard konard commented Sep 4, 2026

Copy link
Copy Markdown
Member

Fixes #199.

Every false positive, false negative, warning and error found in this repository's CI/CD, with the root cause of each, the fix, and a test that fails without the fix. The full investigation — downloaded run logs, API snapshots, verbatim copies of both pipeline templates, tool output before and after each change — is committed under dev/log/issues/199/pulls/200/, and the write-up with the timeline, the requirement table and all 25 root causes is dev/log/issues/199/pulls/200/analysis/README.md. The resulting rules are documented for humans in docs/CI-CD.md.

What started this

Release run 33914574283 reported a failed npm publish for command-stream@0.20.1. The version was live on npm. The publish had succeeded, the retry hit npm's E409 Cannot publish over previously staged version, and the classifier did not recognise that message as "already published" — a red release for a shipped artifact.

Pulling that thread showed the opposite failure mode was more common: checks that were green because they were not running.

False positives fixed

# Symptom Root cause
4.1 Successful release reported as failed stagedpublished; npm's E409 was not in the already-published pattern list
4.9 Matrix jobs collide in the checks list Job names omitted the key that distinguishes them
4.13 zizmor reported findings in other projects' workflows Its input was ., which also collects the archived copies under docs/case-studies/**
4.17 A formatting error in a workflow first turned red on somebody else's JavaScript PR format:check reads the whole tree but ran only behind js.yml's js/** filter
4.21 Checks validated a merge preview that no longer matches main refs/pull/N/merge is computed when the branch is last synchronised
4.22 A lychee gate would report 20 broken links on an unmodified tree npmjs.com answers 403 to non-browser clients; GitHub serves /stargazers and /settings/ only to a signed-in session
4.25 The publish tests failed with Received: "" on one run and passed on the next The offline guard probed npm view, but the script dies at its module-scope fetch of use-m from unpkg — a service that fails independently of the registry, fetched with no deadline, no retry and no diagnostics in all 11 release scripts

False negatives fixed

# What was silently passing Root cause
4.2, 4.3 clippy and eslint warnings RUSTFLAGS does not reach clippy's own lints; eslint exits 0 on warnings without --max-warnings 0
4.4 The workflows themselves were never linted No actionlint, no zizmor
4.6 Red checks could still be merged main has no branch protection (see Manual settings below)
4.10 The duplication gate analysed zero files .jscpd.json "format": "console"console is not a language, so nothing matched
4.11 Half the repository was unlintable eslint/prettier treat the config's directory as the project base path; the configs lived in js/
4.16 Two warnings that only exist off Linux No macOS/Windows leg for the affected checks
4.18 Three shipped quality gates never ran rust/scripts/check-{file,crate}-size.rs and check-version-modification.rs were referenced by nothing
4.19 Nothing looked for committed credentials CodeQL does not scan for secrets; the audit jobs only read lockfiles
4.20 Documentation was never validated No size, link or required-section check — two case-study links were already broken
4.23 The new documentation checks validated an empty file list on Windows execSync("git ls-files '*.md'") goes through cmd.exe, which does not strip the quotes
4.24 The workflow audit could not report a checkout that keeps the job token artipacked is a Low-confidence zizmor check, and the job ran at min-confidence: medium

Every fix is applied on both sides of the repository — the JavaScript and the Rust pipeline, every workflow, every job — as the issue asks.

Errors fixed

  • 4.5 — the first actionlint and zizmor runs found 33 and 622 lines of findings (actionlint-before.log, zizmor-before.log); both now exit clean.
  • 4.12 — nothing audited dependencies or analysed sources: CodeQL, dependency review and npm/bun/cargo audit added.
  • 4.14dependency-review-action cannot run until the dependency graph is enabled, so the job probes the API and skips with a warning instead of failing every pull request; any other status still fails it.
  • 4.15CARGO_REGISTRY_TOKEN sat in a workflow-level env:, handing the publish token to seven jobs that compile pull-request code. Publishing credentials now live on the publishing job only.

Structural changes

  • quality.yml (new) — deliberately without a paths: filter: formatting of every tracked file, documentation validation, workflow invariants. The union of the other workflows' filters is not the repository.
  • links.yml (new) — lychee, weekly and on demand. Both templates run it as a merge gate; here that gate is a false-positive generator (§4.22), so the check is split by who can break the link: relative links are resolved offline on every pull request, the network is fetched on a schedule and blocks nothing. .lycheeignore records each known-unreachable URL with the reason above it, and a test rejects an uncommented entry.
  • security.yml, workflows.yml, .github/zizmor.yml, .secretlintrc.json — the missing gates both templates ship.
  • zizmor at --min-confidence lowmedium hides artipacked entirely. At low a checkout either drops the token or is one of the six release jobs that pushes with it and says so inline; the count of suppressions is asserted so a seventh cannot arrive by copy-paste.
  • .github/scripts/simulate-fresh-merge.sh — every pull-request job that reads the tree merges the base branch first. Five jobs are exempt with the reason recorded next to each.
  • Root-level eslint.config.js / .prettierrc / .prettierignore — so the linters can see the whole repository.
  • publish-retry.mjs / npm-registry.mjs / publish-failure-classifier.mjs — the monolithic publish script split the way the template does it, with the E409 case covered by unit tests.
  • js/scripts/use-m-loader.mjs (new) — the eleven release scripts each opened with an inline, module-scope eval(await (await fetch('https://unpkg.com/use-m/use.js')).text()): no deadline, no retry, no status check. They now share one loader with a 15 s deadline per attempt, three attempts with exponential backoff, the HTTP status checked before the eval (an error page is HTML, and eval-ing HTML blames this repository for Unexpected token '<'), ::debug:: tracing that is off by default, and a final error naming the URL, the attempts and the cause. js/tests/use-m-loader.test.mjs pins the behaviour and asserts that no script fetches use.js inline again.

How to reproduce and verify

Reproductions of the two most subtle defects are committed as runnable scripts:

  • experiments/issue-199-publish-false-positive.mjs — feeds npm's real E409 output to the classifier; fails before the fix.

  • experiments/git-ls-files-quoting.mjs — reproduces the Windows quoting bug on Linux: 37 file(s) with a POSIX shell, 0 file(s) with the quotes passed through as cmd.exe does, 37 file(s) with no shell at all.

  • experiments/jscpd-format/ — shows the duplication check analysing zero files.

  • experiments/fresh-merge-simulation.sh, experiments/secretlint-scope.sh — the merge-simulation and secret-scan behaviour.

  • experiments/publish-cdn-unreachable.mjs — points the use-m fetch at a TEST-NET-3 address (RFC 5737, guaranteed unroutable) and runs the legacy inline fetch and the shared loader side by side:

    legacy inline fetch:  "TypeError: fetch failed"
    shared loader:        "Error: Failed to load use-m from https://203.0.113.1/use-m/use.js after 2 attempt(s):
                           The operation was aborted due to timeout. This is a network dependency of the release
                           scripts, not a defect in the published package; re-run the job when the CDN answers again."
    

Automated checks: js/tests/workflow-hygiene.test.mjs (134 tests) parses the workflow files and enforces every invariant in docs/CI-CD.md — concurrency shape, writer jobs, path-filter symmetry, no secrets in workflow env:, which quality gates are wired up, which jobs may skip the merge simulation. js/tests/docs-validation.test.mjs validates the documentation. Both are pinned by a non-empty-input assertion, which is what caught §4.23.

bun test js/tests/workflow-hygiene.test.mjs js/tests/docs-validation.test.mjs
209 pass, 0 fail

Reported upstream

The issue asks that a defect also present in a template be reported there. Each report contains a reproduction, a workaround and the code-level fix:

Issue Repository Defect
#157 js template .jscpd.json "format": "console" makes the duplication check analyse zero files and always pass
#158 js template publish-retry.mjs misses npm's E409 "Cannot publish over previously staged version"
#159 js template links.yml's paths: filter omits .lycheeignore and scripts/check-web-archive.mjs
#160 js template 25 of 27 checkouts persist credentials, and min-confidence: medium hides every artipacked finding
#161 js template use-module.mjs fetches use-m with no timeout and no retry, and seven scripts call it at module scope, so a CDN blip kills them with a bare TypeError: fetch failed and an empty GITHUB_OUTPUT
#149 rust template CARGO_REGISTRY_TOKEN in a workflow-level env: reaches seven jobs that compile pull-request code
#150 rust template No workflow runs rust-script --test, so 78 tests across 9 scripts never execute

Things that looked like defects and are not are listed in §7 of the analysis, with the evidence that cleared them.

Manual settings still required

Two things no workflow can configure, both documented in docs/CI-CD.md:

  1. Branch protection on mainGET /branches/main/protection returns 404 Branch not protected and the ruleset list is empty, so a pull request with red checks can still be merged.
  2. Dependency graph — off for this repository and controlled at the organisation level; enable it at settings/security_analysis. Until then the dependency-review job skips with a warning.

Release

js/.changeset/ci-audit-and-lint-coverage.md and rust/changelog.d/20260904_223000_ci-warnings-and-audit.md carry the bump for the next release.

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

Issue: #199
@konard konard self-assigned this Sep 4, 2026
Issue #199. The release job for command-stream@0.20.1 logged a successful
`changeset publish`, verified the registry once 2 seconds later, got a
read-replica E404, republished, and npm answered

  npm error code E409
  npm error 409 Conflict - PUT https://registry.npmjs.org/command-stream -
    Cannot publish over previously staged version "0.20.1"

which matched the generic 'npm error code e' failure pattern. The job went
red while 0.20.1 was live on npm.

- Split publish orchestration into scripts/publish-retry.mjs: the publish
  command is retried only when the publish itself failed, and verification
  polls the registry with exponential backoff instead of sampling it once.
- Treat 'already published' AND 'previously staged version' (E409) as a cue
  to verify, never as a failure.
- Verify against registry metadata (scripts/npm-registry.mjs) rather than
  `npm view`, whose E404 is indistinguishable from a network hiccup.
- Do not retry auth/registry-configuration failures
  (scripts/publish-failure-classifier.mjs); print actionable guidance instead.
- Add scripts/debug-print.mjs: verbose `::debug::` tracing, default off,
  enabled with CI_SCRIPTS_DEBUG=1 or GitHub's re-run-with-debug-logging.

The #166 guarantee is unchanged and re-asserted: a publish that never
reaches npm still fails the release.

Tests: js/tests/publish-retry.test.mjs, npm-registry.test.mjs,
publish-failure-classifier.test.mjs, debug-print.test.mjs, and three new
end-to-end cases in publish-to-npm.test.mjs driven by a stub registry that
lags on demand. experiments/issue-199-publish-false-positive.mjs reproduces
the old behaviour side by side with the new one.
Issue #199. `cargo clippy --all-targets --all-features` was run without
`-D warnings`, so the Rust lint job printed 15 warnings and exited 0 — a
false negative on every push since the job was introduced. Nothing in the
Rust pipeline could fail on a warning: not clippy, not rustc, not rustdoc.

- rust.yml: RUSTFLAGS/RUSTDOCFLAGS=-Dwarnings at workflow level (registry
  dependencies build with --cap-lints allow, so this only denies warnings in
  our own code), `-- -D warnings` on clippy because RUSTFLAGS does not reach
  clippy's own lints, and a `cargo doc --no-deps` gate for the rustdoc-only
  lints that neither clippy nor `cargo test --doc` reports.
- Cargo.toml: [lints.rust] unsafe_code = "forbid" (the crate has no unsafe
  block) and [lints.clippy] all = warn, mirroring the Rust template.

Warnings fixed, rather than silenced:
- ls: iterate with .flatten(); drop the dead `file_type` binding — the type
  character is already part of `perms`.
- lib.rs: std::io::Error::other; document why `output_rx` is held but never
  read (dropping it closes the channel, which is how `yes` decides to stop).
- touch: check set_file_mtime with .is_err() instead of binding an unused error.
- sleep: format! with no arguments -> to_string.
- pipeline: keep `Pipeline::add` and explain why should_implement_trait does
  not apply — renaming would break the published API and diverge from JS.
- cd: escape `<dir>` in the doc comment; rustdoc read it as an HTML tag.
- Remove unused imports in main.rs, commands/mod.rs and four test files.
- builtin_commands: `ctx_with_cwd` was dead because no test covered
  CommandContext::cwd. Three tests now do (pwd honours cwd, ls resolves a
  relative path against it, ls with no argument lists it).

cargo clippy -D warnings, cargo test --all-features (16 suites), cargo fmt
--check and cargo doc all pass locally.
The run logs downloaded for the investigation were sitting untracked: the
global `*.log` rule in .gitignore swallowed them, so ./dev/log/issues/199/
pulls/200 was incomplete in the repository.

Mirror the exception that already exists for docs/case-studies/**/ci-logs:
commit the small tool outputs under analysis/ verbatim, and commit the seven
full run logs gzipped (1.2 MB instead of 9.8 MB), which is the same shape as
docs/case-studies/issue-166/ci-logs/*.log.gz. Raw *.log stays ignored so a
re-download does not add megabytes to a commit by accident.

ci-logs/README.md indexes every run with its workflow, commit, conclusion and
failing jobs.
…k code

Two of the three JavaScript quality gates were passing without inspecting
anything (issue #199).

check:duplication analysed zero files. jscpd's `format` option is the list of
*languages* to scan, but .jscpd.json set it to the string "console" — a
reporter name. @jscpd/finder filters files with `options.format.includes(format)`,
and "console".includes("javascript") is false, so every file was skipped: 0
sources, 0 clones, exit 0, in half a millisecond. With the languages declared,
jscpd reports 47 clones (4.87%) across src and scripts; threshold 6 keeps that
as a ratchet so the number cannot grow while the pre-existing clones are
refactored separately.

eslint reported 52 warnings and exited 0, because `bun run lint` omitted the
`--max-warnings 0` that lint-staged already passes. The warnings are now fatal
and the tree is clean.

Also:
- `**/reports/**` is ignored by eslint and prettier. It is jscpd output, so
  running check:duplication before lint used to fail on jscpd's bundled
  prism.js — a gate that broke depending on the order the gates ran in.
- The override globs in eslint.config.js are anchored with `**/`. The file had
  both `js/tests/**` and `tests/**` spellings of the same override, one of
  which could never match whichever directory eslint ran from.

tests/duplication-check.test.mjs runs the real jscpd binary against a fixture
containing one clone and asserts it is found, and asserts the old "console"
value still skips every file.
eslint and prettier were configured inside js/, and both tools treat the
directory holding their configuration as the root of the project. Repository-root
JavaScript was therefore unreachable: pointing eslint at it answered "the file
is ignored because it is located outside of the base path". claude-profiles.mjs
and the 20 experiments/ reproductions — including the one added for this issue —
had never been checked, and CI reported green over them (issue #199).

The same split made parts of the configuration dead. js/.prettierignore listed
docs/case-studies/**/data|log-excerpts|templates, but from js/ those patterns
name js/docs/case-studies, which does not exist, so the vendored upstream
template copies were never actually protected from reformatting.

Move eslint.config.js (as a re-export), .prettierrc and .prettierignore to the
repository root and run both tools from there. The 87 problems this uncovered
are all formatting and were fixed with --fix/--write; `git diff -w` over
experiments/ shows no semantic change.

.prettierignore now genuinely covers the archived investigation records
(docs/case-studies, dev/log) and generated files (rust/target, the CHANGELOGs).
Case studies are excluded rather than reformatted because prettier re-parses the
nested backticks quoted in issue-166/upstream-issue.md and destroys the list.

lint-staged moves to a root .lintstagedrc.json for the same reason: it only
looks at files under its working directory, so `cd js && lint-staged` skipped
every root-level file.
Nothing in CI read .github/workflows/**, so workflow defects reached main
unchallenged. Adding the two linters used by the pipeline templates surfaced
four actionlint reports and 35 zizmor findings across js.yml and rust.yml.

New: .github/workflows/workflows.yml runs actionlint from the Docker image that
bundles shellcheck (a native binary without shellcheck silently skips every
`run:` block and exits 0) plus zizmor with .github/zizmor.yml, whose pinning
policy is copied from the templates.

Fixed in js.yml, rust.yml and parity.yml:

- template injection: `${{ github.head_ref }}` was expanded straight into a
  `run:` block, where a fork branch name is attacker-controlled shell code. The
  expansion also made shellcheck compare two literals (SC2193), so the
  "skip automated release PR" branch could never be taken. All ten sites now
  pass values through `env:`.
- unpinned actions: oven-sh/setup-bun, peter-evans/create-pull-request and
  dtolnay/rust-toolchain are hash-pinned; actions/* stay on ref pins per policy.
- excessive permissions: both workflows now default to `contents: read` and the
  release jobs raise their own scopes. rust.yml handed CARGO_REGISTRY_TOKEN to
  every job, including `cargo test` on pull requests; it is declared on the two
  publishing jobs instead.
- SC2086: `$GITHUB_OUTPUT` was unquoted in the changeset count step.
- concurrency (best practice #10): the workflow-level cancellable group could
  cancel a release mid-publish. Checks now use cancellable per-job `check-*`
  groups that include the matrix values, writers share the non-cancellable
  `main-writer-${{ github.repository }}-main` group.
- `always()` replaced by `!cancelled()` in seven job conditions, so a cancelled
  prerequisite no longer lets its dependents start.
- the three Node matrix entries all reported as "Test JavaScript (node on
  ubuntu-latest)"; the version is now part of the name.
- read-only checkouts no longer persist the token in .git/config.

js/tests/workflow-hygiene.test.mjs parses the workflow YAML and asserts these
invariants, which neither actionlint nor zizmor checks.
…ed()

Two assertions in repository-layout.test.mjs described the state of the tree
before this branch changed it, so they now fail for the changes they were
never meant to forbid.

`keeps JavaScript package files inside js/` required the repository root to
have no eslint.config.js. That was true when the whole JavaScript project
lived under js/, but eslint treats the directory holding its configuration as
the base path of the linted project: with the config in js/ only, root-level
JavaScript answered "the file is ignored because it is located outside of the
base path" and was never checked. The root file is a re-export that exists to
widen that base path, not a second package. Split the intent into its own
test that asserts the re-export -- and .prettierrc/.prettierignore/
.lintstagedrc.json alongside it -- is at the root and no longer in js/.

`release jobs evaluate after PR-only gate jobs are skipped on push` required
the literal `always() && !cancelled()`. The point of that condition is to
stop GitHub skipping the release job because a PR-only dependency was
skipped, which `!cancelled()` already does by itself; `always()` next to it is
subsumed and misleads, since deleting the `!cancelled()` half would leave a
condition that publishes out of a cancelled run. Assert `!cancelled()` and the
absence of `always()`, which is the property that actually matters.
`npm audit --package-lock-only --audit-level=high` reported 8 vulnerabilities
(5 high) and `bun audit --audit-level=high` reported 13, all of them in
transitive development dependencies: @humanfs/node, ajv, brace-expansion,
flatted, js-yaml, minimatch, picomatch and yaml. Nothing in CI ran either
command, so none of this was visible on a green build (issue #199).

Raising the direct ranges is not enough on its own. `bun update` left 8 high
findings behind because bun.lock pinned the transitive versions that the new
ranges no longer require; the lockfile had to be resolved from scratch.
Regenerating package-lock.json the same way left one (flatted, reached through
flat-cache), cleared by a follow-up `npm audit fix --package-lock-only`.

Both lockfiles now report clean: npm "found 0 vulnerabilities", bun "No
vulnerabilities found".

prettier 3.9.6 changes how an empty `for` update clause is spaced, which is
why terminal-artifacts.mjs is reformatted at two loops; `bun run check` is
green with the new version.
CI ran lint, tests and release. Nothing read a lockfile for known
vulnerabilities and nothing analysed the sources statically, so a green build
carried no security signal at all (issue #199) -- and the audits were not
clean: npm reported 5 high advisories, bun 13, and cargo one
(RUSTSEC-2026-0007, integer overflow in `BytesMut::reserve`, bytes 1.11.0).
The JavaScript side is fixed in the preceding commit; bytes moves to 1.12.1
here, after which `cargo audit --file Cargo.lock` exits 0 and clippy and
`cargo test` stay green.

security.yml merges the two pipeline templates' security workflows, because
this repository ships a package from each of them:

- CodeQL over javascript-typescript, rust and actions. `build-mode: none` for
  all three -- none of them need compiling to extract, and asking CodeQL to
  autobuild the crate would repeat what rust.yml already does.
- dependency-review on pull requests, failing at high severity.
- npm, bun and cargo audits of the committed lockfiles. Both JavaScript
  lockfiles are audited rather than one: they resolve transitive versions
  independently, and while fixing this issue they disagreed by 8 high-severity
  advisories.

The audits also run weekly. An advisory is published against code that has not
changed, so change-triggered runs alone would hide it until the next commit.

`isWriterJob` in the hygiene test now keys on `contents: write` alone.
dependency-review holds `pull-requests: write` only to post its summary
comment; treating that as a writer would have parked every pull request in
main's non-cancellable release queue.
@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.

…repro

rust.yml used to declare CARGO_REGISTRY_TOKEN in the workflow-level `env:`,
which every job inherits -- including `lint`, `test`, `coverage` and `build`,
all of which run on pull requests and compile code from the branch under review
through build.rs, proc macros and the tests themselves. The token moved onto the
two publishing jobs earlier on this branch; this asserts it stays there.
Verified by reintroducing the declaration, which fails the new test, and
restoring the file byte-identically.

experiments/jscpd-format records the reproduction behind the duplication fix:
jscpd's `format` option names languages, not reporters, so the previous
`"format": "console"` matched no file, analysed nothing and passed a
`threshold: 0`. The script builds two identical files and runs jscpd over them
with each value -- 0 clones and exit 0 against 1 clone, 43.75% and exit 1.

Both defects are also present upstream and reported there:
link-foundation/rust-ai-driven-development-pipeline-template#149 and
link-foundation/js-ai-driven-development-pipeline-template#157.
The Zizmor job failed with 30 findings in `release.yml`, a file this
repository does not have. zizmorcore/zizmor-action defaults to auditing
`.`, so it walked the whole tree and picked up the 14 verbatim copies of
other repositories' workflows archived under docs/case-studies/**. Those
never run here and editing them would falsify the evidence, so the audit
is scoped to .github/workflows and the scope is pinned by a test.

Dependency review failed with "Dependency review is not supported on this
repository": the dependency graph is off and cannot be enabled from a
workflow. A check that can only ever be red teaches reviewers to ignore
red, so the job now probes the compare endpoint and skips with a warning
on 403, fails on any other status, and starts reviewing on its own once
an admin enables the graph.
Both release gates require an entry describing what ships. The JavaScript
change is the dependency refresh that cleared the npm and bun audit
findings; the Rust change is the bytes bump that cleared
RUSTSEC-2026-0007, plus the warning backlog fixed when clippy started
denying warnings.
Both are real defects that the pipeline could not see before clippy and
rustc started denying warnings, and both failed the Rust job on a00126b.

- tests/cd_invocation_isolation.rs: `output_env` is read only by the
  `#[cfg(unix)]` assertions, because the environment dump it parses comes
  from `/usr/bin/env`. On Windows it was an unused function, so the test
  crate would not compile. Verified with
  `cargo check --target x86_64-pc-windows-msvc --all-targets`: it fails
  with "function `output_env` is never used" before the cfg and passes
  after.

- scripts/version-and-commit.rs: `rust-script --test` builds the script as
  a test harness, where `main` is not the entry point, so the twelve
  helpers reachable only from `main` are unreferenced. The imports were
  already gated on `not(test)` for the same reason; the file now carries
  `#![cfg_attr(test, allow(dead_code))]`. Without it the step fails with
  exactly the twelve errors CI reported; with it all six script suites
  pass. The real, non-test build still denies dead code.

Also documents the pipeline in docs/CI-CD.md -- the file the analysis
already pointed at for the two settings a workflow cannot change, branch
protection on main and the dependency graph -- and records the remaining
findings in the issue-199 analysis log, including a correction: the
js/.prettierignore case-study patterns resolved against js/, where no
data/ or templates/ directories exist; the archived copies they were
meant to protect are under the repository-root docs/.
… report

Adds section 4.16 (the two warnings that only appear on Windows and under
rust-script --test) and the rust template issue #150 row to the upstream
report table.
rust/scripts/ ships four check-*.rs gates. Only check-changelog-fragment.rs
was ever executed: check-file-size.rs, check-version-modification.rs and
check-crate-size.rs were referenced by no workflow, no script and no
document, so the pipeline reported "all checks passed" for checks that
never ran (issue #199).

- check-version-modification.rs joins the pull-request-only changelog job,
  which already installs rust-script and checks out full history. A
  hand-edited version in rust/Cargo.toml now fails the PR instead of
  fighting the release job that writes that field.
- check-file-size.rs and check-crate-size.rs join the scripts job, the only
  other job with rust-script installed. File size runs from the repository
  root so rust/src, rust/tests and rust/scripts are all in scope; crate size
  packages with --no-verify and fails before a 10 MiB upload turns into an
  HTTP 413 after the release commit and tag already exist.

All three pass on the current tree. The two job display names now describe
what the jobs do. Pinned by new tests in workflow-hygiene.test.mjs, which
also assert that every rust/scripts entry is either invoked somewhere or
listed as a documented exception, so the next gate cannot go dead unnoticed.
No check looked for secrets in this repository: CodeQL does not search for
them and the three audit jobs only read lockfiles, so a token pasted into a
script, a test fixture or an archived case study would have reached main with
a fully green run (issue #199, hive-mind CI/CD best practice #11).

secretlint with the recommended preset now runs in the security workflow,
which has no paths filter and therefore sees every change, on pull requests,
on pushes to main and on the weekly schedule. Versions are pinned so a new
rule release cannot turn an unrelated pull request red.

experiments/secretlint-scope.sh is the probe behind the configuration: with a
fake ghp_ token planted in three places it shows that the `**/*` glob does
descend into dot-directories such as .github/, and that .secretlintignore is
honoured. The ignore list holds only generated trees (node_modules, cargo
target, jscpd reports, coverage), and a new test fails if anything authored
here is ever added to it. The full scan takes about 10 seconds locally,
including the 12 MB of archived CI logs under dev/log/.
A pull-request run checks out refs/pull/N/merge, which GitHub computes when
the pull request is opened or synchronised. When main moves after that, the
checks validate a combination that will not exist once the branch lands: the
run is green, the merge happens, and main breaks on code no job ever saw
together. This is principle #7 of the hive-mind CI/CD best practices and the
last one of the fifteen that this repository did not implement (issue #199).

.github/scripts/simulate-fresh-merge.sh merges the base branch into the
checkout before the checks run, and turns a merge conflict into a clear
failure on the pull request instead of a surprise at merge time. It is a
no-op when the branch already contains every commit on the base. The five
jobs that read the tree -- lint and test in js.yml, lint, test and scripts in
rust.yml -- now run it and check out full history; the merge is local and
those checkouts keep persist-credentials: false.

experiments/fresh-merge-simulation.sh builds throwaway repositories and
asserts all three paths: no-op when up to date, merged when the base moved,
exit 1 with an ::error:: annotation when the merge conflicts. New hygiene
tests pin the wiring, including that the simulation cannot be added to a job
that checks out shallowly or outside a pull request.
Markdown was the only tracked language with no gate at all: a link could
rot, a README could lose the section the release process points readers
at, and a case study could grow past the point anyone reads it, all with
a green run (issue #199, best practice #12).

The new test checks three things over every tracked *.md file:

  * a 2500-line ceiling, mirroring the 1500-line ceiling eslint enforces
    for JavaScript and check-file-size.rs enforces for Rust;
  * every relative link resolves to a file that exists;
  * the documents other automation depends on still carry the sections
    it expects (README release/development sections, docs/CI-CD.md
    invariants, the two changelog-fragment READMEs).

External links are deliberately not fetched. A network link checker is a
false-positive generator -- rate limits, bot walls and transient 5xx turn
an unrelated pull request red -- and false positives are exactly what
this issue is about.

The link check found two real breakages, fixed here: issue-162 linked to
changelog markers that a release had already consumed, and issue-153
pointed one directory level too shallow at BEST-PRACTICES.md.

Archived evidence under dev/log/** and the verbatim upstream copies under
docs/case-studies/**/templates/** are excluded: they are kept byte-for-byte
as downloaded, so "fixing" their links would falsify the record.
Every workflow was scoped by a `paths:` filter, and the union of those
filters was not the repository (issue #199):

  * a pull request touching only `docs/**` matched no filter at all and
    ran zero checks;
  * prettier reads every tracked file but only ran behind js.yml's
    `js/**` filter, so a formatting violation introduced in a workflow or
    a markdown file first turned red on somebody else's JavaScript pull
    request -- a failure attributed to the wrong change;
  * eslint resolves its config at the repository root and lints
    `experiments/**` and `claude-profiles.mjs`, neither of which was in
    js.yml's trigger;
  * the workflow-hygiene test, which guards the CI invariants themselves,
    ran only behind `js/**` -- the one filter guaranteed not to match a
    workflow-only change.

The new Repository quality checks workflow carries no `paths:` filter on
purpose and runs the three repository-wide gates: formatting, the
documentation validation added in the previous commit, and the workflow
invariants. js.yml's filter grows the root-level eslint inputs.

The fresh-merge simulation is extended from five jobs to every
pull-request job that reads the working tree: the Rust build, the four
audit jobs, the secret scan, actionlint, zizmor and the three new ones.
Five jobs stay exempt with the reason recorded next to them and in the
test: three are diff-based (changeset-check, the Rust changelog checks,
parity), dependency-review never reads the tree, and CodeQL uploads
results keyed to the checked-out commit, which GitHub rejects for a merge
commit created on the runner.

Three new invariants keep this from regressing, each verified to fail
when its guard is removed: a pull-request job must simulate the merge or
appear in the documented exemption list; every file eslint lints outside
js/ must be covered by js.yml's trigger; and a workflow's push and
pull_request path filters must be identical, so a green pull request
keeps predicting a green main.
The CI/CD reference is the document the hygiene test points reviewers at,
so it has to describe what the workflows now do: the repository-wide
quality workflow and why it deliberately carries no paths filter, the
fresh-merge simulation and its five documented exemptions, the identical
push/pull_request filters, the wired Rust gates, the secret scan, and the
documentation validation together with the decision not to fetch external
links.
Best practice #12 names lychee, and both pipeline templates run it as a
pull-request gate. Copying that here would have made unrelated pull requests
red: a lychee run over this tree reports 20 errors, and every one of them is a
link that is correct in the document and unreachable from a runner -- npmjs.com
answers 403 to any non-browser client, and GitHub serves the stargazers list and
the /settings/ pages only to a signed-in session (evidence in
dev/log/issues/199/pulls/200/lychee-run.log).

So the check is split by who can break the link. Relative links, the only ones a
change here can break, are already resolved offline on every pull request by
docs-validation.test.mjs. External links are fetched by the new links.yml on a
weekly schedule and on demand, where a failure means a link that used to work
has stopped working and blocks no merge.

.lycheeignore holds the known-unreachable-but-correct URLs; the hygiene test
rejects an entry without a comment stating why, so nothing can be muted
silently. With it the same run reports 0 errors and 20 exclusions.
The Windows leg of the test matrix failed on both new tests:

  (fail) documentation validation > the file list is not empty and skips
         archived copies -- Expected: > 20, Received: 0
  (fail) every file eslint lints outside js/ triggers the lint job
         -- Received [""]

execSync runs the command through the platform shell: /bin/sh strips the quotes
in `git ls-files '*.md'`, cmd.exe does not, so git looked for a path literally
named `'*.md'`, matched nothing and exited 0. The documentation check was
therefore validating an empty list on Windows -- a silent false negative of
exactly the kind this pull request is about, caught only because the assertion
that the list is non-empty was already there.

execFileSync passes the arguments to git directly, so git expands the pattern
itself on every platform. experiments/git-ls-files-quoting.mjs reproduces both
behaviours on Linux:

  execSync, shell strips the quotes (POSIX): 37 file(s)
  execSync, quotes reach git (what cmd.exe does): 0 file(s)
  execFileSync, no shell at all: 37 file(s)

Also files the two upstream reports for the JavaScript pipeline template
(issues #159 and #160) under dev/log.
@konard konard changed the title [WIP] Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all ci: fix every false positive, false negative, warning and error in the pipeline Sep 4, 2026
@konard
konard marked this pull request as ready for review September 5, 2026 00:04
@konard

konard commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

All six workflows are green on 52ca330 (Language parity, Repository quality, Workflows, JavaScript, Rust, Security), the working tree is clean and main contains nothing this branch is missing, so the PR is out of draft.

Two things still need a repository admin, because no workflow can configure them — both are documented in docs/CI-CD.md:

  1. Branch protection on main. GET /repos/link-foundation/command-stream/branches/main/protection still returns 404 Branch not protected and the ruleset list is empty, so every check added here can be red and the merge still goes through.
  2. Dependency graph. It is off and controlled at the organisation level; until it is enabled the dependency-review job probes the compare endpoint and skips with a warning instead of failing every pull request.

@konard

konard commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

🤖 Solution Draft Log

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

💰 Cost: $50.971421

📊 Context and tokens usage:

Claude Opus 5: (11 sub-sessions)

  1. 115.5K / 1M (12%) input tokens, 15.2K / 128K (12%) output tokens
  2. 116.6K / 1M (12%) input tokens, 40.8K / 128K (32%) output tokens
  3. 116.2K / 1M (12%) input tokens, 34.8K / 128K (27%) output tokens
  4. 117.0K / 1M (12%) input tokens, 31.7K / 128K (25%) output tokens
  5. 115.8K / 1M (12%) input tokens, 38.0K / 128K (30%) output tokens
  6. 115.4K / 1M (12%) input tokens, 40.4K / 128K (32%) output tokens
  7. 116.1K / 1M (12%) input tokens, 40.0K / 128K (31%) output tokens
  8. 116.8K / 1M (12%) input tokens, 46.8K / 128K (37%) output tokens
  9. 115.9K / 1M (12%) input tokens, 40.0K / 128K (31%) output tokens
  10. 115.3K / 1M (12%) input tokens, 41.1K / 128K (32%) output tokens
  11. 111.0K / 1M (11%) input tokens, 28.6K / 128K (22%) output tokens

Total: (26.4K new + 1.1M cache writes + 52.8M cache reads) input tokens, 533.0K output tokens, $50.971421 cost

🤖 Models used:

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

📎 Log file uploaded as Gist (17739KB)


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

@konard

konard commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

🔄 Auto-restart 1/5

Reason: Uncommitted changes detected

Starting new session to address the issues.


Auto-restart-until-mergeable mode is active. This run will stop after 5 restart iterations in total.

@konard
konard marked this pull request as draft September 5, 2026 00:08
Running the suite twice unchanged produced two different results: the first
run failed `reports published for a version already on npm` and
`issue #199: registry propagation lag after a clean publish is not a failure`
with `Expected to contain: "published=true" / Received: ""`, the second
passed all six.

publish-to-npm.mjs opens with a module-scope
`await fetch('https://unpkg.com/use-m/use.js')`, outside main()'s try/catch,
so an unreachable CDN kills it during module initialisation -- before any
GITHUB_OUTPUT line and before its first log. The offline guard only probed
`npm view`, and npm's registry and unpkg fail independently, so a reachable
registry cleared a guard for a dependency that was down.

Probe both endpoints, and raise with the child's exit status, stderr and
GITHUB_OUTPUT when the script never reaches main(), so the next occurrence
names the cause instead of comparing against an empty string.

experiments/publish-cdn-unreachable.mjs reproduces it on demand. With the
fix the same unreachable-CDN run skips all six tests instead of failing them.
The experiment routes the fetch through an unroutable TEST-NET-3 address,
not a closed port, and its null exit status is the harness timeout killing
a child still blocked in the module-scope fetch rather than the script
exiting. Say both, and name the timeout in the output instead of printing
a bare null a reader has to guess at.
@konard
konard marked this pull request as ready for review September 5, 2026 00:50
@konard

konard commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

Working session summary

PR #200 is ready for review: #200

Final state: clean working tree at 2f8fb5f, main fully merged (0 commits behind), mergeStateStatus: CLEAN, all 36 checks green across all six workflows (JavaScript, Rust, Language parity, Workflows, Repository quality, Security).

What this session added. The uncommitted change that triggered the restart was a bare file-mode flip on an archived pre-fix workflow snapshot, so I reverted it — the archive has to reflect the original repo state.

Then I found a 25th root cause, and it's the exact class of defect the issue targets. Running the publish suite twice unchanged gave different results: six failures with Expected to contain: "published=true" / Received: "", then six passes. publish-to-npm.mjs opens with a module-scope await fetch('https://unpkg.com/use-m/use.js') sitting outside main()'s try/catch, so an unreachable CDN kills the script during module initialisation — before any GITHUB_OUTPUT write, before the first log line. Two compounding defects: the offline guard probed only npm view (npm and unpkg fail independently), and the assertion named neither the CDN nor the network. Fixed with a two-endpoint guard plus assertScriptStarted(), which now raises with the child's exit status, stderr and GITHUB_OUTPUT contents.

Verified: HTTPS_PROXY=http://127.0.0.1:1 bun test js/tests/publish-to-npm.test.mjs now skips all six instead of hard-failing, and experiments/publish-cdn-unreachable.mjs reproduces the original opaque failure on demand.

One correction to my own artifact. The analysis doc described that experiment as using a closed port when the committed version uses an unroutable TEST-NET-3 address, and it quoted a bare exit status null — which is the harness timeout killing a child still stuck in the fetch, not the script exiting. Both are now stated accurately, since a reader would otherwise draw the wrong conclusion about how the failure manifests.

Two items cannot be done from a pull request and need repository admin action — both are recorded in docs/CI-CD.md and the PR body:

  • Branch protection on main (API returns 404 Branch not protected, ruleset list empty).
  • Enabling the dependency graph — it's an organisation-level setting. The dependency-review job probes the API and skips with a warning on 403 rather than failing, so it won't produce a false red in the meantime.

One caveat on local verification: jq isn't installed in this sandbox, which fails 28 tests locally that pass on GitHub runners (they ship it preinstalled). That's an environment gap, not a repo defect — CI is green on those tests.


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

@konard

konard commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

🔄 Auto-restart-until-mergeable Log 1/5

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

💰 Cost: $5.861350

📊 Context and tokens usage:

Claude Opus 5: (2 sub-sessions)

  1. 116.4K / 1M (12%) input tokens, 34.5K / 128K (27%) output tokens
  2. 36.3K / 1M (4%) input tokens, 6.7K / 128K (5%) output tokens

Total: (2.4K new + 129.0K cache writes + 6.5M cache reads) input tokens, 51.8K output tokens, $5.861350 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (20295KB)


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

@konard

konard commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

🔄 Auto-restart 2/5

Reason: Uncommitted changes detected

Starting new session to address the issues.


Auto-restart-until-mergeable mode is active. This run will stop after 5 restart iterations in total.

@konard
konard marked this pull request as draft September 5, 2026 00:55
check-language-parity.sh was archived under dev/log with its executable bit,
which the checkout environment flipped back to 644 on its own twice, each time
producing a working tree that was dirty without any content change. The
snapshot is read as evidence and never run from there, so it does not need the
bit, and a test now asserts that no tracked file under dev/log carries it.
…ries

The eleven release scripts each opened with a module-scope
`eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())`:
no deadline, no retry, no status check, and outside main()'s try/catch.
A CDN blip therefore killed the script during module initialisation --
no log line, nothing in GITHUB_OUTPUT, and a bare `TypeError: fetch
failed` -- which the release job reported as a publish defect.

js/scripts/use-m-loader.mjs gives that load a per-attempt deadline,
bounded retries with exponential backoff, a status check before the
eval, ::debug:: tracing that is off by default, and a final error naming
the URL, the attempt count and the cause.

experiments/publish-cdn-unreachable.mjs now runs both shapes against a
TEST-NET-3 address:
  legacy inline fetch  TypeError: fetch failed
  shared loader        Error: Failed to load use-m from ... after 2
                       attempt(s): The operation was aborted due to
                       timeout. This is a network dependency of the
                       release scripts, not a defect in the published
                       package; re-run the job when the CDN answers again.
The root-level CLI carried the same inline module-scope fetch the eleven
release scripts did, so a CDN blip killed it before its first line with a
bare `TypeError: fetch failed`. The invariant test now scans every
tracked .mjs/.js/.cjs file instead of js/scripts/ alone.
@konard
konard marked this pull request as ready for review September 5, 2026 01:26
@konard

konard commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

Working session summary

Done. PR #200 is out of draft, mergeable and green.

What this iteration added — the uncommitted-change loop was already root-caused and fixed (the archived evidence copy is now tracked 100644, with a test asserting nothing under dev/log/ is executable). The remaining real gap was that root cause §4.25 had been fixed only on the test side:

  • js/scripts/use-m-loader.mjs (new): every caller of use-m now loads it through one hardened path — a 15 s AbortSignal.timeout per attempt, 3 attempts with exponential backoff, response.ok checked before the eval (a CDN error page is HTML, and eval-ing HTML blamed this repo for Unexpected token '<'), one ::debug:: line per attempt (off by default), and a final error naming the URL, the attempt count and the cause.
  • Applied in all 12 places that had the inline module-scope fetch: the 11 js/scripts/*.mjs release scripts plus the root-level claude-profiles.mjs (node claude-profiles.mjs --help still exits 0).
  • js/tests/use-m-loader.test.mjs (12 tests): deadline present on every attempt, retry-then-succeed with the exact backoff, bounded attempts with the cause preserved, error page reported by status instead of eval-ed, injected fetches bypassing the cache, and a tree-wide scan asserting nothing fetches use.js inline again.
  • experiments/publish-cdn-unreachable.mjs now runs both shapes against a TEST-NET-3 address (RFC 5737): TypeError: fetch failed before, versus Failed to load use-m from … after 2 attempt(s): The operation was aborted due to timeout. This is a network dependency of the release scripts, not a defect in the published package… after.

Reported upstream: js-template#161use-module.mjs:113 fetches use-m with no timeout and no retry, and 7 scripts call it at module scope. Two reproductions run against the template at 7ae16b0: an unreachable CDN (TypeError: fetch failed, bounded only by undici's 10 s connect timeout) and a server that accepts and never answers (after 25047ms: still waiting), plus the workaround and the diff. That makes seven upstream reports from this issue; the analysis §7 table and its "Seven defects" wording now match.

Docs: docs/CI-CD.md gained the loader invariant and a "Debugging a failed run" section covering CI_SCRIPTS_DEBUG=1 / RUNNER_DEBUG=1 / ACTIONS_STEP_DEBUG=true, which was implemented but undocumented. dev/log/issues/199/pulls/200/analysis/README.md §4.25 now carries the third compounding defect, §7 the #161 detail, §8 the library survey (p-retry/fetch-retry rejected: these scripts run with no package.json dependencies, so a retry library would have to be fetched over the network path it protects).

Verification: bun test js/tests/ → 1134 pass / 5 skip / 0 fail; eslint --max-warnings 0 and prettier --check . clean; all 30 CI checks pass at 25a6493 (6 skipping are the release jobs), origin/main merged, mergeStateStatus: CLEAN, working tree clean. #200


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

@konard

konard commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

🔄 Auto-restart-until-mergeable Log 2/5

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

💰 Cost: $9.408661

📊 Context and tokens usage:

Claude Opus 5: (2 sub-sessions)

  1. 116.4K / 1M (12%) input tokens, 30.0K / 128K (23%) output tokens
  2. 116.1K / 1M (12%) input tokens, 41.2K / 128K (32%) output tokens

Total: (3.7K new + 208.7K cache writes + 10.6M cache reads) input tokens, 80.0K output tokens, $9.408661 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (23885KB)


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

@konard
konard merged commit 4089d02 into main Sep 5, 2026
36 checks passed
@konard

konard commented Sep 5, 2026

Copy link
Copy Markdown
Member 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