Skip to content

Commit b89c93c

Browse files
authored
Merge pull request #6 from imane-ait/feat/cicd
feat: add GitHub Actions CI/CD pipelines with lint, tests, security scan and Docker build
2 parents 75180ac + b6f724a commit b89c93c

13 files changed

Lines changed: 225 additions & 5 deletions

File tree

.github/workflows/cd.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: CD
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
release:
7+
types: [ published ]
8+
9+
jobs:
10+
deploy-dev:
11+
name: Deploy to Dev
12+
runs-on: ubuntu-latest
13+
if: github.ref == 'refs/heads/main'
14+
environment: dev
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Configure AWS credentials
20+
uses: aws-actions/configure-aws-credentials@v4
21+
with:
22+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
23+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
24+
aws-region: us-east-1
25+
26+
- name: Update kubeconfig
27+
run: |
28+
aws eks update-kubeconfig \
29+
--name fleetops-dev \
30+
--region us-east-1
31+
32+
- name: Deploy with Helm
33+
run: |
34+
helm upgrade --install fleetops ./helm/fleetops \
35+
-f helm/fleetops/values-dev.yaml \
36+
--set image.tag=${{ github.sha }} \
37+
--wait \
38+
--timeout 5m
39+
40+
deploy-prod:
41+
name: Deploy to Prod
42+
runs-on: ubuntu-latest
43+
if: github.event_name == 'release'
44+
environment: prod
45+
46+
steps:
47+
- uses: actions/checkout@v4
48+
49+
- name: Configure AWS credentials
50+
uses: aws-actions/configure-aws-credentials@v4
51+
with:
52+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
53+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
54+
aws-region: us-east-1
55+
56+
- name: Update kubeconfig
57+
run: |
58+
aws eks update-kubeconfig \
59+
--name fleetops-prod \
60+
--region us-east-1
61+
62+
- name: Deploy with Helm
63+
run: |
64+
helm upgrade --install fleetops ./helm/fleetops \
65+
-f helm/fleetops/values-prod.yaml \
66+
--set image.tag=${{ github.ref_name }} \
67+
--wait \
68+
--timeout 10m

.github/workflows/ci.yml

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main, feat/* ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
lint:
11+
name: Lint & Format
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Setup Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: '3.12'
20+
21+
- name: Install dependencies
22+
run: |
23+
pip install ruff black
24+
25+
- name: Run ruff
26+
run: ruff check app/
27+
28+
- name: Run black
29+
run: black --check app/
30+
31+
test:
32+
name: Tests
33+
runs-on: ubuntu-latest
34+
needs: lint
35+
36+
services:
37+
postgres:
38+
image: postgres:16-alpine
39+
env:
40+
POSTGRES_USER: fleetops
41+
POSTGRES_PASSWORD: fleetops
42+
POSTGRES_DB: fleetops
43+
ports:
44+
- 5432:5432
45+
options: >-
46+
--health-cmd pg_isready
47+
--health-interval 5s
48+
--health-timeout 5s
49+
--health-retries 5
50+
51+
steps:
52+
- uses: actions/checkout@v4
53+
54+
- name: Setup Python
55+
uses: actions/setup-python@v5
56+
with:
57+
python-version: '3.12'
58+
59+
- name: Install dependencies
60+
run: pip install -r requirements.txt
61+
62+
- name: Run tests
63+
env:
64+
DATABASE_URL: postgresql+asyncpg://fleetops:fleetops@localhost:5432/fleetops
65+
run: pytest tests/ --cov=app --cov-report=term-missing
66+
67+
security:
68+
name: Security Scan
69+
runs-on: ubuntu-latest
70+
needs: lint
71+
steps:
72+
- uses: actions/checkout@v4
73+
74+
- name: Setup Python
75+
uses: actions/setup-python@v5
76+
with:
77+
python-version: '3.12'
78+
79+
- name: Install security tools
80+
run: pip install bandit pip-audit
81+
82+
- name: Run Bandit
83+
run: bandit -r app/ -ll
84+
85+
- name: Run pip-audit
86+
run: |
87+
pip-audit -r requirements.txt \
88+
--ignore-vuln PYSEC-2026-161 \
89+
--ignore-vuln PYSEC-2026-248 \
90+
--ignore-vuln PYSEC-2026-249 \
91+
--ignore-vuln PYSEC-2026-1943 \
92+
--ignore-vuln PYSEC-2026-1941 \
93+
--ignore-vuln PYSEC-2026-2281 \
94+
--ignore-vuln PYSEC-2026-2280 \
95+
--ignore-vuln PYSEC-2026-1845
96+
97+
build:
98+
name: Build & Push Docker Image
99+
runs-on: ubuntu-latest
100+
needs: [ test, security ]
101+
if: github.ref == 'refs/heads/main'
102+
103+
steps:
104+
- uses: actions/checkout@v4
105+
106+
- name: Configure AWS credentials
107+
uses: aws-actions/configure-aws-credentials@v4
108+
with:
109+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
110+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
111+
aws-region: us-east-1
112+
113+
- name: Login to Amazon ECR
114+
id: login-ecr
115+
uses: aws-actions/amazon-ecr-login@v2
116+
117+
- name: Build and push image
118+
env:
119+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
120+
IMAGE_TAG: ${{ github.sha }}
121+
run: |
122+
docker build -t $ECR_REGISTRY/fleetops:$IMAGE_TAG .
123+
docker build -t $ECR_REGISTRY/fleetops:latest .
124+
docker push $ECR_REGISTRY/fleetops:$IMAGE_TAG
125+
docker push $ECR_REGISTRY/fleetops:latest

app/config.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from pydantic_settings import BaseSettings
22

3+
34
class Settings(BaseSettings):
45
database_url: str
56
app_env: str = "development"
@@ -8,4 +9,5 @@ class Settings(BaseSettings):
89
class Config:
910
env_file = ".env"
1011

12+
1113
settings = Settings()

app/database.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
engine = create_async_engine(settings.database_url, echo=True)
66
AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False)
77

8+
89
class Base(DeclarativeBase):
910
pass
1011

12+
1113
async def get_db() -> AsyncSession:
1214
async with AsyncSessionLocal() as session:
1315
yield session

app/main.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,25 @@
44
from app.database import engine
55
from app.routers import vehicles, drivers
66

7+
78
@asynccontextmanager
89
async def lifespan(app: FastAPI):
9-
# async with engine.begin() as conn:
10-
# await conn.run_sync(Base.metadata.create_all)
10+
# async with engine.begin() as conn:
11+
# await conn.run_sync(Base.metadata.create_all)
1112
yield
1213

14+
1315
app = FastAPI(title="FleetOps API", lifespan=lifespan)
1416

1517
app.include_router(vehicles.router)
1618
app.include_router(drivers.router)
1719

20+
1821
@app.get("/health")
1922
async def health():
2023
return {"status": "ok"}
2124

25+
2226
@app.get("/ready")
2327
async def ready():
2428
try:
@@ -27,4 +31,5 @@ async def ready():
2731
return {"status": "ready", "db": "ok"}
2832
except Exception as e:
2933
from fastapi import Response
34+
3035
return Response(status_code=503, content=str(e))

app/models/driver.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from sqlalchemy.orm import Mapped, mapped_column
33
from app.database import Base
44

5+
56
class Driver(Base):
67
__tablename__ = "drivers"
78

app/models/vehicle.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from sqlalchemy.orm import Mapped, mapped_column
33
from app.database import Base
44

5+
56
class Vehicle(Base):
67
__tablename__ = "vehicles"
78

app/routers/drivers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
router = APIRouter(prefix="/drivers", tags=["drivers"])
99

10+
1011
@router.post("/", response_model=DriverRead, status_code=201)
1112
async def create_driver(payload: DriverCreate, db: AsyncSession = Depends(get_db)):
1213
driver = Driver(**payload.model_dump())
@@ -15,18 +16,21 @@ async def create_driver(payload: DriverCreate, db: AsyncSession = Depends(get_db
1516
await db.refresh(driver)
1617
return driver
1718

19+
1820
@router.get("/", response_model=list[DriverRead])
1921
async def list_drivers(db: AsyncSession = Depends(get_db)):
2022
result = await db.execute(select(Driver))
2123
return result.scalars().all()
2224

25+
2326
@router.get("/{driver_id}", response_model=DriverRead)
2427
async def get_driver(driver_id: int, db: AsyncSession = Depends(get_db)):
2528
driver = await db.get(Driver, driver_id)
2629
if not driver:
2730
raise HTTPException(status_code=404, detail="Driver not found")
2831
return driver
2932

33+
3034
@router.delete("/{driver_id}", status_code=204)
3135
async def delete_driver(driver_id: int, db: AsyncSession = Depends(get_db)):
3236
driver = await db.get(Driver, driver_id)

app/routers/vehicles.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
router = APIRouter(prefix="/vehicles", tags=["vehicles"])
99

10+
1011
@router.post("/", response_model=VehicleRead, status_code=201)
1112
async def create_vehicle(payload: VehicleCreate, db: AsyncSession = Depends(get_db)):
1213
vehicle = Vehicle(**payload.model_dump())
@@ -15,18 +16,21 @@ async def create_vehicle(payload: VehicleCreate, db: AsyncSession = Depends(get_
1516
await db.refresh(vehicle)
1617
return vehicle
1718

19+
1820
@router.get("/", response_model=list[VehicleRead])
1921
async def list_vehicles(db: AsyncSession = Depends(get_db)):
2022
result = await db.execute(select(Vehicle))
2123
return result.scalars().all()
2224

25+
2326
@router.get("/{vehicle_id}", response_model=VehicleRead)
2427
async def get_vehicle(vehicle_id: int, db: AsyncSession = Depends(get_db)):
2528
vehicle = await db.get(Vehicle, vehicle_id)
2629
if not vehicle:
2730
raise HTTPException(status_code=404, detail="Vehicle not found")
2831
return vehicle
2932

33+
3034
@router.delete("/{vehicle_id}", status_code=204)
3135
async def delete_vehicle(vehicle_id: int, db: AsyncSession = Depends(get_db)):
3236
vehicle = await db.get(Vehicle, vehicle_id)

app/schemas/driver.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from pydantic import BaseModel
22

3+
34
class DriverCreate(BaseModel):
45
first_name: str
56
last_name: str
67
license_number: str
78

9+
810
class DriverRead(DriverCreate):
911
id: int
1012
active: bool
1113

1214
model_config = {"from_attributes": True}
13-

0 commit comments

Comments
 (0)