Skip to content

Commit d338ff7

Browse files
committed
feat(lab03): add CI and badge
1 parent c9cc0f4 commit d338ff7

5 files changed

Lines changed: 138 additions & 2 deletions

File tree

.github/workflows/python-ci.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Python CI (Lab03)
2+
3+
on:
4+
push:
5+
branches: [ "main", "master", "lab3", "lab03" ]
6+
paths:
7+
- "app_python/**"
8+
- ".github/workflows/python-ci.yml"
9+
pull_request:
10+
paths:
11+
- "app_python/**"
12+
- ".github/workflows/python-ci.yml"
13+
14+
concurrency:
15+
group: python-ci-${{ github.ref }}
16+
cancel-in-progress: true
17+
18+
jobs:
19+
test-lint:
20+
runs-on: ubuntu-latest
21+
defaults:
22+
run:
23+
working-directory: app_python
24+
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Set up Python
29+
uses: actions/setup-python@v5
30+
with:
31+
python-version: "3.11"
32+
cache: "pip"
33+
cache-dependency-path: "app_python/requirements.txt"
34+
35+
- name: Install deps
36+
run: |
37+
python -m pip install --upgrade pip
38+
pip install -r requirements.txt
39+
40+
- name: Lint (ruff)
41+
run: ruff check .
42+
43+
- name: Tests
44+
run: pytest -q
45+
46+
docker-build-push:
47+
needs: test-lint
48+
runs-on: ubuntu-latest
49+
if: github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'master' || github.ref_name == 'lab3' || github.ref_name == 'lab03')
50+
51+
steps:
52+
- uses: actions/checkout@v4
53+
54+
- name: Set version (CalVer)
55+
run: |
56+
echo "CALVER=$(date +%Y.%m)" >> $GITHUB_ENV
57+
echo "BUILD=${{ github.run_number }}" >> $GITHUB_ENV
58+
59+
- name: Login to Docker Hub
60+
uses: docker/login-action@v3
61+
with:
62+
username: ${{ secrets.DOCKER_USERNAME }}
63+
password: ${{ secrets.DOCKER_PASSWORD }}
64+
65+
- name: Build & Push
66+
uses: docker/build-push-action@v6
67+
with:
68+
context: ./app_python
69+
file: ./app_python/Dockerfile
70+
push: true
71+
tags: |
72+
${{ secrets.DOCKER_USERNAME }}/devops-info-service:${{ env.CALVER }}.${{ env.BUILD }}
73+
${{ secrets.DOCKER_USERNAME }}/devops-info-service:latest

app_python/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
# DevOps Info Service (Lab 01)
1+
![CI](https://github.com/ostxxp/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)
2+
3+
# DevOps Info Service (Labs 01–03)
24

35
## Overview
46
Simple web service that returns service, system, runtime and request information.

app_python/requirements.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
Flask==3.1.0
1+
Flask==3.1.0
2+
pytest
3+
pytest-cov
4+
ruff

app_python/tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import os
2+
import sys
3+
4+
# add app_python/ to import path so "import app" works
5+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

app_python/tests/test_api.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import pytest
2+
from app import app
3+
4+
5+
@pytest.fixture
6+
def client():
7+
app.config["TESTING"] = True
8+
with app.test_client() as c:
9+
yield c
10+
11+
12+
def test_root_ok_and_structure(client):
13+
r = client.get("/")
14+
assert r.status_code == 200
15+
16+
data = r.get_json()
17+
assert isinstance(data, dict)
18+
19+
# service
20+
assert "service" in data
21+
assert data["service"]["framework"] == "Flask"
22+
23+
# system
24+
assert "system" in data
25+
assert "hostname" in data["system"]
26+
assert "python_version" in data["system"]
27+
28+
# runtime
29+
assert "runtime" in data
30+
assert "uptime_seconds" in data["runtime"]
31+
assert "current_time" in data["runtime"]
32+
33+
# endpoints
34+
assert "endpoints" in data
35+
assert isinstance(data["endpoints"], list)
36+
37+
38+
def test_health_ok_and_structure(client):
39+
r = client.get("/health")
40+
assert r.status_code == 200
41+
42+
data = r.get_json()
43+
assert isinstance(data, dict)
44+
assert data["status"] == "healthy"
45+
assert "timestamp" in data
46+
assert "uptime_seconds" in data
47+
48+
49+
def test_404_json(client):
50+
r = client.get("/nope")
51+
assert r.status_code == 404
52+
data = r.get_json()
53+
assert data["error"] == "Not Found"

0 commit comments

Comments
 (0)