Skip to content

Fix all false positives, false negatives, warnings and errors in CI/CD - #288

Merged
konard merged 13 commits into
mainfrom
issue-287-aa65665cb2fd
Jul 31, 2026
Merged

Fix all false positives, false negatives, warnings and errors in CI/CD#288
konard merged 13 commits into
mainfrom
issue-287-aa65665cb2fd

Conversation

@konard

@konard konard commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Fixes #287.

Audit of every CI/CD workflow and helper script in this repository against the two pipeline templates (js, rust) and the hive-mind CI/CD best practices. Every defect below is covered by a test that fails before the fix.

False negatives — CI was green while something was broken

Defect Effect
The unit test step ran a hardcoded four-file list 48 of the 52 unit test files never ran in CI
npm publish verification did not wait for the registry A publish could be reported as successful without the version being resolvable
refs/pull/N/merge is built at synchronize time A pull request could be green while its merge result was broken
No secret scanning Committed credentials would not be caught
scripts/ was linted and formatted by neither the hook nor CI Style and lint errors landed in shared CI helpers

Running the full suite on all three matrix platforms surfaced two real defects that the four-file list had hidden:

Defect Effect
sleep() in src/provider/retry-fetch.ts unref'd its retry timer While a rate limit wait is in flight that timer is the only pending work, so the runtime may stop waiting on it and drop the retry. On Bun for Windows the job wedged in tests/retry-fetch.ts and burned the whole 20 minute timeout (run 30658208241); with the timer referenced the same suite finishes in ~2 minutes
tests/storage-migration.ts asserted a literal POSIX path path.resolve returns D:\workspace\... on Windows, so the assertion could only ever hold on the two POSIX runners

False positives — CI was red or a release was blocked while nothing was wrong

Defect Effect
verbose-hi.js asserted a literal HTTP 200 from a real free-tier provider A provider 429/503 failed the job, and release needs it, so a rate limit blocked releases (run 24122902674)
Rust release looked up the crate name with a bare grep Exit code 3 failed the job when the pattern did not match
cargo publish failure classifier treated "already published" as a failure Re-runs failed instead of being idempotent
cat ~/.config/opencode/opencode.json || echo "Config not found" Masked a genuinely missing config

Warnings and errors

  • Every job in every workflow logged hint: Using 'master' as the name for the initial branch, because actions/checkout runs git init before any config exists (run 30657021842). Fixed with the GIT_CONFIG_* env the js template uses.
  • js/package.json packaging metadata produced npm warnings and omitted files from the tarball.
  • The model-tests capability step appended unfiltered output to $GITHUB_OUTPUT, which fails the step with Invalid format.

Security and reliability policy applied to all four workflows

  • A least-privilege top-level permissions: contents: read; publishing jobs escalate individually.
  • timeout-minutes on all 17 jobs that lacked one.
  • always()${{ !cancelled() }} in 9 job conditions, so cancelling a run stops the job graph (hive-mind #1278).
  • workflow_dispatch inputs and github.head_ref are passed through env: instead of being interpolated into run: scripts, where they would execute as shell code.
  • concurrency groups on the two workflows that had none, and cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} so a push to main can no longer cancel an in-flight publish and leave a version tagged but unpublished.
  • bun test --timeout 30000, so a hung test reports before burning the job timeout.
  • Third-party actions pinned to the major versions the templates use (actions/cache@v4@v5, peter-evans/create-pull-request@v7@v8), with a policy test rejecting floating refs such as @main.
  • integration-tests.yml gained defaults.run.shell: bash (windows-latest defaults to pwsh) and a matrix resolver job; model-tests.yml gained the missing job name.

Tests

New, all failing before their fix:

  • js/tests/workflow-policy.js — 12 executable policy rules (permissions, timeouts, cancellation, input interpolation, concurrency, main-cancellation, default-branch env, fresh merge, secret scan). 6 of the original 7 rules failed when written: 4 workflows without permissions, 17 jobs without timeouts, 9 always() conditions, 9 input interpolations, 2 workflows without concurrency.
  • js/tests/verbose-http-log.js + js/tests/lib/verbose-http-log.js — the verbose logging contract, including "still passes when the provider rate limits the request".
  • js/tests/simulate-fresh-merge.js — merge simulation, including the conflict path.
  • js/tests/ci-scripts.js — the Rust/npm publish helpers.
  • js/tests/retry-fetch-wait.js — runs a rate limit wait in a fresh process, where nothing else keeps the event loop alive.

js/tests/workflow-policy.js is now 13 rules (the 13th rejects unpinned actions).

Local verification: npm run check clean, bun run test → 684 tests, 0 failures. CI on this branch: JS and Rust pipelines both green (runs 30660339994 and 30660339979), including all three unit test platforms.

Deliberately out of scope: the templates' documentation link checker (lycheeverse/lychee-action). It is an added check rather than a fix for an existing false result, and it would need a separate pass over the repository's docs; it is not part of this pull request.

Reported upstream

The same defects exist in the templates and the best-practices document, as the issue asks:

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

Issue: #287
@konard konard self-assigned this Jul 31, 2026
konard added 9 commits July 31, 2026 18:41
…nd packaging

Root causes found by reproducing the failing CI runs locally (issue #287):

1. Rust "Auto Release" failed with a bare `exit code 3` and no step output.
   `grep -Po '(?<=^name = ")[^"]*' rust/Cargo.toml` matched every section that
   declares a name ([package], [lib], [[bin]] and ~38 [[test]] blocks), so
   CRATE_NAME became a newline-joined list, the crates.io URL was malformed and
   curl exited 3 under `set -e`. Added scripts/rust-package-info.mjs, which
   parses only the [package] table, and wired it into rust.yml. curl now also
   retries and tolerates transient network failures instead of failing the job.

2. The npm release job reported "Failed to publish after 3 attempts" while
   attempt 1 had actually published. Verification ran after a single 2s sleep
   and lost the race with registry propagation, so the loop republished and hit
   EPUBLISHCONFLICT. Ported the template's publish-retry / npm-registry /
   publish-failure-classifier modules: a publish success or an already-published
   conflict now moves to bounded exponential-backoff verification and never
   re-enters the publish path.

3. js/package.json shipped four `../` entries in `files`, which npm silently
   drops, so the published tarball contained no LICENSE. Added js/LICENSE and
   removed the dead entries; also normalized `bin.agent` and `repository.url`
   so `npm publish` stops emitting fixup warnings.

4. js.yml ran 4 of 52 unit test files from a hardcoded list, hiding regressions
   behind a green check. It now runs the same `bun run test` suite as local
   development and as documented in TESTING.md.

Added js/tests/ci-scripts.js covering all of the above (21 tests).
The Rust "Auto Release" job failed on main in five further runs, in the
"Publish to crates.io" and "Collect changelog and bump version" steps.

publish-to-crates.mjs scanned `cargo publish --verbose` output for the
substrings "error: " and "error[E" *before* consulting the exit code, and did
so even when cargo had exited 0. Verbose cargo output routinely contains both
(dependency diagnostics, test names, doc examples), so a successful publish
could be classified as failed and retried. Unlike `changeset publish`, cargo
reports failure reliably through its exit code, so classification now starts
there (scripts/cargo-publish-result.mjs) and text matching is used only to
recognise "already uploaded" and to mark auth errors as non-retryable. The
retry/verify split is shared with the npm path via publish-retry.mjs, and
verification goes through a dedicated crates.io client
(scripts/crates-registry.mjs) that distinguishes 404 from a registry outage.
An accepted upload that the index has not exposed yet is still reported as
published rather than failing a completed release.

rust-version-and-commit.mjs fetched origin/main without tags, so
checkTagExists() reported "not released" for a version whose tag already
existed upstream; the release proceeded and `git push --tags` was rejected.
It now fetches tags, pushes only the tag it created instead of every local
tag, reads and rewrites the version scoped to the [package] table (via the new
setPackageVersion helper) instead of matching the first `version = "..."` line
in the file, and builds commit/tag messages with execFileSync so quotes,
backticks and $VAR in a description cannot be interpreted by the shell.

Extends js/tests/ci-scripts.js to 31 tests covering the new modules.
`eslint .` and `prettier --check .` were run from js/, so the release
helper scripts in scripts/ - which the whole CI/CD pipeline depends on -
were never checked. Fixing that surfaced 4 real lint violations.

- add scripts/eslint.config.mjs re-exporting the js/ flat config (ESLint 9
  refuses to lint files outside its config file's base path)
- move .prettierrc/.prettierignore to the repo root so prettier's upward
  config resolution covers both js/ and scripts/ (without it prettier fell
  back to defaults and reported 757 false errors)
- wire scripts/ into lint, lint:fix, format and format:check
- extend .husky/pre-commit to cover staged scripts/*.mjs
- fix the violations: unused statSync import, two useless escapes, and a
  needless async main()
…-safety policy

Adds js/tests/workflow-policy.js, an executable version of the rules in
hive-mind docs/CI-CD-BEST-PRACTICES.md and the pipeline templates'
docs/BEST-PRACTICES.md (mirroring their workflow-permissions.test.js and
ci-timeouts.test.js). Six of its rules failed on this repository:

- no workflow declared a top-level permissions block, so every job
  inherited the repository default token scope
- none of the 17 jobs declared timeout-minutes, so a hang ran for the
  GitHub Actions default of six hours
- 9 job conditions used always(), which keeps dependent jobs running
  after a run is cancelled (hive-mind #1278); now ${{ !cancelled() }}
- workflow_dispatch inputs (custom_model, test_pattern, description,
  bump_type) and github.head_ref were interpolated straight into run:
  scripts, so a value with shell metacharacters executed as code; they
  now travel through env:
- integration-tests.yml and model-tests.yml had no concurrency group
- the bun unit test script had no per-test timeout

Also fixed while auditing the two manual workflows:
- integration-tests.yml duplicated its whole job body for the single-OS
  and all-OS cases, and both were named "Integration Tests (${{ matrix.os }})",
  rendering as "Integration Tests ()" for the job that has no matrix.
  Replaced by one matrix job fed by a resolver job.
- the MCP config check ended in '|| echo "Config not found"', so a
  broken 'mcp add' still produced a green step
- integration tests ran with the pwsh default shell on windows-latest,
  where the POSIX 'if [ -n ... ]' is a syntax error
- get-model-info.mjs output was appended verbatim to $GITHUB_OUTPUT;
  any non key=value line fails the step with 'Invalid format'
- the model-test Summary step was skipped exactly when a test failed
…bose test

The verbose-hi integration test asserted `"status": 200` from a real
free-tier provider. js.yml gates the release job on this job succeeding,
so a provider-side 429 or 503 reported the repository as broken and
blocked a release even though verbose logging worked correctly.

Extract the logging contract into tests/lib/verbose-http-log.js, which
reports the observed status but never asserts it, and unit test it in
tests/verbose-http-log.js against synthetic 200 and 429 logs.
Both pipelines used workflow-level `cancel-in-progress: true`, so a push
to main while a release run was publishing cancelled it, potentially
leaving the version bumped and tagged but never published. The
release/instant-release jobs' own `release-main` group does not protect
against this because workflow-level cancellation cancels the whole run.

Matches the js pipeline template rule enforced in
tests/workflow-reliability.test.js.
actions/checkout runs `git init` before any git config exists, so every
job in every workflow logged the 'Using master as the name for the
initial branch' hint (observed in run 30657021842). Setting
GIT_CONFIG_COUNT/KEY/VALUE at the workflow level, as the js pipeline
template does, removes it without an extra step.
Two best practices from the pipeline templates that this repository was
missing (principles 7 and 11 of the hive-mind CI/CD best practices):

- refs/pull/N/merge is built when the pull request is synchronized, so
  commits landing on the base branch afterwards were never checked: a
  pull request could be green while its merge result was broken. Both
  lint jobs now merge the base branch first via
  scripts/simulate-fresh-merge.sh, covered by tests/simulate-fresh-merge.js.
- The js lint job now runs secretlint over the repository.
@konard konard changed the title [WIP] Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all Fix all false positives, false negatives, warnings and errors in CI/CD Jul 31, 2026
konard added 3 commits July 31, 2026 19:40
The sleep() timer was unref'd, so while a retry wait is in flight the only
pending work is a timer the runtime may stop waiting on. On Bun for Windows
the unit test job wedged in tests/retry-fetch.ts and burned its 20 minute
timeout. Every caller awaits the sleep and the abort path clears the timer,
so the #213 concern (not delaying process exit) still holds.
@konard
konard marked this pull request as ready for review July 31, 2026 19:51
@konard

konard commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Working session summary

The Windows hang is resolved and the pipeline is green.

Root cause of the Windows job timeout (run 30658208241): sleep() in js/src/provider/retry-fetch.ts called timeout.unref(). While a rate-limit retry wait is in flight, that timer is the only pending work, so the runtime is free to stop waiting on it — on Bun for Windows the process stopped making progress inside tests/retry-fetch.ts > retries on 429 and succeeds on second attempt and burned the full 20-minute cap. The unref was unnecessary: every caller awaits the sleep, and the abort path clears the timer, so the original concern (#213, not delaying process exit) still holds. With the timer referenced, the same Windows suite now finishes in ~2 minutes. This was latent until this PR — the job previously ran only a hardcoded four-file list.

That fix exposed one more Windows-only defect: tests/storage-migration.ts asserted a literal POSIX path, while path.resolve returns D:\workspace\... on Windows. Fixed by comparing against path.resolve of the expected path.

Also in this round: actions/cache@v4@v5 and peter-evans/create-pull-request@v7@v8 to match the templates, plus a 13th policy rule rejecting floating action refs (@main/@master).

Verification:

  • CI on issue-287-aa65665cb2fd at bfe3af9: JS pipeline run 30660339994 success (all three unit-test platforms), Rust pipeline run 30660339979 success.
  • Local: npm run check clean, bun run test → 680 pass, 4 todo, 0 fail (684 tests, 57 files).
  • Working tree clean, origin/main already merged.

PR #288 is updated and marked ready for review. One item is explicitly out of scope and stated in the PR body: the templates' lychee documentation link checker — it is an added check rather than a fix for an existing false result.

💰 Cost estimation:

  • Calculated by Anthropic: $19.721387

📊 Context and tokens usage:

Claude Opus 5: (5 sub-sessions)

  1. 116.7K / 1M (12%) input tokens, 20.9K / 128K (16%) output tokens
  2. 116.1K / 1M (12%) input tokens, 32.2K / 128K (25%) output tokens
  3. 115.6K / 1M (12%) input tokens, 24.2K / 128K (19%) output tokens
  4. 117.4K / 1M (12%) input tokens, 23.9K / 128K (19%) output tokens
  5. 78.2K / 1M (8%) input tokens, 17.2K / 128K (13%) output tokens

Total: (19.4K new + 448.1K cache writes + 22.3M cache reads) input tokens, 159.5K output tokens, $19.721387 cost


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

@konard

konard commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Solution Draft Log

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

💰 Cost: $19.721387

📊 Context and tokens usage:

Claude Opus 5: (5 sub-sessions)

  1. 116.7K / 1M (12%) input tokens, 20.9K / 128K (16%) output tokens
  2. 116.1K / 1M (12%) input tokens, 32.2K / 128K (25%) output tokens
  3. 115.6K / 1M (12%) input tokens, 24.2K / 128K (19%) output tokens
  4. 117.4K / 1M (12%) input tokens, 23.9K / 128K (19%) output tokens
  5. 78.2K / 1M (8%) input tokens, 17.2K / 128K (13%) output tokens

Total: (19.4K new + 448.1K cache writes + 22.3M cache reads) input tokens, 159.5K output tokens, $19.721387 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: off (disabled)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (7652KB)


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

@konard
konard merged commit 1c8f9c7 into main Jul 31, 2026
17 checks passed
@konard

konard commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

🎉 Auto-merged

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

  • All CI checks have passed

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant