diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..982e3ed --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,57 @@ +name: Deploy Flask App to VM + +on: + push: + branches: + - main # orr your default branch + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python 3.x + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies (for lint/test, optional) + run: | + python -m venv venv + source venv/bin/activate + pip install --upgrade pip + pip install -r requirements.txt + + - name: Copy files to VM + uses: appleboy/scp-action@v0.1.5 + with: + host: ${{ secrets.VM_HOST }} + username: ${{ secrets.VM_USER }} + key: ${{ secrets.VM_SSH_KEY }} + port: 22 + source: "." + target: "/home/${{ secrets.VM_USER }}/ci-cd-tutorial-sample-app" + + - name: Run deploy commands on VM + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.VM_HOST }} + username: ${{ secrets.VM_USER }} + key: ${{ secrets.VM_SSH_KEY }} + port: 22 + script: | + cd ~/ci-cd-tutorial-sample-app + # Activate virtual environment or create if missing + if [ ! -d "venv" ]; then + python3 -m venv venv + fi + source venv/bin/activate + pip install --upgrade pip + pip install -r requirements.txt + + # Restart Gunicorn (adjust service name or command as needed) + pkill gunicorn || true + nohup gunicorn --bind 0.0.0.0:8000 app:app > gunicorn.log 2>&1 & diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a301e44 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: Python Flask CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9.17 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pytest + + - name: Run tests + run: python -m pytest # Change this line diff --git a/.github/workflows/cloudrunner.yml b/.github/workflows/cloudrunner.yml new file mode 100644 index 0000000..fbe92df --- /dev/null +++ b/.github/workflows/cloudrunner.yml @@ -0,0 +1,49 @@ +name: 'Build and Deploy to Cloud Run' + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + PROJECT_ID: 'thermal-hour-467308-u4' + GAR_NAME: 'gh-demo' + REGION: 'us-central1' + SERVICE: 'gitactionnew' + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: 'Checkout' + uses: actions/checkout@v4 + + - name: 'Authenticate to Google Cloud with SA Key' + uses: google-github-actions/auth@v2 + with: + credentials_json: '${{ secrets.ABC }}' + + - name: 'Set up gcloud CLI' + uses: google-github-actions/setup-gcloud@v2 + + - name: 'Docker Auth' + run: gcloud auth configure-docker "${{ env.REGION }}-docker.pkg.dev" + + - name: 'Build and Push Docker Image' + run: | + IMAGE="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.GAR_NAME }}/${{ env.SERVICE }}:${{ github.sha }}" + docker build -t "$IMAGE" . + docker push "$IMAGE" + + - name: 'Deploy to Cloud Run' + id: deploy + uses: google-github-actions/deploy-cloudrun@v2 + with: + service: ${{ env.SERVICE }} + region: ${{ env.REGION }} + image: "${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.GAR_NAME }}/${{ env.SERVICE }}:${{ github.sha }}" + + - name: Show Deployed URL + run: echo ${{ steps.deploy.outputs.url }} diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 4151f4c..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,67 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ master ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ master ] - schedule: - - cron: '31 0 * * 1' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - language: [ 'python' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] - # Learn more: - # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed - - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v1 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v1 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6831739 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,12 @@ +name: Test Workflow + +on: + push: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Echo message + run: echo "✅ GitHub Actions are working!" diff --git a/Dockerfile b/Dockerfile index d85316f..464ec29 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,20 +1,40 @@ -FROM ubuntu:18.04 +# Use the official lightweight Python image. +FROM python:3.11-slim -RUN apt-get update && \ - apt-get -y upgrade && \ - DEBIAN_FRONTEND=noninteractive apt-get install -yq libpq-dev gcc python3.8 python3-pip && \ - apt-get clean +# Set environment variables for Python in Docker +# Prevents Python from writing .pyc files +ENV PYTHONDONTWRITEBYTECODE=1 +# Ensures Python output is sent immediately to the terminal +ENV PYTHONUNBUFFERED=1 +# Add /app to PYTHONPATH so Python can find your 'app' package +ENV PYTHONPATH=/app:$PYTHONPATH -WORKDIR /sample-app +# Set the working directory inside the container +WORKDIR /app -COPY . /sample-app/ +# Expose port 8080. Cloud Run typically expects services to listen on this port. +EXPOSE 8080 -RUN pip3 install -r requirements.txt && \ - pip3 install -r requirements-server.txt +# Install dependencies +# Copy requirements files first to leverage Docker's caching. +COPY requirements.txt . +COPY requirements-server.txt . -ENV LC_ALL="C.UTF-8" -ENV LANG="C.UTF-8" +# Install Python dependencies. +RUN pip install --no-cache-dir --upgrade pip && \ + pip install -r requirements.txt && \ + pip install -r requirements-server.txt -EXPOSE 8000/tcp +# Copy the rest of your application code into the container +COPY . . -CMD ["/bin/sh", "-c", "flask db upgrade && gunicorn app:app -b 0.0.0.0:8000"] +# Copy the entrypoint script and make it executable +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Use the entrypoint script. +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] + +# The CMD provides default arguments to the ENTRYPOINT script. +# Since Gunicorn is started by entrypoint.sh, this can be empty or used for further arguments. +CMD [] diff --git a/README.md b/README.md index 0af701b..9b809b6 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ # CD/CI Tutorial Sample Application -## Description +## Descriptio -This sample Python REST API application was written for a tutorial on implementing Continuous Integration and Delivery pipelines. +This sample Python REST API application was written for a tutorial on implementing Continuous Integration and Delivery pipelines It demonstrates how to: diff --git a/app/routes.py b/app/routes.py index 8aa3782..a3eeb5f 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,11 +1,14 @@ -from flask import json, jsonify +from flask import jsonify from app import app from app import db from app.models import Menu @app.route('/') def home(): - return jsonify({ "status": "ok" }) + return jsonify({ + "message": "Welcome to Tharushi's CI/CD demo app 🎉", + "status": "ok" + }) @app.route('/menu') def menu(): @@ -16,4 +19,4 @@ def menu(): else: body = { "error": "Sorry, the service is not available today." } status = 404 - return jsonify(body), status \ No newline at end of file + return jsonify(body), status diff --git a/cd.yml b/cd.yml new file mode 100644 index 0000000..621bba7 --- /dev/null +++ b/cd.yml @@ -0,0 +1,57 @@ +name: Deploy Flask App to VM + +on: + push: + branches: + - main # or your default branch + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python 3.x + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + - name: Install dependencies (for lint/test, optional) + run: | + python -m venv venv + source venv/bin/activate + pip install --upgrade pip + pip install -r requirements.txt + + - name: Copy files to VM + uses: appleboy/scp-action@v0.1.5 + with: + host: ${{ secrets.VM_HOST }} + username: ${{ secrets.VM_USER }} + key: ${{ secrets.VM_SSH_KEY }} + port: 22 + source: "." + target: "/home/${{ secrets.VM_USER }}/ci-cd-tutorial-sample-app" + + - name: Run deploy commands on VM + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.VM_HOST }} + username: ${{ secrets.VM_USER }} + key: ${{ secrets.VM_SSH_KEY }} + port: 22 + script: | + cd ~/ci-cd-tutorial-sample-app + # Activate virtual environment or create if missing + if [ ! -d "venv" ]; then + python3 -m venv venv + fi + source venv/bin/activate + pip install --upgrade pip + pip install -r requirements.txt + + # Restart Gunicorn (adjust service name or command as needed) + pkill gunicorn || true + nohup gunicorn --bind 0.0.0.0:8000 app:app > gunicorn.log 2>&1 & diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..24147ae --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +echo "Starting entrypoint script..." + +# Set FLASK_APP to 'app' (the package name). +# This is crucial for Flask CLI commands and Gunicorn to find your application instance. +export FLASK_APP=app + +echo "Running database migrations..." +# Execute migrations. Redirecting stderr to stdout (2>&1) ensures errors are logged to Cloud Logging. +# The 'set -e' (often implied by shebang or default shell behavior) will cause the script to exit +# immediately if 'flask db upgrade' fails, which is desired for failed deployments. +if flask db upgrade 2>&1; then + echo "Database migrations completed successfully." +else + echo "ERROR: Database migrations failed!" + # Exit with a non-zero status to indicate failure to Cloud Run. + exit 1 +fi + +echo "Starting Gunicorn server..." +# Cloud Run injects the PORT environment variable (defaulting to 8080). +# Ensure Gunicorn binds to 0.0.0.0 and uses this PORT variable. +# The ${PORT:-8080} syntax provides a fallback to 8080 if PORT isn't set (e.g., for local testing). +exec gunicorn app:app -b 0.0.0.0:${PORT:-8080} diff --git a/requirements.txt b/requirements.txt index d78cc59..7d3bae8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -Flask==1.1.2 -Flask-Migrate==2.5.3 -Flask-SQLAlchemy==2.4.4 +Flask +Jinja2 +Flask-SQLAlchemy +Flask-Migrate diff --git a/tests/test_routes.py b/tests/test_routes.py index c8609b9..13fa81f 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -4,7 +4,7 @@ import json -# Add parent directory to path for import +# Add parent directory to path for import #comment sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)) from app import app, db