Skip to content

Commit

Permalink
add action to create PR-specific apps (#116)
Browse files Browse the repository at this point in the history
Co-authored-by: Raphael Paul Laude <[email protected]>
  • Loading branch information
mariogiampieri and raphaellaude authored Oct 21, 2024
1 parent 401341f commit 535da85
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 19 deletions.
173 changes: 173 additions & 0 deletions .github/workflows/fly-deploy-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
name: Fly Deploy Disctictr V2 PR
on:
pull_request:
types: [opened, reopened, synchronize, closed]
env:
FLY_API_TOKEN: ${{ secrets.FLY_ORG_TOTKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
FLY_REGION: "iad"
FLY_ORG: "mggg"
jobs:
pr_review_app:
runs-on: ubuntu-latest

concurrency:
group: pr-${{ github.event.number }}

environment:
name: pr-${{ github.event.number }}

steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master

# Set up common variables
- name: Set Variables
run: |
echo "db_name=pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-db" >> $GITHUB_ENV
echo "api_app_name=pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api" >> $GITHUB_ENV
echo "frontend_app_name=pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-app" >> $GITHUB_ENV
- name: Destroy Resources
if: github.event.action == 'closed'
run: |
app_name="pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api"
frontend_app_name="pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-app"
db_name="pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-db"
echo "Destroying app $app_name"
flyctl apps destroy "$app_name"
echo "Destroying frontend app $frontend_app_name"
flyctl apps destroy "$frontend_app_name"
echo "Destroying database $db_name"
flyctl postgres destroy "$db_name"
echo "Resources for PR #${{ github.event.number }} have been destroyed."
env:
FLY_API_TOKEN: ${{ secrets.FLY_ORG_TOKEN }}

# fork new db from existing db if it doesn't already exist
- name: Fork From DB
id: fork-db
run: |
if flyctl postgres list | grep -q pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-db; then
echo "DB already exists"
else
flyctl postgres create \
--name pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-db \
--region ewr \
--initial-cluster-size 1 \
--vm-size shared-cpu-2x \
-p ${{ secrets.FLY_PR_PG_PASSWORD }} \
--org mggg \
--fork-from districtr-v2-db
if [ $? -eq 0 ]; then
echo "Database created successfully."
else
echo "Failed to create database."
exit 1
fi
fi
echo "::set-output name=name::pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-db"
# manually launch and deploy the api app
- name: launch api app
run: |
app="pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api"
db_name="pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-db"
config="fly.toml"
# Check if the app exists
if flyctl apps list | grep -q "$app"; then
echo "App $app already exists. Skipping launch."
else
flyctl launch \
--no-deploy --copy-config --name "$app"
echo "App $app launched successfully."
fi
# Output app name for use in the deploy step
echo "api_app_name=$app" >> $GITHUB_ENV
working-directory: backend

- name: deploy api app
run: |
flyctl deploy \
--config fly.toml --app "pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api" \
--strategy immediate '--ha=false' --vm-cpu-kind shared --vm-cpus 1 --vm-memory 256
flyctl secrets set \
-a pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api \
POSTGRES_SCHEME="postgresql+psycopg" \
POSTGRES_SERVER="pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-db.flycast" \
POSTGRES_USER="postgres" \
POSTGRES_PASSWORD=${{ secrets.FLY_PR_PG_PASSWORD }} \
POSTGRES_DB="districtr_v2_api" \
BACKEND_CORS_ORIGINS="https://pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-app.fly.dev,https://districtr-v2-frontend.fly.dev" \
DATABASE_URL="postgresql://postgres:${{ secrets.FLY_PR_PG_PASSWORD }}@${{ steps.fork-db.outputs.name }}.flycast:5432/districtr_v2_api?sslmode=disable&options=-csearch_path%3Dpublic"
echo "set $app secrets"
working-directory: backend

- name: Check and Launch Frontend App
id: launch
run: |
app="pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-app"
api_app="pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api"
config="fly.toml"
# Check if the app exists
if flyctl apps list | grep -q "$app"; then
echo "App $app already exists. Skipping launch."
else
echo "Launching app $app."
# Run the flyctl launch command
flyctl launch \
--no-deploy --copy-config --name "pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-app" \
--build-arg NEXT_PUBLIC_API_URL="https://pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api.fly.dev" \
--build-arg NEXT_PUBLIC_S3_BUCKET_URL=${{ secrets.NEXT_PUBLIC_S3_BUCKET_URL }} \
--build-secret NEXT_PUBLIC_API_URL="https://pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api.fly.dev" \
--build-secret NEXT_PUBLIC_S3_BUCKET_URL=${{ secrets.NEXT_PUBLIC_S3_BUCKET_URL }}

echo "App $app launched successfully."
fi

# Output app name for use in the deploy step
echo "frontend_app_name=$app" >> $GITHUB_ENV
working-directory: app

- name: Deploy Frontend App
run: |
app_name="pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-app"
config="fly.toml"
# Deploy the app
flyctl deploy --config "$config" --app "pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-app" \
--build-arg NEXT_PUBLIC_API_URL="https://pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api.fly.dev" \
--build-arg NEXT_PUBLIC_S3_BUCKET_URL=${{ secrets.NEXT_PUBLIC_S3_BUCKET_URL }} \
--build-secret NEXT_PUBLIC_API_URL="https://pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api.fly.dev" \
--build-secret NEXT_PUBLIC_S3_BUCKET_URL=${{ secrets.NEXT_PUBLIC_S3_BUCKET_URL }} \
--strategy immediate '--ha=false' \
--vm-cpu-kind shared --vm-cpus 1 --vm-memory 256
working-directory: app

# set secrets for f/e app
- name: Set App Secrets
run: |
flyctl secrets set \
-a "pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-app" \
NEXT_PUBLIC_API_URL="https://pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api.fly.dev" \
NEXT_PUBLIC_S3_BUCKET_URL=${{secrets.NEXT_PUBLIC_S3_BUCKET_URL}}
- name: run database migrations
run: |
flyctl ssh console \
--app "pr-${{ github.event.number }}-${{ github.repository_owner }}-${{ github.event.repository.name }}-api" \
--command "alembic upgrade head"
working-directory: backend

# provision appropriately, do not over-resource
# make volume like two gb
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ When reviewing a PR, use the "HIPPO" method to provide feedback:
| **PP** - Personal preference | Possible changes requested. Something the reviewer would do but is non-blocking. |
| **O** - Opinion | Comment for discussion. Non-blocking. Could be a bigger idea that's relevant to the PR. |

Open PRs will spin up a set of test apps for review, following the convention `pr-<pr number>-districtr-districtr-v2-<sub app>`, and would be available for testing at e.g. `https://pr-116-districtr-districtr-v2-app.fly.dev/map`. This behavior can be tweaks via `.github/workflows/fly-deploy-pr.yml`

Updates to PRs will trigger updates to staging apps, including re-running of migrations on the testing db.

### CI/CD

Deployments are managed with GitHub Actions.
9 changes: 9 additions & 0 deletions app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ RUN npm ci --include=dev
# Copy application code
COPY --link . .

# add args for build-time variables from github actions secrets
ARG NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ARG NEXT_PUBLIC_S3_BUCKET_URL=${NEXT_PUBLIC_S3_BUCKET_URL}

ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
ENV NEXT_PUBLIC_S3_BUCKET_URL=${NEXT_PUBLIC_S3_BUCKET_URL}



# Build application
RUN npm run build

Expand Down
2 changes: 1 addition & 1 deletion app/fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ primary_region = 'ewr'
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = 'stop'
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
processes = ['app']
Expand Down
4 changes: 1 addition & 3 deletions app/src/app/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { Protocol } from "pmtiles";
import type { MutableRefObject } from "react";
import React, { useEffect, useRef } from "react";
import { MAP_OPTIONS } from "../constants/configuration";
import {
mapEvents,
} from "../utils/events/mapEvents";
import { mapEvents } from "../utils/events/mapEvents";
import { INTERACTIVE_LAYERS } from "../constants/layers";
import { useMapStore } from "../store/mapStore";

Expand Down
26 changes: 13 additions & 13 deletions app/src/app/utils/api/apiHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useMapStore } from "@/app/store/mapStore";

export const FormatAssignments = () => {
const assignments = Array.from(
useMapStore.getState().zoneAssignments.entries(),
useMapStore.getState().zoneAssignments.entries()
).map(
// @ts-ignore
([geo_id, zone]: [string, number]): {
Expand All @@ -16,7 +16,7 @@ export const FormatAssignments = () => {
useMapStore.getState().mapDocument?.document_id.toString() ?? "",
geo_id,
zone,
}),
})
);
return assignments;
};
Expand Down Expand Up @@ -76,7 +76,7 @@ export interface DocumentCreate {
}

export const createMapDocument: (
document: DocumentCreate,
document: DocumentCreate
) => Promise<DocumentObject> = async (document: DocumentCreate) => {
return await axios
.post(`${process.env.NEXT_PUBLIC_API_URL}/api/create_document`, {
Expand All @@ -93,7 +93,7 @@ export const createMapDocument: (
* @returns Promise<DocumentObject>
*/
export const getDocument: (
document_id: string,
document_id: string
) => Promise<DocumentObject> = async (document_id: string) => {
if (document_id) {
return await axios
Expand All @@ -107,12 +107,12 @@ export const getDocument: (
};

export const getAssignments: (
mapDocument: DocumentObject,
mapDocument: DocumentObject
) => Promise<Assignment[]> = async (mapDocument) => {
if (mapDocument) {
return await axios
.get(
`${process.env.NEXT_PUBLIC_API_URL}/api/get_assignments/${mapDocument.document_id}`,
`${process.env.NEXT_PUBLIC_API_URL}/api/get_assignments/${mapDocument.document_id}`
)
.then((res) => {
return res.data;
Expand Down Expand Up @@ -140,12 +140,12 @@ export interface ZonePopulation {
* @returns Promise<ZonePopulation[]>
*/
export const getZonePopulations: (
mapDocument: DocumentObject,
mapDocument: DocumentObject
) => Promise<ZonePopulation[]> = async (mapDocument) => {
if (mapDocument) {
return await axios
.get(
`${process.env.NEXT_PUBLIC_API_URL}/api/document/${mapDocument.document_id}/total_pop`,
`${process.env.NEXT_PUBLIC_API_URL}/api/document/${mapDocument.document_id}/total_pop`
)
.then((res) => {
return res.data;
Expand All @@ -163,11 +163,11 @@ export const getZonePopulations: (
*/
export const getAvailableDistrictrMaps: (
limit?: number,
offset?: number,
offset?: number
) => Promise<DistrictrMap[]> = async (limit = 10, offset = 0) => {
return await axios
.get(
`${process.env.NEXT_PUBLIC_API_URL}/api/gerrydb/views?limit=${limit}&offset=${offset}`,
`${process.env.NEXT_PUBLIC_API_URL}/api/gerrydb/views?limit=${limit}&offset=${offset}`
)
.then((res) => {
return res.data;
Expand All @@ -185,7 +185,7 @@ export interface Assignment {
document_id: string;
geo_id: string;
zone: number;
parent_path?: string
parent_path?: string;
}

/**
Expand All @@ -203,7 +203,7 @@ export interface AssignmentsCreate {
* @returns server object containing the updated assignments per geoid
*/
export const patchUpdateAssignments: (
assignments: Assignment[],
assignments: Assignment[]
) => Promise<AssignmentsCreate> = async (assignments: Assignment[]) => {
return await axios
.patch(`${process.env.NEXT_PUBLIC_API_URL}/api/update_assignments`, {
Expand Down Expand Up @@ -241,7 +241,7 @@ export const patchShatterParents: (params: {
`${process.env.NEXT_PUBLIC_API_URL}/api/update_assignments/${document_id}/shatter_parents`,
{
geoids: geoids,
},
}
)
.then((res) => {
return res.data;
Expand Down
6 changes: 4 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,10 @@ async def update_assignments(
async def shatter_parent(
document_id: str, data: GEOIDS, session: Session = Depends(get_session)
):
stmt = text("""SELECT *
FROM shatter_parent(:input_document_id, :parent_geoids)""").bindparams(
stmt = text(
"""SELECT *
FROM shatter_parent(:input_document_id, :parent_geoids)"""
).bindparams(
bindparam(key="input_document_id", type_=UUIDType),
bindparam(key="parent_geoids", type_=ARRAY(String)),
)
Expand Down

0 comments on commit 535da85

Please sign in to comment.