Skip to content

fix(ci): eliminate CI/CD false positives, false negatives and warnings - #76

Merged
konard merged 16 commits into
mainfrom
issue-75-bea1ba5d2c9e
Aug 2, 2026
Merged

fix(ci): eliminate CI/CD false positives, false negatives and warnings#76
konard merged 16 commits into
mainfrom
issue-75-bea1ba5d2c9e

Conversation

@konard

@konard konard commented Aug 2, 2026

Copy link
Copy Markdown
Member

Closes #75.

Audits every GitHub Actions workflow and CI/CD script in the repository against the three pipeline templates and link-assistant/hive-mind/docs/CI-CD-BEST-PRACTICES.md, fixes each defect found, and turns each fix into a permanent policy check so it cannot regress.

Full evidence and analysis: dev/log/issues/75/pulls/76/ANALYSIS.md, alongside the 40-run runs.json and the seven downloaded CI logs it cites.

Warnings CI actually emitted

##[warning]Node.js 20 is deprecated ... actions/setup-python@v5 (see ci-logs/run-30738733161.log). The repository pinned actions/setup-python@v5 in seven places in python.yml while the Python template had already moved to v6. scripts/check-ci-workflows.mjs exists precisely to stop this drift but had no setup-python pattern, so it never saw it.

Also brought in line with the templates: codecov/codecov-action v6 → v7, peter-evans/create-pull-request v7 → v8.

Errors — workflows that would not parse

if: !cancelled() && ... on a single line is invalid YAML: a plain scalar may not begin with !, the tag indicator. Every such condition is now a block scalar (if: >-).

False negatives — gates that could not fail

Location Defect
js.yml instant-release No needs: at all — a manual instant release published to npm with lint and tests never having run
js.yml changeset-pr No needs: at all
python.yml manual-release Unreachable: detect-changes is skipped on workflow_dispatch, which skipped lint, which skipped this job (actions/runner#491)
python.yml build Short-circuited on github.event_name == 'push', so a failing lint on main still produced a build artifact
python.yml / rust.yml changelog ::warning:: followed by exit 0 — structurally impossible to fail

All are now gated on explicit needs.<job>.result values, and the changelog checks emit ::error:: and exit 1.

Script injection

origin/${{ github.base_ref }} and ${{ github.event.inputs.description }} were interpolated straight into run: bodies. A branch name is attacker-influenced on pull_request, and a free-form workflow_dispatch input is controlled by anyone who can trigger the workflow. All now pass through env:. bump_type and release_mode are type: choice inputs, which GitHub constrains to their declared options, so they stay inline.

Cancellation semantics

Every always() became !cancelled(), and the comments that still explained the old behaviour were rewritten.

Regression guards (this is the part that keeps it fixed)

scripts/check-ci-workflows.mjs gained rules for each class of defect above:

  • actions/setup-python@v[1-5], codecov/codecov-action@v[1-6], peter-evans/create-pull-request@v[1-7]
  • ${{ github.base_ref }} outside an env: assignment
  • an if: value starting with ! (the YAML hazard)
  • untrusted ${{ github.event.inputs.* }} inside a run: body — the checker parses the workflow_dispatch.inputs block and only allows inputs declared type: choice

New best-practice coverage

.github/workflows/quality.yml adds two repository-wide, language-agnostic gates:

  • Secrets scan (principle 11) — secretlint with the recommended preset, plus .secretlintrc.json
  • File line limits (principle 2) — scripts/check-file-line-limits.sh walks every tracked .js/.mjs/.cjs/.md/.py/.rs file and every workflow, warning at 1350 lines and failing at 1500

scripts/detect-code-changes.mjs now excludes dev/log/ so investigation artifacts cannot make a docs-only change look like a code change.

Tests

js/tests/unit/scripts/ci-workflow-policy.test.js gains three cases, each of which fails against the old checker:

  • untrusted expressions in run: bodies, asserting bump_type (a choice input) and a create-pull-request title: input are not flagged
  • an if: starting with the YAML tag indicator
  • outdated setup-python and codecov versions

5 pass, 0 fail.

Runtime warning found by auditing the post-fix logs

With the workflow noise gone, one real defect became visible in the Python suite on all three operating systems:

ResourceWarning: unclosed database in <sqlite3.Connection object at ...>

sqlite3.Connection.__exit__ only commits or rolls back the transaction — it does not close the connection — so with _open_cookie_database(path) as database: leaked one handle per cookie read. Fixed with contextlib.closing in browser_cookies.py and in the two test fixtures that build the profile databases.

Reproducing test: test_closes_the_cookie_database_connection asserts every opened connection raises ProgrammingError: closed database afterwards. It fails without the fix (DID NOT RAISE) and passes with it. pytest tests -W error::ResourceWarning now reports no unclosed database.

The JavaScript implementation was checked for the same defect and has none — js/src/browser/browser-cookies.js already closes the database in a finally block, so the error path does not leak either.

Upstream reports

The same defects in the templates this repository was seeded from:

The rust template was checked for the same defects and has none.

Verification

All six workflows parse; node scripts/check-ci-workflows.mjs, scripts/check-file-line-limits.sh, secretlint, npm run lint and npm run format:check all pass.

Post-fix CI at 72a637d: all six workflows succeed, and ##[warning] and ResourceWarning both appear zero times across all six logs (dev/log/issues/75/pulls/76/ci-logs-after/).

npm test locally: 512 pass, 6 fail — all six are browser-cookies.test.js cases needing node:sqlite, which the local Node 20.20.2 lacks. CI runs Node 24, where they pass.

Remaining template gates, now implemented

The four items an earlier revision listed as "deliberately left open" are implemented here. Each is one language-agnostic gate rather than three per-language copies that would drift apart.

  • Fresh-merge simulation (principle 7) — scripts/simulate-fresh-merge.sh, run from the lint and test jobs of all three pipelines with fetch-depth: 0. GitHub builds refs/pull/N/merge once and does not rebuild it when the base moves, so a semantic conflict between two pull requests that each pass alone stays invisible until both land. The base ref is bound through env: BASE_REF, never spliced into the script.
  • Version-modification check (principle 9) — scripts/check-version-modification.mjs + the version-check job in quality.yml, covering js/package.json, python/pyproject.toml and rust/Cargo.toml, skipping changeset-release/* branches. 6 new tests.
  • Link checking (principle 12) — .github/workflows/links.yml: lychee with fail: false, then scripts/check-web-archive.mjs. A link only fails the job when the Wayback Machine has no copy either, so a rate-limited third-party host does not turn an unrelated pull request red — a link check that goes red on an outage is itself the false positive this issue is about.
  • Coverage — Rust gains a cargo-llvm-cov job. Both the Rust and Python uploads now gate on CODECOV_TOKEN (forks have no secrets) and set fail_ci_if_error: true. Python previously used false, so a genuinely failed upload still reported success: coverage could stop updating with nothing saying so.

scripts/check-ci-workflows.mjs whitelisted only the literal name GITHUB_BASE_REF: when checking for github.base_ref injection, which rejected the equally safe BASE_REF: binding. It now accepts any SCREAMING_SNAKE_CASE name — the name carries no security meaning, only the binding does. Two regression tests cover both directions.

Real defect the link gate caught on its first run

docs/case-studies/issue-38/README.md cited https://pptr.dev/api/puppeteer.page.on, which 404s. The passage describes the Dialog object's accept(text?) / dismiss() / message() / type(), so the link now points at https://pptr.dev/api/puppeteer.dialog. After the fix lychee reports 131 links, 128 OK, 0 errors.

Evidence logs were not actually committed

ANALYSIS.md and this description cite ci-logs/*.log, but git ls-files showed none of them were tracked: the generic *.log rule in .gitignore was silently excluding every one. A !dev/log/**/*.log negation commits them (19 files, 7.7 MB). The machine-generated dev/log/**/sessions/ transcripts stay ignored — they can echo credentials. secretlint passes over the newly included files.

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

Issue: #75
@konard konard self-assigned this Aug 2, 2026
@konard konard changed the title [WIP] Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all fix(ci): eliminate CI/CD false positives, false negatives and warnings Aug 2, 2026
konard added 2 commits August 2, 2026 13:47
sqlite3.Connection.__exit__ only commits or rolls back the transaction, it
does not close the connection, so every cookie read leaked a handle and CI
reported "ResourceWarning: unclosed database" on all three operating systems.

Wrap the connection in contextlib.closing in the reader and in the two test
fixtures that build the profile databases, and add a reproducing test that
fails without the fix. The JavaScript implementation already closes in a
finally block, so it needed no change.
@konard
konard marked this pull request as ready for review August 2, 2026 13:51
@konard

konard commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Working session summary

Done. PR #76 is ready for review: #76

What closed out this round:

The JS cookie reader was checked first and needed no change — database.close() at js/src/browser/browser-cookies.js:[REDACTED] already sits in a finally`, so its error path doesn't leak.

The Python side did leak. sqlite3.Connection.__exit__ only ends the transaction, it doesn't close the connection, so with _open_cookie_database(path) as database: leaked a handle per read. That was the ResourceWarning: unclosed database visible on all three OSes once the workflow noise was gone. Fixed with contextlib.closing in `browser_cookies.py:[REDACTED]

Running the full suite under -W error::ResourceWarning showed the warnings did not stop there — the two test fixtures at test_browser_cookies.py:[REDACTED],144 had the identical bug. Both now use with closing(sqlite3.connect(...)) as database, database: so the handle closes and the inner context manager still commits. After that, grep -c "unclosed database" over the whole suite is 0.

Reproducing test test_closes_the_cookie_database_connection fails without the fix (DID NOT RAISE) and passes with it. Added python/changelog.d/75.fixed.md since the strict changelog gate this PR introduced now applies to itself.

Verification (commit 72a637d, after merging origin/main): all six workflows succeed, and both ##[warning] and ResourceWarning appear zero times across all six downloaded logs. ruff check, ruff format --check, mypy (40 files), the workflow policy checker and the line-limit script all pass. Working tree clean.

One caveat unchanged from before, stated in the PR body rather than papered over: npm test locally has 6 failures in browser-cookies.test.js that need node:sqlite, which the local Node 20.20.2 lacks — CI on Node 24 runs them green.

💰 Cost estimation:

  • Calculated by Anthropic: $10.385578

📊 Context and tokens usage:

Claude Opus 5: (3 sub-sessions)

  1. 117.4K / 1M (12%) input tokens, 19.7K / 128K (15%) output tokens
  2. 116.6K / 1M (12%) input tokens, 38.6K / 128K (30%) output tokens
  3. 58.5K / 1M (6%) input tokens, 6.8K / 128K (5%) output tokens

Total: (12.2K new + 250.7K cache writes + 11.1M cache reads) input tokens, 90.8K output tokens, $10.362573 cost

Claude Haiku 4.5:

  • 11.2K / 200K (6%) input tokens, 365 / 64K (1%) output tokens

Total: 11.2K input tokens, 365 output tokens, $0.023005 cost


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

@konard

konard commented Aug 2, 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: $10.385578

📊 Context and tokens usage:

Claude Opus 5: (3 sub-sessions)

  1. 117.4K / 1M (12%) input tokens, 19.7K / 128K (15%) output tokens
  2. 116.6K / 1M (12%) input tokens, 38.6K / 128K (30%) output tokens
  3. 58.5K / 1M (6%) input tokens, 6.8K / 128K (5%) output tokens

Total: (12.2K new + 250.7K cache writes + 11.1M cache reads) input tokens, 90.8K output tokens, $10.362573 cost

Claude Haiku 4.5:

  • 11.2K / 200K (6%) input tokens, 365 / 64K (1%) output tokens

Total: 11.2K input tokens, 365 output tokens, $0.023005 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: off (disabled)
  • Main model: Claude Opus 5 (claude-opus-5)
  • Additional models:
    • Claude Haiku 4.5 (claude-haiku-4-5-20251001)

📎 Log file uploaded as Gist (4260KB)


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

@konard

konard commented Aug 2, 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 August 2, 2026 13:53
konard added 8 commits August 2, 2026 13:56
The generic `*.log` rule in .gitignore excluded every CI log downloaded for
this investigation, so ANALYSIS.md and the pull request description cited
evidence files that were not actually in the repository. Negate the rule for
dev/log/ so the before/after logs are committed alongside the analysis, and
ignore solver session transcripts, which are machine-generated and may echo
credentials.
…ests

GitHub builds refs/pull/N/merge when a pull request is opened or synced and
does not rebuild it when the base branch moves, so every job that checks that
ref validates a stale merge preview. A pull request can therefore be green
while the state that actually lands on main is broken -- the semantic-conflict
case, where two changes are individually correct and produce no textual
conflict but break each other.

Add scripts/simulate-fresh-merge.sh, the language-agnostic script the three
pipeline templates each ship, and run it before the checks in the lint and test
jobs of js.yml, python.yml and rust.yml. Those checkouts move to fetch-depth: 0
because a shallow clone has no merge base to merge against.

The base ref is bound to BASE_REF rather than interpolated into the run body.
check-ci-workflows.mjs previously accepted only the literal name
GITHUB_BASE_REF, which would have rejected that equally safe binding, so the
rule now recognises any SCREAMING_SNAKE_CASE environment binding; two tests
cover the accepted binding and the still-rejected run-body splice.
Every published version here is chosen by a release job: js/package.json by
changesets, python/pyproject.toml and rust/Cargo.toml by the auto-release jobs.
A version edited by hand in a pull request either collides with the number the
pipeline is about to pick or skips one, and in both cases the git tag, the
changelog and the registry end up disagreeing about what a release contains.

Add scripts/check-version-modification.mjs and run it from quality.yml on pull
requests. It is one language-agnostic job over all three manifests rather than
three per-language copies, matching the reasoning already recorded in
quality.yml: copies in js.yml, python.yml and rust.yml would drift apart.

Branches the release pipeline opens itself (changeset-release/*,
changeset-manual-release-*) are skipped, since changing the version is their
entire purpose. Six unit tests inject the diff instead of shelling out to git,
covering each manifest, a removed version line, an unrelated manifest edit, and
pyproject's non-numeric `version = "literal: ..."` key.
Best practice #12. Documentation rots quietly: a link that 404s breaks no
build, so nothing reports it until a reader hits it.

A naive link check is itself a source of false positives, which is what this
issue is about -- an unrelated pull request should not go red because a
third-party host had an outage or rate-limited the runner. So lychee runs with
fail: false and a broken link only fails the job once the Wayback Machine has
no archived copy either, at which point the link really is unrecoverable.
scripts/check-web-archive.mjs and .lycheeignore are taken from the JavaScript
pipeline template, which already solves this.

dev/log is excluded: it holds downloaded CI logs and analysis notes citing run
URLs from other repositories, which are evidence of what was investigated
rather than documentation to keep reachable.

The gate found one real defect on its first run: docs/case-studies/issue-38
cited https://pptr.dev/api/puppeteer.page.on, which Puppeteer's documentation
restructuring turned into a 404. The passage describes the Dialog object's
accept/dismiss/message/type methods, so it now points at
https://pptr.dev/api/puppeteer.dialog. lychee reports 0 errors over all 47
tracked Markdown and HTML files.
The Python pipeline reported coverage but the Rust one did not, so Rust
coverage could regress unobserved. Add a cargo-llvm-cov job mirroring the
Python one.

Both uploads now gate on CODECOV_TOKEN (forks have no secrets) and set
fail_ci_if_error: true. The Python upload previously used false, so a
genuinely failed upload still reported success -- a false negative of
exactly the class this audit targets.
The job default working-directory is the language subdirectory, so
'bash scripts/simulate-fresh-merge.sh' resolved to js/scripts/... and
failed with exit 127. Pin working-directory to '.' and set shell: bash,
which the Windows matrix legs need because they default to pwsh.
The job also re-checked detect-changes outputs, which skipped the report
on runs where the tests did execute (this pull request itself: it edits
rust.yml but no .rs or .toml file). Gate it like build does, on the test
result alone.

Also record the post-fix CI logs: all seven workflows succeed and
'##[warning]' appears zero times across all of them.
@konard
konard marked this pull request as ready for review August 2, 2026 14:34
@konard

konard commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

Working session summary

Done. PR #76 is ready for review: #76

What closed out this round — the four template gates that an earlier revision had listed as "deliberately left open" are now implemented, so nothing from issue #75 remains outstanding:

  • Fresh-merge simulationscripts/simulate-fresh-merge.sh in the lint and test jobs of all three pipelines. GitHub builds refs/pull/N/merge once and never rebuilds it when the base moves, so a semantic conflict between two PRs that each pass alone stays invisible until both land.
  • Version-modification check — one version-check job covering all three manifests, skipping changeset-release/* branches. 6 new tests.
  • Link checkinglinks.yml: lychee with fail: false, then a Wayback Machine fallback, so a rate-limited third-party host doesn't turn an unrelated PR red. It caught a real 404 on its first run (pptr.dev/api/puppeteer.page.on in docs/case-studies/issue-38/README.md, corrected to puppeteer.dialog). After the fix: 131 links, 0 errors.
  • Coverage — a Rust cargo-llvm-cov job; both the Rust and Python uploads gate on CODECOV_TOKEN and set fail_ci_if_error: true. Python was false, so a genuinely failed upload still reported success.

Two defects surfaced during this round and were fixed rather than papered over:

  • The fresh-merge step failed with exit 127 on every leg — the job default working-directory is the language subdirectory, so the root-level script path didn't resolve. Fixed with working-directory: . and an explicit shell: bash (the Windows legs default to pwsh).
  • The Rust coverage job re-checked the detect-changes outputs on top of needs.test, which skipped the report on this very PR. That's the same class of silent gap the issue is about, so it now gates on the test result alone, like build does.

Also worth flagging: ANALYSIS.md and the PR description cited ci-logs/*.log as evidence, but git ls-files showed none of those files were tracked — the generic *.log rule in .gitignore was excluding all of them. They're committed now (19 files); the machine-generated session transcripts stay ignored because they can echo credentials.

Final state: all 7 workflows succeed at HEAD, ##[warning] appears zero times across every log, working tree clean, branch 16 ahead of main and 0 behind, PR mergeable. No reviewer comments were left unaddressed — the only PR comments are automated session logs.

💰 Cost estimation:

  • Calculated by Anthropic: $7.902738

📊 Context and tokens usage:

Claude Opus 5: (2 sub-sessions)

  1. 116.3K / 1M (12%) input tokens, 40.1K / 128K (31%) output tokens
  2. 78.7K / 1M (8%) input tokens, 13.7K / 128K (11%) output tokens

Total: (2.7K new + 160.1K cache writes + 9.2M cache reads) input tokens, 67.7K output tokens, $7.902738 cost


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

@konard

konard commented Aug 2, 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: $7.902738

📊 Context and tokens usage:

Claude Opus 5: (2 sub-sessions)

  1. 116.3K / 1M (12%) input tokens, 40.1K / 128K (31%) output tokens
  2. 78.7K / 1M (8%) input tokens, 13.7K / 128K (11%) output tokens

Total: (2.7K new + 160.1K cache writes + 9.2M cache reads) input tokens, 67.7K output tokens, $7.902738 cost

🤖 Models used:

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

📎 Log file uploaded as Gist (17964KB)


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

@konard
konard merged commit ac2be18 into main Aug 2, 2026
35 checks passed
@konard

konard commented Aug 2, 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

1 participant