ci(publish): gate kaniko image builds + publish on changed paths (CI throughput) #619

Merged
jason.woltje merged 1 commits from feat/ci-publish-path-gating into main 2026-06-22 13:14:32 +00:00
Owner

ci(publish): gate kaniko image builds + publish on changed paths

The bottleneck. Every main merge runs publish.yml, which unconditionally rebuilds the three kaniko images (gateway/appservice/web, ~25 min); each Dockerfile's COPY . . busts kaniko's cache on any change. But none of those apps depend on @mosaicstack/mosaic — so the entire constitution + fleet PR stream (all packages/mosaic/** + docs/**) rebuilds all three images for nothing, saturating the runners.

Change (minimal — publish.yml when:path only)

  • build-gateway / build-appservice / build-web — step-level when (shared anchor): build on tag always; on a main push, build unless the changed files are only packages/mosaic/**, docs/**, **/*.md, .woodpecker/**.
  • publish-npm — run only when packages/** changed (or on tag); a pure-docs merge runs no publish.
  • install/build unchanged.

Why exclude-list (not per-image include-lists)

My proposal floated per-image include-lists, but those risk under-including an app's transitive package closure → a needed image silently skipped → stale deploy. An exclude-list is correctness-safe: the default is to build; only the known npm-only/docs change classes skip. Same waste elimination, no staleness risk.

Woodpecker semantics (docs-confirmed)

Per the Woodpecker workflow-syntax docs:

  • when list entries are OR'd ("if at least one of the conditions … evaluates true the step is executed"); sub-conditions within an entry are AND'd.
  • path conditions apply to push/pull_request onlynot tag events — which is exactly why each gated step has a separate event: tag entry so releases always build.
  • Step-level when governs the step independently of the file-level when (the file-level when gates whether the workflow runs; ungated steps still inherit it).

Validation & safety

  • The real skip-validation happens on the next merge after this lands: a docs-only merge (e.g. #613) should show 0 image builds; an apps/gateway/** change should still build gateway.
  • Skipping is safe — prior :latest / :sha-* images remain; nothing is deleted, deploys still pull the last good image.

Deferred (follow-up PRs, per Lead)

Dockerfile COPY . . tightening (better cache reuse for builds that do run); scoping publish-npm's build dependency to --filter "@mosaicstack/*". Not adopting tag-gate-everything (publish-on-merge → publish-on-tag is a deploy-workflow change = Jason's call).

🤖 Generated with Claude Code

## ci(publish): gate kaniko image builds + publish on changed paths **The bottleneck.** Every `main` merge runs `publish.yml`, which unconditionally rebuilds the three kaniko images (gateway/appservice/web, **~25 min**); each Dockerfile's `COPY . .` busts kaniko's cache on any change. But **none of those apps depend on `@mosaicstack/mosaic`** — so the entire constitution + fleet PR stream (all `packages/mosaic/**` + `docs/**`) rebuilds all three images **for nothing**, saturating the runners. ### Change (minimal — `publish.yml` `when:path` only) - **`build-gateway` / `build-appservice` / `build-web`** — step-level `when` (shared anchor): build on **tag** always; on a `main` push, build **unless** the changed files are *only* `packages/mosaic/**`, `docs/**`, `**/*.md`, `.woodpecker/**`. - **`publish-npm`** — run only when `packages/**` changed (or on tag); a pure-docs merge runs no publish. - `install`/`build` unchanged. ### Why exclude-list (not per-image include-lists) My proposal floated per-image include-lists, but those risk **under-including** an app's transitive package closure → a needed image silently skipped → stale deploy. An **exclude-list is correctness-safe**: the default is to build; only the known npm-only/docs change classes skip. Same waste elimination, no staleness risk. ### Woodpecker semantics (docs-confirmed) Per the [Woodpecker workflow-syntax docs](https://woodpecker-ci.org/docs/usage/workflow-syntax): - **`when` list entries are OR'd** ("if at least one of the conditions … evaluates true the step is executed"); sub-conditions within an entry are AND'd. - **`path` conditions apply to `push`/`pull_request` only** — *not* tag events — which is exactly why each gated step has a **separate `event: tag`** entry so releases always build. - **Step-level `when` governs the step independently** of the file-level `when` (the file-level `when` gates whether the workflow runs; ungated steps still inherit it). ### Validation & safety - The real skip-validation happens on the **next merge after this lands**: a docs-only merge (e.g. #613) should show **0 image builds**; an `apps/gateway/**` change should still build gateway. - **Skipping is safe** — prior `:latest` / `:sha-*` images remain; nothing is deleted, deploys still pull the last good image. ### Deferred (follow-up PRs, per Lead) Dockerfile `COPY . .` tightening (better cache reuse for builds that *do* run); scoping `publish-npm`'s build dependency to `--filter "@mosaicstack/*"`. **Not** adopting tag-gate-everything (publish-on-merge → publish-on-tag is a deploy-workflow change = Jason's call). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jason.woltje added 1 commit 2026-06-22 08:29:25 +00:00
ci(publish): gate kaniko image builds + publish on changed paths (CI throughput)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
7d06c1c761
Every main merge runs publish.yml, which unconditionally rebuilds the three
kaniko images (gateway/appservice/web, ~25 min) — and each Dockerfile's
`COPY . .` busts kaniko's cache on any change. But none of those apps depend
on @mosaicstack/mosaic, so the entire constitution + fleet PR stream (all
packages/mosaic/** + docs/**) rebuilds all three images for nothing, saturating
the runners.

Gate the heavy steps with step-level `when: path`:
- build-gateway/appservice/web: skip when a main push touches ONLY non-image
  paths (packages/mosaic/**, docs/**, **/*.md, .woodpecker/**); always build on
  tag. Exclude-list keeps the default SAFE — any non-excluded change still
  builds, so no transitive dep can silently go stale (chosen over per-image
  include-lists, which risked under-including an app's transitive closure).
- publish-npm: run only when packages/** changed (or on tag) — a pure-docs
  merge now runs no publish.

Woodpecker semantics (docs-confirmed): `when` entries are OR'd; `path` applies
to push/PR only (hence the separate `event: tag` entry); step-level `when`
governs the step independently of the file-level `when`.

install/build remain ungated (deferred: scoping the build + tightening the
Dockerfile COPY are follow-ups). Skip-validation lands on the next real merge
(a docs-only merge should show 0 image builds); skipping is safe — prior
:latest/:sha images remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EsgTQzV5YUGk1JtCLP4B83
jason.woltje merged commit dd0a0d38c6 into main 2026-06-22 13:14:32 +00:00
jason.woltje deleted branch feat/ci-publish-path-gating 2026-06-22 13:14:33 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#619