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
134 changes: 134 additions & 0 deletions .github/workflows/uat.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
name: UAT Deploy

on:
push:
branches: [main]

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.changes.outputs.backend }}
steps:
- uses: actions/checkout@v4

- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
backend:
- 'apps/backend/**'

backend-deploy:
needs: detect-changes
if: needs.detect-changes.outputs.backend == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write

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

- name: Checkout infra repo
uses: actions/checkout@v4
with:
repository: Dev-Card/devcard-infra
path: infra
token: ${{ secrets.INFRA_REPO_TOKEN }}

- name: Authenticate to GCP
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}

- name: Setup gcloud
uses: google-github-actions/setup-gcloud@v2

- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker asia-south1-docker.pkg.dev

- name: Set image tag
id: tag
run: echo "sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: apps/backend/package-lock.json

- name: Install dependencies
working-directory: apps/backend
run: npm ci

- name: Run tests
working-directory: apps/backend
run: npm test

- name: Build and push Docker image
run: |
docker build \
-f docker/backend.Dockerfile \
-t asia-south1-docker.pkg.dev/devcard-prod/devcard/backend:${{ steps.tag.outputs.sha }} \
.
docker push asia-south1-docker.pkg.dev/devcard-prod/devcard/backend:${{ steps.tag.outputs.sha }}

- name: Get GKE credentials
uses: google-github-actions/get-gke-credentials@v2
with:
cluster_name: devcard-cluster
location: asia-south1

- name: Run Prisma migrations
run: |
cat <<EOF | kubectl apply -f -
apiVersion: batch/v1
kind: Job
metadata:
name: prisma-migrate-${{ steps.tag.outputs.sha }}
namespace: uat
spec:
ttlSecondsAfterFinished: 300
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: asia-south1-docker.pkg.dev/devcard-prod/devcard/backend:${{ steps.tag.outputs.sha }}
command: ["npx", "prisma", "migrate", "deploy"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: devcard-secret
key: database-url
EOF
kubectl wait --for=condition=complete \
job/prisma-migrate-${{ steps.tag.outputs.sha }} \
-n uat --timeout=120s

- name: Update image tag in kustomize
run: |
cd infra/k8s/overlays/uat
kustomize edit set image IMAGE_TAG_PLACEHOLDER=asia-south1-docker.pkg.dev/devcard-prod/devcard/backend:${{ steps.tag.outputs.sha }}

- name: Commit and push image tag to infra repo
run: |
cd infra
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add k8s/overlays/uat/kustomization.yaml
git commit -m "chore: update uat backend image to ${{ steps.tag.outputs.sha }}"
git push

- name: Deploy to UAT
run: kubectl apply -k infra/k8s/overlays/uat

- name: Wait for rollout
run: |
kubectl rollout status deployment/backend \
-n uat --timeout=5m
11 changes: 6 additions & 5 deletions apps/backend/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import process from 'node:process';

Check failure on line 1 in apps/backend/src/env.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Unexpected use of 'require("process")'. Use the global variable 'process' instead
import path from 'node:path';

Check failure on line 2 in apps/backend/src/env.ts

View workflow job for this annotation

GitHub Actions / backend-ci

`node:path` import should occur before import of `node:process`
import { fileURLToPath } from 'node:url';

Check failure on line 3 in apps/backend/src/env.ts

View workflow job for this annotation

GitHub Actions / backend-ci

There should be at least one empty line between import groups
import dotenv from 'dotenv';
Expand All @@ -8,9 +8,10 @@
const result = dotenv.config({ path: envPath });

if (result.error) {
// Keep failing fast but avoid leaking via console in production code paths.
// This file runs before the Fastify logger is available; throw so the process exits.
throw result.error;
} else {
// .env loaded successfully
if (process.env.NODE_ENV === 'production') {
// In production, env vars come from Kubernetes secrets — .env file is not expected.
} else {
// In development, .env is required. Fail fast.
throw result.error;
}
}
2 changes: 1 addition & 1 deletion apps/backend/src/routes/cards.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as cardService from '../services/cardService'
import * as cardService from '../services/cardService.js'
import { handleDbError } from '../utils/error.util.js';
import { createCardSchema, updateCardSchema } from '../utils/validators.js';

Expand Down Expand Up @@ -69,7 +69,7 @@

// ─── Create Card ───

app.post('/', async (request: FastifyRequest<{ Body: CreateCardBody }>, reply: FastifyReply): Promise<Card | void> => {

Check warning on line 72 in apps/backend/src/routes/cards.ts

View workflow job for this annotation

GitHub Actions / backend-ci

'Card' is an 'error' type that acts as 'any' and overrides all other types in this union type
const userId = (request.user as { id: string }).id;
const parsed = createCardSchema.safeParse(request.body);

Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/routes/event.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

Check failure on line 1 in apps/backend/src/routes/event.ts

View workflow job for this annotation

GitHub Actions / backend-ci

There should be at least one empty line between import groups
import { createEventSchema, joinEventSchema} from '../validations/event.validation';
import { createEventSchema, joinEventSchema} from '../validations/event.validation.js';

Check failure on line 2 in apps/backend/src/routes/event.ts

View workflow job for this annotation

GitHub Actions / backend-ci

'joinEventSchema' is defined but never used. Allowed unused vars must match /^_/u

Check failure on line 2 in apps/backend/src/routes/event.ts

View workflow job for this annotation

GitHub Actions / backend-ci

`../validations/event.validation.js` import should occur before type import of `fastify`

Check failure on line 2 in apps/backend/src/routes/event.ts

View workflow job for this annotation

GitHub Actions / backend-ci

There should be no empty line within import group

import {generateUniqueSlug} from '../utils/slug'
import {generateUniqueSlug} from '../utils/slug.js'

Check failure on line 4 in apps/backend/src/routes/event.ts

View workflow job for this annotation

GitHub Actions / backend-ci

`../utils/slug.js` import should occur before type import of `fastify`


type EventDetails = {
Expand Down Expand Up @@ -57,12 +57,12 @@
}[];
}

export async function eventRoutes(app:FastifyInstance) {

Check warning on line 60 in apps/backend/src/routes/event.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
app.post('/', { preHandler: [async (request, reply) => {
const server = request.server as any;
if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return }
if (typeof (app as any).authenticate === 'function') { await (app as any).authenticate(request, reply); return }
try { await request.jwtVerify() } catch (e) { reply.status(401).send({ error: 'Unauthorized' }) }

Check failure on line 65 in apps/backend/src/routes/event.ts

View workflow job for this annotation

GitHub Actions / backend-ci

'e' is defined but never used. Allowed unused caught errors must match /^_/u
}] }, async (request: FastifyRequest<{
Body: {
name: string,
Expand All @@ -80,7 +80,7 @@

const {name, description, startDate, endDate, isPublic ,location} = parsed.data

let finalSlug = await generateUniqueSlug(name, async(slug) => {

Check failure on line 83 in apps/backend/src/routes/event.ts

View workflow job for this annotation

GitHub Actions / backend-ci

'finalSlug' is never reassigned. Use 'const' instead
const existing = await app.prisma.event.findUnique({where: {slug : slug}})

return !!existing
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/routes/public.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as publicService from '../services/publicService';
import * as publicService from '../services/publicService.js';
import { generateQRBuffer, generateQRSvg } from '../utils/qr.js';

import type { FastifyContextConfig, FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/src/routes/team.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {Prisma, TeamRole } from '@prisma/client';
import QRCode from 'qrcode'

import {generateUniqueSlug} from '../utils/slug'
import { createTeamScehma,inviteMembers,updateTeam } from '../validations/team.validation';
import {generateUniqueSlug} from '../utils/slug.js'
import { createTeamScehma,inviteMembers,updateTeam } from '../validations/team.validation.js';

import type {PlatformLink, PublicProfile} from '@devcard/shared'
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';

type TeamMember = PublicProfile & {

Check warning on line 10 in apps/backend/src/routes/team.ts

View workflow job for this annotation

GitHub Actions / backend-ci

'PublicProfile' is an 'error' type that acts as 'any' and overrides all other types in this intersection type
teamRole: TeamRole
joinedAt: Date;
}
Expand All @@ -24,7 +24,7 @@
members: TeamMember[];
}

export async function teamRoutes(app:FastifyInstance){

Check warning on line 27 in apps/backend/src/routes/team.ts

View workflow job for this annotation

GitHub Actions / backend-ci

Missing return type on function
app.post('/', { preHandler: [async (request, reply) => {
const server = request.server as any;
if (typeof server?.authenticate === 'function') { await server.authenticate(request, reply); return }
Expand Down
52 changes: 52 additions & 0 deletions docker/backend.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# syntax=docker/dockerfile:1

FROM node:20-alpine AS builder

WORKDIR /app

COPY packages/shared/package*.json ./packages/shared/
COPY apps/backend/package*.json ./apps/backend/
COPY apps/backend/prisma ./apps/backend/prisma
COPY packages/shared ./packages/shared

# Build shared package first
WORKDIR /app/packages/shared
RUN --mount=type=cache,target=/root/.npm \
npm ci --cache /root/.npm
RUN npm run build

# Install and build backend
WORKDIR /app/apps/backend
RUN --mount=type=cache,target=/root/.npm \
npm ci --cache /root/.npm

COPY apps/backend ./

RUN npm run build

FROM node:20-alpine AS runner

WORKDIR /app

ENV NODE_ENV=production

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Copy shared package (compiled dist + package.json only)
COPY --from=builder /app/packages/shared/package.json ./packages/shared/
COPY --from=builder /app/packages/shared/dist ./packages/shared/dist

COPY --from=builder /app/apps/backend/package*.json ./apps/backend/
COPY --from=builder /app/apps/backend/node_modules ./apps/backend/node_modules
COPY --from=builder /app/apps/backend/dist ./apps/backend/dist
COPY --from=builder /app/apps/backend/prisma ./apps/backend/prisma

RUN chown -R appuser:appgroup /app

USER appuser

EXPOSE 3000

WORKDIR /app/apps/backend

CMD ["node", "dist/server.js"]
5 changes: 3 additions & 2 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
"version": "1.0.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"test": "vitest run"
Expand Down
6 changes: 3 additions & 3 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './platforms';
export * from './types';
export * from './cards';
export * from './platforms.js';
export * from './types.js';
export * from './cards.js';
Loading