diff --git a/.github/workflows/ci-build-deploy.yml b/.github/workflows/ci-build-deploy.yml new file mode 100644 index 00000000..51606dcb --- /dev/null +++ b/.github/workflows/ci-build-deploy.yml @@ -0,0 +1,85 @@ +name: CI — Test, Build, Push, Deploy + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install test deps + run: python -m pip install --upgrade pip pytest + - name: Run tests + run: python -m pytest -q samples/book-app-project/tests + + build-and-push: + if: github.event_name == 'push' + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + steps: + - uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Log in to GHCR + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push image + uses: docker/build-push-action@v4 + with: + context: samples/book-app-project + file: samples/book-app-project/Dockerfile + push: true + tags: ghcr.io/${{ github.repository_owner }}/bookkeeper-app:latest + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/heads/main') }} + steps: + - uses: actions/checkout@v4 + - name: Check KUBE_CONFIG secret + run: | + if [ -z "${{ secrets.KUBE_CONFIG }}" ]; then + echo "KUBE_CONFIG secret is not configured; skipping deploy." + exit 0 + fi + - name: Install kubectl + if: ${{ secrets.KUBE_CONFIG != '' }} + uses: azure/setup-kubectl@v3 + with: + version: 'latest' + - name: Patch Job image to GHCR + if: ${{ secrets.KUBE_CONFIG != '' }} + run: | + sed -i "s|image: bookkeeper-app:latest|image: ghcr.io/${{ github.repository_owner }}/bookkeeper-app:latest|" samples/book-app-project/k8s/job-list.yaml + - name: Configure kubeconfig + if: ${{ secrets.KUBE_CONFIG != '' }} + run: echo "${{ secrets.KUBE_CONFIG }}" > kubeconfig + - name: Apply k8s manifests + if: ${{ secrets.KUBE_CONFIG != '' }} + run: | + kubectl --kubeconfig=kubeconfig apply -f samples/book-app-project/k8s/configmap-data.yaml + kubectl --kubeconfig=kubeconfig apply -f samples/book-app-project/k8s/job-list.yaml + - name: Wait for job completion + if: ${{ secrets.KUBE_CONFIG != '' }} + run: kubectl --kubeconfig=kubeconfig wait --for=condition=complete job/bookkeeper-list-job --timeout=120s || true + - name: Fetch job logs + if: ${{ secrets.KUBE_CONFIG != '' }} + run: kubectl --kubeconfig=kubeconfig logs job/bookkeeper-list-job || true diff --git a/.github/workflows/pr-preview-build.yml b/.github/workflows/pr-preview-build.yml new file mode 100644 index 00000000..0c96a240 --- /dev/null +++ b/.github/workflows/pr-preview-build.yml @@ -0,0 +1,33 @@ +name: PR Preview Build + +on: + pull_request: + branches: [ main ] + +jobs: + build-preview-image: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.10' + - name: Install test deps + run: python -m pip install --upgrade pip pytest + - name: Run tests + run: python -m pytest -q samples/book-app-project/tests + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: Build Docker preview image + uses: docker/build-push-action@v4 + with: + context: samples/book-app-project + file: samples/book-app-project/Dockerfile + load: true + push: false + tags: bookkeeper-app:pr-${{ github.event.pull_request.number }} diff --git a/samples/book-app-project/Dockerfile b/samples/book-app-project/Dockerfile new file mode 100644 index 00000000..b008ae7f --- /dev/null +++ b/samples/book-app-project/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.10-slim + +WORKDIR /app + +# Install test tool (no runtime dependencies required for the app itself) +RUN pip install --no-cache-dir pytest + +# Copy application files +COPY . /app + +# Default to showing help when container runs without args +CMD ["python", "book_app.py", "help"] diff --git a/samples/book-app-project/README.deploy.md b/samples/book-app-project/README.deploy.md new file mode 100644 index 00000000..d2ba8534 --- /dev/null +++ b/samples/book-app-project/README.deploy.md @@ -0,0 +1,27 @@ +Deployment instructions for the sample book app + +Build locally with Docker: + +```bash +cd samples/book-app-project +docker build -t bookkeeper-app:local . +``` + +Run the container (interactive CLI): + +```bash +docker run --rm -it -v "$(pwd)/data.json:/app/data.json" bookkeeper-app:local python book_app.py list +``` + +Or use docker-compose for an interactive shell session: + +```bash +cd samples/book-app-project +docker-compose run --rm bookkeeper-app list +``` + +Run tests inside the image: + +```bash +docker run --rm bookkeeper-app:local pytest -q +``` diff --git a/samples/book-app-project/docker-compose.yml b/samples/book-app-project/docker-compose.yml new file mode 100644 index 00000000..50fd12a3 --- /dev/null +++ b/samples/book-app-project/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.8" + +services: + bookkeeper-app: + build: . + image: bookkeeper-app:local + volumes: + - ./data.json:/app/data.json + stdin_open: true + tty: true + entrypoint: ["python", "book_app.py"] diff --git a/samples/book-app-project/k8s/README.md b/samples/book-app-project/k8s/README.md new file mode 100644 index 00000000..bb3bd886 --- /dev/null +++ b/samples/book-app-project/k8s/README.md @@ -0,0 +1,26 @@ +Kubernetes deployment for the Bookkeeper sample + +This folder contains a `ConfigMap` with `data.json` and a `Job` manifest that runs the CLI command `python book_app.py list`. + +Usage (build and push image to a registry first): + +```bash +# Build and tag +cd samples/book-app-project +docker build -t ghcr.io//bookkeeper-app:latest . + +# Push (example: GitHub Container Registry) +docker push ghcr.io//bookkeeper-app:latest + +# Apply k8s resources +kubectl apply -f k8s/configmap-data.yaml +kubectl apply -f k8s/job-list.yaml + +# Watch job +kubectl get jobs +kubectl logs job/bookkeeper-list-job +``` + +Notes: +- The sample app is a CLI, not a long-running server. A `Job` is used to execute a one-time command. +- If you use a local cluster (kind/minikube) you may need to load the local image into the cluster instead of pushing to a registry. diff --git a/samples/book-app-project/k8s/configmap-data.yaml b/samples/book-app-project/k8s/configmap-data.yaml new file mode 100644 index 00000000..62dc6cfe --- /dev/null +++ b/samples/book-app-project/k8s/configmap-data.yaml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: book-data +data: + data.json: |- + [ + { + "title": "The Hobbit", + "author": "J.R.R. Tolkien", + "year": 1937, + "read": false + }, + { + "title": "1984", + "author": "George Orwell", + "year": 1949, + "read": true + }, + { + "title": "Dune", + "author": "Frank Herbert", + "year": 1965, + "read": false + }, + { + "title": "To Kill a Mockingbird", + "author": "Harper Lee", + "year": 1960, + "read": false + }, + { + "title": "Mysterious Book", + "author": "", + "year": 0, + "read": false + } + ] diff --git a/samples/book-app-project/k8s/job-list.yaml b/samples/book-app-project/k8s/job-list.yaml new file mode 100644 index 00000000..95b2ca10 --- /dev/null +++ b/samples/book-app-project/k8s/job-list.yaml @@ -0,0 +1,27 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: bookkeeper-list-job + labels: + app: bookkeeper +spec: + template: + metadata: + labels: + app: bookkeeper + spec: + containers: + - name: bookkeeper + image: bookkeeper-app:latest + imagePullPolicy: IfNotPresent + command: ["python", "book_app.py", "list"] + volumeMounts: + - name: data-json + mountPath: /app/data.json + subPath: data.json + restartPolicy: Never + volumes: + - name: data-json + configMap: + name: book-data + backoffLimit: 2