Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CI testing workflow #32

Merged
merged 23 commits into from
Nov 19, 2023
Merged
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
29 changes: 29 additions & 0 deletions .github/workflows/Build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Build

on:
workflow_call:

jobs:
build-image:
name: Build Docker Image
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

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

- name: Build image
uses: docker/build-push-action@v5
with:
context: .
tags: test-image:latest
outputs: type=docker,dest=/tmp/test-image.tar

- name: Upload image artifact
uses: actions/upload-artifact@v3
with:
name: test-image
path: /tmp/test-image.tar
30 changes: 30 additions & 0 deletions .github/workflows/PullRequest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Pull Request

on:
pull_request:

jobs:
build:
name: Build
uses: ./.github/workflows/Build.yml

test:
name: Test
needs: build
uses: ./.github/workflows/Test.yml

qa:
name: QA
needs: build
uses: ./.github/workflows/QA.yml

report-pr-status:
name: Report PR Status
runs-on: ubuntu-latest
needs: [ test, qa ]
if: always()

steps:
- name: Check PR status
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
38 changes: 38 additions & 0 deletions .github/workflows/QA.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: QA

on:
workflow_call:

jobs:
codeql:
name: CodeQL
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: python

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: /language:${{matrix.language}}

report-qa-status:
name: Report QA Status
runs-on: ubuntu-latest
needs: [ codeql ]
if: always()

steps:
- name: Check QA status
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
60 changes: 60 additions & 0 deletions .github/workflows/Test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Test

on:
workflow_call:

jobs:
api-status:
name: API Status
runs-on: ubuntu-latest

steps:
- name: Fetch image artifact
uses: actions/download-artifact@v3
with:
name: test-image
path: /tmp

- name: Load image
run: docker load --input /tmp/test-image.tar

- name: Check API health
uses: addnab/docker-run-action@v3
with:
image: test-image
run: |
endpoint="http://localhost:8000/health"
httpstatus=$(curl --silent --output /dev/null --write-out "%{http_code}" $endpoint)

# If the status is not 200, print the full status report from the endpoint
if [ "$httpstatus" -ne 200 ]; then
curl $endpoint
exit 1
fi

app-tests:
name: Application Tests
runs-on: ubuntu-latest

steps:
- name: Fetch image artifact
uses: actions/download-artifact@v3
with:
name: test-image
path: /tmp

- name: Run tests
run: |
docker load --input /tmp/test-image.tar
docker run test-image test

report-test-status:
name: Report Test Status
runs-on: ubuntu-latest
needs: [ api-status, app-tests ]
if: always()

steps:
- name: Check test status
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ coverage.xml
*.sqlite3-journal
crc_service_api/email/
crc_service_api/static_root/
health # API health checks

# Pycharm stuff
.idea/
Expand Down
15 changes: 11 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

# Install system dependencies
RUN apt-get update && apt-get install -y \

Check notice on line 10 in Dockerfile

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Dockerfile#L10

Avoid additional packages by specifying `--no-install-recommends`

Check notice on line 10 in Dockerfile

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Dockerfile#L10

Delete the apt-get lists after installing something

Check warning on line 10 in Dockerfile

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

Dockerfile#L10

Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
# Required for LDAP
build-essential \
libsasl2-dev \
libldap2-dev \
# Required for celery
redis

# Copy only the files needed to build the application
WORKDIR /app
COPY crc_service_api crc_service_api
COPY pyproject.toml pyproject.toml
COPY README.md README.md

# Install system dependencies
RUN apt-get update && apt-get install -y build-essential libsasl2-dev libldap2-dev

# Install the application
ENV PIP_ROOT_USER_ACTION=ignore
RUN pip install -e .

# Setup and launch the application
CMD ["crc-service-api", "quickstart", "--static", "--migrate", "--uvicorn"]
ENTRYPOINT ["crc-service-api"]
CMD ["quickstart", "--static", "--migrate", "--celery", "--gunicorn"]
41 changes: 26 additions & 15 deletions crc_service_api/apps/admin_utils/management/commands/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@
|------------|------------------------------------------------------------------|
| --static | Collect static files |
| --migrate | Run database migrations |
| --celery | Launch a Celery worker with a Redis backend |
| --gunicorn | Run a web server using Gunicorn |
| --uvicorn | Run a web server using Uvicorn |
| --no-input | Do not prompt for user input of any kind |
| --gunicorn | Launch a web server using Gunicorn |
| --uvicorn | Launch a web server using Uvicorn |
| --host | Specify the host for the development server (default: '0.0.0.0') |
| --port | Specify the port for the development server (default: 8000) |
"""

import subprocess
Expand All @@ -37,30 +36,33 @@

parser.add_argument('--static', action='store_true', help='Collect static files.')
parser.add_argument('--migrate', action='store_true', help='Run database migrations.')
parser.add_argument('--no-input', action='store_true', help='Do not prompt for user input of any kind.')
parser.add_argument('--celery', action='store_true', help='Launch a background Celery worker.')

server_type = parser.add_mutually_exclusive_group()
server_type.add_argument('--gunicorn', action='store_true', help='Run a web server using Gunicorn.')
server_type.add_argument('--uvicorn', action='store_true', help='Run a web server using Uvicorn.')
parser.add_argument('--host', default='0.0.0.0', help='Bind socket to this host.')
parser.add_argument('--port', type=int, default=8000, help='Bind socket to this port.')

def handle(self, *args, **options):
parser.add_argument('--no-input', action='store_true', help='Do not prompt for user input of any kind.')

def handle(self, *args, **options) -> None:
"""Handle the command execution.

Args:
*args: Additional positional arguments.
**options: Additional keyword arguments.

"""

if options['static']:
self.stdout.write(self.style.SUCCESS('Collecting static files...'))
call_command('collectstatic', no_input=not options['no_input'])

if options['migrate']:
self.stdout.write(self.style.SUCCESS('Running database migrations...'))
call_command('migrate', no_input=not options['no_input'])

if options['static']:
self.stdout.write(self.style.SUCCESS('Collecting static files...'))
call_command('collectstatic', no_input=not options['no_input'])
if options['celery']:
self.stdout.write(self.style.SUCCESS('Starting Celery worker...'))
self.run_celery()

if options['gunicorn']:
self.stdout.write(self.style.SUCCESS('Starting Gunicorn server...'))
Expand All @@ -70,18 +72,27 @@
self.stdout.write(self.style.SUCCESS('Starting Uvicorn server...'))
self.run_uvicorn()

def run_gunicorn(self, host: str = '0.0.0.0', port: int = 8000):
@staticmethod
def run_celery() -> None:
"""Start a Celery worker."""

subprocess.Popen(['redis-server'])

Check warning on line 79 in crc_service_api/apps/admin_utils/management/commands/quickstart.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

crc_service_api/apps/admin_utils/management/commands/quickstart.py#L79

Starting a process with a partial executable path

Check warning on line 79 in crc_service_api/apps/admin_utils/management/commands/quickstart.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

crc_service_api/apps/admin_utils/management/commands/quickstart.py#L79

subprocess call - check for execution of untrusted input.
subprocess.Popen(['celery', '-A', 'crc_service_api.apps.scheduler', 'worker', '-D'])

Check warning on line 80 in crc_service_api/apps/admin_utils/management/commands/quickstart.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

crc_service_api/apps/admin_utils/management/commands/quickstart.py#L80

Starting a process with a partial executable path

Check warning on line 80 in crc_service_api/apps/admin_utils/management/commands/quickstart.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

crc_service_api/apps/admin_utils/management/commands/quickstart.py#L80

subprocess call - check for execution of untrusted input.

@staticmethod
def run_gunicorn(host: str = '0.0.0.0', port: int = 8000) -> None:

Check warning on line 83 in crc_service_api/apps/admin_utils/management/commands/quickstart.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

crc_service_api/apps/admin_utils/management/commands/quickstart.py#L83

Possible binding to all interfaces.
"""Start a Gunicorn server.

Args:
host: The host to bind to
port: The port to bind to
"""

command = ['gunicorn', '--bind', f'{host}:{port}', 'crc_service_api.main.wsgi:application', ]
command = ['gunicorn', '--bind', f'{host}:{port}', 'crc_service_api.main.wsgi:application']
subprocess.run(command, check=True)

def run_uvicorn(self, host: str = '0.0.0.0', port: int = 8000):
@staticmethod
def run_uvicorn(host: str = '0.0.0.0', port: int = 8000) -> None:

Check warning on line 95 in crc_service_api/apps/admin_utils/management/commands/quickstart.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

crc_service_api/apps/admin_utils/management/commands/quickstart.py#L95

Possible binding to all interfaces.
"""Start a Uvicorn server.

Args:
Expand Down
Loading