ci: build (and conditionally push) bake targets to ghcr #35

Merged
jasoncouture merged 16 commits from ci/container-publish into main 2026-05-07 14:08:25 -04:00
jasoncouture commented 2026-05-07 11:10:27 -04:00 (Migrated from github.com)

Summary

Container build pipeline. One workflow handles both pushes to main/release/** (build + push) and PRs against the same bases (build only, no push). One shared composite action does the actual build so the trigger-specific logic doesn't drift.

Bake targets are enumerated dynamically via docker/bake-action/subaction/list-targets@v6, fanned out into a matrix — currently just llamashears, future services pick up with no workflow edit.

What's in here

.github/actions/build-container/action.yml

Composite action; one bake target per invocation. Inputs: target, tags (newline-separated full image refs), push ("true"/"false"). Steps:

  1. docker/setup-buildx-action@v3.
  2. docker/login-action@v3 to ghcr.io — only when push == "true".
  3. docker buildx bake --set <target>.cache-from=type=gha --set <target>.cache-to=type=gha,mode=max --set <target>.tags=<tag>... [--push] <target>.

.github/workflows/container-publish.yml

Two jobs:

  • prepare — checkout, setup-dotnet, list-targets@v6, install nbgv global tool, compute SemVer2 (sanitised by replacing + with _ so the OCI tag rules don't reject build metadata).
  • build — matrix over targets. Per-job: compute the lowercased GHCR image ref ghcr.io/<owner>/<repo>/<service>, decide tag list (always version; on release/** push, add :latest if this version is the highest stable semver across existing GHCR tags + this build), invoke the local build-container action with push: ${{ github.event_name == 'push' }}.

Tag strategy (per spec)

  • Everything: SemVer2 from NBGV, sanitized for OCI (+_).
  • main: pre-release, since NBGV's version.json produces 1.0-pre.<height>. No :latest.
  • release/**: stable when NBGV is configured to treat the branch as public-release. :latest is added only when this build's version is the highest stable semver among the existing GHCR tags plus itself. First-ever publish qualifies.
  • PRs: build only, no push, no tags published.

"Latest" detection

Uses the GitHub API package endpoint — /users/<owner>/packages/container/<repo>%2F<service>/versions — to enumerate existing tags, filters to stable semver (^[0-9]+\.[0-9]+\.[0-9]+$), takes the max, and tags :latest only when this build's version is that max. 404 on first-ever run is swallowed and treats this build as the latest.

Why the bake-action subaction (not a static matrix)

A static matrix would need a workflow edit every time a service is added or removed. list-targets reads the current docker-bake.hcl + compose.yaml so the build set tracks the source of truth.

Caching

type=gha, mode=max for cache-to. Subsequent builds reuse layers across PRs and main runs at no manual cache management cost.

Action-pinning policy

Per repo policy: floating major (@v3, @v4, @v6).

Test plan

  • Husky docs-api-up-to-date pre-push check passed.
  • docker buildx bake --print locally returns { target: { llamashears: {...} } }; matrix will receive ["llamashears"].
  • Verifying real GHCR push, latest-detection logic, and per-target tags requires the workflow to actually run after merge. First push to main exercises the build-only path of the publish job; :latest correctness needs an actual release/** cut.

Stacking note

Targets ci/nuget-publish (PR #34). Will retarget main automatically as the chain merges.

🤖 Generated with Claude Code

## Summary Container build pipeline. One workflow handles both pushes to `main`/`release/**` (build + push) and PRs against the same bases (build only, no push). One shared composite action does the actual build so the trigger-specific logic doesn't drift. Bake targets are enumerated dynamically via `docker/bake-action/subaction/list-targets@v6`, fanned out into a matrix — currently just `llamashears`, future services pick up with no workflow edit. ## What's in here ### `.github/actions/build-container/action.yml` Composite action; one bake target per invocation. Inputs: `target`, `tags` (newline-separated full image refs), `push` (`"true"`/`"false"`). Steps: 1. `docker/setup-buildx-action@v3`. 2. `docker/login-action@v3` to `ghcr.io` — only when `push == "true"`. 3. `docker buildx bake --set <target>.cache-from=type=gha --set <target>.cache-to=type=gha,mode=max --set <target>.tags=<tag>... [--push] <target>`. ### `.github/workflows/container-publish.yml` Two jobs: - **`prepare`** — checkout, setup-dotnet, `list-targets@v6`, install `nbgv` global tool, compute `SemVer2` (sanitised by replacing `+` with `_` so the OCI tag rules don't reject build metadata). - **`build`** — matrix over targets. Per-job: compute the lowercased GHCR image ref `ghcr.io/<owner>/<repo>/<service>`, decide tag list (always version; on `release/**` push, add `:latest` if this version is the highest stable semver across existing GHCR tags + this build), invoke the local `build-container` action with `push: ${{ github.event_name == 'push' }}`. ## Tag strategy (per spec) - **Everything:** SemVer2 from NBGV, sanitized for OCI (`+` → `_`). - **`main`:** pre-release, since NBGV's `version.json` produces `1.0-pre.<height>`. No `:latest`. - **`release/**`:** stable when NBGV is configured to treat the branch as public-release. `:latest` is added only when this build's version is the highest stable semver among the existing GHCR tags plus itself. First-ever publish qualifies. - **PRs:** build only, no push, no tags published. ## "Latest" detection Uses the GitHub API package endpoint — `/users/<owner>/packages/container/<repo>%2F<service>/versions` — to enumerate existing tags, filters to stable semver (`^[0-9]+\.[0-9]+\.[0-9]+$`), takes the max, and tags `:latest` only when this build's version is that max. 404 on first-ever run is swallowed and treats this build as the latest. ## Why the bake-action subaction (not a static matrix) A static matrix would need a workflow edit every time a service is added or removed. `list-targets` reads the current `docker-bake.hcl` + `compose.yaml` so the build set tracks the source of truth. ## Caching `type=gha, mode=max` for cache-to. Subsequent builds reuse layers across PRs and main runs at no manual cache management cost. ## Action-pinning policy Per repo policy: floating major (`@v3`, `@v4`, `@v6`). ## Test plan - [x] Husky `docs-api-up-to-date` pre-push check passed. - [x] `docker buildx bake --print` locally returns `{ target: { llamashears: {...} } }`; matrix will receive `["llamashears"]`. - [ ] Verifying real GHCR push, latest-detection logic, and per-target tags requires the workflow to actually run after merge. First push to `main` exercises the build-only path of the publish job; `:latest` correctness needs an actual `release/**` cut. ## Stacking note Targets `ci/nuget-publish` (PR #34). Will retarget `main` automatically as the chain merges. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
CLAassistant commented 2026-05-07 11:10:40 -04:00 (Migrated from github.com)

CLA assistant check
All committers have signed the CLA.

[![CLA assistant check](https://cla-assistant.io/pull/badge/signed)](https://cla-assistant.io/jasoncouture/llama-shears?pullRequest=35) <br/>All committers have signed the CLA.
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-05-07 11:14:34 -04:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Pull request overview

Adds a unified GitHub Actions pipeline for container builds using Docker Bake, with conditional publishing to GHCR on push events and build-only validation on PRs. This introduces a reusable composite action to keep build logic consistent while dynamically discovering bake targets.

Changes:

  • Added .github/workflows/container-publish.yml to build on PRs and build+push on main/release/**, dynamically generating a matrix from Bake targets.
  • Added .github/actions/build-container composite action to build a single bake target with GHA cache wiring and optional registry push.
  • Implemented version/tag derivation (NBGV SemVer2 sanitization and conditional :latest tagging on release/** pushes).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
.github/workflows/container-publish.yml New workflow to prepare target/version info and build/publish per bake target with conditional tagging.
.github/actions/build-container/action.yml New composite action wrapping docker buildx bake with cache and optional push/login.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

## Pull request overview Adds a unified GitHub Actions pipeline for container builds using Docker Bake, with conditional publishing to GHCR on `push` events and build-only validation on PRs. This introduces a reusable composite action to keep build logic consistent while dynamically discovering bake targets. **Changes:** - Added `.github/workflows/container-publish.yml` to build on PRs and build+push on `main`/`release/**`, dynamically generating a matrix from Bake targets. - Added `.github/actions/build-container` composite action to build a single bake target with GHA cache wiring and optional registry push. - Implemented version/tag derivation (NBGV SemVer2 sanitization and conditional `:latest` tagging on `release/**` pushes). ### Reviewed changes Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments. | File | Description | | ---- | ----------- | | .github/workflows/container-publish.yml | New workflow to prepare target/version info and build/publish per bake target with conditional tagging. | | .github/actions/build-container/action.yml | New composite action wrapping `docker buildx bake` with cache and optional push/login. | --- 💡 <a href="/jasoncouture/llama-shears/new/ci/nuget-publish?filename=.github/instructions/*.instructions.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add Copilot custom instructions</a> for smarter, more guided reviews. <a href="https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn how to get started</a>.
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-07 11:14:33 -04:00

docker buildx bake --set <target>.tags=... is being emitted once per tag. Buildx treats --set as an override for the field, so repeating it typically results in only the last tag being applied. Consider joining all tags into a single comma-separated value (or otherwise passing them in one --set) so all requested tags are applied consistently (e.g., version tag and latest).

`docker buildx bake --set <target>.tags=...` is being emitted once per tag. Buildx treats `--set` as an override for the field, so repeating it typically results in only the *last* tag being applied. Consider joining all tags into a single comma-separated value (or otherwise passing them in one `--set`) so all requested tags are applied consistently (e.g., version tag and `latest`).
@ -0,0 +1,76 @@
name: Build/Push Containers
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-07 11:14:34 -04:00

The existing=$(gh api ... | jq ... || true) pattern will treat any API failure (rate limit, 403, transient network, etc.) the same as a 404, which can incorrectly cause :latest to be added. It would be safer to only swallow the expected 404-for-first-publish case and fail (or retry) on other non-success responses so latest can't be minted on an incomplete view of existing tags.

The `existing=$(gh api ... | jq ... || true)` pattern will treat *any* API failure (rate limit, 403, transient network, etc.) the same as a 404, which can incorrectly cause `:latest` to be added. It would be safer to only swallow the expected 404-for-first-publish case and fail (or retry) on other non-success responses so `latest` can't be minted on an incomplete view of existing tags.
@ -0,0 +13,4 @@
permissions:
contents: read
copilot-pull-request-reviewer[bot] (Migrated from github.com) commented 2026-05-07 11:14:33 -04:00

The workflow requests packages: write at the top level, but PR runs only need read access (and shouldn't be able to push). Consider defaulting to minimal permissions (e.g., packages: read) and granting packages: write only for the push/build job that actually publishes, to reduce the blast radius if a PR workflow step is ever expanded.

The workflow requests `packages: write` at the top level, but PR runs only need read access (and shouldn't be able to push). Consider defaulting to minimal permissions (e.g., `packages: read`) and granting `packages: write` only for the push/build job that actually publishes, to reduce the blast radius if a PR workflow step is ever expanded.
github-actions[bot] commented 2026-05-07 13:25:34 -04:00 (Migrated from github.com)
Package Line Rate Branch Rate Complexity Health
LlamaShears.Core.Abstractions.Context 100% 100% 4
LlamaShears.Provider.Ollama 3% 1% 166
LlamaShears.IntegrationTests 87% 73% 71
LlamaShears.Core 50% 35% 761
LlamaShears.Core.Abstractions.Content 0% 100% 1
LlamaShears.Core.Abstractions.Caching 100% 100% 1
LlamaShears.Core.Eventing 96% 76% 51
StrangeSoft.Plugins.Host 20% 21% 87
LlamaShears.Core.Abstractions.Provider 32% 20% 66
LlamaShears.Core.Abstractions.Memory 0% 100% 3
LlamaShears.Api.Web 35% 20% 333
LlamaShears.Hosting 100% 100% 4
LlamaShears.Core.Abstractions.Events 21% 6% 79
LlamaShears.Core.Abstractions.SystemPrompt 100% 100% 2
LlamaShears 65% 25% 11
LlamaShears.Core.Abstractions.PromptContext 89% 100% 2
LlamaShears.Provider.Onnx.Embeddings 4% 0% 68
LlamaShears.Plugins.Host 34% 24% 36
LlamaShears.Core.Abstractions.Agent 73% 100% 11
LlamaShears.Api 11% 3% 302
LlamaShears.Plugins 0% 100% 1
LlamaShears.Core.Eventing.Extensions 100% 100% 1
LlamaShears.Core.Abstractions.Context 100% 100% 4
LlamaShears.Provider.Ollama 3% 1% 166
LlamaShears.Core 49% 34% 761
LlamaShears.Core.Abstractions.Content 0% 100% 1
LlamaShears.Core.Abstractions.Caching 100% 100% 1
LlamaShears.Core.Eventing 96% 76% 51
StrangeSoft.Plugins.Host 20% 21% 87
LlamaShears.Core.Abstractions.Provider 32% 20% 66
LlamaShears.Core.Abstractions.Memory 0% 100% 3
LlamaShears.Api.Web 22% 12% 333
LlamaShears.Hosting 100% 100% 4
LlamaShears.Core.Abstractions.Events 21% 6% 79
LlamaShears.Core.Abstractions.SystemPrompt 100% 100% 2
LlamaShears 65% 25% 11
LlamaShears.Core.Abstractions.PromptContext 89% 100% 2
LlamaShears.Provider.Onnx.Embeddings 4% 0% 68
LlamaShears.Plugins.Host 34% 24% 36
LlamaShears.Core.Abstractions.Agent 73% 100% 11
LlamaShears.Api 9% 1% 302
LlamaShears.Plugins 0% 100% 1
LlamaShears.Core.Eventing.Extensions 100% 100% 1
LlamaShears.Core.Abstractions.Context 100% 100% 4
LlamaShears.Provider.Ollama 44% 24% 166
LlamaShears.Core 44% 41% 761
LlamaShears.Core.Abstractions.Content 0% 100% 1
LlamaShears.Core.Abstractions.Caching 100% 100% 1
LlamaShears.Core.Eventing 93% 86% 51
LlamaShears.Core.Abstractions.Provider 78% 64% 66
LlamaShears.Core.Abstractions.Memory 100% 100% 3
LlamaShears.Hosting 100% 100% 4
LlamaShears.Core.Abstractions.Events 15% 3% 79
LlamaShears.Core.Abstractions.SystemPrompt 100% 100% 2
LlamaShears.Core.Abstractions.PromptContext 89% 100% 2
LlamaShears.Provider.Onnx.Embeddings 33% 36% 68
LlamaShears.Core.Abstractions.Agent 86% 100% 11
LlamaShears.Api 31% 33% 302
LlamaShears.Core.Eventing.Extensions 100% 100% 1
LlamaShears.Analyzers 89% 76% 199
LlamaShears.Analyzers.CodeFixes 85% 69% 60
Summary 51% (7754 / 19360) 39% (1524 / 5347) 5832
Package | Line Rate | Branch Rate | Complexity | Health -------- | --------- | ----------- | ---------- | ------ LlamaShears.Core.Abstractions.Context | 100% | 100% | 4 | ✔ LlamaShears.Provider.Ollama | 3% | 1% | 166 | ❌ LlamaShears.IntegrationTests | 87% | 73% | 71 | ✔ LlamaShears.Core | 50% | 35% | 761 | ❌ LlamaShears.Core.Abstractions.Content | 0% | 100% | 1 | ❌ LlamaShears.Core.Abstractions.Caching | 100% | 100% | 1 | ✔ LlamaShears.Core.Eventing | 96% | 76% | 51 | ✔ StrangeSoft.Plugins.Host | 20% | 21% | 87 | ❌ LlamaShears.Core.Abstractions.Provider | 32% | 20% | 66 | ❌ LlamaShears.Core.Abstractions.Memory | 0% | 100% | 3 | ❌ LlamaShears.Api.Web | 35% | 20% | 333 | ❌ LlamaShears.Hosting | 100% | 100% | 4 | ✔ LlamaShears.Core.Abstractions.Events | 21% | 6% | 79 | ❌ LlamaShears.Core.Abstractions.SystemPrompt | 100% | 100% | 2 | ✔ LlamaShears | 65% | 25% | 11 | ➖ LlamaShears.Core.Abstractions.PromptContext | 89% | 100% | 2 | ✔ LlamaShears.Provider.Onnx.Embeddings | 4% | 0% | 68 | ❌ LlamaShears.Plugins.Host | 34% | 24% | 36 | ❌ LlamaShears.Core.Abstractions.Agent | 73% | 100% | 11 | ➖ LlamaShears.Api | 11% | 3% | 302 | ❌ LlamaShears.Plugins | 0% | 100% | 1 | ❌ LlamaShears.Core.Eventing.Extensions | 100% | 100% | 1 | ✔ LlamaShears.Core.Abstractions.Context | 100% | 100% | 4 | ✔ LlamaShears.Provider.Ollama | 3% | 1% | 166 | ❌ LlamaShears.Core | 49% | 34% | 761 | ❌ LlamaShears.Core.Abstractions.Content | 0% | 100% | 1 | ❌ LlamaShears.Core.Abstractions.Caching | 100% | 100% | 1 | ✔ LlamaShears.Core.Eventing | 96% | 76% | 51 | ✔ StrangeSoft.Plugins.Host | 20% | 21% | 87 | ❌ LlamaShears.Core.Abstractions.Provider | 32% | 20% | 66 | ❌ LlamaShears.Core.Abstractions.Memory | 0% | 100% | 3 | ❌ LlamaShears.Api.Web | 22% | 12% | 333 | ❌ LlamaShears.Hosting | 100% | 100% | 4 | ✔ LlamaShears.Core.Abstractions.Events | 21% | 6% | 79 | ❌ LlamaShears.Core.Abstractions.SystemPrompt | 100% | 100% | 2 | ✔ LlamaShears | 65% | 25% | 11 | ➖ LlamaShears.Core.Abstractions.PromptContext | 89% | 100% | 2 | ✔ LlamaShears.Provider.Onnx.Embeddings | 4% | 0% | 68 | ❌ LlamaShears.Plugins.Host | 34% | 24% | 36 | ❌ LlamaShears.Core.Abstractions.Agent | 73% | 100% | 11 | ➖ LlamaShears.Api | 9% | 1% | 302 | ❌ LlamaShears.Plugins | 0% | 100% | 1 | ❌ LlamaShears.Core.Eventing.Extensions | 100% | 100% | 1 | ✔ LlamaShears.Core.Abstractions.Context | 100% | 100% | 4 | ✔ LlamaShears.Provider.Ollama | 44% | 24% | 166 | ❌ LlamaShears.Core | 44% | 41% | 761 | ❌ LlamaShears.Core.Abstractions.Content | 0% | 100% | 1 | ❌ LlamaShears.Core.Abstractions.Caching | 100% | 100% | 1 | ✔ LlamaShears.Core.Eventing | 93% | 86% | 51 | ✔ LlamaShears.Core.Abstractions.Provider | 78% | 64% | 66 | ✔ LlamaShears.Core.Abstractions.Memory | 100% | 100% | 3 | ✔ LlamaShears.Hosting | 100% | 100% | 4 | ✔ LlamaShears.Core.Abstractions.Events | 15% | 3% | 79 | ❌ LlamaShears.Core.Abstractions.SystemPrompt | 100% | 100% | 2 | ✔ LlamaShears.Core.Abstractions.PromptContext | 89% | 100% | 2 | ✔ LlamaShears.Provider.Onnx.Embeddings | 33% | 36% | 68 | ❌ LlamaShears.Core.Abstractions.Agent | 86% | 100% | 11 | ✔ LlamaShears.Api | 31% | 33% | 302 | ❌ LlamaShears.Core.Eventing.Extensions | 100% | 100% | 1 | ✔ LlamaShears.Analyzers | 89% | 76% | 199 | ✔ LlamaShears.Analyzers.CodeFixes | 85% | 69% | 60 | ✔ **Summary** | **51%** (7754 / 19360) | **39%** (1524 / 5347) | **5832** | ➖ <!-- Sticky Pull Request Commentcoverage -->
Sign in to join this conversation.
No description provided.