With 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:

  1. Docker Image – guarantees the same runtime everywhere.
  2. GitHub Actions CI/CD – lint → tests → image build → optional registry push.
  3. Release Notes – human‑readable changelog attached to the v1.0.0 tag.

Once merged, main is effectively production‑ready and version‑controlled via semantic tags.

Notebook Overview

  1. Writing The Dockerfile
  2. Authoring The GitHub Actions Workflow
  3. Generating Release Notes Programmatically
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, textwrap

1. Writing The Dockerfile


  • Multi‑Stage Build – isolates dependency installation from the final runtime layer, keeping the image slim.
  • Python 3.11‑Slim Base – minimal footprint while still receiving security updates.
  • Entrypointuvicorn 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.

Code
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

2. Authoring The GitHub Actions Workflow


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.

Code
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

3. Generating Release Notes Programmatically


Instead of hand‑writing Markdown, the notebook pulls the latest reports/metrics_model_v1.json and interpolates key numbers:

  • Accuracy, Macro F1, Macro ROC AUC – headline metrics recruiters care about.
  • Install & Run section – one‑liner 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.

Code
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