Skip to content
Merged
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
87 changes: 87 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Python CI (app_python)

on:
push:
branches: ["master"]
paths:
- "app_python/**"
- ".github/workflows/python-ci.yml"
pull_request:
paths:
- "app_python/**"
- ".github/workflows/python-ci.yml"

concurrency:
group: python-ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test-and-lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: app_python

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: |
app_python/requirements.txt
app_python/requirements-dev.txt

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt -r requirements-dev.txt

- name: Lint (ruff)
run: |
ruff check .

- name: Run tests (pytest)
run: |
pytest -q

- name: Install Snyk CLI
run: npm install -g snyk

- name: Snyk scan (dependencies)
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run: snyk test --file=requirements.txt --severity-threshold=high || true


docker-build-and-push:
runs-on: ubuntu-latest
needs: test-and-lint
if: github.event_name == 'push' && github.ref == 'refs/heads/master'

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Generate CalVer version
run: |
echo "VERSION=$(date -u +%Y.%m.%d)-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./app_python
file: ./app_python/Dockerfile
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ env.VERSION }}
${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:latest
14 changes: 14 additions & 0 deletions app_python/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# DevOps Info Service (FastAPI)

[![Python CI (app_python)](https://github.com/fayz131/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/fayzullin/DevOps-Core-Course/actions/workflows/python-ci.yml)


## Overview
DevOps Info Service is a web application that provides information about the running service and the system it is running on. The application is designed as a foundation for future DevOps labs, including containerization, CI/CD, and monitoring.

Expand Down Expand Up @@ -64,3 +67,14 @@ From Docker Hub
docker pull fayzullin/devops-info-service:lab2
docker run --rm -p 5000:5000 fayzullin/devops-info-service:lab2
```

## Testing

Install dev dependencies and run tests:

```bash
pip install -r requirements.txt -r requirements-dev.txt
pytest
```


83 changes: 83 additions & 0 deletions app_python/docs/LAB03.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Lab 3 — Continuous Integration (CI/CD)

## Overview

This lab introduces automated testing and CI/CD using GitHub Actions for the FastAPI DevOps Info Service.

The pipeline performs:
- Linting (ruff)
- Unit testing (pytest)
- Security scanning (Snyk)
- Docker image build and push to Docker Hub

## Testing Framework

**Framework used:** pytest

Pytest was chosen because:
- Simple and readable assertions
- Great integration with FastAPI
- Industry standard in modern Python projects

### Tests Implemented

- `GET /` — validates response structure and required fields
- `GET /health` — validates health check structure
- `404 handler` — validates JSON error response

### Run tests locally

```bash
pip install -r requirements.txt -r requirements-dev.txt
pytest
```

## CI Workflow

Workflow file:
.github/workflows/python-ci.yml

### Trigger Strategy

Workflow runs on:

* Pull requests affecting app_python/**
* Push to master affecting app_python/**

Path filters prevent unnecessary runs in monorepo.

### Versioning Strategy

Strategy: Calendar Versioning (CalVer)

Format:
YYYY.MM.DD-<run_number>

Docker tags created:

* fayzullin/devops-info-service:<version>

* fayzullin/devops-info-service:latest

This is suitable for continuously deployed services.

## CI Best Practices Applied

Fail fast — Docker build runs only if tests pass.

Dependency caching — pip cache speeds up builds.

Path filters — workflow runs only when app_python changes.

Concurrency control — cancels outdated runs.

## Security Scanning

Snyk is integrated to scan dependencies.
Build fails only on high severity vulnerabilities

## Evidence

GitHub Actions run: (add link after successful run)

Docker Hub: https://hub.docker.com/r/fayzullin/devops-info-service
4 changes: 4 additions & 0 deletions app_python/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pytest==8.3.3
httpx==0.27.2
ruff==0.7.2

2 changes: 1 addition & 1 deletion app_python/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
fastapi==0.115.0
fastapi==0.115.8
uvicorn[standard]==0.32.0
60 changes: 60 additions & 0 deletions app_python/tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from fastapi.testclient import TestClient

from app import app

client = TestClient(app)


def test_root_returns_required_structure():
response = client.get("/", headers={"User-Agent": "pytest"})
assert response.status_code == 200

data = response.json()

assert "service" in data
assert "system" in data
assert "runtime" in data
assert "request" in data
assert "endpoints" in data

service = data["service"]
assert service["name"] == "devops-info-service"
assert service["version"] == "1.0.0"
assert service["framework"] == "FastAPI"

system = data["system"]
for key in ["hostname", "platform", "platform_version", "architecture", "cpu_count", "python_version"]:
assert key in system

runtime = data["runtime"]
assert isinstance(runtime["uptime_seconds"], int)
assert runtime["uptime_seconds"] >= 0
assert isinstance(runtime["uptime_human"], str)
assert isinstance(runtime["current_time"], str)
assert runtime["timezone"] == "UTC"

req = data["request"]
assert req["method"] == "GET"
assert req["path"] == "/"
assert isinstance(req["user_agent"], (str, type(None)))


def test_health_endpoint():
response = client.get("/health")
assert response.status_code == 200

data = response.json()
assert data["status"] == "healthy"
assert isinstance(data["timestamp"], str)
assert isinstance(data["uptime_seconds"], int)
assert data["uptime_seconds"] >= 0


def test_404_returns_json():
response = client.get("/does-not-exist")
assert response.status_code == 404

data = response.json()
assert data["error"] == "Not Found"
assert "message" in data