Skip to content

Adopt the shared CI pipeline - #46

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/shared-ci-canary
Sep 16, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/shared-ci-canary

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

What

The canary for the org-wide shared-CI migration. Three changes:

  • add .github/workflows/ci.yml — 36 lines, calls ktsu-dev/.github/.github/workflows/ci-shared.yml@release
  • delete .github/workflows/dotnet.yml — 618 lines, the local copy of the pipeline
  • update .github/workflows/dependabot-merge.yml — workflows: [".NET Workflow"] → ["CI"], ref @main → @release

Net +38 / −620.

Why the Dependabot change is in the same commit

dependabot-merge.yml lists the workflows whose completion re-opens the merge question by workflow name. A repository whose CI is called CI while its gate still lists .NET Workflow never re-evaluates — a genuinely green Dependabot PR would sit unmerged with nothing reporting a failure. Splitting these into two commits would open exactly that window.

Why this is a pull request and not a push

This PR is the test. ci.yml triggers on pull_request, so the new pipeline runs here, on this change, before main stops carrying a pipeline of its own. If it doesn't work, main is untouched.

What it proves, in order:

  1. ci-shared.yml@release resolves cross-repo from a caller.
  2. detect classifies this repository — dotnet topic and global.json present, public — and selects dotnet-public.
  3. The relative uses: ./.github/workflows/dotnet.yml inside ci-shared.yml resolves within ktsu-dev/.github, not against this repository. This is the one genuinely unverified assumption in the whole design. GitHub's docs say the relative form resolves to "the same commit as the caller workflow", which in the nested case should mean ci-shared.yml's own repository — but the docs don't address nesting explicitly. If it resolves against the caller instead, this PR fails here with a missing-workflow error and ci-shared.yml needs absolute @release refs before anything else migrates.
  4. The pipeline itself behaves identically — same jobs, same matrix, secrets: inherit chains two levels deep.

Expected check names

Checks will be nested one level deeper: ci / dotnet-public / Test on ubuntu-latest rather than Test on ubuntu-latest. Harmless for the Dependabot gate, which enumerates check runs rather than matching names, and whose self-exclusion is already suffix-based.

Not in this PR

update-sdks.yml stays as-is for now — it has a separate, unrelated defect (a [System.Version]::Parse failure on prerelease versions) being handled on its own.

Blast radius

One repository. If CI here is green, the same change fans out to the rest; if not, nothing else has moved.

🤖 Generated with Claude Code

https://claude.ai/code/session_014RABe2NufFc9hwm94iB3Rf


Generated by Claude Code

Replace this repository's 28.7 KB copy of dotnet.yml with a ci.yml that calls
ktsu-dev/.github's ci-shared.yml. The dispatcher works out what kind of
repository this is -- the dotnet topic plus a global.json -- and selects the
pipeline, so changing how ktsu builds .NET repositories stops being a change
here.

The Dependabot gate moves in the same commit, not a later one. It lists the
workflows whose completion re-opens the merge question by NAME, so a repository
whose CI is called `CI` while its gate still lists `.NET Workflow` never
re-evaluates: a genuinely green Dependabot PR would sit unmerged with nothing
reporting a failure. Both refs also move to @Release, which is promoted
deliberately rather than following main.

This is the canary for the org-wide migration. It is a pull request rather than
a push so that the new pipeline runs here, on this change, before main stops
carrying a pipeline of its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RABe2NufFc9hwm94iB3Rf
Comment thread .github/workflows/ci.yml Fixed
Two findings on this pull request, one root cause.

The CI run ended in startup_failure with no jobs: a called workflow cannot
elevate above what its caller granted, and neither this caller nor the
dispatcher granted enough for dotnet.yml's release job, which asks for
`contents: write` and `packages: write`.

CodeQL flagged the same file for having no permissions block at all, which is
the other half of it -- without one the caller silently takes the repository
default, which is both unstated and, here, insufficient.

Grant the union the shared pipelines request. The file stays byte-identical to
the template published in ktsu-dev/.github's docs/shared-ci.md, so the fan-out
remains a copy rather than a per-repository edit.

This alone does not turn the run green: the dispatcher also caps its callees at
`contents: read`, fixed in ktsu-dev/.github#21. The tag this repository
resolves, @Release, still points at the version with that cap, so CI here stays
red until #21 merges and the tag is re-promoted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RABe2NufFc9hwm94iB3Rf

Copy link
Copy Markdown
Contributor Author

Status: CI here stays red until ktsu-dev/.github#21 merges. Naming the blocker rather than leaving it to be rediscovered.

What this canary has already established

The first run failed, but not before proving the assumption the whole shared-CI design rests on. It recorded its referenced_workflows as:

ktsu-dev/.github/.github/workflows/ci-shared.yml@release        (refs/tags/release)
ktsu-dev/.github/.github/workflows/dotnet.yml@905bdc6…          (refs/tags/release)
ktsu-dev/.github/.github/workflows/dotnet-private.yml@905bdc6…  (refs/tags/release)

A relative uses: ./.github/workflows/dotnet.yml inside ci-shared.yml resolves within ktsu-dev/.github, at the same commit as ci-shared.yml — not against this repository. GitHub's docs state the rule for the simple case but do not address nesting, so this was inference until now. It is also the first real use of the @release tag, which resolved correctly.

What is still broken

startup_failure, zero jobs. A called workflow cannot elevate above what its caller granted, and the permission chain was too narrow in two places:

  1. This caller had no permissions block, so it took the repository default. Fixed here in 8d2dca2 — also what CodeQL flagged above.
  2. ci-shared.yml declares permissions: contents: read at workflow level, which becomes the ceiling for every pipeline it dispatches. dotnet.yml's release job needs contents: write and packages: write, so it could never be satisfied. Fixed in Stop ci-shared capping the pipelines' permissions .github#21.

The second one is not fixable from this repository. @release still points at the version carrying the cap, so this PR remains red regardless of what is pushed here.

Sequence to green

  1. Merge Stop ci-shared capping the pipelines' permissions .github#21.
  2. Dispatch Promote release (ref main) to move the tag onto the fix.
  3. Re-run CI here — no push needed; the tag move is what changes the outcome.

No further pushes to this branch are planned before then.


Generated by Claude Code

@sonarqubecloud

Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor Author

Green. ktsu-dev/.github#21 merged, release was promoted onto it, and re-running this PR picked the moved tag up without a push. Run 35080437964 attempt 2:

Job Result
ci / Classify repository ✅ success
ci / .NET / Discover Test Projects ✅ success
ci / .NET / Test on ubuntu-latest ✅ success
ci / .NET / Test on windows-latest ✅ success
ci / .NET / Test on macos-latest ✅ success
ci / .NET / Analyze & Release ✅ success
ci / .NET / Security Scanning ⏭ skipped
ci / .NET (private) ⏭ skipped

SonarQube Quality Gate passed.

What this canary established

Everything the org-wide migration depends on, observed rather than inferred:

  • The relative ./ inside ci-shared.yml resolves within ktsu-dev/.github. referenced_workflows recorded dotnet.yml@af44498 — the same commit as ci-shared.yml, not this repository. GitHub's docs state the rule for the simple case but are silent on nesting, so this was the one genuinely unverified assumption in the design.
  • A moved tag is picked up by a re-run. Attempt 1 resolved @release at 905bdc6; attempt 2 resolved af44498 with no new commit. Promotions therefore take effect without empty commits.
  • secrets: inherit chains two levels. Install KtsuBuild and the SonarQube steps both needed credentials and both worked.
  • The github context resolves to the caller, two levels down. The Sonar project key came out ktsu-dev_Sorting, built from github.repository_owner and github.event.repository.name inside the nested workflow.
  • Pull-request gating is preserved. Release skipped inside Analyze & Release, and Security Scanning skipped entirely — same as the old dotnet.yml, and what the Dependabot gate's "skipped and neutral count as green" logic already accounts for.
  • The three-platform matrix survived the move: ubuntu, windows and macOS, all green.

What it cost to find out

Two defects, both on this one repository with main untouched:

  1. ci-shared.yml capped its callees at contents: read, so dotnet.yml's release job could never be satisfied — startup_failure, zero jobs. Fixed in Stop ci-shared capping the pipelines' permissions .github#21.
  2. This caller had no permissions block at all and silently took the repository default — also what CodeQL flagged. Fixed in 8d2dca2.

Both would have hit all fifty repositories simultaneously had this gone out as a fan-out.

Net effect here

+38 / −620. The 618-line local copy of dotnet.yml is gone; what remains is a 36-line caller and a Dependabot gate that now watches CI.


Generated by Claude Code

@matt-edmondson
matt-edmondson merged commit 35a7470 into main Sep 16, 2026
14 checks passed
@matt-edmondson
matt-edmondson deleted the claude/shared-ci-canary branch September 16, 2026 10:29
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.

3 participants