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
224 changes: 224 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
name: Production Pipeline

on:
push:
branches: [ "master", "main" ]

env:
REGISTRY: ghcr.io
IMAGE_NAMESPACE: maheshroy50

jobs:
# ====================================================
# STAGE 1: LINT & TEST
# ====================================================
quality-check:
runs-on: ubuntu-latest
strategy:
matrix:
service: [client, backend]
steps:
- name: Checkout code
uses: actions/checkout@v3

# --- Frontend ---
- if: matrix.service == 'client'
name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: ./client/package-lock.json

- if: matrix.service == 'client'
name: Frontend Lint
working-directory: ./client
run: |
npm ci
npm run lint

# --- Backend ---
- if: matrix.service == 'backend'
name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
cache: 'pip'

- if: matrix.service == 'backend'
name: Backend Lint
working-directory: ./backend
run: |
pip install -r requirements.txt
pip install flake8
flake8 .

# ====================================================
# STAGE 2: BUILD, SCAN & PUSH
# ====================================================
build-scan-push:
needs: quality-check
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
security-events: write

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

- name: Log in to Container Registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# --- 1. BUILD BACKEND (Load to Daemon for Scanning) ---
- name: Build Backend (Load)
uses: docker/build-push-action@v4
with:
context: ./backend
push: false
load: true # FIX #1: Load into Docker daemon so Trivy can see it
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/backend:${{ github.sha }}
cache-from: type=gha # FIX #7: Use GitHub Actions Cache
cache-to: type=gha,mode=max

# --- 2. SCAN BACKEND ---
- name: Scan Backend
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/backend:${{ github.sha }}
format: 'table'
exit-code: '1'
ignore-unfixed: true
severity: 'CRITICAL,HIGH'

# --- 3. PUSH BACKEND (Only if Scan passed) ---
- name: Push Backend
uses: docker/build-push-action@v4
with:
context: ./backend
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/backend:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/backend:latest
cache-from: type=gha
cache-to: type=gha,mode=max

# --- 4. BUILD & PUSH CLIENT ---
- name: Build and Push Client
uses: docker/build-push-action@v4
with:
context: ./client
push: true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/client:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/client:latest
cache-from: type=gha
cache-to: type=gha,mode=max

# ====================================================
# STAGE 3: DEPLOY (With Error Handling & Health Checks)
# ====================================================
deploy-prod:
needs: build-scan-push
runs-on: ubuntu-latest

steps:
- name: Deploy to AWS EC2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_KEY }}
script: |
set -e # FIX #6: Stop script immediately if any command fails

# Validate SHA - FIX #10
if [ -z "${{ github.sha }}" ]; then
echo "Error: No commit SHA found"
exit 1
fi

mkdir -p my-app
cd my-app

# FIX #5: Update to version 3.8
cat <<EOF > docker-compose.yml
version: '3.8'
services:
db:
image: postgres:13
restart: always
environment:
POSTGRES_DB: sport_stats
POSTGRES_USER: myuser
POSTGRES_PASSWORD: ${{ secrets.DB_PASSWORD }}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d sport_stats"]
interval: 10s
timeout: 5s
retries: 5

flask-api:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/backend:${{ github.sha }}
restart: always
ports:
- "5000:5000"
depends_on:
db:
condition: service_healthy
environment:
DATABASE_URL: postgresql://myuser:${{ secrets.DB_PASSWORD }}@db:5432/sport_stats

client:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/client:${{ github.sha }}
restart: always
ports:
- "3000:3000"
depends_on:
- flask-api

volumes:
db_data:
EOF

# FIX #9: Use PAT if available, otherwise fallback to GITHUB_TOKEN (See notes below)
echo ${{ secrets.CR_PAT }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin

docker-compose pull
docker-compose down
docker-compose up -d

# FIX #2 & #8: Wait for Backend to be ready (Health Check Loop)
echo "Waiting for Backend to accept connections..."
timeout 60 bash -c 'until curl -s http://localhost:5000/ping > /dev/null; do sleep 2; done'
echo "Backend is Up!"

echo "Waiting for Frontend..."
# Wait up to 2 minutes for localhost:3000 to respond
timeout 120 bash -c 'until curl -s http://localhost:3000 > /dev/null; do sleep 5; done'

# FIX #2: Run Migrations only after DB and App are ready
#echo "Running Migrations..."
#docker-compose exec -T flask-api python manage.py recreate_db

# SMOKE TESTS
echo "Running Smoke Tests..."
if curl -s --head --request GET http://localhost:3000 | grep "200 OK" > /dev/null; then
echo "✅ Frontend is UP"
else
echo "❌ Frontend is DOWN"
exit 1
fi

# Prune old images
docker system prune -f
2 changes: 1 addition & 1 deletion Readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ReactJS + Flask (REST API) + PostgreSQL boilerplate with Docker

This project allows to run a quick application with ReactJS (Javascript front-end), Flask (Python backend) and PostgreSQL (relational database) by running it on Docker containers.
This project allows to run a quick application with ReactJS (Javascript front-end), Flask (Python backend) and PostgreSQL (relational database) by running it on Docker conta.

### Features

Expand Down
7 changes: 7 additions & 0 deletions backend/.flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[flake8]
# Match Black's line length limit
max-line-length = 88

# Ignore errors that clash with Black
# E203: Whitespace before ':' (Black does this intentionally)
extend-ignore = E203
21 changes: 19 additions & 2 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
FROM python:3-onbuild
COPY . /usr/src/app
# 1. Use a modern, official Python image
FROM python:3.9-slim

# 2. Set the working directory inside the container
WORKDIR /usr/src/app

# 3. Copy requirements first (This leverages Docker caching)
COPY requirements.txt .

# 4. UPGRADE PIP (Crucial step to fix your error)
RUN pip install --upgrade pip

# 5. Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# 6. Copy the rest of the application code
COPY . .

# 7. Command to run the app
CMD ["python", "app.py"]
4 changes: 3 additions & 1 deletion backend/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

api = Api()


class Player(Resource):
def get(self):
return jsonify([to_dict(player) for player in PlayerModel.query.all()])

api.add_resource(Player, '/')

api.add_resource(Player, "/")
6 changes: 4 additions & 2 deletions backend/api/config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
class Config(object):
DEBUG = True
TESTING = True
SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://myuser:mypassword@192.168.99.100/sport_stats'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_DATABASE_URI = (
"postgresql+psycopg2://myuser:mypassword@192.168.99.100/sport_stats"
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
14 changes: 9 additions & 5 deletions backend/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,29 @@

db = SQLAlchemy()


class Player(db.Model):
__tablename__ = 'players'
__tablename__ = "players"
firstname = db.Column(db.String(100), nullable=False, primary_key=True)
lastname = db.Column(db.String(100), nullable=False)

def __repr__(self):
return '<Player %r>' % self.firstname + ' ' + self.lastname
return "<Player %r>" % self.firstname + " " + self.lastname


def to_dict(obj):
if isinstance(obj.__class__, DeclarativeMeta):
# an SQLAlchemy class
fields = {}
for field in [x for x in dir(obj) if not x.startswith('_') and x != 'metadata']:
for field in [x for x in dir(obj) if not x.startswith("_") and x != "metadata"]:
data = obj.__getattribute__(field)
try:
json.dumps(data) # this will fail on non-encodable values, like other classes
json.dumps(
data
) # this will fail on non-encodable values, like other classes
if data is not None:
fields[field] = data
except TypeError:
pass
# a json-encodable dict
return fields
return fields
12 changes: 7 additions & 5 deletions backend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from api.models import db
from api.config import Config


def create_app(config):
app = Flask(__name__)
CORS(app)
Expand All @@ -17,10 +18,11 @@ def register_extensions(app):
api.init_app(app)
db.init_app(app)

app = create_app(Config)

app = create_app(Config)

# Run the application
# if __name__ == '__main__':
# app = create_app(Config)
# app.run(host='0.0.0.0', port=80, debug=True, threaded=True)
# --- FIXED SECTION BELOW ---
if __name__ == '__main__':
# host='0.0.0.0' makes it accessible outside the container
# port=5000 matches your docker-compose and pipeline settings
app.run(host='0.0.0.0', port=5000, debug=True, threaded=True)
11 changes: 6 additions & 5 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Flask==0.12
flask-restful==0.3.5
flask-cors==3.0.6
flask_sqlalchemy==2.3
psycopg2
Flask==2.3.3
flask-restful==0.3.10
Flask-Cors>=4.0.2
Flask-SQLAlchemy==3.0.5
psycopg2-binary==2.9.9
gunicorn>=22.0.0
3 changes: 3 additions & 0 deletions client/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "react-app"
}
Loading