Code
from __future__ import annotations
import datetime
import json
from pathlib import Path
import textwrap
# Project root
repo_root = Path.cwd().parent
from pathlib import Path, PurePosixPath
import json, datetime, textwrapWith packaging and inference in place, the final milestone is to turn the repository into a deployable artifact that anyone can spin up in one command. M10 delivers three things:
Once merged, main is effectively production‑ready and version‑controlled via semantic tags.
uvicorn src.app:app … launches the FastAPI service on port 8000, so the same image can run locally, in Docker Compose, or on Kubernetes without edits.Average build size on an M1 laptop is ~110 MB compressed — small enough for free‑tier registries and quick CI pipelines.
repo_root = Path.cwd().resolve().parents[0]
docker_path = repo_root / "Dockerfile"
dockerfile = f"""
# -------- build stage --------
FROM python:3.11-slim AS builder
WORKDIR /app
COPY . .
RUN pip install --upgrade pip && \\
pip install --no-cache-dir -r requirements.txt
# -------- runtime stage --------
FROM python:3.11-slim
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /app /app
EXPOSE 8000
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000"]
"""
docker_path.write_text(dockerfile.strip() + "\n", encoding="utf-8")
print(f"✓ Wrote {docker_path.relative_to(repo_root)}")✓ Wrote Dockerfile
The CI job performs five stages:
| Stage | Reason |
|---|---|
| Checkout | Pulls repo at the correct SHA. |
| Setup‑Python | Matches the Docker base image (3.11). |
| Install & Lint | Runs pre‑commit hooks to enforce Black + Ruff on all files. |
| PyTest | Ensures unit tests remain green after packaging changes. |
| Docker Build | Builds the image on every commit; pushes to GHCR only when building a release tag (v*.*.*). |
This keeps mainline commits fast, while tags trigger a full publish pipeline.
workflow_dir = repo_root / ".github" / "workflows"
workflow_dir.mkdir(parents=True, exist_ok=True)
workflow_path = workflow_dir / "ci.yml"
ci_yaml = r"""
name: CI
on:
push:
branches: [ main ]
tags: [ 'v*.*.*' ]
pull_request:
branches: [ main ]
jobs:
test-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install --upgrade pip
pip install -r requirements.txt
pre-commit run --all-files
pytest -q
- name: Build Docker image
run: docker build -t airline-sentiment:${{ github.sha }} .
- name: Push image (only on tag)
if: startsWith(github.ref, 'refs/tags/')
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker tag airline-sentiment:${{ github.sha }} ghcr.io/${{ github.repository }}/airline-sentiment:${{ github.ref_name }}
docker push ghcr.io/${{ github.repository }}/airline-sentiment:${{ github.ref_name }}
"""
workflow_path.write_text(ci_yaml.lstrip(), encoding="utf-8")
print(f"✓ Wrote {workflow_path.relative_to(repo_root)}")✓ Wrote .github\workflows\ci.yml
Instead of hand‑writing Markdown, the notebook pulls the latest reports/metrics_model_v1.json and interpolates key numbers:
docker pull … plus curl example.Placing the notes under docs/release_notes_v1.md means they render nicely in GitHub and can be reused in the Releases tab without extra formatting work.
metrics_path = repo_root / "reports" / "metrics_model_v1.json"
metrics = json.loads(metrics_path.read_text())
now = datetime.date.today().isoformat()
notes = textwrap.dedent(f"""
# v1.0.0 — {now}
**Highlights**
* End‑to‑end packaging: Dockerfile + FastAPI micro‑service.
* Automated CI/CD via GitHub Actions (lint → tests → image build → GHCR push).
* Model performance on held‑out test set
* Accuracy **{metrics['accuracy']:.3f}**
* Macro F1 **{metrics['f1_macro']:.3f}**
* Macro ROC AUC **{metrics['roc_auc_macro']:.3f}**
**Install & Run**
```bash
docker pull ghcr.io/justin-castillo/airline-sentiment:v1.0.0
docker run -p 8000:8000 airline-sentiment:v1.0.0
# → POST text to http://localhost:8000/predict
```
""").strip() + "\n"
rel_notes_path = repo_root / "docs" / "release_notes_v1.md"
rel_notes_path.write_text(notes, encoding="utf-8")
print(f"✓ Wrote {rel_notes_path.relative_to(repo_root)}")✓ Wrote docs\release_notes_v1.md