Skip to content
Open
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
11 changes: 11 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.venv/
.venv-ci/
__pycache__/
.pytest_cache/
.sonar/
instance/
coverage.xml
.git
.gitignore
.vscode/
README.md
78 changes: 78 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: Flask Todo CI

on:
pull_request:
branches: [ "master" ]
push:
branches: [ "master" ]

permissions:
contents: read
id-token: write

env:
AWS_REGION: ap-southeast-2
ECR_REPOSITORY: flask-todo

jobs:
test-build-push:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: '3.12'

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

- name: Run tests with coverage
run: |
pytest -v --cov=app --cov-report=xml

- name: SonarQube Cloud Scan
uses: SonarSource/sonarqube-scan-action@v7.1.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
with:
args: >
-Dsonar.qualitygate.wait=true

- name: Build Docker image
run: |
docker build -t flask-todo:ci .

- name: Set image tag
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
echo "IMAGE_TAG=${GITHUB_RUN_NUMBER}-${SHORT_SHA}" >> $GITHUB_ENV

- name: Configure AWS credentials
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::730335329548:role/github-actions-ecr-role
aws-region: ${{ env.AWS_REGION }}

- name: Login to Amazon ECR
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: Tag and push image to ECR
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
docker tag flask-todo:ci $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "Pushed image: $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,12 @@ dmypy.json
.pytype/

# Cython debug symbols
cython_debug/
cython_debug/

# VS Code / macOS / JetBrains
.vscode/
.DS_Store
.idea/

coverage.xml
.sonar/
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt

COPY . .

RUN mkdir -p instance

ENV APP_HOST=0.0.0.0
ENV PORT=5000

EXPOSE 5000

CMD ["python", "app.py"]
87 changes: 87 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
pipeline {
agent any

options {
timestamps()
}

environment {
SONAR_HOST_URL = 'http://172.31.14.190:9000'
AWS_REGION = 'ap-southeast-2'
ECR_REPO = 'flask-todo'
}

stages {
stage('Checkout') {
steps {
checkout scm
}
}

stage('Set up Python environment') {
steps {
sh '''
rm -rf .venv-ci
python3.12 -m venv .venv-ci
. .venv-ci/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txt
'''
}
}

stage('Run tests') {
steps {
sh '''
. .venv-ci/bin/activate
pytest -v --cov=app --cov-report=xml
'''
}
}

stage('Run SonarQube analysis') {
steps {
withCredentials([string(credentialsId: 'sonar-token-flask-todo', variable: 'SONAR_TOKEN')]) {
sh '''
. .venv-ci/bin/activate
pysonar \
--sonar-host-url="${SONAR_HOST_URL}" \
--sonar-token="${SONAR_TOKEN}"
'''
}
}
}

stage('Build Docker image') {
steps {
sh '''
docker build -t flask-todo:ci .
'''
}
}
stage('Push Docker image to ECR') {
steps {
sh '''
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_REGISTRY=${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
SHORT_SHA=$(git rev-parse --short HEAD)
IMAGE_TAG=${BUILD_NUMBER}-${SHORT_SHA}

aws ecr get-login-password --region ${AWS_REGION} | \
docker login --username AWS --password-stdin ${ECR_REGISTRY}

docker tag flask-todo:ci ${ECR_REGISTRY}/${ECR_REPO}:${IMAGE_TAG}
docker push ${ECR_REGISTRY}/${ECR_REPO}:${IMAGE_TAG}

echo "Pushed image: ${ECR_REGISTRY}/${ECR_REPO}:${IMAGE_TAG}"
'''
}
}
}

post {
always {
archiveArtifacts artifacts: 'coverage.xml', allowEmptyArchive: true
}
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ Run the app
```console
$ flask run
```

10 changes: 8 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)

Expand Down Expand Up @@ -45,6 +46,11 @@ def delete(todo_id):
db.session.commit()
return redirect(url_for("home"))


if __name__ == "__main__":
db.create_all()
app.run(debug=True)
with app.app_context():
db.create_all()

host = os.getenv("APP_HOST", "127.0.0.1")
port = int(os.getenv("PORT", "5000"))
app.run(host=host, port=port)
30 changes: 30 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
blinker==1.9.0
certifi==2026.2.25
charset-normalizer==3.4.7
click==8.3.2
coverage==7.13.5
Flask==3.1.3
Flask-SQLAlchemy==3.1.1
greenlet==3.4.0
idna==3.11
iniconfig==2.3.0
itsdangerous==2.2.0
Jinja2==3.1.6
jproperties==2.1.2
MarkupSafe==3.0.3
packaging==26.1
pluggy==1.6.0
pyfakefs==5.9.3
Pygments==2.20.0
pysonar==1.4.0.4676
pytest==9.0.3
pytest-cov==7.1.0
PyYAML==6.0.3
requests==2.32.5
responses==0.25.8
six==1.17.0
SQLAlchemy==2.0.49
tomli==2.2.1
typing_extensions==4.15.0
urllib3==2.6.3
Werkzeug==3.1.8
13 changes: 13 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
sonar.projectKey=apeCry_flask-todo
sonar.organization=apecry

sonar.sources=app.py
sonar.tests=tests

sonar.python.coverage.reportPaths=coverage.xml

sonar.exclusions=.venv/**,instance/**,__pycache__/**,.pytest_cache/**,.sonar/**,.git/**

sonar.sourceEncoding=UTF-8

sonar.python.version=3.12
47 changes: 47 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os
import sys
import tempfile
import pytest

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

from app import app, db


@pytest.fixture
def client():
db_fd, db_path = tempfile.mkstemp()

app.config["TESTING"] = True
app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}"

with app.app_context():
db.drop_all()
db.create_all()

with app.test_client() as client:
yield client

with app.app_context():
db.session.remove()
db.drop_all()

os.close(db_fd)
os.unlink(db_path)


def test_home_page_loads(client):
response = client.get("/")
assert response.status_code == 200
assert b"To Do App" in response.data


def test_add_todo(client):
response = client.post(
"/add",
data={"title": "test task"},
follow_redirects=True
)

assert response.status_code == 200
assert b"test task" in response.data