diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bc673d..a8b9446 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,11 @@ permissions: jobs: # ── relctl: PR metadata + version preview ───────────────────────────────── build-info: + # The reusable workflow posts relctl's help comment on the pull request and + # requests pull-requests: write; the caller must grant at least that. + permissions: + contents: read + pull-requests: write uses: layer87-labs/relctl-action/.github/workflows/generate-build-infos.yml@main secrets: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/component-release.yml b/.github/workflows/component-release.yml new file mode 100644 index 0000000..741bd5a --- /dev/null +++ b/.github/workflows/component-release.yml @@ -0,0 +1,44 @@ +name: Component release + +# Creates the draft GitHub release of one relctl component (CalVer, own tag +# prefix, own counter). Called by release.yml; the caller builds the artifacts and +# publishes the release afterwards. +# +# relctl-action's create-release.yml has no `component` input yet, so relctl is +# called directly through the setup action. +on: + workflow_call: + inputs: + component: + description: "Component name from .relctl.yaml" + required: true + type: string + outputs: + release-id: + description: "ID of the draft release; empty when the release already exists" + value: ${{ jobs.release.outputs.release-id }} + version: + description: "Released version (CalVer, without the tag prefix)" + value: ${{ jobs.release.outputs.version }} + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + outputs: + release-id: ${{ steps.create.outputs.RELCTL_RELEASE_ID }} + version: ${{ steps.create.outputs.RELCTL_VERSION }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # CalVer counter and tags come from the git history + + - name: Create release (${{ inputs.component }}) + id: create + uses: layer87-labs/relctl-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + relctl-version: "2.11.0" + run: relctl release create --component ${{ inputs.component }} --version-scheme calver >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release-models.yml b/.github/workflows/release-models.yml deleted file mode 100644 index 6fc8d8d..0000000 --- a/.github/workflows/release-models.yml +++ /dev/null @@ -1,155 +0,0 @@ -name: Release Models - -# Trigger: manually via workflow_dispatch, or on changes to model Containerfiles. -on: - workflow_dispatch: - inputs: - targets: - description: "Which model images to build (comma-separated: embedding,reranker,whisper or 'all')" - required: true - default: "all" - type: string - push: - branches: [main] - paths: - - "deploy/Containerfile.tei-model-init" - - "deploy/Containerfile.tei-reranker-model-init" - - "deploy/Containerfile.whisper" - - "deploy/whisper/**" - -permissions: - contents: read - packages: write - id-token: write # cosign keyless OIDC signing - -env: - REGISTRY: ghcr.io/layer87-labs - -jobs: - embedding-model: - if: > - github.event_name == 'push' || - contains(github.event.inputs.targets, 'embedding') || - github.event.inputs.targets == 'all' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Log in to GHCR - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set model tag - run: echo "MODEL_TAG=bge-m3-$(date +%Y.%m)" >> $GITHUB_ENV - - - name: Build and push TEI model init (embedding) - run: | - docker build \ - -f deploy/Containerfile.tei-model-init \ - --build-arg MODEL_ID=BAAI/bge-m3 \ - -t ${{ env.REGISTRY }}/tei-model-init:${{ env.MODEL_TAG }} \ - -t ${{ env.REGISTRY }}/tei-model-init:latest \ - . - docker push ${{ env.REGISTRY }}/tei-model-init:${{ env.MODEL_TAG }} - docker push ${{ env.REGISTRY }}/tei-model-init:latest - - - name: Install cosign - uses: sigstore/cosign-installer@v3 - - - name: Sign image (keyless via OIDC, by digest) - run: scripts/sign-image.sh "${{ env.REGISTRY }}/tei-model-init:${{ env.MODEL_TAG }}" - - reranker-model: - if: > - github.event_name == 'push' || - contains(github.event.inputs.targets, 'reranker') || - github.event.inputs.targets == 'all' - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - # Default: small multilingual cross-encoder that ships its own ONNX file. - - slug: mmarco-mminilmv2-l12-h384-v1 - model_id: cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 - revision: 1427fd652930e4ba29e8149678df786c240d8825 - push_latest: true - # Higher quality, much slower on CPU; exported to ONNX at build time. - - slug: bge-reranker-v2-m3 - model_id: BAAI/bge-reranker-v2-m3 - revision: "" - push_latest: false - steps: - - uses: actions/checkout@v7 - - - name: Log in to GHCR - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set model tag - run: echo "MODEL_TAG=${{ matrix.slug }}-$(date +%Y.%m)" >> $GITHUB_ENV - - - name: Build and push TEI reranker model init (ONNX) - run: | - docker build \ - -f deploy/Containerfile.tei-reranker-model-init \ - --build-arg MODEL_ID=${{ matrix.model_id }} \ - --build-arg MODEL_REVISION=${{ matrix.revision }} \ - --build-arg MODEL_DATE=$(date +%Y.%m) \ - -t ${{ env.REGISTRY }}/tei-reranker-model-init:${{ env.MODEL_TAG }} \ - . - docker push ${{ env.REGISTRY }}/tei-reranker-model-init:${{ env.MODEL_TAG }} - if [ "${{ matrix.push_latest }}" = "true" ]; then - docker tag ${{ env.REGISTRY }}/tei-reranker-model-init:${{ env.MODEL_TAG }} ${{ env.REGISTRY }}/tei-reranker-model-init:latest - docker push ${{ env.REGISTRY }}/tei-reranker-model-init:latest - fi - - - name: Install cosign - uses: sigstore/cosign-installer@v3 - - - name: Sign image (keyless via OIDC, by digest) - run: scripts/sign-image.sh "${{ env.REGISTRY }}/tei-reranker-model-init:${{ env.MODEL_TAG }}" - - whisper-model: - if: > - github.event_name == 'push' || - contains(github.event.inputs.targets, 'whisper') || - github.event.inputs.targets == 'all' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Log in to GHCR - uses: docker/login-action@v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract whisper model from Containerfile - run: | - MODEL=$(grep '^ARG WHISPER_MODEL=' deploy/Containerfile.whisper | sed 's/ARG WHISPER_MODEL=//') - echo "WHISPER_MODEL=${MODEL}" >> $GITHUB_ENV - echo "MODEL_TAG=${MODEL}-$(date +%Y.%m)" >> $GITHUB_ENV - - - name: Build and push whisper - run: | - docker build \ - -f deploy/Containerfile.whisper \ - -t ${{ env.REGISTRY }}/whisper:${{ env.MODEL_TAG }} \ - -t ${{ env.REGISTRY }}/whisper:latest \ - . - docker push ${{ env.REGISTRY }}/whisper:${{ env.MODEL_TAG }} - docker push ${{ env.REGISTRY }}/whisper:latest - - - name: Install cosign - uses: sigstore/cosign-installer@v3 - - - name: Sign image (keyless via OIDC, by digest) - run: scripts/sign-image.sh "${{ env.REGISTRY }}/whisper:${{ env.MODEL_TAG }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b628568..3a46d7b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,34 +1,121 @@ name: Release +# Every independently releasable part of the repository is a relctl component +# (see .relctl.yaml). On a push to main only the components whose paths changed +# are released and built; each has its own tag (/), latest +# release and CalVer counter. Manual runs can force components. on: push: branches: [main] # Only trigger on merged PRs (push to main), not on tag pushes tags-ignore: ["**"] + workflow_dispatch: + inputs: + components: + description: "Components to release even if nothing changed (comma-separated: router,tei,embedding-model,reranker-model,whisper,chart or 'all')" + required: false + default: "" + type: string permissions: contents: write packages: write id-token: write # cosign keyless OIDC signing +# Release creation counts per day from git tags: never run two releases at once. +concurrency: + group: release + cancel-in-progress: false + env: REGISTRY: ghcr.io/layer87-labs + RELCTL_TOOL_VERSION: "2.11.0" jobs: - # ── Step 1: relctl creates a GitHub Release (CalVer tag) ────────────────── - create-release: - uses: layer87-labs/relctl-action/.github/workflows/create-release.yml@main + # ── Which components changed since their latest release? ────────────────── + changes: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + outputs: + router: ${{ steps.decide.outputs.router }} + tei: ${{ steps.decide.outputs.tei }} + embedding-model: ${{ steps.decide.outputs.embedding-model }} + reranker-model: ${{ steps.decide.outputs.reranker-model }} + whisper: ${{ steps.decide.outputs.whisper }} + chart: ${{ steps.decide.outputs.chart }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # compares with the tag of each component's latest release + + - name: Install relctl + uses: layer87-labs/relctl-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + relctl-version: ${{ env.RELCTL_TOOL_VERSION }} + + - name: Changed (router) + id: router + run: relctl release changed --component router + - name: Changed (tei) + id: tei + run: relctl release changed --component tei + - name: Changed (embedding-model) + id: embedding-model + run: relctl release changed --component embedding-model + - name: Changed (reranker-model) + id: reranker-model + run: relctl release changed --component reranker-model + - name: Changed (whisper) + id: whisper + run: relctl release changed --component whisper + - name: Changed (chart) + id: chart + run: relctl release changed --component chart + + - name: Decide + id: decide + env: + FORCE: ${{ github.event_name == 'workflow_dispatch' && inputs.components || '' }} + CHANGED_ROUTER: ${{ steps.router.outputs.RELCTL_CHANGED }} + CHANGED_TEI: ${{ steps.tei.outputs.RELCTL_CHANGED }} + CHANGED_EMBEDDING_MODEL: ${{ steps.embedding-model.outputs.RELCTL_CHANGED }} + CHANGED_RERANKER_MODEL: ${{ steps.reranker-model.outputs.RELCTL_CHANGED }} + CHANGED_WHISPER: ${{ steps.whisper.outputs.RELCTL_CHANGED }} + CHANGED_CHART: ${{ steps.chart.outputs.RELCTL_CHANGED }} + run: | + decide() { # name changed + if [ "$2" = "true" ] || [ "${FORCE}" = "all" ] || [[ ",${FORCE// /}," == *",$1,"* ]]; then + echo "$1=true" >> "$GITHUB_OUTPUT" + echo "$1: release" + else + echo "$1=false" >> "$GITHUB_OUTPUT" + echo "$1: unchanged" + fi + } + decide router "${CHANGED_ROUTER}" + decide tei "${CHANGED_TEI}" + decide embedding-model "${CHANGED_EMBEDDING_MODEL}" + decide reranker-model "${CHANGED_RERANKER_MODEL}" + decide whisper "${CHANGED_WHISPER}" + decide chart "${CHANGED_CHART}" + + # ── router ──────────────────────────────────────────────────────────────── + router-release: + needs: changes + if: needs.changes.outputs.router == 'true' + uses: ./.github/workflows/component-release.yml with: - version-scheme: calver - secrets: - token: ${{ secrets.GITHUB_TOKEN }} + component: router - # ── Step 2: Build and push code images ──────────────────────────────────── - build-and-push: - needs: create-release + router-build: + needs: router-release + # release-id is empty when the release already exists (published): nothing to build + if: needs.router-release.outputs.release-id != '' runs-on: ubuntu-latest env: - VERSION: ${{ needs.create-release.outputs.version }} + VERSION: ${{ needs.router-release.outputs.version }} steps: - uses: actions/checkout@v7 @@ -39,9 +126,6 @@ jobs: - name: Test run: go test -race ./... - - name: Build router binary - run: make build/router - - name: Log in to GHCR uses: docker/login-action@v4 with: @@ -49,46 +133,316 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - # ── Code images (CalVer from relctl) ──────────────────────────────── - - name: Build and push router run: | - make docker/router VERSION=${{ env.VERSION }} - make push/router VERSION=${{ env.VERSION }} + make docker/router VERSION=${VERSION} + make push/router VERSION=${VERSION} + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Sign image (keyless via OIDC, by digest) + run: scripts/sign-image.sh "${{ env.REGISTRY }}/inference-router:${VERSION}" + + - name: Publish release + uses: layer87-labs/relctl-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + relctl-version: ${{ env.RELCTL_TOOL_VERSION }} + run: relctl release publish --component router --release-id ${{ needs.router-release.outputs.release-id }} + + # ── tei (tei-base + tei-runtime share one version) ──────────────────────── + tei-release: + needs: changes + if: needs.changes.outputs.tei == 'true' + uses: ./.github/workflows/component-release.yml + with: + component: tei + + tei-build: + needs: tei-release + if: needs.tei-release.outputs.release-id != '' + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.tei-release.outputs.version }} + steps: + - uses: actions/checkout@v7 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push TEI base run: | - make docker/tei-base VERSION=${{ env.VERSION }} - docker push ${{ env.REGISTRY }}/tei-base:${{ env.VERSION }} + make docker/tei-base VERSION=${VERSION} + docker push ${{ env.REGISTRY }}/tei-base:${VERSION} - name: Build and push TEI runtime run: | - make docker/tei-runtime VERSION=${{ env.VERSION }} - docker push ${{ env.REGISTRY }}/tei-runtime:${{ env.VERSION }} - - # ── Sign images (keyless cosign via GitHub OIDC, by digest) ───────── + make docker/tei-runtime VERSION=${VERSION} + docker push ${{ env.REGISTRY }}/tei-runtime:${VERSION} - name: Install cosign uses: sigstore/cosign-installer@v3 - - name: Sign code images + - name: Sign images (keyless via OIDC, by digest) run: | scripts/sign-image.sh \ - "${{ env.REGISTRY }}/inference-router:${{ env.VERSION }}" \ - "${{ env.REGISTRY }}/tei-base:${{ env.VERSION }}" \ - "${{ env.REGISTRY }}/tei-runtime:${{ env.VERSION }}" + "${{ env.REGISTRY }}/tei-base:${VERSION}" \ + "${{ env.REGISTRY }}/tei-runtime:${VERSION}" - # ── Helm chart (OCI → ghcr.io) ─────────────────────────────────────── + - name: Publish release + uses: layer87-labs/relctl-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + relctl-version: ${{ env.RELCTL_TOOL_VERSION }} + run: relctl release publish --component tei --release-id ${{ needs.tei-release.outputs.release-id }} - - name: Package and push Helm chart + # ── embedding-model (image tag: bge-m3- and latest) ────────────── + embedding-model-release: + needs: changes + if: needs.changes.outputs.embedding-model == 'true' + uses: ./.github/workflows/component-release.yml + with: + component: embedding-model + + embedding-model-build: + needs: embedding-model-release + if: needs.embedding-model-release.outputs.release-id != '' + runs-on: ubuntu-latest + env: + MODEL_TAG: bge-m3-${{ needs.embedding-model-release.outputs.version }} + steps: + - uses: actions/checkout@v7 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push TEI model init (embedding) + run: | + docker build \ + -f deploy/Containerfile.tei-model-init \ + --build-arg MODEL_ID=BAAI/bge-m3 \ + --build-arg MODEL_DATE=${{ needs.embedding-model-release.outputs.version }} \ + -t ${{ env.REGISTRY }}/tei-model-init:${MODEL_TAG} \ + -t ${{ env.REGISTRY }}/tei-model-init:latest \ + . + docker push ${{ env.REGISTRY }}/tei-model-init:${MODEL_TAG} + docker push ${{ env.REGISTRY }}/tei-model-init:latest + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Sign image (keyless via OIDC, by digest) + run: scripts/sign-image.sh "${{ env.REGISTRY }}/tei-model-init:${MODEL_TAG}" + + - name: Publish release + uses: layer87-labs/relctl-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + relctl-version: ${{ env.RELCTL_TOOL_VERSION }} + run: relctl release publish --component embedding-model --release-id ${{ needs.embedding-model-release.outputs.release-id }} + + # ── reranker-model (one version for the component, one image per model) ─── + reranker-model-release: + needs: changes + if: needs.changes.outputs.reranker-model == 'true' + uses: ./.github/workflows/component-release.yml + with: + component: reranker-model + + reranker-model-build: + needs: reranker-model-release + if: needs.reranker-model-release.outputs.release-id != '' + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # Default: small multilingual cross-encoder that ships its own ONNX file. + - slug: mmarco-mminilmv2-l12-h384-v1 + model_id: cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 + revision: 1427fd652930e4ba29e8149678df786c240d8825 + push_latest: true + # Higher quality, much slower on CPU; exported to ONNX at build time. + - slug: bge-reranker-v2-m3 + model_id: BAAI/bge-reranker-v2-m3 + revision: "" + push_latest: false + env: + VERSION: ${{ needs.reranker-model-release.outputs.version }} + MODEL_TAG: ${{ matrix.slug }}-${{ needs.reranker-model-release.outputs.version }} + steps: + - uses: actions/checkout@v7 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push TEI reranker model init (ONNX) + run: | + docker build \ + -f deploy/Containerfile.tei-reranker-model-init \ + --build-arg MODEL_ID=${{ matrix.model_id }} \ + --build-arg MODEL_REVISION=${{ matrix.revision }} \ + --build-arg MODEL_DATE=${VERSION} \ + -t ${{ env.REGISTRY }}/tei-reranker-model-init:${MODEL_TAG} \ + . + docker push ${{ env.REGISTRY }}/tei-reranker-model-init:${MODEL_TAG} + if [ "${{ matrix.push_latest }}" = "true" ]; then + docker tag ${{ env.REGISTRY }}/tei-reranker-model-init:${MODEL_TAG} ${{ env.REGISTRY }}/tei-reranker-model-init:latest + docker push ${{ env.REGISTRY }}/tei-reranker-model-init:latest + fi + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Sign image (keyless via OIDC, by digest) + run: scripts/sign-image.sh "${{ env.REGISTRY }}/tei-reranker-model-init:${MODEL_TAG}" + + # Publishes once, after every model of the matrix is built and signed. + reranker-model-publish: + needs: [reranker-model-release, reranker-model-build] + if: needs.reranker-model-release.outputs.release-id != '' + runs-on: ubuntu-latest + steps: + - name: Publish release + uses: layer87-labs/relctl-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + relctl-version: ${{ env.RELCTL_TOOL_VERSION }} + run: relctl release publish --component reranker-model --release-id ${{ needs.reranker-model-release.outputs.release-id }} + + # ── whisper (image tag: - and latest) ───────────── + whisper-release: + needs: changes + if: needs.changes.outputs.whisper == 'true' + uses: ./.github/workflows/component-release.yml + with: + component: whisper + + whisper-build: + needs: whisper-release + if: needs.whisper-release.outputs.release-id != '' + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.whisper-release.outputs.version }} + steps: + - uses: actions/checkout@v7 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract whisper model from Containerfile + run: | + MODEL=$(grep '^ARG WHISPER_MODEL=' deploy/Containerfile.whisper | sed 's/ARG WHISPER_MODEL=//') + echo "MODEL_TAG=${MODEL}-${VERSION}" >> "$GITHUB_ENV" + + - name: Build and push whisper run: | - helm package deploy/helm/ --version ${{ env.VERSION }} --app-version ${{ env.VERSION }} - helm push inference-stack-${{ env.VERSION }}.tgz oci://${{ env.REGISTRY }}/charts + docker build \ + -f deploy/Containerfile.whisper \ + -t ${{ env.REGISTRY }}/whisper:${MODEL_TAG} \ + -t ${{ env.REGISTRY }}/whisper:latest \ + . + docker push ${{ env.REGISTRY }}/whisper:${MODEL_TAG} + docker push ${{ env.REGISTRY }}/whisper:latest + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Sign image (keyless via OIDC, by digest) + run: scripts/sign-image.sh "${{ env.REGISTRY }}/whisper:${MODEL_TAG}" + + - name: Publish release + uses: layer87-labs/relctl-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + relctl-version: ${{ env.RELCTL_TOOL_VERSION }} + run: relctl release publish --component whisper --release-id ${{ needs.whisper-release.outputs.release-id }} + + # ── chart (OCI → ghcr.io) ───────────────────────────────────────────────── + # Runs after router and tei so that the chart pins the versions just released. + chart-release: + needs: [changes, router-build, tei-build] + if: >- + ${{ !cancelled() && needs.changes.result == 'success' + && !contains(needs.*.result, 'failure') + && (needs.changes.outputs.chart == 'true' + || needs.router-build.result == 'success' + || needs.tei-build.result == 'success') }} + uses: ./.github/workflows/component-release.yml + with: + component: chart + + chart-build: + needs: chart-release + if: needs.chart-release.outputs.release-id != '' + runs-on: ubuntu-latest + env: + VERSION: ${{ needs.chart-release.outputs.version }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # latest router and tei versions come from their tags + + - name: Install relctl + uses: layer87-labs/relctl-action@v1 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + relctl-version: ${{ env.RELCTL_TOOL_VERSION }} + + - name: Latest router version + id: router + run: relctl release changed --component router + - name: Latest tei version + id: tei + run: relctl release changed --component tei - # ── Publish the GitHub Release ────────────────────────────────────── + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # router, tei-runtime and tei-base are versioned separately now, so the + # chart cannot default their tags to its appVersion any more: the packaged + # values pin the latest released versions. The source values stay unchanged. + - name: Package and push Helm chart + env: + ROUTER_VERSION: ${{ steps.router.outputs.RELCTL_LATEST_VERSION }} + TEI_IMAGE_VERSION: ${{ steps.tei.outputs.RELCTL_LATEST_VERSION }} + run: | + if [ -z "${ROUTER_VERSION}" ] || [ -z "${TEI_IMAGE_VERSION}" ]; then + echo "::error::no released router or tei version found; release both components first (manual run with components=router,tei)" + exit 1 + fi + cp -r deploy/helm "${RUNNER_TEMP}/chart" + yq -i '.router.image.tag = strenv(ROUTER_VERSION) + | .embedding.image.tag = strenv(TEI_IMAGE_VERSION) + | .reranker.image.tag = strenv(TEI_IMAGE_VERSION)' "${RUNNER_TEMP}/chart/values.yaml" + helm package "${RUNNER_TEMP}/chart" --version "${VERSION}" --app-version "${ROUTER_VERSION}" --destination "${RUNNER_TEMP}" + helm push "${RUNNER_TEMP}/inference-stack-${VERSION}.tgz" oci://${{ env.REGISTRY }}/charts - name: Publish release uses: layer87-labs/relctl-action@v1 with: github-token: ${{ secrets.GITHUB_TOKEN }} - run: relctl release publish --release-id ${{ needs.create-release.outputs.release-id }} + relctl-version: ${{ env.RELCTL_TOOL_VERSION }} + run: relctl release publish --component chart --release-id ${{ needs.chart-release.outputs.release-id }} diff --git a/.relctl.yaml b/.relctl.yaml index 23b7319..a692a96 100644 --- a/.relctl.yaml +++ b/.relctl.yaml @@ -1,2 +1,46 @@ version_scheme: calver default_branch: main + +# Independently released parts of the repository. Every component has its own +# tag (/YYYYMM.D.N), its own latest release and its own CalVer counter, +# and is built and released only when one of its paths changed. +components: + router: + paths: + - "cmd/**" + - "internal/**" + - "go.mod" + - "go.sum" + - "deploy/Containerfile.router" + version_scheme: calver + tei: + paths: + - "deploy/Containerfile.tei-base" + - "deploy/Containerfile.tei-runtime" + version_scheme: calver + embedding-model: + paths: + - "deploy/Containerfile.tei-model-init" + version_scheme: calver + reranker-model: + paths: + - "deploy/Containerfile.tei-reranker-model-init" + version_scheme: calver + whisper: + paths: + - "deploy/whisper/**" + - "deploy/Containerfile.whisper" + version_scheme: calver + # The chart pins the router and tei image tags when it is packaged, so it is + # also released when one of those components is. + chart: + paths: + - "deploy/helm/**" + - "cmd/**" + - "internal/**" + - "go.mod" + - "go.sum" + - "deploy/Containerfile.router" + - "deploy/Containerfile.tei-base" + - "deploy/Containerfile.tei-runtime" + version_scheme: calver diff --git a/AGENTS.md b/AGENTS.md index cd4f558..b521405 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,36 @@ deploy/ ## Container Image Versioning -All images use **CalVer** (`YYYYMM.DD.PATCH`). +Every independently releasable part is a [relctl](https://github.com/layer87-labs/relctl) **component** +(`.relctl.yaml`, `components:`). A component has its own tag (`/YYYYMM.D.N`), its own latest +release and its own CalVer counter, and it is built and released only when one of its `paths` changed +(`relctl release changed --component NAME`, see `.github/workflows/release.yml`). + +relctl's CalVer is `YYYYMM.D.N`: year and month, day of the month **without a leading zero**, and the number of +the release that day (for example `202611.3.1`). + +| Component | Paths (short) | Images / artifacts | Image tag | +|---|---|---|---| +| `router` | `cmd/`, `internal/`, `go.mod`, `Containerfile.router` | `inference-router` | `` | +| `tei` | `Containerfile.tei-base`, `Containerfile.tei-runtime` | `tei-base`, `tei-runtime` | `` | +| `embedding-model` | `Containerfile.tei-model-init` | `tei-model-init` | `bge-m3-`, `latest` | +| `reranker-model` | `Containerfile.tei-reranker-model-init` | `tei-reranker-model-init` (matrix of models) | `-`; `latest` for the default model only | +| `whisper` | `deploy/whisper/`, `Containerfile.whisper` | `whisper` | `-`, `latest` | +| `chart` | `deploy/helm/` and the router and tei paths | Helm chart (OCI) | chart version = `` | + +Notes: + +- The version is **bare** in image tags and chart versions (`202611.3.1`); only the git tag carries the + `/` prefix. +- The chart is released with every router or tei release because it pins their tags. At packaging time the + workflow writes the latest released router and tei versions into the packaged `values.yaml` (the file in git + keeps `tag: ""`) and sets `appVersion` to the router version. Model and whisper tags in `values.yaml` are + pinned by hand: bump them when you want the chart to use a newer model image. +- Pipelines guard publish/build jobs with `release-id != ''`: relctl exports the release id only for a draft. +- A component can be forced without a change: run the Release workflow manually and list it in `components`. +- Locally, `make versions` shows the image names; `VERSION` (or `ROUTER_VERSION`, `TEI_IMAGE_VERSION`, + `EMBEDDING_MODEL_VERSION`, `RERANKER_MODEL_VERSION`, `WHISPER_IMAGE_VERSION`) sets the tag. +- New releasable part: add it under `components:` in `.relctl.yaml` and to `release.yml`. Registry: `ghcr.io/layer87-labs/:` diff --git a/Makefile b/Makefile index f11c513..9fe3c81 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,19 @@ REGISTRY ?= ghcr.io/layer87-labs GO_VERSION ?= $(shell go env GOVERSION | sed 's/^go//') -VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null || echo "dev") +# Release tags are per component (router/202611.3.1); the image tag is the bare CalVer. +VERSION ?= $(shell git describe --tags --exact-match 2>/dev/null | sed 's|^.*/||' | grep . || echo "dev") +# Every component has its own version in CI (see .relctl.yaml). Locally they all default to VERSION. +ROUTER_VERSION ?= $(VERSION) +TEI_IMAGE_VERSION ?= $(VERSION) +EMBEDDING_MODEL_VERSION ?= $(VERSION) +RERANKER_MODEL_VERSION ?= $(VERSION) +WHISPER_IMAGE_VERSION ?= $(VERSION) COMMIT_HASH ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") BUILD_DATE ?= $(shell date +%FT%T%z) -MODEL_DATE ?= $(shell date +%Y.%m) GOFLAGS ?= -trimpath LDFLAGS ?= -s -w LDSYMS ?= \ - -X github.com/layer87-labs/inference-stack/internal/build.Version=$(VERSION) \ + -X github.com/layer87-labs/inference-stack/internal/build.Version=$(ROUTER_VERSION) \ -X github.com/layer87-labs/inference-stack/internal/build.CommitHash=$(COMMIT_HASH) \ -X github.com/layer87-labs/inference-stack/internal/build.BuildDate=$(BUILD_DATE) @@ -54,13 +60,13 @@ test-local: build test-containers: bash scripts/test-containers.sh -## ── Docker / Track A: CalVer ───────────────────────────────────────────────── +## ── Docker / Track A: router and TEI (CalVer per component) ───────────────────────────────────────────────── # inference-router — CalVer tag docker/router: docker build \ -f deploy/Containerfile.router \ - -t $(REGISTRY)/inference-router:$(VERSION) \ + -t $(REGISTRY)/inference-router:$(ROUTER_VERSION) \ . # tei-base — CalVer tag, embeds TEI_VERSION as build arg @@ -68,31 +74,31 @@ docker/tei-base: docker build \ -f deploy/Containerfile.tei-base \ --build-arg TEI_VERSION=$(TEI_VERSION) \ - -t $(REGISTRY)/tei-base:$(VERSION) \ + -t $(REGISTRY)/tei-base:$(TEI_IMAGE_VERSION) \ . ## ── Docker / Track B: TEI runtime ─────────────────────────────────────────── # tei-runtime — tag = CalVer -# Requires tei-base to be available at $(REGISTRY)/tei-base:$(VERSION) +# Requires tei-base to be available at $(REGISTRY)/tei-base:$(TEI_IMAGE_VERSION) docker/tei-runtime: docker build \ -f deploy/Containerfile.tei-runtime \ --build-arg REGISTRY=$(REGISTRY) \ - --build-arg BASE_TAG=$(VERSION) \ + --build-arg BASE_TAG=$(TEI_IMAGE_VERSION) \ --build-arg TEI_VERSION=$(TEI_VERSION) \ - -t $(REGISTRY)/tei-runtime:$(VERSION) \ + -t $(REGISTRY)/tei-runtime:$(TEI_IMAGE_VERSION) \ . ## ── Docker / Track C: Model init ──────────────────────────────────────────── -# bge-m3 init container — tag = CalVer +# bge-m3 init container — tag = bge-m3- docker/tei-model-init-embedding: docker build \ -f deploy/Containerfile.tei-model-init \ --build-arg MODEL_ID=BAAI/bge-m3 \ - --build-arg MODEL_DATE=$(MODEL_DATE) \ - -t $(REGISTRY)/tei-model-init:$(VERSION) \ + --build-arg MODEL_DATE=$(EMBEDDING_MODEL_VERSION) \ + -t $(REGISTRY)/tei-model-init:bge-m3-$(EMBEDDING_MODEL_VERSION) \ . ## ── Docker / Track E: Reranker (TEI/ONNX) ────────────────────────────────── @@ -100,23 +106,25 @@ docker/tei-model-init-embedding: # tei-reranker-model-init — reranker as ONNX for TEI's ORT backend. # Uses the model's own onnx/model.onnx if it ships one, else optimum-cli exports it. # Pick the model with RERANKER_MODEL_ID, e.g. RERANKER_MODEL_ID=BAAI/bge-reranker-v2-m3 +# RERANKER_SLUG is the model part of the image tag (-), as in the release workflow. RERANKER_MODEL_ID ?= cross-encoder/mmarco-mMiniLMv2-L12-H384-v1 +RERANKER_SLUG ?= mmarco-mminilmv2-l12-h384-v1 docker/tei-reranker-model-init: docker build \ -f deploy/Containerfile.tei-reranker-model-init \ --build-arg MODEL_ID=$(RERANKER_MODEL_ID) \ - --build-arg MODEL_DATE=$(MODEL_DATE) \ - -t $(REGISTRY)/tei-reranker-model-init:$(VERSION) \ + --build-arg MODEL_DATE=$(RERANKER_MODEL_VERSION) \ + -t $(REGISTRY)/tei-reranker-model-init:$(RERANKER_SLUG)-$(RERANKER_MODEL_VERSION) \ . ## ── Docker / Track D: Whisper ─────────────────────────────────────────────── -# whisper — tag = CalVer +# whisper — tag = - docker/whisper: docker build \ -f deploy/Containerfile.whisper \ --build-arg WHISPER_MODEL=$(WHISPER_MODEL) \ - -t $(REGISTRY)/whisper:$(VERSION) \ + -t $(REGISTRY)/whisper:$(WHISPER_MODEL)-$(WHISPER_IMAGE_VERSION) \ . ## ── Build all ───────────────────────────────────────────────────────────────── @@ -129,29 +137,29 @@ docker: docker/router docker/tei-base docker/tei-runtime \ ## ── Push ───────────────────────────────────────────────────────────────────── push/router: - docker push $(REGISTRY)/inference-router:$(VERSION) + docker push $(REGISTRY)/inference-router:$(ROUTER_VERSION) push/tei: - docker push $(REGISTRY)/tei-base:$(VERSION) - docker push $(REGISTRY)/tei-runtime:$(VERSION) - docker push $(REGISTRY)/tei-model-init:$(VERSION) + docker push $(REGISTRY)/tei-base:$(TEI_IMAGE_VERSION) + docker push $(REGISTRY)/tei-runtime:$(TEI_IMAGE_VERSION) + docker push $(REGISTRY)/tei-model-init:bge-m3-$(EMBEDDING_MODEL_VERSION) push/reranker: - docker push $(REGISTRY)/tei-reranker-model-init:$(VERSION) + docker push $(REGISTRY)/tei-reranker-model-init:$(RERANKER_SLUG)-$(RERANKER_MODEL_VERSION) push/whisper: - docker push $(REGISTRY)/whisper:$(VERSION) + docker push $(REGISTRY)/whisper:$(WHISPER_MODEL)-$(WHISPER_IMAGE_VERSION) push: push/router push/tei push/reranker push/whisper ## ── Info ────────────────────────────────────────────────────────────────────── versions: - @echo "All images use CalVer: $(VERSION)" + @echo "Each component has its own CalVer (default: $(VERSION)):" @echo "" - @echo " inference-router → $(REGISTRY)/inference-router:$(VERSION)" - @echo " tei-base → $(REGISTRY)/tei-base:$(VERSION)" - @echo " tei-runtime → $(REGISTRY)/tei-runtime:$(VERSION)" - @echo " tei-model-init → $(REGISTRY)/tei-model-init:$(VERSION)" - @echo " tei-reranker-model-init → $(REGISTRY)/tei-reranker-model-init:$(VERSION)" - @echo " whisper → $(REGISTRY)/whisper:$(VERSION)" + @echo " inference-router → $(REGISTRY)/inference-router:$(ROUTER_VERSION)" + @echo " tei-base → $(REGISTRY)/tei-base:$(TEI_IMAGE_VERSION)" + @echo " tei-runtime → $(REGISTRY)/tei-runtime:$(TEI_IMAGE_VERSION)" + @echo " tei-model-init → $(REGISTRY)/tei-model-init:bge-m3-$(EMBEDDING_MODEL_VERSION)" + @echo " tei-reranker-model-init → $(REGISTRY)/tei-reranker-model-init:$(RERANKER_SLUG)-$(RERANKER_MODEL_VERSION)" + @echo " whisper → $(REGISTRY)/whisper:$(WHISPER_MODEL)-$(WHISPER_IMAGE_VERSION)" diff --git a/README.md b/README.md index d7af4b9..bf115c8 100644 --- a/README.md +++ b/README.md @@ -147,9 +147,16 @@ make build make docker # Push to registry -make push REGISTRY=ghcr.io/layer87-labs VERSION=0.1.0 +make push REGISTRY=ghcr.io/layer87-labs VERSION=202611.3.1 ``` +## Releases + +The router, TEI images, embedding model, reranker models, Whisper and the Helm chart are released +independently as [relctl](https://github.com/layer87-labs/relctl) components with CalVer versions +(`YYYYMM.D.N`, tag `/`). Only the parts whose files changed are built on a merge to +`main`. See `AGENTS.md` for the component table and image tag formats. + ## Metrics Prometheus metrics on `:9090/metrics`: @@ -253,7 +260,7 @@ reranker: modelName: "BAAI/bge-reranker-v2-m3" modelInit: image: - tag: "bge-reranker-v2-m3-" # see the published tags + tag: "bge-reranker-v2-m3-" # e.g. bge-reranker-v2-m3-202611.3.1, see the published tags ``` The init step copies with `--ignore-existing`, so when you change the model on diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index b4b8109..c280595 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -29,7 +29,7 @@ router: image: repository: inference-router - tag: "" # defaults to .Chart.AppVersion + tag: "" # pinned to the latest release when the chart is packaged; else .Chart.AppVersion pullPolicy: IfNotPresent replicas: 1 @@ -92,15 +92,16 @@ embedding: # tei-runtime image — TEI embedding server (BGE-M3) image: repository: tei-runtime - tag: "" # defaults to .Chart.AppVersion + tag: "" # pinned to the latest release when the chart is packaged; else .Chart.AppVersion pullPolicy: IfNotPresent # Model init container — copies baked model to shared /model volume - # Model images are versioned independently: - + # Model images are versioned independently: - + # (older images use -; newer ones -) modelInit: image: repository: tei-model-init - tag: "bge-m3-2026.06" # model images have their own version scheme + tag: "bge-m3-2026.06" # pinned by hand; bump to use a newer model image pullPolicy: IfNotPresent securityContext: runAsNonRoot: true @@ -222,9 +223,9 @@ embedding: # Router rewrites /v1/rerank → /rerank before forwarding to this backend. # # The model is selected by the init image tag (the tag names the model): -# mmarco-mminilmv2-l12-h384-v1- 118M params, multilingual, ~0.04 s per +# mmarco-mminilmv2-l12-h384-v1- 118M params, multilingual, ~0.04 s per # document pair on 4 CPUs (default) -# bge-reranker-v2-m3- 568M params, higher capacity, ~0.4-0.5 s +# bge-reranker-v2-m3- 568M params, higher capacity, ~0.4-0.5 s # per pair on 4 CPUs; for more CPU or a GPU # To switch, set reranker.modelInit.image.tag and reranker.modelName, and delete # the old reranker model PVC (the init step never overwrites existing files). @@ -239,7 +240,7 @@ reranker: # tei-runtime image — same runtime as embedding, handles reranking via ONNX image: repository: tei-runtime - tag: "" # defaults to .Chart.AppVersion + tag: "" # pinned to the latest release when the chart is packaged; else .Chart.AppVersion pullPolicy: IfNotPresent # Display name of the model baked into modelInit (shown in NOTES.txt only; @@ -350,7 +351,7 @@ reranker: whisper: enabled: false - # Image has the model baked in — versioned as - + # Image has the model baked in — versioned as - image: repository: whisper tag: "large-v3-turbo-2026.10"