Skip to content

Commit 31761ff

Browse files
pepegapepega
authored andcommitted
feat: implement lab03 ci/cd pipeline
- Add pytest unit tests (15 tests covering all endpoints) - Add GitHub Actions workflow with matrix testing (Python 3.11, 3.12) - Add ruff linter integration - Add Docker build/push with CalVer versioning - Add status badge to README - Add LAB03.md documentation Best practices: - Dependency caching via setup-python - Docker layer caching via Buildx - Job dependencies (docker needs lint-test) - Fail-fast matrix strategy - Concurrency with cancel-in-progress - Path filters for monorepo efficiency
1 parent cb5bacf commit 31761ff

5 files changed

Lines changed: 515 additions & 0 deletions

File tree

.github/workflows/python-ci.yml

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# GitHub Actions CI/CD Pipeline for Python DevOps Info Service
2+
# Triggers: push/PR to master/lab03 branches (only for app_python changes)
3+
# Features: linting, testing, Docker build/push with CalVer versioning
4+
5+
name: Python CI
6+
7+
on:
8+
push:
9+
branches:
10+
- master
11+
- lab03
12+
paths:
13+
- "app_python/**"
14+
- ".github/workflows/python-ci.yml"
15+
pull_request:
16+
branches:
17+
- master
18+
paths:
19+
- "app_python/**"
20+
- ".github/workflows/python-ci.yml"
21+
22+
# Permissions: read-only for security
23+
permissions:
24+
contents: read
25+
26+
# Cancel previous runs on same branch
27+
concurrency:
28+
group: python-ci-${{ github.ref }}
29+
cancel-in-progress: true
30+
31+
env:
32+
PIP_DISABLE_PIP_VERSION_CHECK: "1"
33+
DOCKER_IMAGE: pepegx/devops-info-service
34+
35+
jobs:
36+
# ========================================
37+
# Job 1: Lint and Test (Matrix Build)
38+
# ========================================
39+
lint-test:
40+
name: Lint & Test (Python ${{ matrix.python-version }})
41+
runs-on: ubuntu-latest
42+
43+
strategy:
44+
fail-fast: true
45+
matrix:
46+
python-version: ["3.11", "3.12"]
47+
48+
defaults:
49+
run:
50+
working-directory: app_python
51+
52+
steps:
53+
- name: Checkout code
54+
uses: actions/checkout@v4
55+
56+
- name: Set up Python ${{ matrix.python-version }}
57+
uses: actions/setup-python@v5
58+
with:
59+
python-version: ${{ matrix.python-version }}
60+
cache: "pip"
61+
cache-dependency-path: app_python/requirements.txt
62+
63+
- name: Install dependencies
64+
run: |
65+
python -m pip install --upgrade pip
66+
python -m pip install -r requirements.txt
67+
68+
- name: Lint with ruff
69+
run: python -m ruff check .
70+
71+
- name: Run unit tests
72+
run: python -m pytest -v tests/
73+
74+
# ========================================
75+
# Job 2: Build and Push Docker Image
76+
# ========================================
77+
docker-build-push:
78+
name: Build & Push Docker Image
79+
runs-on: ubuntu-latest
80+
needs: lint-test
81+
# Only push on actual commits to master/lab03, not PRs
82+
if: github.event_name == 'push'
83+
84+
steps:
85+
- name: Checkout code
86+
uses: actions/checkout@v4
87+
88+
- name: Set up Docker Buildx
89+
uses: docker/setup-buildx-action@v3
90+
91+
- name: Log in to Docker Hub
92+
uses: docker/login-action@v3
93+
with:
94+
username: ${{ secrets.DOCKERHUB_USERNAME }}
95+
password: ${{ secrets.DOCKERHUB_TOKEN }}
96+
97+
- name: Generate CalVer version
98+
id: version
99+
run: |
100+
# CalVer format: YYYY.MM.BUILD_NUMBER
101+
CALVER=$(date +"%Y.%m")
102+
VERSION="${CALVER}.${{ github.run_number }}"
103+
echo "calver=${CALVER}" >> $GITHUB_OUTPUT
104+
echo "version=${VERSION}" >> $GITHUB_OUTPUT
105+
echo "Generated version: ${VERSION}"
106+
107+
- name: Build and push Docker image
108+
uses: docker/build-push-action@v6
109+
with:
110+
context: app_python
111+
file: app_python/Dockerfile
112+
push: true
113+
tags: |
114+
${{ env.DOCKER_IMAGE }}:${{ steps.version.outputs.version }}
115+
${{ env.DOCKER_IMAGE }}:${{ steps.version.outputs.calver }}
116+
${{ env.DOCKER_IMAGE }}:latest
117+
cache-from: type=gha
118+
cache-to: type=gha,mode=max
119+
labels: |
120+
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
121+
org.opencontainers.image.revision=${{ github.sha }}
122+
org.opencontainers.image.created=${{ github.event.head_commit.timestamp }}

app_python/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# DevOps Info Service
22

3+
[![Python CI](https://github.com/pepegx/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/pepegx/DevOps-Core-Course/actions/workflows/python-ci.yml)
4+
35
> A web service that provides comprehensive system and runtime information for DevOps monitoring and diagnostics.
46
57
## Overview
@@ -112,6 +114,37 @@ HOST=127.0.0.1 PORT=3000 DEBUG=true python app.py
112114
gunicorn -w 4 -b 0.0.0.0:5000 app:app
113115
```
114116

117+
## Testing
118+
119+
This project uses **pytest** with **pytest-flask** for testing.
120+
121+
### Run Tests Locally
122+
123+
```bash
124+
# From app_python directory
125+
python -m pytest -v tests/
126+
127+
# With coverage (if pytest-cov installed)
128+
python -m pytest --cov=. --cov-report=term tests/
129+
```
130+
131+
### Run Linter
132+
133+
```bash
134+
# Check for style issues
135+
python -m ruff check .
136+
137+
# Auto-fix issues
138+
python -m ruff check --fix .
139+
```
140+
141+
### Test Structure
142+
143+
- `tests/test_app.py` - Unit tests for all endpoints
144+
- `TestIndexEndpoint` - Tests for `GET /`
145+
- `TestHealthEndpoint` - Tests for `GET /health`
146+
- `TestErrorHandling` - Tests for 404 handler
147+
115148
## Docker
116149

117150
### Build Image (pattern)

app_python/docs/LAB03.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Lab 3 — CI/CD: Implementation Report
2+
3+
**Student:** Danil Fishchenko
4+
**Date:** January 31, 2026
5+
**App:** DevOps Info Service (Flask)
6+
7+
---
8+
9+
## 1. Overview
10+
11+
| Aspect | Decision |
12+
|--------|----------|
13+
| **Testing Framework** | `pytest` with `pytest-flask` |
14+
| **Linter** | `ruff` (fast, modern Python linter) |
15+
| **CI Trigger** | Push to `master`/`lab03`, PRs to `master` |
16+
| **Path Filter** | Only `app_python/**` changes trigger CI |
17+
| **Versioning** | CalVer (`YYYY.MM.BUILD`) |
18+
19+
### Why pytest?
20+
21+
- **Simple syntax:** No boilerplate, just functions with assertions
22+
- **Fixtures:** Reusable test setup with `@pytest.fixture`
23+
- **Plugin ecosystem:** `pytest-flask` provides test client out of the box
24+
- **Industry standard:** Most popular Python testing framework
25+
26+
### Why CalVer?
27+
28+
Calendar Versioning fits continuous delivery:
29+
- **Time-based:** Easy to understand release timeline
30+
- **No manual bumping:** Version auto-generated from date + build number
31+
- **Tags:** `2026.01.1`, `2026.01`, `latest`
32+
33+
---
34+
35+
## 2. Test Coverage
36+
37+
### Endpoints Tested
38+
39+
| Endpoint | Tests | What's Covered |
40+
|----------|-------|----------------|
41+
| `GET /` | 8 tests | Status code, JSON structure, service/system/runtime/request info |
42+
| `GET /health` | 4 tests | Status code, healthy status, required fields |
43+
| `404 Handler` | 3 tests | Status code, JSON error format |
44+
45+
### Test Classes
46+
47+
```
48+
tests/test_app.py
49+
├── TestIndexEndpoint (8 tests)
50+
│ ├── test_index_returns_200
51+
│ ├── test_index_returns_json
52+
│ ├── test_index_has_required_sections
53+
│ ├── test_index_service_info
54+
│ ├── test_index_system_info
55+
│ ├── test_index_runtime_info
56+
│ ├── test_index_request_info
57+
│ └── test_index_endpoints_list
58+
├── TestHealthEndpoint (4 tests)
59+
│ ├── test_health_returns_200
60+
│ ├── test_health_returns_json
61+
│ ├── test_health_status_healthy
62+
│ └── test_health_has_required_fields
63+
└── TestErrorHandling (3 tests)
64+
├── test_404_not_found
65+
├── test_404_returns_json
66+
└── test_404_error_structure
67+
```
68+
69+
**Total: 15 tests**
70+
71+
---
72+
73+
## 3. CI Workflow
74+
75+
### Workflow File
76+
77+
`.github/workflows/python-ci.yml`
78+
79+
### Jobs
80+
81+
1. **lint-test** (Matrix: Python 3.11, 3.12)
82+
- Checkout code
83+
- Setup Python with pip caching
84+
- Install dependencies
85+
- Run ruff linter
86+
- Run pytest
87+
88+
2. **docker-build-push** (depends on lint-test)
89+
- Only runs on push (not PRs)
90+
- Login to Docker Hub
91+
- Generate CalVer version
92+
- Build and push with Buildx
93+
- Tags: `version`, `calver`, `latest`
94+
95+
### Workflow Diagram
96+
97+
```
98+
push/PR → lint-test (3.11) ─┬─→ docker-build-push → Docker Hub
99+
lint-test (3.12) ─┘
100+
```
101+
102+
---
103+
104+
## 4. Best Practices Implemented
105+
106+
| Practice | Implementation | Benefit |
107+
|----------|----------------|---------|
108+
| **Matrix Testing** | Python 3.11 & 3.12 | Catches version-specific issues |
109+
| **Dependency Caching** | `actions/setup-python` with cache | Faster CI runs |
110+
| **Docker Layer Cache** | Buildx with `cache-from/to: gha` | Faster Docker builds |
111+
| **Job Dependencies** | `needs: lint-test` | Docker push only if tests pass |
112+
| **Fail Fast** | `fail-fast: true` | Stop on first failure |
113+
| **Concurrency** | `cancel-in-progress: true` | Cancels outdated runs |
114+
| **Least Privilege** | `permissions: contents: read` | Security hardening |
115+
| **Path Filters** | Only `app_python/**` triggers | No unnecessary CI runs |
116+
| **Working Directory** | `defaults.run.working-directory` | Cleaner step commands |
117+
118+
---
119+
120+
## 5. Workflow Evidence
121+
122+
### Local Tests
123+
124+
```
125+
$ python -m pytest -v tests/
126+
========================== test session starts ==========================
127+
collected 15 items
128+
129+
tests/test_app.py::TestIndexEndpoint::test_index_returns_200 PASSED
130+
tests/test_app.py::TestIndexEndpoint::test_index_returns_json PASSED
131+
tests/test_app.py::TestIndexEndpoint::test_index_has_required_sections PASSED
132+
tests/test_app.py::TestIndexEndpoint::test_index_service_info PASSED
133+
tests/test_app.py::TestIndexEndpoint::test_index_system_info PASSED
134+
tests/test_app.py::TestIndexEndpoint::test_index_runtime_info PASSED
135+
tests/test_app.py::TestIndexEndpoint::test_index_request_info PASSED
136+
tests/test_app.py::TestIndexEndpoint::test_index_endpoints_list PASSED
137+
tests/test_app.py::TestHealthEndpoint::test_health_returns_200 PASSED
138+
tests/test_app.py::TestHealthEndpoint::test_health_returns_json PASSED
139+
tests/test_app.py::TestHealthEndpoint::test_health_status_healthy PASSED
140+
tests/test_app.py::TestHealthEndpoint::test_health_has_required_fields PASSED
141+
tests/test_app.py::TestErrorHandling::test_404_not_found PASSED
142+
tests/test_app.py::TestErrorHandling::test_404_returns_json PASSED
143+
tests/test_app.py::TestErrorHandling::test_404_error_structure PASSED
144+
145+
=========================== 15 passed ===========================
146+
```
147+
148+
### Local Lint
149+
150+
```
151+
$ python -m ruff check .
152+
All checks passed!
153+
```
154+
155+
### Links
156+
157+
- **Workflow Runs:** https://github.com/pepegx/DevOps-Core-Course/actions/workflows/python-ci.yml
158+
- **Docker Hub:** https://hub.docker.com/r/pepegx/devops-info-service
159+
160+
---
161+
162+
## 6. Key Decisions
163+
164+
### Versioning Strategy
165+
166+
**Choice:** CalVer (`YYYY.MM.BUILD_NUMBER`)
167+
168+
**Reasoning:**
169+
- Continuous delivery model — releases are time-based
170+
- No manual version management needed
171+
- Easy to understand release timeline (January 2026, build #1)
172+
- Avoids semantic versioning debates for a service (not a library)
173+
174+
### Docker Tags
175+
176+
| Tag | Purpose |
177+
|-----|---------|
178+
| `2026.01.1` | Specific build (immutable) |
179+
| `2026.01` | Latest in month (rolling) |
180+
| `latest` | Most recent build |
181+
182+
### Workflow Triggers
183+
184+
- **Push to master/lab03:** Full CI + Docker push
185+
- **PR to master:** Lint + test only (no Docker push)
186+
- **Path filter:** Only `app_python/**` changes
187+
188+
### What's NOT Tested
189+
190+
- `if __name__ == '__main__'` block (entry point, not testable without subprocess)
191+
- Startup logs (side effects, low value)
192+
- Gunicorn integration (requires running server)
193+
194+
---
195+
196+
## 7. Challenges & Solutions
197+
198+
| Challenge | Solution |
199+
|-----------|----------|
200+
| Snyk action versioning issues | Removed Snyk (optional feature, requires token) |
201+
| Working directory in steps | Used `defaults.run.working-directory` |
202+
| Cache invalidation | Hash-based cache key from requirements.txt |

app_python/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ gunicorn==21.2.0
77
# Development and Testing
88
pytest==7.4.3
99
pytest-flask==1.3.0
10+
ruff==0.9.4

0 commit comments

Comments
 (0)