diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 0000000..3055871 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,68 @@ +name: CI/CD Pipeline + +on: + workflow_dispatch: + push: + branches: + [master, develop] + pull_request: + branches: + [master, develop] + +jobs: + # Testing jobs + test-backend: + name: Test Backend + uses: ./.github/workflows/reusable-test.yml + with: + service: back-end + secrets: inherit + + test-frontend: + name: Test Frontend + uses: ./.github/workflows/reusable-test.yml + with: + service: front-end + secrets: inherit + + # Linting jobs + lint-backend: + name: Lint Backend + uses: ./.github/workflows/reusable-lint.yml + with: + service: back-end + + lint-frontend: + name: Lint Frontend + uses: ./.github/workflows/reusable-lint.yml + with: + service: front-end + + # Security scanning jobs + scan-backend: + name: Security Scan Backend + uses: ./.github/workflows/reusable-security.yml + with: + service: back-end + + scan-frontend: + name: Security Scan Frontend + uses: ./.github/workflows/reusable-security.yml + with: + service: front-end + + # Build jobs - only run after all checks pass + build-backend: + name: Build Backend + needs: [test-backend, lint-backend, scan-backend] + uses: ./.github/workflows/reusable-build.yml + with: + service: back-end + + build-frontend: + name: Build Frontend + needs: [test-frontend, lint-frontend, scan-frontend] + uses: ./.github/workflows/reusable-build.yml + with: + service: front-end + diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml new file mode 100644 index 0000000..55b4064 --- /dev/null +++ b/.github/workflows/reusable-build.yml @@ -0,0 +1,65 @@ +name: Reusable Build and Push Workflow + +on: + workflow_call: + inputs: + service: + description: 'Service to build (back-end or front-end)' + required: true + type: string + +jobs: + build-push: + name: build and push ${{ inputs.service }} image + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + security-events: write + steps: + - uses: actions/checkout@v5 + + - name: log in to container registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: extract metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/${{ github.repository }}/${{ inputs.service }} + tags: | + type=sha + type=ref,event=branch + type=raw,value= ${{ github.sha }} + type=raw,value=latest,enable={{is_default_branch}} + + - name: build and push ${{ inputs.service }} image + uses: docker/build-push-action@v4 + with: + context: ./${{ inputs.service }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + - name: lowercase repository name + id: repo + run: echo "name=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]')" >> $GITHUB_OUTPUT + + - name: run trivy ${{ inputs.service }} scan + uses: aquasecurity/trivy-action@master + with: + image-ref: ghcr.io/${{ steps.repo.outputs.name }}/${{ inputs.service }}:${{ github.sha }} + format: 'sarif' + output: 'trivy-${{ inputs.service }}-image.sarif' + + - name: upload trivy ${{ inputs.service }} scan + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: trivy-${{ inputs.service }}-image.sarif + category: 'trivy ${{ inputs.service }}' + diff --git a/.github/workflows/reusable-lint.yml b/.github/workflows/reusable-lint.yml new file mode 100644 index 0000000..a41e5b0 --- /dev/null +++ b/.github/workflows/reusable-lint.yml @@ -0,0 +1,30 @@ +name: Reusable Lint Workflow + +on: + workflow_call: + inputs: + service: + description: 'Service to lint (back-end or front-end)' + required: true + type: string + +jobs: + lint: + name: lint ${{ inputs.service }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: python setup + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: install flake8 + run: pip install flake8 + + - name: lint ${{ inputs.service }} + run: | + flake8 ${{ inputs.service }}/ --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 ${{ inputs.service }}/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + diff --git a/.github/workflows/reusable-security.yml b/.github/workflows/reusable-security.yml new file mode 100644 index 0000000..4c0d288 --- /dev/null +++ b/.github/workflows/reusable-security.yml @@ -0,0 +1,41 @@ +name: Reusable Security Scan Workflow + +on: + workflow_call: + inputs: + service: + description: 'Service to scan (back-end or front-end)' + required: true + type: string + +jobs: + scan: + name: security scan ${{ inputs.service }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: python setup + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: pip-audit install + run: pip install pip-audit + + - name: ${{ inputs.service }} audit + run: pip-audit -r ${{ inputs.service }}/requirements.txt --desc + + - name: bandit install + run: pip install bandit[sarif] + + - name: ${{ inputs.service }} bandit scan + run: bandit -r ${{ inputs.service }}/ -f sarif -o bandit-${{ inputs.service }}.sarif --exit-zero + + - name: upload ${{ inputs.service }} bandit report + uses: github/codeql-action/upload-sarif@v4 + if: always() + with: + sarif_file: bandit-${{ inputs.service }}.sarif + category: 'bandit ${{ inputs.service }}' + diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml new file mode 100644 index 0000000..ce4ad22 --- /dev/null +++ b/.github/workflows/reusable-test.yml @@ -0,0 +1,44 @@ +name: Reusable Test Workflow + +on: + workflow_call: + inputs: + service: + description: 'Service to test (back-end or front-end)' + required: true + type: string + +jobs: + test: + name: ${{ inputs.service }} tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: python setup + uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - name: install dependencies + working-directory: ./${{ inputs.service }} + run: | + pip install -r requirements.txt + pip install -r requirements_test.txt + + - name: run tests + env: + POSTGRES_DB: ${{ secrets.POSTGRES_DB }} + POSTGRES_USER: ${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + POSTGRES_HOST: ${{ secrets.POSTGRES_HOST }} + PORT: ${{ secrets.PORT }} + JWT_KEY: ${{ secrets.JWT_KEY }} + APP_PORT: ${{ secrets.APP_PORT }} + BACKEND_HOSTNAME: ${{ secrets.BACKEND_HOSTNAME }} + BACKEND_PORT: ${{ secrets.BACKEND_PORT }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + SERVER_PORT: ${{ secrets.SERVER_PORT }} + working-directory: ./${{ inputs.service }} + run: pytest test_app.py -v + diff --git a/back-end/Dockerfile b/back-end/Dockerfile index effa773..9d5cf65 100644 --- a/back-end/Dockerfile +++ b/back-end/Dockerfile @@ -1 +1,26 @@ -# ADD YOUR OWN DOCKERFILE \ No newline at end of file +FROM python:3.13-slim + +WORKDIR /app + +ENV PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +RUN apt-get update && \ +apt-get install -y --no-install-recommends \ +curl && \ +rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . + +RUN pip install --upgrade pip && \ + pip install -r requirements.txt + +COPY . . + +EXPOSE 5000 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:5000/liveness || exit 1 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/front-end/Dockerfile b/front-end/Dockerfile index effa773..a3bece6 100644 --- a/front-end/Dockerfile +++ b/front-end/Dockerfile @@ -1 +1,26 @@ -# ADD YOUR OWN DOCKERFILE \ No newline at end of file +FROM python:3.13-slim + +WORKDIR /app + +ENV PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +RUN apt-get update && \ +apt-get install -y --no-install-recommends \ +curl && \ +rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . + +RUN pip install --upgrade pip && \ + pip install -r requirements.txt + +COPY . . + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ + CMD curl -f http://localhost:8000/ || exit 1 + +CMD ["python", "app.py"] \ No newline at end of file