Skip to content

feat: matrix mode — plan command + release --only / --pin-version#7

Merged
blairham merged 2 commits into
mainfrom
feat/plan-matrix-mode
Jul 5, 2026
Merged

feat: matrix mode — plan command + release --only / --pin-version#7
blairham merged 2 commits into
mainfrom
feat/plan-matrix-mode

Conversation

@blairham

@blairham blairham commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Closes #5.

What

Splits deciding from building so CI can fan one runner out per build group — almost like a matrix build, because it is one:

  • stevedore plan — resolves per-image versions, runs change detection (marker refs / --changed-since / --only-changed), applies build-once grouping, and prints the plan as one compact JSON line without building. include is GitHub Actions matrix shape: one entry per build group with ids, only (csv), per-member versions, ready-made pins flags, and the build reason. Skipped images are listed with their reason. Progress → stderr; stdout carries only the document.
  • release --only <ids> — builds exactly those images, unconditionally (selection was the planner's decision). Unknown ids fail fast.
  • release --pin-version id=ver (repeatable) — tags exactly what the plan resolved. Pinned images skip remote version resolution entirely, so a matrix job needs no registry read for versioning.
  • Markers advance on any real push — previously only in marker-detection mode; --only matrix jobs and --changed-since runs now keep every image's baseline current (the marker records last released, not last marker-diffed).
  • action.ymlcommand: plan exposes the document as the plan output for strategy.matrix.
jobs:
  plan:
    outputs: {matrix: ${{ steps.plan.outputs.plan }}}
    steps:
      - uses: blairham/stevedore@v1
        id: plan
        with: {command: plan}
  build:
    needs: plan
    if: ${{ fromJson(needs.plan.outputs.matrix).include[0] != null }}
    strategy: {matrix: ${{ fromJson(needs.plan.outputs.matrix) }}}
    steps:
      - uses: blairham/stevedore@v1
        with: {command: release, args: --only ${{ matrix.only }} ${{ matrix.pins }}}

How

The eval/group pre-pass is extracted from Release into evaluateImages/groupPlans (internal/pipeline/plan.go) and shared by both paths — existing modes behave identically.

Verified

  • make check green (lint + race tests); new table tests for grouping, plan rendering, --only filtering/validation, pin parsing and precedence.
  • Against real multi-image configs (live ECR): a 5-member identical-spec group plans as one entry with per-member versions (and picks up marker refs previously advanced by a different CI system); 13 build-arg-distinct images plan as 13 entries with .csproj-graph change reasons; release --only dis --pin-version dis=X --dry-run builds just that image with the pinned tag and no registry access.

blairham and others added 2 commits July 5, 2026 11:29
Split deciding from building so CI can fan one runner out per build group
(#5):

- stevedore plan: resolves per-image versions, runs change detection
  (marker refs / --changed-since / --only-changed), applies build-once
  grouping, and prints the plan as compact JSON without building. The
  include array is GitHub Actions matrix shape — one entry per group,
  carrying ids, only (csv), per-member versions, ready-made
  --pin-version flags, and the build reason; skips are listed with
  their reason. Progress goes to stderr, stdout is only the document.
- release --only <ids>: build exactly those images, unconditionally —
  selection was the planner's decision. Unknown ids fail fast.
- release --pin-version id=ver (repeatable): tag exactly what the plan
  resolved; pinned images skip remote version resolution entirely, so
  matrix jobs need no registry read for versioning.
- Release markers now advance after ANY real push (previously only in
  marker-detection mode), so --only matrix jobs and --changed-since
  runs keep every image's baseline current.
- action.yml: plan output wired for strategy.matrix consumption.

The eval/group pre-pass is extracted from Release and shared with Plan
(internal/pipeline/plan.go); behavior is unchanged for existing modes.
Verified against real multi-image configs: a 5-member identical-spec
group plans as one entry with per-member ECR versions; 13 build-arg-
distinct images plan as 13 entries with .csproj-graph change reasons.
release --only resolved every config image's version before filtering,
so a matrix job still issued a registry read per excluded image — and
the release-level version anchored on cfg.Images[0], reading a repo the
job may not even have access to once per-service push roles land.

- resolvePlans now takes the --only selection and skips excluded images
  before version resolution (filterPlans folded in).
- resolveVersion anchors on the first *selected* image; when that image
  is pinned in per-image registry mode the pin is the release version,
  so a fully-pinned matrix job performs no registry read at all.
- validateImageIDs moved ahead of version resolution so an unknown
  --only id fails fast with the intended error.

Verified against the real dis config: `release --only dis
--pin-version dis=0.0.160 --dry-run` and a full 5-way group replay from
`plan` output both run with no AWS credentials.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@blairham
blairham merged commit a86540d into main Jul 5, 2026
2 checks passed
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.

feat: matrix mode — plan/fan-out so builds run across multiple GitHub runners

1 participant