Skip to content
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
7 changes: 4 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ defaults:
GOOGLE_PROJECT_ID: "excellent-zoo-300106"
GOOGLE_COMPUTE_ZONE: "us-central1-a"
GOOGLE_CLUSTER_NAME: "ipno"
CLOUDSDK_CORE_DISABLE_PROMPTS: "1"
- &set-push-env
USER_NAME: "East Agile"
USER_EMAIL: "[email protected]"
Expand Down Expand Up @@ -148,7 +149,7 @@ jobs:
django_migrate:
description: Migrate database
machine:
image: ubuntu-2004:202010-01
image: ubuntu-2204:current
environment: *gcloud-env
steps:
- run: *set-gcloud-service-key
Expand All @@ -169,7 +170,7 @@ jobs:
django_collect_static:
description: Collect static
machine:
image: ubuntu-2004:202010-01
image: ubuntu-2204:current
environment: *gcloud-env
steps:
- run: *set-gcloud-service-key
Expand All @@ -190,7 +191,7 @@ jobs:
deploy:
description: Deploy application to Google Kubernetes Engine
machine:
image: ubuntu-2004:202010-01
image: ubuntu-2204:current
environment: *gcloud-env
steps:
- checkout
Expand Down
256 changes: 256 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
name: Deploy

on:
push:
branches:
- main
- staging

env:
GOOGLE_PROJECT_ID: excellent-zoo-300106
GOOGLE_COMPUTE_ZONE: us-central1-a
GOOGLE_CLUSTER_NAME: ipno
Comment on lines +10 to +12
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sensitive information like Google Project ID is hardcoded in the workflow file. While this may not be highly sensitive, it's a best practice to store such configuration in GitHub secrets or variables to make it easier to change without modifying the workflow file. Consider using:

GOOGLE_PROJECT_ID: ${{ secrets.GOOGLE_PROJECT_ID }}

or using GitHub environments for different deployment targets.

Suggested change
GOOGLE_PROJECT_ID: excellent-zoo-300106
GOOGLE_COMPUTE_ZONE: us-central1-a
GOOGLE_CLUSTER_NAME: ipno
GOOGLE_PROJECT_ID: ${{ secrets.GOOGLE_PROJECT_ID }}
GOOGLE_COMPUTE_ZONE: ${{ secrets.GOOGLE_COMPUTE_ZONE }}
GOOGLE_CLUSTER_NAME: ${{ secrets.GOOGLE_CLUSTER_NAME }}

Copilot uses AI. Check for mistakes.

jobs:
test:
runs-on: ubuntu-latest

services:
postgres:
image: postgres:13.7
env:
POSTGRES_USER: ipno
POSTGRES_DB: ipno
POSTGRES_PASSWORD: password
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

elasticsearch:
image: elasticsearch:7.10.1
env:
discovery.type: single-node
ports:
- 9200:9200
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The elasticsearch service in the deploy workflow is missing health check configuration, while the test.yml workflow includes it. This could cause the tests to fail intermittently if they run before Elasticsearch is fully ready.

Add the health check options:

options: >-
  --health-cmd "curl -f http://localhost:9200/_cluster/health"
  --health-interval 10s
  --health-timeout 5s
  --health-retries 10
Suggested change
- 9200:9200
- 9200:9200
options: >-
--health-cmd "curl -f http://localhost:9200/_cluster/health"
--health-interval 10s
--health-timeout 5s
--health-retries 10

Copilot uses AI. Check for mistakes.

redis:
image: redis:7.0.5
ports:
- 6379:6379
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The redis service in the deploy workflow is missing health check configuration, while the test.yml workflow includes it. This could cause the tests to fail intermittently if they run before Redis is fully ready.

Add the health check options:

options: >-
  --health-cmd "redis-cli ping"
  --health-interval 10s
  --health-timeout 5s
  --health-retries 5
Suggested change
- 6379:6379
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

Copilot uses AI. Check for mistakes.

steps:
- uses: actions/checkout@v4

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

- name: Install native packages
run: |
sudo apt-get update
sudo apt-get -y install ghostscript python3-wand

- name: Install dependencies
run: |
pip install "pip<24.1"
pip install -r requirements/dev.txt

- name: Install spacy model
run: python -m spacy download en_core_web_sm

- name: Lint
run: bin/lint.sh

- name: Run tests
env:
IPNO_API_KEY: ${{ secrets.IPNO_API_KEY }}
DJANGO_SECRET_KEY: ${{ secrets.DJANGO_SECRET_KEY }}
POSTGRES_HOST: localhost
POSTGRES_USER: ipno
POSTGRES_DB: ipno
POSTGRES_PASSWORD: password
ELASTICSEARCH_HOST: localhost:9200
CELERY_BROKER_URL: redis://localhost:6379
run: |
python -m pytest --cov-report term --cov=ipno ipno

build-and-push:
needs: test
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.set-tag.outputs.tag }}
namespace: ${{ steps.set-env.outputs.NAMESPACE }}
deploy-env: ${{ steps.set-env.outputs.DEPLOY_ENV }}
celery-workers: ${{ steps.set-env.outputs.CELERY_NUM_WORKER }}
cloud-sql-db: ${{ steps.set-env.outputs.CLOUD_SQL_DB }}

steps:
- uses: actions/checkout@v4

- name: Set deployment environment
id: set-env
run: |
if [ "${{ github.ref_name }}" == "main" ]; then
echo "DEPLOY_ENV=--production" >> $GITHUB_OUTPUT
echo "NAMESPACE=ipno-production" >> $GITHUB_OUTPUT
echo "DOCKER_PRETAG=backend-production" >> $GITHUB_OUTPUT
echo "CELERY_NUM_WORKER=4" >> $GITHUB_OUTPUT
echo "CLOUD_SQL_DB=ipno-database-production" >> $GITHUB_OUTPUT
else
echo "DEPLOY_ENV=--staging" >> $GITHUB_OUTPUT
echo "NAMESPACE=ipno-staging" >> $GITHUB_OUTPUT
echo "DOCKER_PRETAG=backend-staging" >> $GITHUB_OUTPUT
echo "CELERY_NUM_WORKER=2" >> $GITHUB_OUTPUT
echo "CLOUD_SQL_DB=ipno-database-staging" >> $GITHUB_OUTPUT
fi

- name: Set image tag
id: set-tag
run: |
echo "tag=${{ steps.set-env.outputs.DOCKER_PRETAG }}-${{ github.run_number }}" >> $GITHUB_OUTPUT

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY_BASE64 }}
Comment on lines +118 to +121
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Decode service key before google-github-actions/auth

The deploy workflow feeds GCLOUD_SERVICE_KEY_BASE64 directly into google-github-actions/auth (credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY_BASE64 }} at build step). The auth action expects the raw service-account JSON, not a base64 blob; the repository docs still instruct storing this secret base64‑encoded (docs/circleci.md, “cat <gcloud-credentials.json> | base64”). With the secret in that documented format, the action will fail to parse the credentials and every step that needs GCP (image push, migrations, deploy) will error out before running.

Useful? React with 👍 / 👎.

Comment on lines +118 to +121
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The credentials_json parameter expects raw JSON content, but based on the CircleCI configuration and documentation (see docs/circleci.md and .circleci/config.yml lines 19, 122), GCLOUD_SERVICE_KEY_BASE64 contains base64-encoded credentials that need to be decoded first.

The Google GitHub Actions auth action cannot directly use base64-encoded credentials. You have two options:

  1. Recommended: Create a new secret GCLOUD_SERVICE_KEY with the decoded JSON content and use it:
credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY }}
  1. Alternative: If you must keep the base64-encoded secret, decode it inline (though this is less clean):
- name: Decode GCloud credentials
  run: echo "${{ secrets.GCLOUD_SERVICE_KEY_BASE64 }}" | base64 -d > /tmp/gcloud-key.json
- name: Authenticate to Google Cloud
  uses: google-github-actions/auth@v2
  with:
    credentials_json: ${{ steps.decode.outputs.key }}

This issue affects all four auth steps in this workflow (lines 118, 152, 181, 210).

Suggested change
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY_BASE64 }}
- name: Decode GCloud credentials
run: echo "${{ secrets.GCLOUD_SERVICE_KEY_BASE64 }}" | base64 -d > /tmp/gcloud-key.json
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ readFile('/tmp/gcloud-key.json') }}

Copilot uses AI. Check for mistakes.

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Configure Docker
run: gcloud auth configure-docker

- name: Build image
run: docker build -t ipno-backend .

- name: Tag and push image
run: |
BRANCH_SLUG="${{ github.ref_name }}"
BRANCH_SLUG="${BRANCH_SLUG//\//-}"

docker tag ipno-backend us.gcr.io/${{ env.GOOGLE_PROJECT_ID }}/ipno-backend:${{ steps.set-tag.outputs.tag }}
docker tag ipno-backend us.gcr.io/${{ env.GOOGLE_PROJECT_ID }}/ipno-backend:$BRANCH_SLUG
docker tag ipno-backend us.gcr.io/${{ env.GOOGLE_PROJECT_ID }}/ipno-backend:${{ steps.set-env.outputs.DOCKER_PRETAG }}-latest

docker push us.gcr.io/${{ env.GOOGLE_PROJECT_ID }}/ipno-backend:${{ steps.set-tag.outputs.tag }}
docker push us.gcr.io/${{ env.GOOGLE_PROJECT_ID }}/ipno-backend:$BRANCH_SLUG
docker push us.gcr.io/${{ env.GOOGLE_PROJECT_ID }}/ipno-backend:${{ steps.set-env.outputs.DOCKER_PRETAG }}-latest

migrate:
needs: build-and-push
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY_BASE64 }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
install_components: 'gke-gcloud-auth-plugin,kubectl'

- name: Get GKE credentials
run: |
gcloud container clusters get-credentials ${{ env.GOOGLE_CLUSTER_NAME }} \
--zone ${{ env.GOOGLE_COMPUTE_ZONE }} \
--project ${{ env.GOOGLE_PROJECT_ID }}

- name: Run migrations
env:
CLOUD_SQL_DATABASE: ${{ needs.build-and-push.outputs.cloud-sql-db }}
run: |
bin/run_spot_job.sh ${{ needs.build-and-push.outputs.deploy-env }} ${{ needs.build-and-push.outputs.image-tag }} migrate

collect-static:
needs: build-and-push
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY_BASE64 }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
install_components: 'gke-gcloud-auth-plugin,kubectl'

- name: Get GKE credentials
run: |
gcloud container clusters get-credentials ${{ env.GOOGLE_CLUSTER_NAME }} \
--zone ${{ env.GOOGLE_COMPUTE_ZONE }} \
--project ${{ env.GOOGLE_PROJECT_ID }}

- name: Collect static
env:
CLOUD_SQL_DATABASE: ${{ needs.build-and-push.outputs.cloud-sql-db }}
run: |
bin/run_spot_job.sh ${{ needs.build-and-push.outputs.deploy-env }} ${{ needs.build-and-push.outputs.image-tag }} collectstatic --no-input

deploy:
needs: [build-and-push, migrate, collect-static]
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCLOUD_SERVICE_KEY_BASE64 }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2
with:
install_components: 'gke-gcloud-auth-plugin,kubectl'

- name: Get GKE credentials
run: |
gcloud container clusters get-credentials ${{ env.GOOGLE_CLUSTER_NAME }} \
--zone ${{ env.GOOGLE_COMPUTE_ZONE }} \
--project ${{ env.GOOGLE_PROJECT_ID }}

- name: Create initial project config
env:
CLOUD_SQL_DATABASE: ${{ needs.build-and-push.outputs.cloud-sql-db }}
run: |
bin/run_spot_job.sh ${{ needs.build-and-push.outputs.deploy-env }} ${{ needs.build-and-push.outputs.image-tag }} init_project_config

- name: Deploy celery
env:
BACKEND_IMAGE_TAG: ${{ needs.build-and-push.outputs.image-tag }}
CELERY_NUM_WORKER: ${{ needs.build-and-push.outputs.celery-workers }}
CLOUD_SQL_DATABASE: ${{ needs.build-and-push.outputs.cloud-sql-db }}
run: |
cat kubernetes/celery.yml | envsubst | kubectl apply -n ${{ needs.build-and-push.outputs.namespace }} -f -

- name: Deploy application
env:
BACKEND_IMAGE_TAG: ${{ needs.build-and-push.outputs.image-tag }}
CLOUD_SQL_DATABASE: ${{ needs.build-and-push.outputs.cloud-sql-db }}
run: |
cat kubernetes/ipno.yml | envsubst | kubectl apply -n ${{ needs.build-and-push.outputs.namespace }} -f -

- name: Setup cronjobs
env:
CLOUD_SQL_DATABASE: ${{ needs.build-and-push.outputs.cloud-sql-db }}
DAILY_TIME: ${{ github.ref_name == 'main' && secrets.PROD_DAILY_TIME || secrets.STAGING_DAILY_TIME }}
run: |
bin/run_cronjob.sh ${{ needs.build-and-push.outputs.deploy-env }} ${{ needs.build-and-push.outputs.image-tag }} run_daily_tasks "$DAILY_TIME"

- name: Verify deployment
run: |
kubectl rollout status deployment/ipno-backend -n ${{ needs.build-and-push.outputs.namespace }} --timeout=300s
Loading
Loading