ci: fix every false positive, false negative, warning and error in the pipeline - #200
Conversation
Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: #199
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.
|
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:
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.
|
All six workflows are green on Two things still need a repository admin, because no workflow can configure them — both are documented in
|
🤖 Solution Draft LogThis 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)
Total: (26.4K new + 1.1M cache writes + 52.8M cache reads) input tokens, 533.0K output tokens, $50.971421 cost 🤖 Models used:
📎 Log file uploaded as Gist (17739KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
🔄 Auto-restart 1/5Reason: 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. |
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.
Working session summaryPR #200 is ready for review: #200 Final state: clean working tree at 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 Verified: 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 Two items cannot be done from a pull request and need repository admin action — both are recorded in
One caveat on local verification: This summary was automatically extracted from the AI working session output. |
🔄 Auto-restart-until-mergeable Log 1/5This 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)
Total: (2.4K new + 129.0K cache writes + 6.5M cache reads) input tokens, 51.8K output tokens, $5.861350 cost 🤖 Models used:
📎 Log file uploaded as Gist (20295KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
🔄 Auto-restart 2/5Reason: 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. |
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.
Working session summaryDone. 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
Reported upstream: js-template#161 — Docs: Verification: This summary was automatically extracted from the AI working session output. |
🔄 Auto-restart-until-mergeable Log 2/5This 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)
Total: (3.7K new + 208.7K cache writes + 10.6M cache reads) input tokens, 80.0K output tokens, $9.408661 cost 🤖 Models used:
📎 Log file uploaded as Gist (23885KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
🎉 Auto-mergedThis pull request has been automatically merged by hive-mind.
Auto-merged by hive-mind with --auto-merge flag |
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 isdev/log/issues/199/pulls/200/analysis/README.md. The resulting rules are documented for humans indocs/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'sE409 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
staged≠published; npm's E409 was not in the already-published pattern list., which also collects the archived copies underdocs/case-studies/**format:checkreads the whole tree but ran only behindjs.yml'sjs/**filtermainrefs/pull/N/mergeis computed when the branch is last synchronised403to non-browser clients; GitHub serves/stargazersand/settings/only to a signed-in sessionReceived: ""on one run and passed on the nextnpm view, but the script dies at its module-scopefetchof 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 scriptsFalse negatives fixed
RUSTFLAGSdoes not reach clippy's own lints; eslint exits 0 on warnings without--max-warnings 0mainhas no branch protection (see Manual settings below).jscpd.json"format": "console"—consoleis not a language, so nothing matchedjs/rust/scripts/check-{file,crate}-size.rsandcheck-version-modification.rswere referenced by nothingexecSync("git ls-files '*.md'")goes throughcmd.exe, which does not strip the quotesartipackedis a Low-confidence zizmor check, and the job ran atmin-confidence: mediumEvery 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
actionlint-before.log,zizmor-before.log); both now exit clean.dependency-review-actioncannot 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.CARGO_REGISTRY_TOKENsat in a workflow-levelenv:, 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 apaths: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..lycheeignorerecords 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.--min-confidence low—mediumhidesartipackedentirely. Atlowa 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.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-scopeeval(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 forUnexpected 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.mjspins 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 ascmd.exedoes,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:Automated checks:
js/tests/workflow-hygiene.test.mjs(134 tests) parses the workflow files and enforces every invariant indocs/CI-CD.md— concurrency shape, writer jobs, path-filter symmetry, no secrets in workflowenv:, which quality gates are wired up, which jobs may skip the merge simulation.js/tests/docs-validation.test.mjsvalidates the documentation. Both are pinned by a non-empty-input assertion, which is what caught §4.23.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:
.jscpd.json"format": "console"makes the duplication check analyse zero files and always passpublish-retry.mjsmisses npm's E409 "Cannot publish over previously staged version"links.yml'spaths:filter omits.lycheeignoreandscripts/check-web-archive.mjsmin-confidence: mediumhides everyartipackedfindinguse-module.mjsfetches use-m with no timeout and no retry, and seven scripts call it at module scope, so a CDN blip kills them with a bareTypeError: fetch failedand an emptyGITHUB_OUTPUTCARGO_REGISTRY_TOKENin a workflow-levelenv:reaches seven jobs that compile pull-request coderust-script --test, so 78 tests across 9 scripts never executeThings 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:main—GET /branches/main/protectionreturns404 Branch not protectedand the ruleset list is empty, so a pull request with red checks can still be merged.Release
js/.changeset/ci-audit-and-lint-coverage.mdandrust/changelog.d/20260904_223000_ci-warnings-and-audit.mdcarry the bump for the next release.