MLOps Automation
Goal
To elevate the codebase to production standards by adding Task Automation (mise), Git Hooks (lefthook), Containerization (docker), CI/CD (github-actions), and Experiment Tracking (mlflow).
Prerequisites
- Language: Python 3.14
- Manager:
uv - Context: Preparing for scale and deployment.
Instructions
1. Task Automation
Expose a single, shared task vocabulary with mise (replaces just/make).
- Tool:
mise— pins the toolchain and defines tasks inmise.toml. - Vocabulary:
install,format,check,test,build,watch. Run everything viamise run <task>so hooks and CI reuse the same entrypoints. - Core Tasks:
format: Format code and config (ruff format,dprint fmt).check: Static checks in parallel (ruff check,ty,pip-audit,gitleaks,trivy,actionlint+zizmor).test: Runpytest.build: Build the wheel (uv build).
- The Gate: define
all = ["mise run format", "mise run check", "mise run test", "mise run build"]. This is the one command a developer, a hook, an agent, or CI runs; nothing else is allowed to define "ready". - Pinned Tools: declare every non-Python tool under
[tools], runmise lock, and commitmise.lock— it records the exact version, URL, and checksum per platform, souv.lockpins the libraries andmise.lockpins the binaries. - No Silent Installs: set
[settings.task] run_auto_install = falseso a task fails loudly on a missing tool instead of downloading one mid-run.
2. Git Hooks
Catch issues locally with lefthook (replaces pre-commit).
Framework:
lefthookwith thin hooks — every command delegates to amise runtask so hooks and CI stay identical.Explicit Priorities: lefthook orders commands alphabetically, so state the order yourself — formatters at 10, the staged secret scan at 20, the whole-tree checks at 30. Without priorities,
checkcan read files that the formatter has not rewritten yet.Staged Files: pass
{staged_files}to the formatters and setstage_fixed: true, so a reformat is restaged into the commit being made rather than left dirty in the working tree.Secret Gate: run
mise run check:leaks --stagedbefore the checks. A history scan does not look at the commit you are about to create;--stageddoes.pre-push: Run
mise run test.Example:
# lefthook.yml pre-commit: parallel: false commands: format:dprint: priority: 10 glob: "*.{json,md,toml,yaml,yml}" run: mise run format:dprint {staged_files} stage_fixed: true format:python: priority: 10 glob: "*.py" run: mise run format:python {staged_files} stage_fixed: true check:leaks: priority: 20 run: mise run check:leaks --staged check: priority: 30 run: mise run check pre-push: commands: test: run: mise run testCommits: Enforce Conventional Commits (e.g.,
feat: add new model) sogit-cliffcan generate the changelog.
3. Containerization
Reproducibility anywhere.
- Tool:
docker. - Base Image: Use
python:3.14-slimfor a minimal footprint; copy a pinneduvbinary into the build stage (ghcr.io/astral-sh/uv:0.12.3, never:latest— a floating tag is invisible to Dependabot). - Optimization:
- Layer Caching: Copy
uv.lock+pyproject.tomland runuv syncbefore copyingsrc/. - Multi-stage: Build inputs in one stage, copy only artifacts (the resolved
.venvordist/*.whl) to the runtime stage.
- Layer Caching: Copy
- Non-root: Run as a fixed numeric user (
USER 10001:10001) and copy with--chown=10001:10001, so ownership is stable across rebuilds and readable by a host that does not share the image's/etc/passwd. - Lint It: add
check:dockerfile = "hadolint Dockerfile"tocheck; the Dockerfile is code and deserves the same gate. - Registry: ask for the company artifact registry, or use
ghcr.iofor GitHub.
4. CI/CD Workflows
Automate verification and release with GitHub Actions.
Platform: ask for the company CI/CD platform, or use
github-actionsfor GitHub.One Step: CI runs
mise run alland nothing else. A workflow that listsformat, thencheck, thentestas separate steps will drift from the local gate; a workflow with one step cannot.Hardening: pin the runner (
ubuntu-24.04), settimeout-minutes, keeppermissions: contents: read, and check out withpersist-credentials: falseso no step inherits a push token it does not need.Prove Cleanliness: end with
test -z "$(git status --porcelain)". Becausemise run allstarts by formatting, a dirty tree at the end means a contributor committed unformatted files.Example:
# .github/workflows/ci.yml name: CI on: push: branches: [main] pull_request: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} jobs: check: runs-on: ubuntu-24.04 timeout-minutes: 20 steps: - name: Checkout repository uses: actions/checkout@v7 with: persist-credentials: false # no step pushes back to the repository - name: Install mise system uses: jdx/mise-action@v4 with: cache: true # reuse the tools pinned by mise.lock - name: Run canonical gate run: mise run all - name: Verify no changes run: test -z "$(git status --porcelain)"Audit the Workflows: add
check:actions = ["actionlint", "zizmor --offline .github/workflows/"]tocheck.actionlintcatches invalid syntax and shell bugs;zizmorcatches workflow security issues (script injection, over-broad permissions, credential persistence). Commit.github/zizmor.ymlto record the pinning policy the project actually follows, so the audit enforces it instead of fighting it.Scheduled Security Pass: push CI only scans recent commits, so add
.github/workflows/security.ymlon a weeklyschedule(plusworkflow_dispatch) that checks out withfetch-depth: 0and runs the full-historygitleaks gitand a fulltrivy fs .. Keepfetch-depth: 0out ofci.yml: a full clone on every push costs time and buys nothing the scheduled job does not already cover.Release:
cd.ymlon Release builds the image and publishes docs via the official GitHub Pages Actions (configure-pages,upload-pages-artifact,deploy-pages).Dependencies: add
.github/dependabot.ymlfor every ecosystem in use (uv,github-actions,docker), grouping minor and patch updates into one pull request per ecosystem so majors are still reviewed alone.
5. AI/ML Experiments & Registry
Manage the ML lifecycle with MLflow 3.15.
- Platform:
MLflow(3.15+). - Backend: use a SQL store, locally as well as in production.
sqlite:///mlflow.dbis a real SQLAlchemy backend that supports the model registry and shares its shape with a production Postgres, so promotion is aMLFLOW_TRACKING_URIchange rather than a rewrite. The file store is deprecated: it never supported the registry, andMLFLOW_ALLOW_FILE_STORE=trueis an escape hatch to migrate off, not a configuration to ship. - Local Server:
mlflow server --backend-store-uri=sqlite:///mlflow.db --artifacts-destination=./mlartifacts. Addmlflow.dbto.gitignore— it is local state, and committing it puts a binary database in the history. - Tracking: Use
mlflow.autolog(); log metrics, params, and artifacts. - Models: Log models with the keyword
name=(e.g.,mlflow.pyfunc.log_model(name=...)). - Validation: Gate promotion with
mlflow.validate_evaluation_resultsagainst explicit metric thresholds. - Registry:
- Register top models manually or via CI.
- Aliases: Use
@championor@productionfor stable deployment pointers. Never rely on moving versions (e.g.,v1->v2).
6. Design Patterns
Write flexible code.
- Strategy: For swappable algorithms (e.g., different model types).
- Factory: For creating objects from config (e.g.,
ModelFactory). - Adapter: For standardizing mismatched interfaces.
7. Write It Down
Automation only helps if the next contributor finds it.
AGENTS.md: the commands, the definition of done, the conventions, and the layout — for AI assistants.README.md: the same ground truth for humans, without the agent-facing rules.- Rule: when a task changes, both files change in the same commit. A stale command in
AGENTS.mdis worse than no command at all.
Self-Correction Checklist
- Task Vocabulary: Does
mise run all(andformat/check/test/build) work? - Pinning: Are
mise.lockanduv.lockcommitted? - Hooks: Do the hooks use explicit priorities,
{staged_files}, andcheck:leaks --staged? - Image: Is the Dockerfile multi-stage on
python:3.14-slim, non-root, with a pinneduv, and doeshadolintpass? - CI/CD: Is
ci.ymla singlemise run allstep withpersist-credentials: falseand the porcelain assertion? - Workflow Audit: Do
actionlintandzizmorpass, and doessecurity.ymlrescan the full history weekly? - Tracking: Do runs land in a SQL-backed MLflow store, with validated thresholds before promotion?