Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
e1d6e9c
schema and noofentry in prompt datasets queries
psaesha Jun 17, 2026
0f1ad02
Merge pull request #81 from CivicDataLab/prompt_datasets_sdk
psaesha Jun 17, 2026
bb6dc73
Bump SDK version to 0.5.03
github-actions[bot] Jun 17, 2026
6408941
no of entries for file formats other than csv
psaesha Jun 24, 2026
8b747e0
Merge pull request #83 from CivicDataLab/prompt_count_fix
psaesha Jun 25, 2026
1b9636e
Update dataindex command to support multiple file formats for indexing
psaesha Jun 25, 2026
be949dc
Add initialization for y_axis_columns
saqibmanan Jun 25, 2026
cd10ee8
Update type_resource_chart.py
saqibmanan Jun 25, 2026
e48ba82
Update Dockerfile
saqibmanan Jun 25, 2026
04e076e
feat(publications): add Publication data model foundation
Deepthi-Chand Jul 18, 2026
011696a
feat(publications): add GraphQL CRUD, permissions and listing
Deepthi-Chand Jul 18, 2026
5eec625
feat(publications): add content blocks, uploads, YouTube and gated do…
Deepthi-Chand Jul 18, 2026
348b035
feat(publications): add search — document, per-entity + unified, signals
Deepthi-Chand Jul 18, 2026
b345816
feat(publications): link published resources into use cases & collabo…
Deepthi-Chand Jul 18, 2026
92a2f89
feat(publications): add PublicationClient to the SDK
Deepthi-Chand Jul 18, 2026
20195b8
fix(publications): authorize the resource link mutations (IDOR)
Deepthi-Chand Jul 18, 2026
12d3002
docs(publications): backend architecture doc + Layer 5 journey scripts
Deepthi-Chand Jul 18, 2026
25fe49f
fix(publications): address QC blockers (link-field leak, search defau…
Deepthi-Chand Jul 18, 2026
850e7e6
feat(publications): add resourceTypes query for the create/edit form
Deepthi-Chand Jul 20, 2026
e54e74c
docs(publications): mark frontend implemented in backend arch doc
Deepthi-Chand Jul 20, 2026
0ca3c0d
fix: health check now returns 503 when a dependency is unhealthy
saqibmanan Aug 17, 2026
ed4b116
test: cover health check status codes and git_sha field
saqibmanan Aug 17, 2026
029689a
build: plumb GIT_COMMIT_SHA into the image
saqibmanan Aug 17, 2026
4d82139
fix: drop makemigrations from the container boot path
saqibmanan Aug 17, 2026
2ada230
fix: always run the CloudFormation deploy, don't guess from head_commit
saqibmanan Aug 17, 2026
abeb9d1
fix: stop deploy-app running on a cancelled workflow
saqibmanan Aug 17, 2026
4761b41
feat: run migrations as an explicit ECS one-off task before deploy
saqibmanan Aug 17, 2026
dd9a68c
chore: drop the now-dead migrations-directory scaffolding
saqibmanan Aug 17, 2026
5234618
feat: gate the ECS deploy on a real smoke-test run
saqibmanan Aug 17, 2026
ba349a6
feat: auto-rollback the ECS service when smoke tests fail
saqibmanan Aug 17, 2026
8c50ddd
Merge pull request #108 from CivicDataLab/fix/health-check-status-code
saqibmanan Aug 18, 2026
7cff213
Merge pull request #111 from CivicDataLab/feat/ecs-smoke-gate
saqibmanan Aug 18, 2026
011d66e
Merge pull request #112 from CivicDataLab/feat/ecs-explicit-migration…
saqibmanan Aug 18, 2026
16e7318
Merge pull request #104 from CivicDataLab/feature/resources
saqibmanan Aug 26, 2026
558211d
fix: make backend deployable by image, not just local build
saqibmanan Aug 18, 2026
1484ae1
feat: opt-in hot-reload overlay for local dev
saqibmanan Aug 18, 2026
470b4cf
feat: add EC2 deploy pipeline for the real dev backend host
saqibmanan Aug 18, 2026
3777b12
Fix dev EC2 deploy pipeline before its first real run
saqibmanan Sep 1, 2026
404ec35
Merge pull request #113 from CivicDataLab/feat/ec2-deploy-pipeline
saqibmanan Sep 1, 2026
068e4ea
Add missing Buildx setup step with docker-container driver
saqibmanan Sep 1, 2026
782de94
Fix slow cache export: drop mode=max, bump build timeout to 45m
saqibmanan Sep 1, 2026
ba02803
Create logs/ dir in image: LOGGING writes there, dir never existed
saqibmanan Sep 1, 2026
d061d78
Retry the sanity-check image pull, GHCR rate-limits briefly after push
saqibmanan Sep 1, 2026
4531149
Retry docker pull in ci-deploy.sh/ci-rollback.sh, same GHCR rate-limit
saqibmanan Sep 1, 2026
ecbfa86
Give GHCR rate-limit retries real runway before next attempt
saqibmanan Sep 1, 2026
ce1d0d3
Wait 90s before the first sanity-check pull, not just retry after fai…
saqibmanan Sep 1, 2026
f4f226f
Bump deploy/rollback command_timeout to 40m, jobs to 50m
saqibmanan Sep 2, 2026
044f0d8
Stop telemetry from gating /health/'s overall status
saqibmanan Sep 2, 2026
1ead789
Ensure every search index exists on deploy, not just populated ones
saqibmanan Sep 2, 2026
8fd1e56
Skip the telemetry probe entirely when TELEMETRY_URL is unset
saqibmanan Sep 2, 2026
091dec9
Mount uploaded media into the container -- image cutover dropped it
saqibmanan Sep 2, 2026
2f6d961
fix(sdk): stop hardcoding /auth in Keycloak URLs
saqibmanan Sep 2, 2026
619bef0
Merge pull request #129 from CivicDataLab/fix/sdk-keycloak-relative-path
saqibmanan Sep 2, 2026
c7ca820
Bump SDK version to 0.5.04
github-actions[bot] Sep 2, 2026
3181576
fix(sdk): DataSpaceClient did not forward keycloak_base_path to AuthC…
saqibmanan Sep 2, 2026
57dfea7
Merge pull request #130 from CivicDataLab/fix/sdk-client-forward-base…
saqibmanan Sep 2, 2026
50ac6b0
Bump SDK version to 0.5.05
github-actions[bot] Sep 2, 2026
986828b
ci: pass KEYCLOAK_CLIENT_SECRET to the smoke workflow
saqibmanan Sep 2, 2026
a413148
Merge pull request #133 from CivicDataLab/ci/pass-keycloak-secret-to-…
saqibmanan Sep 2, 2026
8a72b2f
fix: serve with multiple uvicorn workers to stop DB exhaustion
saqibmanan Sep 3, 2026
384c0a9
perf: stop making three Keycloak round-trips per login
saqibmanan Sep 3, 2026
826c831
fix: make the health check detect connection exhaustion
saqibmanan Sep 3, 2026
d7fd135
fix: size uvicorn workers to the memory actually available
saqibmanan Sep 3, 2026
f134e01
Merge pull request #135 from CivicDataLab/fix/backend-concurrency-db-…
saqibmanan Sep 3, 2026
8fda535
fix: stop rewriting the user row on every login
saqibmanan Sep 3, 2026
1959a80
Merge pull request #136 from CivicDataLab/fix/user-save-row-lock-cont…
saqibmanan Sep 3, 2026
299cf78
fix: install CPU-only torch so the image can actually be deployed
saqibmanan Sep 3, 2026
14364b1
Merge pull request #137 from CivicDataLab/fix/cpu-only-torch-image-size
saqibmanan Sep 3, 2026
340fa43
ci: fail the build if the image exceeds a size ceiling
saqibmanan Sep 3, 2026
0f41b4e
Merge pull request #138 from CivicDataLab/ci/image-size-ceiling
saqibmanan Sep 3, 2026
965ad21
chore: remove the dead DRF throttle config that misled a diagnosis
saqibmanan Sep 3, 2026
4399ed9
Merge pull request #143 from CivicDataLab/fix/remove-dead-throttle-co…
saqibmanan Sep 3, 2026
64ee72b
Merge dev into main: reconcile 3 months of divergence since the June …
saqibmanan Sep 10, 2026
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
336 changes: 336 additions & 0 deletions .github/workflows/deploy-backend.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
# Dev CD pipeline: build once in CI, deploy that exact artifact, verify it
# with real API tests, and roll the image back automatically if verification
# fails.
#
# build -> deploy -> smoke-tests -> { rollback-on-smoke-failure | finalize }
#
# Deploys are pinned to an immutable digest, never a tag: `dev` is a moving
# pointer, so "roll back to the previous dev tag" is not a thing you can
# express. The host records the digest it was running before each deploy in
# .deploy/previous_image, which is what makes cross-job rollback possible at
# all (rollback runs on a different runner, so no shell state survives).
#
# MIGRATIONS ARE NOT ROLLED BACK. A rollback restores the previous image and
# says so loudly; the schema stays forward. Migrations must therefore be
# written additively / backward-compatibly, so the previous image can still
# run against the newer schema. This is a policy constraint on how you write
# migrations, not something this pipeline can enforce for you.
#
# The target host is not a standalone checkout of this repo -- it's the
# DataExBackend submodule inside the separate CivicDataLab/DataExchange
# superproject, already running its own self-contained compose project
# there (confirmed via `docker ps` compose labels: project=dataexbackend,
# workdir=~/DataExchange/DataExBackend). This pipeline only ever touches
# that directory; it never touches the DataExchange repo or its top-level
# compose file.
#
# Several structural choices here mirror ParakhAPI's proven dev CD pipeline
# (deploy-parakh-api-dev.yml in CivicDataLab/ParakhAI-Backend) -- see the
# notes at each site before "simplifying" them. In particular:
# appleboy/ssh-action's inline multi-line `script:` input was found there to
# reproducibly fail with a spurious "syntax error near unexpected token ';'"
# for reasons never fully root-caused (confirmed the script text itself was
# valid bash both locally and on the real target host every time -- the
# corruption happened somewhere in the action's own transport). scp-action
# never had that problem. So no SSH step here ever carries more than a
# single trivial invocation line; all real logic lives in scripts/ci-*.sh,
# shipped as files.

name: Deploy Backend to Dev EC2

on:
push:
branches: ['dev']
workflow_dispatch:
inputs:
force_smoke_failure:
description: "Deliberately fail the smoke gate, to exercise the rollback path. Testing only."
type: boolean
required: false
default: false

# Queue overlapping deploys rather than cancelling: a cancelled run mid-deploy
# could leave .deploy/ state and the running containers disagreeing.
concurrency:
group: dataspace-backend-dev-deploy
cancel-in-progress: false

env:
REGISTRY: ghcr.io
IMAGE_NAME: civicdatalab/dataspacebackend
DEPLOY_PATH: ${{ vars.DEPLOY_PATH || 'DataExchange/DataExBackend' }}

jobs:
build:
name: Build and push image
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
packages: write
outputs:
image_ref: ${{ steps.ref.outputs.image_ref }}
steps:
- name: Checkout code
uses: actions/checkout@v4

# driver: docker-container explicitly, not the ambient default --
# this runner's default buildx context reports driver "docker",
# which does not support cache export (cache-to: type=gha below
# fails outright without this).
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
with:
driver: docker-container

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

- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
build-args: |
GIT_COMMIT_SHA=${{ github.sha }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
# mode=min (the default), not max: this Dockerfile is single-stage
# (no multi-stage FROM ... AS builder), so mode=max's extra
# intermediate-stage caching buys nothing here -- it only added a
# slow cache-export step that got stuck writing one large layer
# (chromium + apt packages) and blew the job's timeout on the
# first (cold-cache) run, even though the actual image build and
# push had already completed successfully by that point.
cache-from: type=gha
cache-to: type=gha

- name: Pin image reference by digest
id: ref
run: |
echo "image_ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}" >> "$GITHUB_OUTPUT"

# A size ceiling, because image size is a deploy-time failure mode here
# and nothing else surfaces it. The image reached 14.1GB - 6.6GB of it
# CUDA runtime on a GPU-less box - and `docker pull` then ran past the
# deploy step's 40 minute command_timeout, so deploys failed with
# "Run Command Timeout" and no indication of why. Builds stayed green
# throughout; the cost only appeared on the host.
#
# Fails the build rather than the deploy, so the feedback lands on the
# PR that caused it instead of an hour later on a broken environment.
# Raise MAX_IMAGE_GB deliberately if the image legitimately grows.
- name: Enforce image size ceiling
env:
MAX_IMAGE_GB: 8
run: |
set -euo pipefail
docker pull -q "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
BYTES=$(docker image inspect \
"${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}" \
--format "{{.Size}}")
GB=$(awk -v b="$BYTES" 'BEGIN{printf "%.2f", b/1024/1024/1024}')
echo "Image size: ${GB} GB (ceiling ${MAX_IMAGE_GB} GB)"
echo "- image size: **${GB} GB** (ceiling ${MAX_IMAGE_GB} GB)" >> "$GITHUB_STEP_SUMMARY"
if awk -v g="$GB" -v m="$MAX_IMAGE_GB" 'BEGIN{exit !(g > m)}'; then
echo "::error::Image is ${GB} GB, over the ${MAX_IMAGE_GB} GB ceiling. Pulling this on the deploy host will run past the SSH command_timeout and the deploy will fail. Check for CUDA/GPU wheels (nvidia/*, torch, triton) being pulled in place of CPU builds."
exit 1
fi

- name: Sanity-check the built image
# Cheap, real gate: catches import errors and bad settings before
# anything touches the host. Live testing showed every layer
# downloading successfully every single retry, with toomanyrequests
# firing right after the LAST layer completes (the final
# manifest/config fetch) -- consistently, even across 10 attempts
# 30s apart. That pattern looks like GHCR hasn't finished settling
# a just-pushed manifest yet, not a generic quota, so this waits
# up front before the first attempt rather than only reacting
# after failures.
run: |
sleep 90
for attempt in 1 2 3 4 5 6 7 8 9 10; do
if docker run --rm \
-e SECRET_KEY=ci-sanity-check-not-a-real-key \
-e URL_WHITELIST=http://localhost \
-e DB_ENGINE=django.db.backends.sqlite3 \
--entrypoint python \
"${{ steps.ref.outputs.image_ref }}" \
manage.py check; then
exit 0
fi
echo "attempt $attempt failed, retrying in 30s..."
sleep 30
done
echo "::error::Sanity check failed after 10 attempts."
exit 1

deploy:
name: Deploy to EC2
needs: build
runs-on: ubuntu-latest
environment: development
# 50m: must comfortably exceed the Deploy step's own 40m
# command_timeout (see that step's comment for why it's 40m).
timeout-minutes: 50
# packages: read -- GITHUB_TOKEN needs this explicitly granted to pull
# from GHCR; it isn't covered by the repo's default token permissions.
permissions:
contents: read
packages: read
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Write GHCR token file
run: printf '%s' "${{ secrets.GITHUB_TOKEN }}" > .ghcr_token

# Ship the compose files rather than relying on the DataExBackend
# submodule pointer inside the separate DataExchange repo -- bumping
# that pointer is a change to a different, shared repo and out of
# scope here. Shipping the file directly keeps this pipeline
# self-contained.
- name: Ship deploy files to host
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: docker-compose.yml,docker-compose.hotreload.yml,scripts/ci-deploy.sh,.ghcr_token
target: ${{ env.DEPLOY_PATH }}

- name: Deploy
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script_stop: true
# 40m: live testing showed one large layer (chromium + its X11
# libs, almost certainly) take ~15 minutes just downloading via
# Docker's own internal per-layer retry (not our retry loop --
# this is a single docker pull invocation struggling), then hang
# with zero output for several more minutes afterward (most
# likely extraction stalling under memory pressure -- this host
# runs Postgres, two separate Elasticsearch instances, Redis,
# Keycloak, and telemetry tooling alongside the app, confirmed
# via `free -h`: ~169Mi truly free, 828Mi already swapped). The
# previous 20m ceiling cut it off mid-extraction, right after
# the layer had already finished downloading.
command_timeout: 40m
# sudo: docker/docker compose require it on this host (confirmed
# passwordless -- sudo -n succeeds non-interactively).
script: cd "$HOME/${{ env.DEPLOY_PATH }}" && sudo bash scripts/ci-deploy.sh "${{ needs.build.outputs.image_ref }}" "${{ vars.HEALTH_CHECK_URL || 'http://127.0.0.1:8000/health/' }}" "${{ github.actor }}"

smoke-tests:
name: Smoke Tests
needs: deploy
# No `environment:` here -- GitHub rejects the entire workflow file at
# parse time if a `uses:` job declares one. Consequence: this job also
# cannot see environment-scoped vars, which is why api_base_url comes
# from a repo-level var.
uses: CivicDataLab/CivicDataSpace-test/.github/workflows/run-smoke.yml@CI
with:
api_base_url: ${{ vars.DEV_API_BASE_URL }}
# An obviously-wrong sentinel SHA fails the reusable workflow's own
# deployed-SHA-vs-live-/health/ assertion on purpose, for the
# force_smoke_failure test path.
deployed_sha: ${{ (inputs.force_smoke_failure == true && 'forced-failure-sentinel') || github.sha }}
min_passed: ${{ inputs.force_smoke_failure && 999 || 1 }}
secrets:
HOME_URL_DEV: ${{ secrets.HOME_URL_DEV }}
TEST_EMAIL_1: ${{ secrets.TEST_EMAIL_1 }}
TEST_PASSWORD_1: ${{ secrets.TEST_PASSWORD_1 }}
TEST_EMAIL_2: ${{ secrets.TEST_EMAIL_2 }}
TEST_PASSWORD_2: ${{ secrets.TEST_PASSWORD_2 }}
# api-smoke authenticates against Keycloak via ROPC. `dataspace` is a
# confidential client, so without this the token request returns 401
# and the job fails its preflight.
KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }}

rollback-on-smoke-failure:
Comment on lines +232 to +257
name: Rollback (smoke tests failed)
# `deploy` must be in needs: for needs.deploy.result to resolve here.
needs: [deploy, smoke-tests]
# failure()/success() builtins rather than needs.smoke-tests.result --
# both are false on cancellation, which is the behaviour we want;
# if: always() would ignore cancellation entirely.
if: failure() && needs.deploy.result == 'success'
runs-on: ubuntu-latest
environment: development
# 50m: must comfortably exceed the Restore previous image step's own
# 40m command_timeout (see the Deploy job's equivalent step for why).
timeout-minutes: 50
permissions:
contents: read
packages: read
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Write GHCR token file
run: printf '%s' "${{ secrets.GITHUB_TOKEN }}" > .ghcr_token

- name: Ship rollback files to host
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: scripts/ci-rollback.sh,.ghcr_token
target: ${{ env.DEPLOY_PATH }}

- name: Restore previous image
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script_stop: true
# 40m -- see the Deploy job's equivalent step for why (one large
# layer took ~15 min to download plus several more to extract
# under this host's memory pressure in live testing).
command_timeout: 40m
script: cd "$HOME/${{ env.DEPLOY_PATH }}" && sudo bash scripts/ci-rollback.sh "${{ vars.HEALTH_CHECK_URL || 'http://127.0.0.1:8000/health/' }}" "${{ github.actor }}"

- name: Mark this run as failed
# The mitigation succeeded, but the run must still read RED -- a bad
# deploy that silently self-heals is a bad deploy nobody investigates.
run: |
echo "::error::Smoke tests failed after deploy; the image was rolled back. Migrations were NOT reverted -- see the rollback step's log."
exit 1

finalize-deploy:
name: Finalize Deploy
needs: [deploy, smoke-tests]
if: success()
runs-on: ubuntu-latest
environment: development
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Ship finalize script to host
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
source: scripts/ci-finalize.sh
target: ${{ env.DEPLOY_PATH }}

- name: Prune to current + previous image
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ vars.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script_stop: true
script: cd "$HOME/${{ env.DEPLOY_PATH }}" && sudo bash scripts/ci-finalize.sh
Comment on lines +310 to +336
Loading
Loading