Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/ci-build-deploy.yml
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions .github/workflows/pr-preview-build.yml
Original file line number Diff line number Diff line change
@@ -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 }}
12 changes: 12 additions & 0 deletions samples/book-app-project/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
27 changes: 27 additions & 0 deletions samples/book-app-project/README.deploy.md
Original file line number Diff line number Diff line change
@@ -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
```
11 changes: 11 additions & 0 deletions samples/book-app-project/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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"]
26 changes: 26 additions & 0 deletions samples/book-app-project/k8s/README.md
Original file line number Diff line number Diff line change
@@ -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/<your-user>/bookkeeper-app:latest .

# Push (example: GitHub Container Registry)
docker push ghcr.io/<your-user>/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.
38 changes: 38 additions & 0 deletions samples/book-app-project/k8s/configmap-data.yaml
Original file line number Diff line number Diff line change
@@ -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
}
]
27 changes: 27 additions & 0 deletions samples/book-app-project/k8s/job-list.yaml
Original file line number Diff line number Diff line change
@@ -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