Skip to content

ci: eliminate the false positives, false negatives and warnings across all four pipelines - #42

Merged
konard merged 29 commits into
mainfrom
issue-41-297a752a4939
Aug 20, 2026
Merged

ci: eliminate the false positives, false negatives and warnings across all four pipelines#42
konard merged 29 commits into
mainfrom
issue-41-297a752a4939

Conversation

@konard

@konard konard commented Aug 20, 2026

Copy link
Copy Markdown
Member

Closes #41.

[skip-parity] — see "Why the parity gate is skipped" at the bottom.

What this pull request does

Issue #41 asked for all false positives, false negatives, warnings and
errors in CI/CD to be found and fixed, for the four pipelines here to be
brought in line with the org's language templates and with
CI-CD-BEST-PRACTICES.md,
and for any defect that also exists upstream to be reported there.

The full evidence trail, timeline, per-requirement root-cause analysis and
execution record live in
dev/log/issues/41/pulls/42/ANALYSIS.md.

The headline bug: change detection classified every path wrongly

git diff --name-only prints paths relative to the repository root, but
scripts/detect-code-changes.mjs runs with working-directory: ./js and
compared those paths against package-relative prefixes (examples/,
docs/, package.json). In this monorepo the real paths are
js/examples/..., so:

  • False positive — a change touching only js/examples/demo.mjs produced
    any-code-changed=true, because none of the documented exclusions could
    ever match.
  • False negativepackage-changed could never be true, because the
    path is js/package.json, not package.json.

This is the same class of defect as #39. Reproduction, runnable end to end
against a scratch repository:

./experiments/detect-code-changes-monorepo-paths.sh

Before the fix it reports any-code-changed=true and lists
js/examples/demo.mjs as a code change; after the fix it prints
Package prefix: js/, Files considered as code changes: (none) and
any-code-changed=false.

The fix resolves the prefix at run time with git rev-parse --show-prefix,
which is empty at the repository root — so a single-package checkout keeps
working unchanged.

Changes

Change detection (js, rust, python, csharp)

  • Package-relative path classification via git rev-parse --show-prefix;
    files belonging to sibling packages are now ignored instead of counted.
  • A .github/workflows/ change still counts as a code change, since it can
    alter how the package is built and published.
  • 40 new unit tests (10 per language) covering prefix stripping, the
    repo-root no-op, examples/changeset-only changes, manifest detection,
    workflow changes and foreign-package changes.
  • New python/scripts/detect_code_changes.py and
    csharp/scripts/detect-code-changes.mjs, which did not exist before.

Workflows

  • New detect-changes gating job in python.yml and csharp.yml (js and
    rust already had one), so a documentation-only pull request no longer runs
    the full lint/test/build matrix. Gates use !cancelled() because
    detect-changes is skipped on workflow_dispatch.
  • New Run CI script tests step in js.yml: npm test only covers
    tests/, so the helper scripts the release jobs depend on had no
    automated coverage at all.
  • Python now runs on an OS matrix, matching the other languages.
  • New security.yml: CodeQL (javascript-typescript, python, csharp,
    actions), npm audit, cargo audit, pip-audit, a NuGet vulnerability
    audit, and a gitleaks working-tree + full-history scan. Deliberately no
    paths: filter — a security scan that can be skipped by touching the right
    file is not a security scan.
  • Link checking, timeouts, least-privilege permissions:, and concurrency
    groups applied across every workflow.

Full per-check detail, including the checks that were found to be
structurally incapable of failing, is in §9.3 of the analysis.

False positives found by the new gates

Both were investigated and are documented in §9.2:

  • lychee reporting 403 from npmjs.com — bot protection, not a broken link.
  • gitleaks flagging key: macOS-cargo-<hex> — a cache key, not a secret.

Two more defects that only CI could find

Both were found by the first pipeline runs of this branch and are documented
in §9.5 and §9.7:

  • shellcheck: nine unquoted >> $GITHUB_OUTPUT redirections. Local
    actionlint exited 0 while CI exited 1, because actionlint runs shellcheck
    only when the binary is on PATH and silently skips that analysis
    otherwise. The green local lint was itself a false negative. All 10 such
    redirections are now quoted, not only the 4 lines the linter pointed at.
  • Dependency review can only ever fail here. The action needs the
    repository's Dependency graph enabled, which a pull request cannot change.
    Rather than hide it behind continue-on-error — an ignored failure being
    the same false negative the issue is about — it is removed. Its only unique
    coverage was NuGet, now handled by dotnet list package --vulnerable --include-transitive. That command exits 0 even when it finds advisories,
    so the job inspects the report rather than the exit code; both paths were
    verified locally.

Both are handled by configuration rather than by disabling the check.

Upstream reports (requirement 3)

The path-prefix defect was verified to exist in two of the four templates by
running their own scripts in synthetic multi-language repositories. The
C# template already solves it (prefixCsharpRoot()) and the Python template
partially solves it (removeprefix("python/")); js and rust do not, despite
js-paths.mjs and rust-paths.rs documenting multi-language support.

Each report carries a runnable reproduction, actual vs. expected output, how
the sibling templates solve it, a suggested code fix and a workaround.

Two further defects hypothesised earlier in the analysis were disproved
by reading the templates in full — tpl-rust does verify the crates.io
publish, and tpl-python's PyPI smoke test does retry and fail the release.
Both are marked withdrawn in place in §7 rather than deleted; a withdrawn
hypothesis is part of the record.

Verification

All four pipelines pass locally:

Pipeline Result
js eslint, prettier, 244 tests, 10 script tests
rust cargo fmt --check, clippy -D warnings, 22 script tests
python ruff check/format on src tests scripts, mypy src, 196 tests, 89% coverage
csharp build with 0 warnings, 171 tests, 10 detect tests
workflows actionlint exit 0 on every workflow

Notes for the reviewer

Branch protection will need attention after merge:

  • The Python required check renames to Test (Python 3.13 on <os>).
  • New checks appear and may need to be marked required: Detect Changes
    (Python and C#), Scan for Committed Secrets, Check Links.

Separately, and not blocking this pull request: enabling Dependency graph,
Dependabot alerts and secret-scanning push protection under
Settings → Security
would let the dependency-review job be restored on top of the audits above.

Why the parity gate is skipped

The parity gate reports "Changed: JavaScript, C# / Missing a matching
change: Python, Rust"
and instructs: "If this change is intentionally
single-language, add [skip-parity] to the pull request title or body."

This pull request changes CI/CD infrastructure, not library behaviour — the
codec's public API is untouched in all four languages, so there is no
cross-language behaviour to keep in parity. The gate's own documented escape
hatch applies.

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

Issue: #41
@konard konard self-assigned this Aug 20, 2026
konard and others added 24 commits August 20, 2026 07:52
The C# release job for 0.2.0 failed with 'is not on NuGet after publish'
while the push had in fact succeeded: the inline verification polled the
flat-container API six times over sixty seconds, and NuGet made the
package visible about six and a half minutes later. NuGet documents
package validation and indexing as taking up to fifteen minutes, so the
budget was fifteen times too small and the red run was a false negative.

scripts/wait-for-registry.mjs replaces the hand-rolled curl loops with
one readback that knows each registry's own indexing window, tells slow
indexing apart from a failed publish in its error message, and has a
verbose mode (off by default) that prints every probe.

A live probe of all four registries caught a second false negative in
the readback itself: crates.io enforces an API data access policy that
answers 403 to clients without a descriptive User-Agent, so an anonymous
check would report a published crate as missing. Probes now identify
themselves.
Auto-release stops failing on a publish that is merely still indexing,
and manual-release gains the verification it never had.
python/scripts/ holds the code that performs the release, yet ruff only
ever saw src and tests. Bringing it into scope surfaced two deprecated
Optional annotations and seven files formatted to a line length the
project does not use; both are fixed here so the wider scope starts
green.
The 0.2.0 release failed with 'invalid-publisher: valid token, but no
corresponding publisher'. https://pypi.org/pypi/lino-objects-codec/json
answers 404, so the project has never been published, and PyPI needs a
*pending* publisher registered before the first upload of a project that
does not exist yet. That is a one-time action in PyPI's settings, so the
repository cannot fix it — but it can stop hiding it.

A preflight now prints the four claims PyPI will be asked to match and
the steps to register them, and a failure handler repeats them with the
claims dumped. Both report without gating, because a correctly
registered pending publisher looks exactly like none at all from outside
until the first upload lands.

A PYPI_API_TOKEN secret, when set, carries the upload instead of OIDC so
a first release is possible before trusted publishing is registered.

The PyPI readback also moves off its twenty-five second budget onto the
shared registry wait.
Rust was the only language that published without reading the result
back, so a rejected upload would still have produced a green run, a tag
and a release for a version nobody can depend on.

The shared scripts/ helpers had no workflow of their own either: only
the pull-request-only parity job ran a test, and only one file of it.
- checkout v4->v7, setup-node v4->v7, setup-python v5->v7,
  setup-dotnet v4->v6, upload-artifact v4->v7, download-artifact v4->v8,
  cache v4->v6, codecov-action v4->v7, create-pull-request v7->v8.
- codecov v5 renamed 'file' to 'files'; rename the input and pass
  CODECOV_TOKEN so protected-branch uploads stop warning.
- csproj: PackageReadmeFile + packed README.md removes 'warn : Readme missing'
  from 'dotnet pack'.
- pyproject: replace the deprecated 'license = {file = ...}' TOML table with the
  PEP 639 SPDX expression plus 'license-files', and add python/LICENSE so
  setuptools stops warning that the file cannot be found.
…nings

_encodeValue (complexity 23) and _decodeLink (complexity 55, 95 statements)
tripped the repository's own complexity/max-statements rules on every CI run.
Extract the memoisation, scalar and collection handling into focused helpers so
the two entry points become plain dispatch. Pure refactor: no behaviour or API
change, all 244 tests and the example still pass, and 'eslint .' is now clean.

This touches js/src/ only, so the pull request carries [skip-parity]: the other
three codecs are unchanged because their linters report no such warning.
Applies best practices #7/#10 from the org CI/CD guide to all four language
pipelines:

- top-level 'permissions: contents: read'; release jobs keep their own
  elevated block, so nothing else gets a writable token by default;
- 'timeout-minutes' on every job, so a hung runner fails instead of burning
  the 6h default;
- job-level concurrency replaces the workflow-level cancellable group. A
  workflow-level 'cancel-in-progress: true' could cancel a release that had
  already started pushing a tag or a package. Read-only checks now cancel per
  job (matrix entries stay independent); every writer shares the repo-scoped
  'main-writer-${{ github.repository }}' group with 'cancel-in-progress: false'
  and 'queue: max', so releases across the four languages queue instead of
  racing for the same branch;
- 'always() && !cancelled()' collapses to '!cancelled()' (a bare always() keeps
  downstream work running after cancellation), with the stale comments updated;
- 'github.head_ref' is passed through an environment variable instead of being
  interpolated into a shell script (actionlint 'expression' rule).
A malformed workflow file is a silent false negative: GitHub refuses to run it
and attaches no check, so the pull request still looks green while a pipeline
has stopped running. actionlint parses all six workflows on every PR and also
flags shell injection through untrusted contexts.
Two whole classes of check were missing, so every pull request was green on
questions nobody was asking (best practices #11 and #12):

- security.yml: CodeQL over javascript-typescript, python, csharp and the
  workflow files themselves; dependency-review on pull requests; and a lockfile
  audit per ecosystem (npm, cargo-audit, pip-audit). No paths: filter and a
  weekly schedule, so newly published advisories surface without a code change.
- links.yml: lychee over every Markdown file, excluding docs/case-studies and
  dev/log, which quote other repositories' issues and runs as evidence.

All five audits were run locally against this tree first: npm audit, cargo
audit and pip-audit report no vulnerabilities.
…heck

A local run of the new lychee gate failed on links this repository cannot
fix: the './**/*.md' glob picked up third-party READMEs under
js/node_modules, and www.npmjs.com answers every automated request with
403 (verified with and without a browser User-Agent), so the package
badges in README.md and js/README.md were reported broken.

Adds .lycheeignore for the npmjs.com host and --exclude-path js/node_modules.
Local run is now 107 OK / 0 errors.
The refs/pull/N/merge preview GitHub checks out is built when the PR is
opened or synced; once the base branch moves it is stale, so lint, test
and build validated code that is not what will land. Adds
scripts/simulate-fresh-merge.sh (with an off-by-default VERBOSE mode that
lists the commits the base is ahead by) and calls it from the read-only
check jobs of all four language workflows, with fetch-depth: 0 so the
base branch history is available.

Verified: actionlint clean, all workflows parse, script runs locally.
csharp/scripts/*.test.mjs existed but no workflow ever executed them, so a
broken release helper would only have surfaced during a real release. The
C# sources also had no line-count ceiling, unlike Rust (scripts/check-file-size.mjs),
Python (scripts/check_file_size.py) and JavaScript (ESLint max-lines).

Ports the template's check-file-size.mjs with the repository-wide 1500-line
limit (best practice #2 range, matching the ESLint rule) plus its test suite
converted to node:test, and adds both steps to the C# lint job.

Verified locally: 10/10 script tests pass, file-size check exits 0.
Nothing scanned this repository for credentials. Adds a gitleaks job that
checks both the working tree and the full git history, using the pinned
MIT-licensed CLI rather than gitleaks-action, which requires a paid licence
for organisation-owned repositories.

An unconfigured run reports 7 findings, all false positives: the
generic-api-key rule matches the "key: <hex>" lines that actions/cache
prints in the CI logs archived under docs/case-studies and dev/log.
.gitleaks.toml allowlists those evidence directories with the reasoning
recorded inline, so the scan stays actionable.

Verified locally: 0 findings on the tree and across 146 commits.
Both checks were false negatives. python.yml and csharp.yml each carried an
inline shell copy that printed "::warning::No changelog fragment found" /
"::warning::No changeset found" and then exited 0, so a PR could change
python/src or csharp/src with no release note and still show a green check.
Both also counted every file in changelog.d/ and .changeset/, so a leftover
fragment from an earlier unreleased PR satisfied the check for a new PR that
added nothing.

Both now decide from the PR diff and exit 1, matching the Rust and JavaScript
implementations:
- csharp/scripts/check-changeset.mjs (new, with unit tests)
- python/scripts/validate_changeset.py (existed but no workflow ever ran it;
  extended with the diff-based requirement, with unit tests)

Both use git diff --relative, without which the monorepo path prefix silently
disables the check (issue #39), and both pass github.base_ref/head_ref through
the environment instead of interpolating them into shell.

Verified locally: 7/7 C# script tests, 9/9 new Python tests, 186 Python tests
total, ruff and mypy clean, actionlint clean.
Python was the only implementation tested on a single operating system;
JavaScript, Rust and C# each run a three-OS matrix. A Windows-only defect
(path separators, line endings, locale-dependent encoding) would have shipped
to PyPI unnoticed. Coverage is uploaded from the ubuntu leg only.

Note: the job name changes from "Test (Python 3.13)" to
"Test (Python 3.13 on <os>)", so the required-check list in branch
protection needs updating.
`git diff --name-only` prints paths relative to the repository root, but
`detect-code-changes.mjs` runs with `working-directory: ./js` (and ./rust)
and compared those paths against package-relative prefixes. In this monorepo
the real paths are `js/examples/demo.mjs`, not `examples/demo.mjs`, so:

* the documented `examples/`, `experiments/`, `docs/` and `.changeset/`
  exclusions never matched -- an examples-only pull request was reported as a
  code change and asked for a changeset it does not need (false positive);
* `package-changed` could never become true (false negative);
* Rust's `toml-changed` fired on any .toml in the repository, including
  `python/pyproject.toml`.

This is the same class of defect as issue #39. The prefix is now resolved at
run time with `git rev-parse --show-prefix`, which keeps the scripts correct
in a single-package checkout too, where the prefix is empty.

experiments/detect-code-changes-monorepo-paths.sh reproduces the original
behaviour end to end; the new unit tests pin it down per case. The js lint job
now runs `node --test scripts/*.test.mjs`, which nothing did before.
All four organisation templates ship a `detect-changes` job; this repository
had one for JavaScript and Rust only, so a documentation-only pull request ran
the full .NET and Python matrices. The new scripts follow the fixed
package-relative semantics from the previous commit and ship with unit tests.

The workflow-level `paths:` filter decides whether the workflow runs at all;
`detect-changes` decides which jobs inside it are worth running. `!cancelled()`
guards every gated job because `detect-changes` is skipped on
`workflow_dispatch`.
Reading the templates in full disproved two of the three hypothesised upstream
defects: tpl-rust does verify the crates.io publish (wait-for-crate.rs, called
from release.yml:632) and tpl-python's PyPI smoke test retries 6 x 20 s and
fails the release. Both claims are withdrawn in place rather than deleted.

The defect that is real is the one this pull request just fixed downstream: the
JavaScript and Rust templates match their change-detection exclusion lists
against repository-root paths, so nothing is excluded in the multi-language
layout their own path helpers support. Reported with reproductions:

  link-foundation/js-ai-driven-development-pipeline-template#141
  link-foundation/rust-ai-driven-development-pipeline-template#139
@konard konard changed the title [WIP] Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all ci: eliminate the false positives, false negatives and warnings across all four pipelines Aug 20, 2026
@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.

The pushed branch's Lint Workflows job failed with nine SC2086 reports for
unquoted `>> $GITHUB_OUTPUT`. Local actionlint had exited 0 because
actionlint only runs shellcheck when the binary is on PATH and skips that
analysis silently otherwise -- a green local lint that was itself a false
negative.

Quotes all 10 such redirections across csharp.yml, python.yml and rust.yml,
not only the 4 lines the linter pointed at. Verified with
`actionlint -shellcheck <path>`, which now exits 0.
The dependency-review job failed with "Dependency review is not supported on
this repository. Please ensure that Dependency graph is enabled" -- a
repository setting a pull request cannot change, leaving a check that could
only ever fail.

It is removed rather than marked continue-on-error, since an ignored failure
is the same false negative issue #41 is about. Its only unique coverage was
NuGet, which is replaced by `dotnet list package --vulnerable
--include-transitive`, so all four ecosystems are audited again.

That command exits 0 even when it finds advisories, so the job inspects the
report instead of the exit code. Both paths verified locally.
@konard
konard marked this pull request as ready for review August 20, 2026 09:10
@konard

konard commented Aug 20, 2026

Copy link
Copy Markdown
Member Author

Working session summary

Done. PR #42 is green across all nine workflows and marked ready for review: #42

This session's work, on top of what was already committed:

Two further defects, both found only by CI itself:

  1. Nine unquoted >> $GITHUB_OUTPUT redirections (SC2086). Local actionlint exited 0 while CI exited 1 — actionlint runs shellcheck only when the binary is on PATH and silently skips that analysis otherwise. The green local lint was itself a false negative, exactly the class of defect issue Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all #41 targets. Fixed all 10 such redirections repository-wide, not just the 4 lines the linter pointed at, and verified with actionlint -shellcheck.

  2. Dependency review could only ever fail here — the action needs the repository's Dependency graph enabled, which a pull request cannot change. Marking it continue-on-error would have produced an ignored failure, so it was removed instead. Its one unique contribution was NuGet coverage, replaced by dotnet list package --vulnerable --include-transitive. That command exits 0 even when it finds advisories, so the job inspects the report rather than the exit code; both the pass and fail paths were verified locally.

Verification — all nine workflows pass on dde3a7c: JavaScript, Python, C#, Rust, Security, Shared CI Scripts, Broken Link Checker, Cross-Language Parity. The only skipped jobs are the release jobs, which is correct on a pull request. Working tree is clean, main is already merged, and there is no outstanding review feedback or open code-scanning alert.

Both new findings are recorded as §9.5 and §9.7 in dev/log/issues/41/pulls/42/ANALYSIS.md, and the PR description was updated to match.

Two things need a human, neither blocking the merge:

  • Branch protection will need updating: the Python required check renames to Test (Python 3.13 on <os>), and Detect Changes (Python/C#), Scan for Committed Secrets and Check Links are new.
  • Repository settings: enabling Dependency graph, Dependabot alerts and secret-scanning push protection would let the dependency-review job be restored on top of the four ecosystem audits.

One caveat on the parity gate: it fails on this branch and the PR body carries [skip-parity] with the reasoning, since this changes CI/CD infrastructure and leaves the codec's public API untouched in all four languages. That is the gate's own documented escape hatch, but it does mean the gate was satisfied by declaration rather than by a matching code change — worth a reviewer's glance.


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

@konard

konard commented Aug 20, 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: $27.192996

📊 Context and tokens usage:

Claude Opus 5: (6 sub-sessions)

  1. 114.8K / 1M (11%) input tokens, 16.0K / 128K (13%) output tokens
  2. 112.6K / 1M (11%) input tokens, 46.6K / 128K (36%) output tokens
  3. 116.7K / 1M (12%) input tokens, 34.5K / 128K (27%) output tokens
  4. 116.3K / 1M (12%) input tokens, 37.4K / 128K (29%) output tokens
  5. 116.6K / 1M (12%) input tokens, 46.9K / 128K (37%) output tokens
  6. 79.8K / 1M (8%) input tokens, 21.3K / 128K (17%) output tokens

Total: (11.4K new + 538.5K cache writes + 30.2M cache reads) input tokens, 262.8K output tokens, $27.126745 cost

Claude Haiku 4.5:

  • 41.0K / 200K (20%) input tokens, 1.1K / 64K (2%) output tokens

Total: 41.0K input tokens, 1.1K output tokens, $0.066251 cost

🤖 Models used:

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

📎 Log file uploaded as Gist (10046KB)


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

@konard
konard merged commit 03aaf8c into main Aug 20, 2026
54 checks passed
@konard

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