Skip to content

fix(ci): eliminate the false positives, false negatives and warnings across all seven publish workflows - #299

Merged
konard merged 7 commits into
mainfrom
issue-298-d29274924d59
Aug 28, 2026
Merged

fix(ci): eliminate the false positives, false negatives and warnings across all seven publish workflows#299
konard merged 7 commits into
mainfrom
issue-298-d29274924d59

Conversation

@konard

@konard konard commented Aug 28, 2026

Copy link
Copy Markdown
Member

Fixes #298.

What was wrong

Run 33168552506 failed on main while the release it was verifying had already shipped. That single run contained three distinct defect classes, and the same patterns were spread across the other six language workflows.

1. False negative — the release that succeeded but reported failure

cargo publish genuinely worked; the collected log shows Published links-notation v0.16.0 at registry crates-io at 11:50:09. The verification step then ran:

curl -fsS "https://crates.io/api/v1/crates/${PACKAGE_NAME}/${PACKAGE_VERSION}" >/dev/null 2>&1

crates.io answers HTTP 403 to clients that do not send an identifying User-Agent — curl's default curl/8.x is rejected. Verified live:

$ curl -s -o /dev/null -w '%{http_code}' https://crates.io/api/v1/crates/serde
403
$ curl -s -o /dev/null -w '%{http_code}' -A 'my-tool (+https://example.com)' https://crates.io/api/v1/crates/serde
200

So all 20 attempts failed for a reason unrelated to the release, and >/dev/null 2>&1 threw away the status code that would have said so — which is why the log holds 20 identical Not visible yet lines and nothing else. The repository already knew this rule: scripts/release-audit.mjs has always sent 'user-agent': 'links-notation-release-audit'. The knowledge never reached the workflows.

The bare-curl / status-discarding pattern existed in 11 call sites across 7 workflows. All of them now go through one shared helper, scripts/ci/registry-probe.sh, so they cannot drift apart again.

2. False positive — an error annotation on an unconfigured optional integration

rust-lang/crates-io-auth-action@v1 fails with No Trusted Publishing config found and writes an ::error:: annotation. continue-on-error: true does not suppress annotations. The step is now gated behind vars.CRATES_IO_TRUSTED_PUBLISHING == 'true'.

3. False positive — release-audit racing the publishes it audits

7 of the 17 annotations on that commit came from the audit finishing 8 seconds after the push (11:48:36) while rust kept publishing until 11:55:14. The audit now waits for this commit's other workflows on push, and reports at ::notice:: instead of ::warning:: on pull requests, where being ahead of the registry is the expected state.

4. Contradictory inputs — PyPI attestations

pypa/gh-action-pypi-publish defaults attestations to true, but attestations require trusted publishing, and a password was being passed. Both inputs are now driven by the same variable.

5. Log noise — rust re-publishing an already-released crate

Every other language workflow checked the registry before pushing; rust did not, so a re-run printed a red already exists on crates.io index. Added.

What is in this PR

  • scripts/ci/registry-probe.sh — shared registry probing: probe_registry, fetch_registry, crates_index_url, crate_version_published, wait_for_crate_version, wait_for_registry_match, wait_for_registry. Every request sends an identifying User-Agent, and every failure message ends with (last status: HTTP <code>) so the next occurrence is diagnosable from the log alone.
  • scripts/ci/registry-probe.test.sh — 21 assertions against a local server that reproduces the crates.io 403 behaviour. No network access, so it cannot go flaky. It fails on the pre-fix code and passes on the fix. One assertion guards a mistake I made while writing this: the sparse index returns 200 for any crate that exists, so a version must be matched inside the body0.1 must not match 0.16.0.
  • .github/workflows/workflows.yml — new ci-scripts job running shellcheck and the probe test. actionlint only reaches shell inlined in a run: block, so a standalone helper needs its own gate.
  • Verbose mode (R10): the existing default-off CI_VERBOSE convention now actually reaches the polls — per-attempt status codes and index contents. Both the silent and verbose paths are covered by tests.
  • .gitignoredev/log/ was matched by the inherited Visual Studio rule [Ll]og/, so every file under it had to be force-added. Un-ignored properly.
  • Version bumped to 0.16.1 across all seven implementations, so the fixed publish path is exercised end to end.

Annotations deliberately left in place

php not registered on Packagist, java Maven Central secrets missing, the go CODECOV_TOKEN notice, and two advisory notices are accurate reports of unconfigured optional integrations. Silencing them would trade a false positive for a false negative. Each is documented with a follow-up action in §5 and §8 of the analysis.

Template comparison and upstream reports

All files were compared against the seven link-foundation/*-ai-driven-development-pipeline-template repos. None uses bare curl — each has a language-native wait script — so the specific #298 trigger does not exist upstream. But the same defect class does, and it is reported with a reproduction, a workaround, and a code-level fix in each:

The csharp template's wait-for-nuget.mjs, which returns {available, status, url}, is the in-family reference both should follow.

Evidence and analysis

Everything collected is committed under dev/log/issues/298/pulls/299/ — 42 files: the raw failing-run logs, the jobs and annotations API dumps, all seven upstream templates, the referenced CI-CD-BEST-PRACTICES.md, and a 336-line analysis with the timeline, requirements R1–R11, per-defect root causes, and the survey of existing components (including why cargo-release, release-plz, and nick-fields/retry were rejected).

Verification

actionlint 1.7.7                    rc=0
shellcheck -x scripts/ci/*.sh       rc=0
zizmor 1.29.0 --min-severity medium No findings to report.
registry-probe.test.sh              passed: 21, failed: 0
version-consistency.mjs             All 7 implementations declare 0.16.1.

Running the audit live independently confirms the diagnosis: js, python, rust, csharp and go were all in sync at 0.16.0 — the release the workflow called a failure had shipped.

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

Issue: #298
@konard konard self-assigned this Aug 28, 2026
konard added 5 commits August 28, 2026 12:37
The rust workflow published links-notation 0.16.0 successfully and then
failed itself. `cargo publish` reported "Published links-notation v0.16.0
at registry crates-io" at 11:50:09, and the very next step spent five
minutes deciding the crate was not there.

The step ran `curl -fsS https://crates.io/api/v1/crates/$NAME/$VERSION`.
crates.io answers 403 to curl's default `User-Agent: curl/8.x`, so every
one of the twenty attempts failed for a reason that had nothing to do
with the release. `>/dev/null 2>&1` then threw the status code away, which
is why twenty identical "Not visible yet" lines were the only evidence.

The same bare-curl pattern was in eleven places across seven workflows, so
rather than patching the one that happened to fail, all of them now go
through scripts/ci/registry-probe.sh:

  - every request carries an identifying User-Agent, as crates.io asks;
  - the status code of the final attempt is always in the failure message,
    so 404 (indexing lag) is distinguishable from 403 (broken probe);
  - CI_VERBOSE=true logs the status of every attempt. Default off.

Two related defects in the same job:

  - the crates.io OIDC step wrote an `::error::` annotation on runs that
    published fine through CARGO_TOKEN, because trusted publishing is not
    registered yet. It is now gated on an opt-in repository variable.
  - the dependency wait polled the JSON API, but `cargo` resolves against
    the sparse index, and the index answers 200 for any existing crate.
    wait_for_crate_version matches the version inside the index document.

scripts/ci/registry-probe.test.sh reproduces the 403 against a local
server, so it needs no network. It fails against the old behaviour.

Refs #298
Two more annotation classes on the same push, neither of which described a
real problem.

release-audit runs on push to main, which is the same event that starts
the seven publish workflows. Run 33168552493 finished at 11:48:36, eight
seconds after the push; rust was still publishing until 11:55:14. The
audit compared the freshly bumped 0.16.0 against registries that could not
possibly have it yet and annotated all seven languages as drifted. The
registries now hold 0.16.0 for js, python, rust, csharp and go, so every
one of those warnings was noise.

The audit now waits for this commit's other workflow runs to complete
before comparing. On a pull request no publish job runs at all, so being
ahead of the registry is the expected state there and is reported as a
notice rather than a warning.

pypa/gh-action-pypi-publish defaults `attestations` to true, but
attestations require trusted publishing, and the step passes PYPI_TOKEN.
Every release therefore logged "the attestations input is ignored". Both
settings now derive from one opt-in variable, so they cannot contradict
each other.

Refs #298
… does

`cargo publish` prints

    error: crate links-notation-macro@0.1.0 already exists on crates.io index

whenever a version is re-published, and the rust workflow re-publishes the
macro crate on every run where only the main crate was bumped. The
function that calls it already handled the case and returned "skipped",
so the job was correct, but the log carried a red error line for a run
that was doing exactly the right thing.

The js, python, csharp, php and java workflows all check the registry
before pushing. Rust now does too, through the same shared helper.

crates_index_url gained a CRATES_INDEX_BASE override so the new
crate_version_published tests can run against the local stand-in server
instead of the real index. Three of them cover the case that made the
first draft of this helper wrong: the sparse index returns 200 for any
crate that exists, so the version has to be matched inside the document,
and 0.1 must not match 0.16.0.

Refs #298
Collects the raw logs, job and annotation inventories, and the seven
pipeline templates under dev/log/issues/298/pulls/299, plus the analysis
they support: the timeline of the failing push, the requirements from the
issue, a root cause for each defect, the template comparison behind the
two upstream reports, and the annotations that remain with the reason
each one stays.

The .gitignore change is what makes this committable at all. `[Ll]og/`,
inherited from the Visual Studio section, matches the `dev/log/`
directory, and `*.log` matches the CI logs collected inside it, so every
file here had to be force-added. Two negation rules make the development
log behave like the deliverable it is.

Refs #298
Prepares the release that exercises the fixed publish path end to end. The
whole premise of #298 is that the pipeline cannot be trusted to report what it
actually did, so the fix is only proven once a version ships through it.

js/package-lock.json carries the version too, so it is bumped alongside
js/package.json; version-consistency.mjs now reports 0.16.1 for all seven.
@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 the false positives, false negatives and warnings across all seven publish workflows Aug 28, 2026
@konard
konard marked this pull request as ready for review August 28, 2026 12:51
…hecks

This PR's first run failed two checks, and both were correct findings about
files it had just added:

- links (33172748012): the verbatim copy of CI-CD-BEST-PRACTICES.md keeps the
  upstream document's repository-relative links, which do not resolve from
  dev/log. Given a provenance header and absolute hive-mind URLs, the same
  treatment the copy under dev/log/issues/290/pulls/291/analysis/ already has.
  All five rewritten URLs verified to return 200.
- bom-check (33172747906): 'gh run view --log' prepends a UTF-8 BOM, so the
  collected publishToCratesIO log carried one. Stripped from byte 1; nothing
  else in the log changed.

Both runs' logs are kept alongside the rest of the evidence, and analysis/
README.md records the two edits so the copies are not mistaken for untouched
downloads.
@konard

konard commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

CI verification on 8e03bb8

All 12 workflows green (auto-merge skipped as designed):

bom-check success   csharp  success   go       success   java   success
js        success   links   success   php      success   python success
release-audit success  rust success   security success   workflows success

Complete annotation sweep across every job of every run on this commit — 0 errors, 0 warnings, 9 notices:

Level Message
notice 7 × <lang>: declared 0.16.1, latest on <registry> is 0.16.0
notice CODECOV_TOKEN is not configured, so coverage was not uploaded
notice Summary report available at: ... (lychee's own job-summary link)

The first row is the release-audit fix demonstrating itself: on main those seven were ::warning:: and fired 8 seconds after the push while the publishes were still running. On a pull request no publish job runs at all, so being ahead of the registry is the expected state, and they are now reported at ::notice::.

The two 0.16.1 / nothing published rows for java and php are accurate — those languages have never released. They are left as-is deliberately, with the follow-up actions in §8 of the analysis; silencing them would trade a false positive for a false negative.

Two findings this PR's own CI produced

The first run failed bom-check and links, and both were correct findings about files this PR had just added — the evidence I collected, not the fixes:

  • gh run view --log prepends a UTF-8 BOM, so the collected publishToCratesIO log carried one. Stripped from byte 1.
  • The verbatim copy of CI-CD-BEST-PRACTICES.md keeps the upstream document's repository-relative links, which cannot resolve from dev/log. Given a provenance header and absolute hive-mind URLs — the same treatment the copy under dev/log/issues/290/pulls/291/analysis/ already has. All five rewritten URLs verified to return 200.

Both runs' logs are committed alongside the rest of the evidence, and the two edits are recorded at the top of analysis/README.md so the copies are not mistaken for untouched downloads.

Local gates

actionlint 1.7.7                    rc=0
shellcheck -x scripts/ci/*.sh       rc=0
zizmor 1.29.0 --min-severity medium No findings to report.
lychee 0.24.2 --offline './**/*.md' 987 unique, 0 errors
registry-probe.test.sh              passed: 21, failed: 0
version-consistency.mjs             All 7 implementations declare 0.16.1.

@konard

konard commented Aug 28, 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: $11.902565

📊 Context and tokens usage:

Claude Opus 5: (3 sub-sessions)

  1. 112.9K / 1M (11%) input tokens, 33.7K / 128K (26%) output tokens
  2. 116.6K / 1M (12%) input tokens, 40.5K / 128K (32%) output tokens
  3. 95.9K / 1M (10%) input tokens, 20.2K / 128K (16%) output tokens

Total: (4.9K new + 268.3K cache writes + 12.3M cache reads) input tokens, 120.3K output tokens, $11.877740 cost

Claude Haiku 4.5:

  • 12.0K / 200K (6%) input tokens, 574 / 64K (1%) output tokens

Total: 12.0K input tokens, 574 output tokens, $0.024825 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 (4276KB)


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

@konard
konard merged commit f0193ce into main Aug 28, 2026
60 checks passed
@konard

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