Skip to content

Fix every false positive, false negative, warning and error in CI/CD - #291

Merged
konard merged 36 commits into
mainfrom
issue-290-642ff58c1ef8
Aug 28, 2026
Merged

Fix every false positive, false negative, warning and error in CI/CD#291
konard merged 36 commits into
mainfrom
issue-290-642ff58c1ef8

Conversation

@konard

@konard konard commented Aug 28, 2026

Copy link
Copy Markdown
Member

Closes #290.

What was wrong

Both default-branch runs named in the issue failed, and neither was an isolated
accident:

  • csharp 33150292349 — the publish step pushed the package to NuGet, then the
    documentation step failed, and the workflow reported failure for a release
    that had actually shipped. The run also carried 56 nullable reference
    warnings and a set of invalid cref values in generated code.
  • js 33150292364 — the publish step failed on credentials while the job
    around it succeeded, so a version that never reached npm was recorded as
    released.

Auditing outward from those two runs turned up eighteen further defects across
every workflow in the repository, including two that nobody had reported: the
C# API reference had been published to gh-pages for months but was
unreachable, because Pages serves the uploaded artefact and not that branch;
and the pre-commit hook that regenerates TEST_CASE_COMPARISON.md had been
failing with ENOENT since the Rust workspace was restructured, so every test
count in that document was stale and every link in it was dead.

The full investigation — timeline, root cause per defect, and the evidence each
claim rests on — is committed under
dev/log/issues/290/pulls/291/.
Start with its README.md.

What changed

Releases now tell the truth. Every publish step verifies the artefact after
pushing it: npm, PyPI, crates.io, NuGet, Maven Central, Packagist and
proxy.golang.org are each polled for the version just released, and the GitHub
release is created only once the registry confirms it. The gate is an explicit
published output rather than result == 'success', which is also true for a
job that published nothing. scripts/release-audit.mjs runs on a schedule and
compares every declared version against what the registries actually serve, so
a silent failure cannot stay silent.

Credentials. npm, PyPI and crates.io publish through OIDC trusted
publishing where it is configured, falling back to the existing token secret
until it is. Checkouts no longer persist the job token; workflow permissions
are declared least-privilege per job.

Correctness of the pipelines themselves. workflow_dispatch did nothing in
any workflow — every job was gated on a changed-file list, which is empty for a
manual trigger. Concurrency groups cancel superseded pull request runs but never
cancel a run on main, and publish jobs queue rather than cancel. always()
was replaced by !cancelled() where a cancelled run should not continue.

New checks, so the next defect of each kind is caught in review:

Workflow Catches
workflows actionlint and zizmor findings in .github/workflows/
security CodeQL for every language with an extractor, dependency review, TruffleHog over the history
links dead links in the repository's Markdown
release-audit a declared version the registry does not serve

Warnings fixed rather than suppressed: all 56 C# nullable warnings, the
setup-go cache pointed at a go.sum that does not exist, a Cargo cache keyed
without a compiler version, setuptools deprecations, google-java-format noise,
Java compiling against the wrong API level, and Prettier resolved at run time.

Verbose mode. Every workflow declares a verbose input and exports
CI_VERBOSE, default off, so diagnostics can be turned on for a single run
without committing anything.

Best practices and templates

All thirteen principles from
hive-mind's CI-CD-BEST-PRACTICES.md
were checked one by one against this repository; the result, including the four
gaps that remain and why, is in
BEST-PRACTICES-COMPLIANCE.md.
All seven *-ai-driven-development-pipeline-template repositories were cloned
and diffed against these workflows. Where a defect fixed here also exists
upstream, it was reported:

Report Defect
java-…-template#5 concurrency, always(), unverified publish
go-…-template#5 the same, plus over-broad permissions
otac0n/Pegasus#137 the code generator emits cref values the C# compiler rejects

Later, once the workflows check on this branch had proved its worth (see
below), the same actionlint + shellcheck sweep was run over all seven
templates. It produced 23 findings and one meta-finding that explains them:
no template runs actionlint or zizmor anywhere in .github/. Seven more
reports were filed, one per template, each closing with the workflows job this
pull request adds:

Report Defect
rust-…#141 the multi-arch manifest's printf format is single-quoted, so ${DOCKERHUB_IMAGE} is never expanded and imagetools create gets a literal ${DOCKERHUB_IMAGE}@sha256:…
csharp-…#49 ${{ github.head_ref }} interpolated into a run: body — script injection on fork PRs — in a step that already declares the value as env
python-…#62 queue: max, which is not a valid concurrency key and is silently ignored
go-…#6 a Codecov upload that cannot succeed, hidden by fail_ci_if_error: false and continue-on-error: true; @v4
java-…#6 Codecov fail_ci_if_error: false, @v4, and the v3-era file: input
js-…#145 no workflow linter; SC2046 and SC2034
php-…#3 no workflow linter (the template is otherwise shellcheck-clean)

Five of those seven had already been fixed elsewhere in the same template
family
— rust#113 and js#117 for queue: max, csharp#34 and python#27 for
Codecov gating, python#34 for @v4, js#115 for the head_ref interpolation,
all closed — so each report cites its precedent rather than arguing from taste.

Each report carries a reproducible example, a workaround and a suggested fix.
The Pegasus one is backed by
experiments/cref-repro/,
which probes six cref shapes and shows that nesting and a single language
keyword both break, while the one shape that does bind resolves to the wrong
(open) generic; rust#141 is backed by
experiments/upstream-templates/rust-manifest-printf-quoting.sh,
which reproduces the unexpanded image name in six lines.

What the new checks caught on their own first run

The workflows check earned its place immediately, and in a way worth
recording, because it also exposed a false negative in how it had been verified
locally:

  • 62 shellcheck findings, mostly unquoted >> $GITHUB_OUTPUT. A local
    actionlint had exited 0 over the same tree ten minutes earlier —
    actionlint without shellcheck on PATH silently skips every run:
    block and still exits 0.
    The job uses the Docker image, which bundles it.
  • The secret scan had never read a commit. TruffleHog aborted in argument
    parsing with flag 'no-update' cannot be repeated, because the action already
    passes that flag. The scanner version was also floating on latest while the
    wrapper action was pinned.
  • The php matrix was cancelling itself. All four legs shared one
    concurrency group, so 8.1, 8.2 and 8.3 were cancelled the same second
    they started while 8.4 completed and the run reported green: the repository
    had been testing one PHP version and reporting four.

With those fixed, all 12 runs went green and an annotation sweep over all 61
jobs returned a single benign notice. That was not sufficient. Downloading all
12 logs in full (3.1 MB) and grepping them found three more defects that no
annotation reported, because plain step output never becomes one:

  • Go coverage had never reached Codecov. Every run logged
    Upload queued for processing failed: {"message":"Token required - not valid tokenless upload"} and the step still ended outcome=success, because
    fail_ci_if_error: false. The step is now gated on the secret's presence,
    fails loudly when an upload that should work does not, and says so out loud
    when it is skipped.
  • An unrelated file was being uploaded under the go flag.
    experiments/test_coverage_data.json was swept up because files: adds to
    what the Codecov CLI finds rather than replacing it — its own action.yml
    documents this. Fixed with disable_search: true.
  • Every website build warned ESM syntax in a file loaded as CommonJS.
    docs/website/package.json now declares "type": "module".

The single sentence the whole investigation reduces to, in all three of its
sequences: a check that is not actually running is indistinguishable from a
check that passes.

Verification

  • actionlint exits 0 over all workflows — run via
    docker://rhysd/actionlint:1.7.7, so shellcheck really is on PATH;
    zizmor --min-severity medium reports no findings.
  • lychee over the repository's Markdown: 1050 links, 0 errors (before: 566
    "File not found" in TEST_CASE_COMPARISON.md alone).
  • The C# nullable warning count is 0, down from 56.
  • experiments/cref-repro and
    experiments/upstream-templates/rust-manifest-printf-quoting.sh reproduce
    their upstream defects on demand.
  • All 13 runs on the head commit are green, and the full text of every log was
    swept rather than only their annotations:
    grep -h "##\[warning\]\|##\[error\]" ci-logs/*.log returns nothing.
    The only two ##[notice] lines are intentional — the links workflow's summary
    link, and the "CODECOV_TOKEN is not configured" line added above, which exists
    precisely so that a missing upload is stated rather than disguised as a
    successful one.
  • The php matrix runs all four legs to completion (8.1, 8.2, 8.3,
    8.4), where three of the four were previously cancelled on every run.

main is merged in. It carried seven Dependabot bumps touching the same lines
this branch rewrote; the resolution keeps this branch's content on the new
action versions, and the bumps were applied to the workflows added here too.

Needs a maintainer

These cannot be done from a pull request. Each is a one-time console action:

  1. Create the npm trusted publisher for @link-foundation/links-notation, or
    set NPM_TOKEN — until then the publish step warns and skips.
  2. Rotate or repair NUGET_API_KEY; the current one failed authentication.
  3. Decide whether Java publishes to Maven Central, and add the credentials if so.
  4. Submit the PHP package to Packagist.
  5. Delete or annotate the [PHP] 0.2.0 release, which points at a version that
    was never published anywhere.
  6. Optionally configure trusted publishing for crates.io and PyPI, which the
    workflows already prefer when present.
  7. Use or delete DEPENDABOT_AUTO_MERGE_TOKEN, which is declared and unused.
  8. After the first Pages deploy from main, remove the
    link-foundation.github.io/links-notation/csharp/ entry from .lycheeignore
    — it is only there because the URL 404s until that deploy lands.

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

Issue: #290
@konard konard self-assigned this Aug 28, 2026
konard added 28 commits August 28, 2026 08:04
setuptools>=77 warns about the deprecated "License :: Public Domain"
classifier and about the license table, both removed on 2027-02-18. Use the
SPDX expression and license-files instead, and add python/LICENSE so the
wheel actually carries the text that license-files points at.

python -m build now completes with zero warnings.
…rmat

maven-compiler-plugin warned "system modules path not set in conjunction with
-source 11" because source/target only change the language level, not the API
the code is checked against. maven.compiler.release does both.

Spotless runs google-java-format inside the Maven JVM, which on JDK 17+ has no
access to jdk.compiler internals and prints a reflective-access warning per
module. .mvn/jvm.config opens exactly the packages it needs.
The format job ran "npx prettier --check .", which downloads whatever version
is newest at that moment, so a Prettier release could turn a green branch red
without a commit. Pin it as a devDependency and add format/format:check scripts
so local runs and CI use the same formatter.
Link<T>.Values is declared nullable, so every use of it outside a null check
produced CS8602 or CS8604. In the library the two affected methods now take a
nullable string and materialise Values once; in the tests the existing Values!
convention is applied to the remaining call sites.

dotnet build now reports 0 warnings and all 196 tests still pass.
The workflow fetched docfx.json, filter.yml, toc.yml and four shell scripts from
the main branch of linksplatform/Scripts at run time and then patched them with
about a dozen sed calls, so the build depended on an unpinned third-party branch
while the NuGet key was in scope, and bugs in those files could not be fixed here.

Vendoring them fixes three of those bugs directly:

- toc.yml pointed at api/Platform.$REPOSITORY_NAME.html. The name is wrong and
  so is the form: DocFX resolves homepage against the *source* file, so the two
  InvalidFileLink warnings only disappear with obj/api/<name>.yml.
- docfx.json targeted netstandard2.0 and branch master.
- publish-docs.sh cloned github.com/linksplatform/links-notation and pushed
  gh-pages there. That only worked because GitHub still redirects the old
  location to link-foundation/links-notation.

PackageIconUrl is removed: it is deprecated (NU5048) and the URL it named
returns 404. experiments/cref-repro records the one warning that remains, which
comes from the Pegasus code generator.

DocFX now builds with 1 warning instead of 3.
…eal push

The workflow downloaded docfx.json, filter.yml, toc.yml and four shell
scripts from the main branch of linksplatform/Scripts at run time and then
patched them with about a dozen sed calls. That made every run depend on an
unpinned third-party branch while the NuGet API key was in scope, and it
meant bugs in those files could not be fixed from this repository. The
vendored copies added in 14beb21 replace all of it.

Other defects fixed here:

- pushToNuget, generatePdfWithCode and publishDocumentation declared
  `needs: [test]` but their `if:` read
  `needs.findChangedCsFiles.outputs.isCsFilesChanged`. A `needs` reference
  to a job that is not in `needs` evaluates to empty, so the condition was
  always false and the three jobs never ran; the workflow reported green
  while shipping nothing.
- publishRelease was gated on `needs.pushToNuget.result == 'success'`. A job
  whose publish step is skipped still reports success, so a missing API key
  produced a green run and a GitHub release for a package that was never
  pushed. It is now gated on an explicit `published` output that is only
  'true' after `dotnet nuget push` has run and the version has been observed
  on api.nuget.org.
- The release tag was `${VERSION}_csharp`; every other language uses
  `<language>_<version>`.
- publishRelease, generatePdfWithCode and publishDocumentation had no
  `timeout-minutes`.
- No `permissions:` block, so every job got the default write token. The
  workflow is now read-only except for the two jobs that write to the
  repository.
- No `concurrency:` groups. gh-pages deploys and NuGet pushes can no longer
  race; check jobs cancel superseded runs on branches but never on main.

Adds an opt-in `verbose` dispatch input (default false, also readable from
the CI_VERBOSE repository variable) so the next failure can be diagnosed
without editing the workflow, and reports credential presence — never the
value — before pushing.
…ipped

`publishRelease` was gated on `needs.publishToNpm.result == 'success'`, and a
job whose publish step is skipped still reports success. Run 33150292364 is
the result: the publish step failed with ENEEDAUTH because NPM_TOKEN was
empty, nothing reached the registry, and the run still went green.

- publishToNpm now exposes a `published` output that is only 'true' after
  `npm publish` succeeded and the version was observed on
  registry.npmjs.org; publishRelease gates on that instead of the job result.
- Authentication moves to npm trusted publishing (OIDC, `id-token: write`,
  npm >= 11.5.1 installed explicitly), with NODE_AUTH_TOKEN as the bootstrap
  fallback. Writing the token into ~/.npmrc with `echo` is gone: it put the
  secret in a file on disk and produced the empty-token failure silently.
- A rejected credential now fails the job with a message that says which of
  the two mechanisms to configure, instead of being swallowed.
- `npx prettier --check .` resolved an unpinned prettier on every run and ran
  without dependencies installed; it now uses the pinned devDependency via
  `bun run format:check`.
- `bun install` -> `bun install --frozen-lockfile` so a stale bun.lock fails
  instead of being silently rewritten.
- Adds least-privilege `permissions:`, per-job `concurrency:` groups, and the
  opt-in `verbose` dispatch input (default false).
Publishing:

- `cargo publish --token ${{ secrets.CARGO_TOKEN }}` passed the token as an
  argv element, where any process on the runner could read it from /proc.
  It now goes through rust-lang/crates-io-auth-action (RFC 3691 trusted
  publishing) with CARGO_REGISTRY_TOKEN as the transport and the long-lived
  CARGO_TOKEN kept only as a bootstrap fallback.
- publishRelease was gated on `needs.publishToCratesIO.result == 'success'`,
  which is also what a job that published nothing reports. It now gates on a
  `published` output that is 'true' only after cargo published the crate and
  the version was observed on crates.io.
- A missing credential warned-and-skipped invisibly; it now emits a warning
  annotation and blocks the release.
- `--allow-dirty` is gone: it suppressed exactly the "you are shipping files
  that are not in the commit" check that publishing needs.
- The macro crate is published first, and the run waits for it to appear in
  the index before publishing the crate that depends on it, instead of
  racing crates.io propagation.

Caching: three hand-rolled actions/cache steps per job are replaced by
Swatinem/rust-cache. The old keys were `${{ runner.os }}-cargo-*-${{
hashFiles('**/Cargo.lock') }}` with no restore-keys, so every lockfile change
was a 100% miss rather than a partial restore, and the `target` key did not
include the compiler version, so a toolchain bump reused stale artifacts.

Also adds least-privilege `permissions:`, per-job `concurrency:` groups and
the opt-in `verbose` dispatch input (default false).
- `pip index versions <pkg> | grep -q "$VERSION"` matched substrings, so a
  version was considered published as soon as any longer version string
  contained it (0.1.0 looks published once 0.1.0.1 exists) and the release
  was silently skipped. Replaced with an exact lookup against
  https://pypi.org/pypi/<name>/<version>/json.
- The version was also parsed by `grep "^version"` on pyproject.toml in
  publishRelease while publishToPyPI used tomli, so the two jobs could
  disagree. Both now read the version once, in publishToPyPI, and
  publishRelease consumes it as a job output.
- `twine upload` with TWINE_PASSWORD is replaced by
  pypa/gh-action-pypi-publish with `id-token: write`, so PyPI trusted
  publishing is used when configured and PYPI_TOKEN stays as the bootstrap
  fallback. `packages-dir: python/dist` is explicit because
  `defaults.run.working-directory` does not apply to action inputs.
- publishRelease gated on `result == 'success'`, which a job that published
  nothing also reports; it now gates on the `published` output, which is only
  set after the upload succeeded and the version was observed on PyPI.
- `twine check dist/*` runs in the test job, so malformed metadata fails on
  pull requests rather than at upload time.

Also adds least-privilege `permissions:`, per-job `concurrency:` groups and
the opt-in `verbose` dispatch input (default false).
Same rewrite the other four languages received:

- `publishToPackagist` exposes an explicit `published` output
  (`true` / `skipped` / `failed`) instead of reusing `should_publish`,
  which was true whenever the version merely was not on Packagist yet,
  and `publishRelease` gates on that output instead of on
  `needs.publishToPackagist.result == 'success'` — a job result that is
  also `success` when the publish step did nothing.

- The version check now distinguishes "the package is not on Packagist
  at all" from "this version is not there yet". It is the first case
  today: both https://repo.packagist.org/p2/link-foundation/links-notation.json
  and https://packagist.org/packages/link-foundation/links-notation.json
  return 404. `api/update-package` only re-crawls an already-submitted
  package, so the PHP publish step has never published anything and
  cannot until someone submits the package once at
  https://packagist.org/packages/submit. The workflow now says so with a
  `::warning::` instead of exiting 0 in silence.

- Missing `PACKAGIST_USERNAME` / `PACKAGIST_TOKEN` is reported the same
  way rather than passing empty credentials to the API.

- After a successful update call the version is verified against
  repo.packagist.org (Packagist crawls asynchronously) before any
  GitHub release is created.

- Adds `workflow_dispatch` with a `verbose` input, `permissions:
  contents: read` at the top with `contents: write` only on
  `publishRelease`, `CI_VERBOSE`, per-job concurrency groups, and the
  quoted `ALL_CHANGED_FILES` change detection.
- `publishToMavenCentral` exposes an explicit `published` output
  (`true` / `skipped` / `failed`). The previous output was
  `have_creds == 'true' && should_publish == 'true'`, which is decided
  before `mvn deploy` runs, so it claimed a publish for a version that
  the Portal may never have accepted.

- Maven Central has no OIDC trusted publishing, so the Portal token and
  GPG key stay. They are now read through `env:` instead of being
  interpolated into the script body with `${{ secrets.* }}`, which put
  the values into the generated shell script.

- `autoPublish` uploads a bundle that the Portal validates and syncs
  asynchronously, so a green `mvn deploy` is not a published artifact.
  A new step polls repo1.maven.org for the .pom (up to 15 minutes)
  before `publishRelease` may run, and `publishRelease` gates on the
  `published` output instead of `result == 'success'`.

- `publishRelease` no longer installs a JDK and shells out to
  `mvn help:evaluate` three times to re-derive coordinates the publish
  job already emitted as outputs.

- Adds `workflow_dispatch` with a `verbose` input, top-level
  `permissions: contents: read` with `contents: write` only on
  `publishRelease`, per-job concurrency groups, quoted
  `ALL_CHANGED_FILES` change detection, and `fail-fast: false` on the
  JDK matrix so one JDK version failing still reports the others.
Go has no registry to push to — the module proxy serves whatever the
`go/vX.Y.Z` tag points at — so creating the tag is the publish. The
release job now reads the version and module path into outputs, reports
`published` as `true`/`skipped`, and then polls
`proxy.golang.org/<module>/@v/<version>.info`. That both proves the
release is consumable and warms the proxy for the first `go get`, which
otherwise pays the fetch cost.

Also: `workflow_dispatch` with a `verbose` input, top-level
`permissions: contents: read` with `contents: write` only on
`publishRelease`, per-job concurrency groups, a `publish-go` group that
does not cancel in progress, quoted `ALL_CHANGED_FILES` change
detection, a non-empty check on `go/VERSION` (an empty file used to
produce the tag `go/v`), and the redundant JDK-style Setup Go step
dropped from a job that runs no Go commands.
`workflow_dispatch` was declared but every job was gated on
`isDocsFilesChanged`, which is derived from a changed-file list that is
empty for a dispatch — so the manual trigger silently did nothing. It
now counts as changed, the same way the language workflows handle it.

Other changes:
- `pages: write` and `id-token: write` were granted to the whole
  workflow, including the job that checks out the repository and runs
  `npm ci` on third-party code. They now live only on `deploy`.
- The site is built on pull requests as well, so a broken build is
  caught in review; upload and deploy still only happen off main.
- `actions/upload-pages-artifact` v4 -> v5 (v5.0.0 is current).
- Node 20 -> 22, matching the version the other workflows use.
- `timeout-minutes` on all three jobs; none had one, so a hung `npm ci`
  could occupy a runner for the six-hour default.
- Quoted `ALL_CHANGED_FILES` change detection.

Verified `npm ci && npm run build` in docs/website still succeeds.
auto-merge:
- `contents: write` + `pull-requests: write` were granted at the
  workflow level, so they applied even to the runs that immediately
  no-op because the author is not Dependabot. Moved onto the job.
- On a `pull_request` event raised by Dependabot, GITHUB_TOKEN is
  read-only regardless of the `permissions:` block, so
  `gh pr merge --auto` gets an HTTP 403. The workflow now prefers
  `secrets.DEPENDABOT_AUTO_MERGE_TOKEN` and, when that secret is
  absent, says so with a `::warning::` instead of failing with an
  unexplained 403.
- Adds `timeout-minutes` and a per-pull-request concurrency group.

bom-check:
- Adds `permissions: contents: read` (it had none, so it ran with the
  repository default), a concurrency group, `workflow_dispatch`, and an
  `::error::` annotation so a BOM shows up in the run summary rather
  than only in the step log.
Adds a `workflows` workflow that runs actionlint and zizmor over
`.github/workflows/` on every change to `.github/`. Nothing checked the
workflows themselves before, which is how csharp.yml shipped three jobs
whose `if:` referenced a `needs.` job they did not depend on — the
expression evaluated to empty, the jobs never ran, and every run was
green. actionlint reports exactly that.

zizmor's remaining findings drove two changes:

- `artipacked` (41 occurrences): `actions/checkout` leaves the
  GITHUB_TOKEN in `.git/config` by default, so every subsequent step —
  including `npm ci`, `cargo build` and the DocFX scripts, which all
  execute third-party code — could read it. Every checkout now sets
  `persist-credentials: false`. The only step that pushes,
  csharp/scripts/publish-docs.sh, already authenticates with an
  explicit `x-access-token:` URL, so nothing depended on the persisted
  credential.

- `adhoc-packages`: `npm install -g npm@latest` is pinned to `npm@11`.
  Node 22 bundles npm 10.x and trusted publishing needs >= 11.5.1, so
  the upgrade stays, but a future npm 12 cannot change publish
  behaviour without a commit here.

`.github/zizmor.yml` relaxes `unpinned-uses` from the default blanket
hash-pin policy to `ref-pin` (tag or SHA, never a mutable branch),
matching how the link-foundation pipeline templates pin actions and how
Dependabot keeps them current. The CI job fails at medium severity and
above; with that config the whole tree is clean.
Adds scripts/release-audit.mjs and a workflow that runs it on every push
to main, weekly, and on demand. It reads the version each language
declares in its source tree and compares it with what that language's
registry actually serves.

Its first run is the evidence for this pull request:

  js:     declared 0.15.0, latest on npm is 0.13.0
  csharp: declared 0.15.0, latest on NuGet.org is 0.13.0
  java:   declared 0.3.0, nothing published on Maven Central
  php:    declared 0.2.0, nothing published on Packagist
  python, rust, go: in sync

Every one of those had green CI. The release jobs gated on
`needs.<publishJob>.result == 'success'`, which is also what a job
reports when its publish step was skipped, so a missing credential or —
in csharp.yml's case — an `if:` referencing a job that was not in
`needs:` looked identical to a successful release.

Drift is reported as annotations, not as a failure: a version bump
legitimately lands before the release that publishes it.
… exist

Every go run logged, twice:

  ##[warning]Restore cache failed: Some specified paths were not
  resolved, unable to cache dependencies.

`cache-dependency-path: go/go.sum` points at a file that is not in the
repository — the module has no dependencies at all, go.mod has no
require block — so setup-go could resolve neither the hash input nor
GOMODCACHE. Turning the dependency cache off removes both warnings and
the pointless cache round-trip. If the module ever gains dependencies,
the go.sum that appears alongside them is the signal to turn it back on.
`publishRelease` depended on `publishDocumentation`, so a DocFX or
gh-pages failure suppressed the GitHub release for a package that was
already on NuGet.org — the release then never appeared at all, because
the next run sees the version as published and skips. The release now
depends only on the NuGet push it is reporting.
Java declared 0.3.0 and PHP declared 0.2.0 while JavaScript, Python,
Rust, C# and Go all declare 0.15.0. The two lagged only because their
publish jobs have never released anything —
`scripts/release-audit.mjs` confirms Maven Central and Packagist hold
nothing at all for this project — so no published version is being
skipped and the numbers are free to move.

Aligning them means every implementation of the same grammar carries
the same version, which is what the single shared CHANGELOG already
assumes, and it lets the release audit report one number instead of
three.
…path

The generator read rust/tests, which the Cargo workspace move turned into a
non-existent directory, so it crashed with ENOENT and the pre-commit hook
that runs it failed every commit touching a test file. The document had been
stale ever since: 137 Python tests recorded against 146 today.

The per-test links also used a `file:line` suffix, which GitHub reads as
part of the path, so all 827 of them 404'd. They now use `#L<line>`.
lychee reports every Markdown link in the repository. The three genuine
failures it found are fixed here: the root READMEs pointed at rust/README.md,
which the workspace move turned into rust/links-notation/README.md, and the
CHANGELOG linked releases under a `vX.Y.Z` tag scheme this repository has
never used.

The C# API reference URLs stay in .lycheeignore until the first Pages
deployment that includes the gh-pages overlay; npmjs.com and stackoverflow.com
reject data-centre traffic and would only ever be false positives.
…able

csharp/scripts/publish-docs.sh pushes the DocFX site to the gh-pages branch,
and the branch does contain csharp/api/*.html. GitHub Pages, however, serves
the artifact this workflow uploads ("build_type": "workflow"), so every
/csharp/ URL the READMEs advertise returned 404 while CI stayed green.

The build now overlays the gh-pages content onto the uploaded site, and a
workflow_run trigger rebuilds the site when the csharp workflow publishes new
documentation.
CodeQL over every language with an extractor (PHP has none; php.yml runs
PHPStan instead), dependency review on pull requests, and an npm audit of both
committed lockfiles. The audit already had something to say: js/package-lock
carried brace-expansion 5.0.8, vulnerable to GHSA-mh99-v99m-4gvg, while
js/bun.lock had moved on to 5.0.9.
Principle 11 of the best-practices document. --results=verified reports only
credentials the issuing service confirms are still live, so the job can fail
the build without drowning it in false positives.
…rejects

The previous reproduction only demonstrated the documentation-ID form that
works. Probing every shape shows the Pegasus output is invalid for a wider
range than assumed: nesting breaks it, and so does a single language keyword
(`IList{string}`), while the one shape that does bind silently resolves to
the open generic. Reported upstream as otac0n/Pegasus#137.
A local run over `./**/*.md` picked up 4271 links, 3400 of them from
js/node_modules and docs/website/node_modules, and ~400 of those were
dead links inside third-party package READMEs we do not control. Nothing
installs dependencies in this job today, so this is a guard against that
changing rather than a fix for a current failure. With the exclusions the
repository's own Markdown checks clean: 1050 total, 0 errors.
`dev/log/` is gitignored (.gitignore carries a `[Ll]og/` pattern), so the
raw material stays local: 3.3 MB of downloaded CI logs and 376 KB of
cloned pipeline templates. What is tracked here is the written analysis
drawn from them, force-added so it survives the ignore rule.

  README.md                     what is here and in what order to read it
  analysis/REQUIREMENTS.md      every requirement in the issue, with status
  analysis/TIMELINE.md          how the two failure sequences unfolded
  analysis/ROOT-CAUSES.md       each defect traced to its cause and fix
  analysis/BEST-PRACTICES-COMPLIANCE.md  the 13 hive-mind principles, checked
  analysis/CI-CD-BEST-PRACTICES.md       the upstream text, pinned as read
  analysis/UPSTREAM.md          the issues filed against other projects
  analysis/PRIOR-ART.md         existing tools surveyed before writing any
  analysis/*.txt, *.json        the measurements the prose cites
Every change this pull request makes to the pipelines, grouped the way
the rest of the file is: what was added, what changed, and what was
broken and is now fixed.
main carried seven Dependabot bumps that touch the same workflow lines
this branch rewrote: actions/checkout v6 to v7, setup-dotnet v5 to v6,
setup-node v6 to v7, upload-pages-artifact v4 to v5, fetch-metadata v2
to v3, and xunit.runner.visualstudio 3.1.5 to 4.0.0. Every conflict was
main's version number against this branch's surrounding change, so the
resolution keeps both: this branch's content on the new action versions.
The bumps were also applied to the workflows added here, which main has
never seen, so no v6 checkout is left anywhere. actionlint exits clean
and zizmor reports no findings.
@konard konard changed the title [WIP] Check for all false positives, false negatives, warnings and errors in CI/CD and fix them all Fix every false positive, false negative, warning and error in CI/CD Aug 28, 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.

konard added 6 commits August 28, 2026 08:58
The workflows check added in this branch went red on its first run, which
is the check working: actionlint's Docker image bundles shellcheck, so it
lints every `run:` block, and 60 of the findings were the same one —
`>> $GITHUB_OUTPUT` unquoted. That is harmless on GitHub's runners today
because the path has no spaces, but it is a real hazard in a script that
writes a job's contract, so all of them are quoted now.

The other two: java.yml wrote three outputs through three separate
redirects (SC2129), now one grouped redirect; and bom-check.yml printed
the BOM byte escapes through `echo`, which is allowed to expand them
(SC2028), now `printf '%s\n'`, so the advice it prints stays literal.

It went unnoticed locally because a native actionlint without shellcheck
on PATH skips the shell checks and exits 0. That trap is now written down
next to the step.
The secret scan failed on its first run with

    trufflehog: error: flag 'no-update' cannot be repeated, try --help

The action's own wrapper already passes --no-update, so repeating it in
extra_args aborted the scan before it read a single commit. Removed, with
a note next to the input so it is not added back.

The action ref pinned the wrapper but not the scanner: the `version`
input defaults to `latest`, so the run pulled whatever image that tag
pointed at. It is now pinned to 3.97.1 — the image tag carries no `v`
prefix, unlike the action ref, which a local `docker run` confirmed
(`manifest unknown` for v3.97.1, `trufflehog 3.97.1` without it).

Verified locally with the exact command the action builds:
6381 chunks, 13.7 MB, verified_secrets 0, exit 0.
The job-level concurrency group left out the matrix value, so all four
PHP versions shared one group and cancelled each other. Two consecutive
runs on this branch show it: 8.1, 8.2 and 8.3 were cancelled at
09:01:30Z, the same second they started, having run no steps, while 8.4
completed. The repository has been testing one PHP version, not four.

java.yml and security.yml already key their groups on the matrix value;
php.yml was the only one that did not.
An annotation sweep over all 61 jobs of the 12 green runs on 31ec6aa returned
one notice and nothing else, so the logs were downloaded in full and grepped
instead. Three findings only existed in step output:

- codecov has answered `Upload queued for processing failed: {"message":
  "Token required - not valid tokenless upload"}` on every run. The repository
  has no CODECOV_TOKEN secret, and `fail_ci_if_error: false` turned that
  permanent failure into a green step. The upload now runs only when the secret
  exists and fails the job when it does; without it the job says so with a
  ::notice:: instead of pretending.
- the same step also found and uploaded experiments/test_coverage_data.json
  under the `go` flag. `files:` narrows what is passed as --file but does not
  disable the CLI's workspace search, so `disable_search: true` was needed.
- the website build warned on every run that vite.config.js is ESM loaded as
  CommonJS. `"type": "module"` in docs/website/package.json fixes it;
  npm ci && npm run build now completes with no warning.

The investigation log gains Class F in ROOT-CAUSES.md, covering these three and
the three defects the new checks caught on their first run (the 62 shellcheck
findings and the local actionlint that missed them, the trufflehog flag
repetition, and the php matrix self-cancellation), plus a third sequence in
TIMELINE.md.
The gate added in 2537067 used codecov-action@v6 and read the secret
directly. Both templates that already solved this problem (csharp #34,
python #27, both closed) settled on the same shape: promote the secret
to job-level env, gate the step on `env.CODECOV_TOKEN != ''`, pass it as
`token: ${{ env.CODECOV_TOKEN }}`, and let a real failure fail the job.
python #34 (also closed) moved off the deprecated @v4; @v7 is current.

Also record the cross-template audit that this branch's work produced.
All seven link-foundation/<lang>-ai-driven-development-pipeline-template
repositories were linted with actionlint 1.7.7 and shellcheck, giving 23
findings and one meta-finding that explains them: no template runs
actionlint or zizmor anywhere in .github. Seven issues are filed, each
with evidence, a reproduction, a workaround and a diff:

  rust   #141  printf in single quotes, ${DOCKERHUB_IMAGE} never expands
  csharp  #49  ${{ github.head_ref }} interpolated into a run: body
  python  #62  unsupported `queue: max` concurrency key
  go       #6  Codecov upload that can never succeed, hidden twice
  java     #6  Codecov fail_ci_if_error: false, @v4, v3-era file: input
  js     #145  no workflow linter; SC2046, SC2034
  php      #3  no workflow linter (otherwise clean)

Five of the seven had already been fixed elsewhere in the same template
family, so each report cites its closed precedent.

experiments/upstream-templates/rust-manifest-printf-quoting.sh
reproduces rust #141 end to end.

Refs #290
All 13 logs downloaded and grepped in full rather than read through
annotations: zero ##[warning], zero ##[error], and the only two
##[notice] lines are intentional. F3-F6 are each confirmed gone at
their source - all four PHP legs complete, the coverage step is
skipped with a spoken notice instead of a silent failed upload, and
the Vite CommonJS warning is absent.

Refs #290
@konard
konard marked this pull request as ready for review August 28, 2026 09:32
@konard

konard commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Working session summary

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

Upstream reports filed — all seven templates now covered, each with quoted evidence, a reproduction, a workaround and a diff:

Repo Issue Defect
rust #141 printf format single-quoted → ${DOCKERHUB_IMAGE} never expands
csharp #49 ${{ github.head_ref }} interpolated into a run: body (fork-PR injection)
python #62 queue: max — not a valid concurrency key, silently ignored
go #6 Codecov upload that can never succeed, hidden twice; @v4
java #6 Codecov fail_ci_if_error: false, @v4, v3-era file:
js #145 no workflow linter; SC2046, SC2034
php #3 no workflow linter (otherwise clean)

Five of the seven cite an already-closed precedent from a sibling template (rust#113/js#117, csharp#34/python#27, python#34, js#115), so they land as missed sweeps rather than opinions. The meta-finding is that no template runs actionlint or zizmor at all — verified across all seven — so every report closes with the workflows job this PR adds.

Verification on the final head 447cf9b — all 13 logs (3.0 MB) downloaded and grepped in full, not read through annotations:

  • ##[warning] / ##[error]: none
  • ##[notice]: only the links summary link and the deliberate "CODECOV_TOKEN is not configured" line
  • php runs all four legs (8.18.4), where three were previously cancelled every run
  • 12/12 workflows green, auto-merge correctly skipped

That distinction mattered: the previous annotation-only sweep returned one benign notice while three real defects (a coverage upload that had never succeeded and was reported as success, an unrelated file uploaded under the go flag, and a per-build Vite CommonJS warning) sat in plain step output. All three are fixed.

Working tree is clean, main is fully merged in (0 behind), and the PR body now covers the shellcheck/trufflehog/php/codecov/Vite work plus the seven new upstream reports.


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

@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: $40.858039

📊 Context and tokens usage:

Claude Opus 5: (10 sub-sessions)

  1. 113.2K / 1M (11%) input tokens, 30.8K / 128K (24%) output tokens
  2. 116.3K / 1M (12%) input tokens, 27.3K / 128K (21%) output tokens
  3. 116.7K / 1M (12%) input tokens, 27.0K / 128K (21%) output tokens
  4. 116.8K / 1M (12%) input tokens, 34.2K / 128K (27%) output tokens
  5. 117.1K / 1M (12%) input tokens, 33.2K / 128K (26%) output tokens
  6. 114.5K / 1M (11%) input tokens, 33.1K / 128K (26%) output tokens
  7. 116.9K / 1M (12%) input tokens, 28.9K / 128K (23%) output tokens
  8. 111.0K / 1M (11%) input tokens, 23.9K / 128K (19%) output tokens
  9. 115.0K / 1M (11%) input tokens, 32.2K / 128K (25%) output tokens
  10. 88.1K / 1M (9%) input tokens, 27.4K / 128K (21%) output tokens

Total: (25.4K new + 931.7K cache writes + 41.9M cache reads) input tokens, 414.9K output tokens, $40.786983 cost

Claude Haiku 4.5:

  • 33.9K / 200K (17%) input tokens, 1.4K / 64K (2%) output tokens

Total: 33.9K input tokens, 1.4K output tokens, $0.071057 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 (12673KB)


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

@konard
konard merged commit 467f30e into main Aug 28, 2026
63 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

2 participants