Reusable build-image workflow
Depends on: #11
Summary
Our goal here is to create a single re-usable build image workflow. We can call into this for when we setup our functional and regression tests to make sure code works in open PRs.
Modeled after llm-d's build-image.yaml.
What this workflow does
New file: .github/workflows/build-image.yaml with workflow_call and workflow_dispatch triggers.
Inputs and outputs
on:
workflow_call:
inputs:
ref:
description: "Git ref to build from"
type: string
default: ""
force_build:
description: "Build all images regardless of change detection"
type: boolean
default: false
outputs:
prediction_image:
description: "Full image ref for the prediction server"
training_image:
description: "Full image ref for the training server"
test_image:
description: "Full image ref for the test runner"
prediction_changed:
description: "Whether prediction source changed"
training_changed:
description: "Whether training source changed"
workflow_dispatch:
inputs:
force_build:
description: "Build all images regardless of change detection"
type: boolean
default: false
workflow_dispatch lets maintainers kick off builds manually from the Actions UI.
Change detection
After the repo restructure, dorny/paths-filter can map changed paths to specific containers:
filters: |
prediction:
- 'src/llm_d_latency_predictor/prediction/**'
- 'src/llm_d_latency_predictor/common/**'
- 'Dockerfile-prediction'
- 'pyproject.toml'
training:
- 'src/llm_d_latency_predictor/training/**'
- 'src/llm_d_latency_predictor/common/**'
- 'Dockerfile-training'
- 'pyproject.toml'
test:
- 'src/llm_d_latency_predictor/test/**'
- 'tests/**'
- 'Dockerfile-test'
- 'pyproject.toml'
When force_build is true, all three get built. Otherwise only containers whose source changed get rebuilt. The test container should also rebuild when prediction or training source changes, since tests run against those images.
This only works after the repo restructure. With the current flat layout, every Python change lands in src/llm_d_latency_predictor/ and there's no way to tell prediction changes from training changes.
Tagging
Use docker/metadata-action@v6:
- uses: docker/metadata-action@v6
with:
images: ghcr.io/llm-d/llm-d-latency-predictor-${{ matrix.service }}
tags: |
type=ref,event=pr
type=raw,value=sha-${{ env.SHORT_SHA }}
type=raw,value=main,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
# ... and more
The conditional logic here is handled by the metadata action's event matching, not by us. Since this is a workflow_call workflow, it inherits the event context from whichever workflow called it. When the caller triggers on pull_request, the type=ref,event=pr rule fires and produces pr-<number>. When the caller triggers on push to main, that rule doesn't fire, but the main enable condition matches. The sha-<commit> tag fires in both cases. No if/else branching needed on our side.
Two contexts for tagging:
On open PRs, images get two tags: pr-<number> and sha-<commit>. The pr-<number> tag always points to the latest push on that PR, so someone who just wants to test the current state of the PR can pull by PR number. The sha-<commit> tag pins to a specific commit, useful when you need to match up exactly which code you're testing.
On merge to main, images get commit-<commit> and main. The main tag only updates on merges main, never on PRs and will basically be our version of a latest tag. This gives us a control mechanism: containers whose source didn't change in a PR can pull main (which is whatever's on main), while containers that did change get the PR-specific image. Downstream test workflows use the build outputs to pick the right tag for each container.
Build and push
docker/build-push-action@v7 with --push to ghcr.io for both amd64 and arm64. The multi-platform build logic, QEMU setup, and registry login that currently live in the composite action (.github/actions/docker-build-and-push) move here.
Concurrency
concurrency:
group: build-image-${{ github.event.pull_request.number || github.ref }}-${{ github.sha }}
cancel-in-progress: true
Prevents duplicate builds when someone pushes twice quickly.
Outputs
The workflow outputs full image refs for each container it built (e.g., ghcr.io/llm-d/llm-d-latency-predictor-prediction:pr-123).
Changes to existing files
.github/workflows/ci-pr-checks.yaml -- the container-build job is redundant once this workflow exists. Either remove it or keep it as a parallel fast-fail check. Lint stays.
.github/workflows/ci-release.yaml -- refactor to call build-image.yaml with force_build: true instead of inlining build logic through the composite action.
.github/actions/docker-build-and-push/action.yml -- can probably be retired once its logic lives in the reusable workflow. Check if anything else references it first.
Acceptance criteria
Reusable build-image workflow
Depends on: #11
Summary
Our goal here is to create a single re-usable build image workflow. We can call into this for when we setup our functional and regression tests to make sure code works in open PRs.
Modeled after llm-d's build-image.yaml.
What this workflow does
New file:
.github/workflows/build-image.yamlwithworkflow_callandworkflow_dispatchtriggers.Inputs and outputs
workflow_dispatchlets maintainers kick off builds manually from the Actions UI.Change detection
After the repo restructure,
dorny/paths-filtercan map changed paths to specific containers:When
force_buildis true, all three get built. Otherwise only containers whose source changed get rebuilt. The test container should also rebuild when prediction or training source changes, since tests run against those images.This only works after the repo restructure. With the current flat layout, every Python change lands in
src/llm_d_latency_predictor/and there's no way to tell prediction changes from training changes.Tagging
Use
docker/metadata-action@v6:The conditional logic here is handled by the metadata action's event matching, not by us. Since this is a
workflow_callworkflow, it inherits the event context from whichever workflow called it. When the caller triggers onpull_request, thetype=ref,event=prrule fires and producespr-<number>. When the caller triggers onpushto main, that rule doesn't fire, but themainenable condition matches. Thesha-<commit>tag fires in both cases. No if/else branching needed on our side.Two contexts for tagging:
On open PRs, images get two tags:
pr-<number>andsha-<commit>. Thepr-<number>tag always points to the latest push on that PR, so someone who just wants to test the current state of the PR can pull by PR number. Thesha-<commit>tag pins to a specific commit, useful when you need to match up exactly which code you're testing.On merge to main, images get
commit-<commit>andmain. Themaintag only updates on merges main, never on PRs and will basically be our version of a latest tag. This gives us a control mechanism: containers whose source didn't change in a PR can pullmain(which is whatever's on main), while containers that did change get the PR-specific image. Downstream test workflows use the build outputs to pick the right tag for each container.Build and push
docker/build-push-action@v7with--pushto ghcr.io for both amd64 and arm64. The multi-platform build logic, QEMU setup, and registry login that currently live in the composite action (.github/actions/docker-build-and-push) move here.Concurrency
Prevents duplicate builds when someone pushes twice quickly.
Outputs
The workflow outputs full image refs for each container it built (e.g.,
ghcr.io/llm-d/llm-d-latency-predictor-prediction:pr-123).Changes to existing files
.github/workflows/ci-pr-checks.yaml-- the container-build job is redundant once this workflow exists. Either remove it or keep it as a parallel fast-fail check. Lint stays..github/workflows/ci-release.yaml-- refactor to callbuild-image.yamlwithforce_build: trueinstead of inlining build logic through the composite action..github/actions/docker-build-and-push/action.yml-- can probably be retired once its logic lives in the reusable workflow. Check if anything else references it first.Acceptance criteria
build-image.yamlexists withworkflow_callandworkflow_dispatchtriggersdorny/paths-filterto map changed paths to containers, using the sub-package structure from the repo restructureforce_buildinput to rebuild everything regardless of changespr-<number>andsha-<commit>tagssha-<commit>andlatesttagslatesttag only updates on main, never on PRslatest(from main)ci-release.yamlcallsbuild-image.yamlwithforce_build: true