Skip to content

Commit 297494b

Browse files
committed
feat(pipeline)!: validate and promote the exact tuned candidate
1 parent 1d09f09 commit 297494b

51 files changed

Lines changed: 1384 additions & 131884 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/cd.yml‎

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,24 @@ on:
66
permissions:
77
contents: read
88
jobs:
9+
checks:
10+
runs-on: ubuntu-24.04
11+
timeout-minutes: 20
12+
steps:
13+
- name: Checkout repository
14+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
15+
with:
16+
persist-credentials: false
17+
- name: Install mise toolchain
18+
uses: jdx/mise-action@08ae2a091c038e1e05ec6f5115dc5c69d5a2a6b8 # v5.1.2
19+
with:
20+
cache: false
21+
- name: Run release gate
22+
run: mise run all
23+
- name: Verify no generated changes
24+
run: test -z "$(git status --porcelain)" || { git status --porcelain; git diff; exit 1; }
925
pages:
26+
needs: checks
1027
runs-on: ubuntu-24.04
1128
timeout-minutes: 20
1229
permissions:
@@ -18,54 +35,67 @@ jobs:
1835
url: ${{ steps.deployment.outputs.page_url }}
1936
steps:
2037
- name: Checkout repository
21-
uses: actions/checkout@v7
38+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2239
with:
2340
persist-credentials: false # no step pushes back to the repository
2441
- name: Install mise toolchain
25-
uses: jdx/mise-action@v5
42+
uses: jdx/mise-action@08ae2a091c038e1e05ec6f5115dc5c69d5a2a6b8 # v5.1.2
2643
with:
2744
cache: false # a poisoned tool cache must never reach published artifacts
2845
- name: Build API documentation
2946
run: mise run docs
3047
- name: Configure Pages
31-
uses: actions/configure-pages@v6
48+
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
3249
- name: Upload Pages artifact
33-
uses: actions/upload-pages-artifact@v5
50+
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
3451
with:
3552
path: docs/
3653
- name: Deploy to GitHub Pages
3754
id: deployment
38-
uses: actions/deploy-pages@v5
55+
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
3956
image:
57+
needs: checks
4058
runs-on: ubuntu-24.04
4159
timeout-minutes: 30
4260
permissions:
4361
contents: read
4462
packages: write
4563
steps:
4664
- name: Checkout repository
47-
uses: actions/checkout@v7
65+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
4866
with:
4967
persist-credentials: false # the registry login below carries its own token
68+
- name: Install mise toolchain
69+
uses: jdx/mise-action@08ae2a091c038e1e05ec6f5115dc5c69d5a2a6b8 # v5.1.2
70+
with:
71+
cache: false
5072
- name: Set lower-case image path
5173
# GitHub expands `${{ }}` before the shell runs, so repository references reach
5274
# the script through `env:` rather than being interpolated into the command.
5375
env:
5476
REPOSITORY: ${{ github.repository }}
5577
run: echo "IMAGE=$(echo "ghcr.io/$REPOSITORY" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
5678
- name: Log in to GitHub Container Registry
57-
uses: docker/login-action@v4
79+
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
5880
with:
5981
registry: ghcr.io
6082
username: ${{ github.actor }}
6183
password: ${{ secrets.GITHUB_TOKEN }}
6284
- name: Set up Docker Buildx
63-
uses: docker/setup-buildx-action@v4
64-
- name: Build and push image
65-
uses: docker/build-push-action@v7
85+
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
86+
- name: Build and push candidate image
87+
id: build
88+
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
6689
with:
6790
context: .
6891
push: true
69-
tags: |
70-
${{ env.IMAGE }}:latest
71-
${{ env.IMAGE }}:${{ github.ref_name }}
92+
tags: ${{ env.IMAGE }}:candidate-${{ github.sha }}
93+
- name: Scan candidate image
94+
env:
95+
IMAGE_REF: ${{ env.IMAGE }}@${{ steps.build.outputs.digest }}
96+
run: mise exec -- trivy --config trivy.yaml image --skip-dirs '' --platform linux/amd64 "$IMAGE_REF"
97+
- name: Promote verified digest
98+
env:
99+
IMAGE_REF: ${{ env.IMAGE }}@${{ steps.build.outputs.digest }}
100+
VERSION: ${{ github.ref_name }}
101+
run: docker buildx imagetools create --tag "$IMAGE:$VERSION" --tag "$IMAGE:latest" "$IMAGE_REF"

‎.github/workflows/ci.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ jobs:
1515
timeout-minutes: 20
1616
steps:
1717
- name: Checkout repository
18-
uses: actions/checkout@v7
18+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
1919
with:
2020
persist-credentials: false # no step pushes back to the repository
2121
- name: Install mise toolchain
22-
uses: jdx/mise-action@v5
22+
uses: jdx/mise-action@08ae2a091c038e1e05ec6f5115dc5c69d5a2a6b8 # v5.1.2
2323
with:
2424
cache: true
2525
- name: Run canonical gate

‎.github/workflows/security.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ jobs:
1919
timeout-minutes: 30
2020
steps:
2121
- name: Checkout complete history
22-
uses: actions/checkout@v7
22+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
2323
with:
2424
fetch-depth: 0
2525
persist-credentials: false # scanners only read the checkout
2626
- name: Install mise toolchain
27-
uses: jdx/mise-action@v5
27+
uses: jdx/mise-action@08ae2a091c038e1e05ec6f5115dc5c69d5a2a6b8 # v5.1.2
2828
- name: Scan complete Git history
2929
run: gitleaks git --redact=100 --verbose
3030
- name: Scan full checkout

‎.github/zizmor.yml‎

Lines changed: 0 additions & 10 deletions
This file was deleted.

‎AGENTS.md‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Context and rules for AI agents working in this repository. Humans should start
44

55
## Project overview
66

7-
- **Name**: bikes — a production-shaped MLOps package that predicts the number of bikes available.
7+
- **Name**: bikes — a production-shaped MLOps package that predicts hourly bike rental demand.
88
- **Description**: reference implementation for the [MLOps Coding Course](https://mlops-coding-course.fmind.dev), generated from [cookiecutter-mlops-package](https://github.com/fmind/cookiecutter-mlops-package).
99
- **Language**: Python 3.14+ (`pyproject.toml`), managed with `uv`.
1010
- **Stack**: MLflow (tracking, registry, projects, evaluation), scikit-learn, pandas, Pydantic + Pandera validation, OmegaConf YAML configs, loguru.
@@ -21,7 +21,12 @@ All work goes through `mise` (see `mise.toml`); git hooks (`lefthook.yml`) and C
2121
- Test: `mise run test` — `pytest` with coverage (fails under 100%); `mise run test:parallel` is a faster, coverage-free local loop.
2222
- Build: `mise run build` — `uv build` (wheel + sdist); `mise run build:image` builds the Docker image.
2323
- Docs: `mise run docs` — `pdoc` API reference into `docs/`.
24-
- MLflow jobs: `mise run project` runs every job; `mise run project:run <name>` runs one (`confs/<name>.yaml`).
24+
- MLflow jobs: `mise run project` runs the connected `PipelineJob` (tune → train → evaluate → promote → infer → explain); `mise run project:run <name>` runs one (`confs/<name>.yaml`).
25+
26+
## Pipeline invariants
27+
28+
- Training uses the full tuning model configuration plus validated selected parameters. Evaluation, promotion and downstream consumers use the exact registered candidate version. Failed evaluation preserves the champion alias. Standalone promotion requires a positive explicit version.
29+
- Input datasets and signatures exclude `casual` and `registered`; raw counts remain in `data/hour.csv` for analysis. Retrain historical registered models to adopt the new serving signature.
2530

2631
## Definition of done
2732

@@ -38,9 +43,9 @@ A change is complete only when, locally, `mise run format` is clean, `mise run c
3843

3944
## Repository layout
4045

41-
- `src/bikes/` — package: `core/` (metrics, models, schemas), `io/` (configs, datasets, registries, services), `jobs/` (tuning, training, promotion, inference, evaluations, explanations), `utils/` (searchers, signers, splitters), plus `settings.py`, `scripts.py`, `__main__.py`.
46+
- `src/bikes/` — package: `core/` (metrics, models, schemas), `io/` (configs, datasets, registries, services), `jobs/` (pipeline, tuning, training, promotion, inference, evaluations, explanations), `utils/` (searchers, signers, splitters), plus `settings.py`, `scripts.py`, `__main__.py`.
4247
- `confs/` — one OmegaConf YAML per MLflow job; `tests/` — `pytest` suite mirroring `src/` with fixtures in `conftest.py`.
4348
- `data/` — bike sharing datasets (Parquet splits and source CSV); `notebooks/` — exploration notebooks (plotly comes from the `notebook` dependency group); `.env.example` — MLflow URIs to copy into `.env`, which `mise.toml` sources.
4449
- `pyproject.toml` — dependencies and `ruff`/`ty`/`pytest` config; `mise.toml`/`mise.lock` — tasks and pinned, locked tools; `lefthook.yml` — git hooks; `dprint.jsonc`/`trivy.yaml`/`cliff.toml` — formatter, scanner, changelog config.
45-
- `.github/` — `workflows/` (`ci.yml` runs `mise run all`, `cd.yml` publishes docs and the image, `security.yml` rescans the full history weekly), `dependabot.yml`, `zizmor.yml`, `rulesets/main.json`.
50+
- `.github/` — `workflows/` (`ci.yml` runs `mise run all`, `cd.yml` runs the gate before publishing docs and promotes a scanned image digest, `security.yml` rescans the full history weekly), `dependabot.yml`, `rulesets/main.json`.
4651
- `Dockerfile`/`docker-compose.yml`/`MLproject` — container image, local MLflow server, and MLflow Projects reproducible runs (`--env-manager=local` reuses the uv environment, so there is no `python_env.yaml`).

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to this project are documented in this file.
44

5+
## [7.0.0] - 2026-10-12
6+
7+
- **Breaking:** training and serving inputs exclude `casual` and `registered`. Retrain historical registered models before sending feature-only inputs.
8+
- **Breaking:** standalone promotion requires a positive explicit model version; it no longer selects the latest version implicitly.
9+
- Connect tuning, training, heldout evaluation, promotion, inference and explanations through one exact candidate version. Failed evaluation preserves the champion.
10+
- Validate strict schemas, row alignment and selected parameters; compute unsigned-target metrics safely.
11+
- Update locked tools and dependencies, documentation and notebooks. Gate documentation publication and scan immutable images before promoting release tags.
12+
513
## [6.1.0] - 2026-10-06
614

715
### 🚀 Features

‎Dockerfile‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# syntax=docker/dockerfile:1
22
# Multi-stage, non-root image built with uv (https://docs.astral.sh/uv/guides/integration/docker/).
33

4-
FROM python:3.14-slim AS build
4+
FROM python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 AS build
55
ENV UV_COMPILE_BYTECODE=1
66
ENV UV_LINK_MODE=copy
77
# Build the venv on the base image's interpreter: `.python-version` would otherwise make
@@ -10,7 +10,7 @@ ENV UV_PYTHON=/usr/local/bin/python3
1010
ENV UV_PYTHON_DOWNLOADS=never
1111
WORKDIR /app
1212
# Pinned, not `:latest` — a floating tag is invisible to Dependabot's docker ecosystem.
13-
COPY --from=ghcr.io/astral-sh/uv:0.12.23 /uv /uvx /bin/
13+
COPY --from=ghcr.io/astral-sh/uv:0.13.0 /uv /uvx /bin/
1414
RUN --mount=type=cache,target=/root/.cache/uv \
1515
--mount=type=bind,source=uv.lock,target=uv.lock \
1616
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
@@ -19,9 +19,13 @@ COPY . /app
1919
RUN --mount=type=cache,target=/root/.cache/uv \
2020
uv sync --frozen --no-dev --no-editable
2121

22-
FROM python:3.14-slim
22+
FROM python:3.14-slim@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170
2323
ENV PYTHONUNBUFFERED=1
2424
WORKDIR /app
25+
# The runtime already contains its dependencies; remove unused pip and its bundled
26+
# bootstrap wheel so their vendored packages do not add a second dependency stack.
27+
RUN PIP_ROOT_USER_ACTION=ignore python -m pip uninstall --yes pip \
28+
&& python -c "import ensurepip, pathlib, shutil; shutil.rmtree(pathlib.Path(ensurepip.__file__).parent)"
2529
# Fixed numeric uid/gid: stable file ownership across rebuilds and bind mounts, and
2630
# resolvable by a host that does not share this image's /etc/passwd.
2731
RUN groupadd -g 10001 app && useradd -u 10001 -g app -m app

0 commit comments

Comments
 (0)