|
| 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 | |
0 commit comments