Skip to content
Closed
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
44 changes: 35 additions & 9 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- main
- develop
- feature/vapor-env-updates
- feature/deploy-testing
workflow_dispatch:
inputs:
environment:
Expand All @@ -21,6 +22,15 @@ on:
required: false
default: false
type: boolean
force_build:
description: 'Force a fresh DigitalOcean build (ignores the build cache)'
required: false
default: false
type: boolean

concurrency:
group: deploy-${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment || (github.ref_name == 'main' && 'production' || 'staging') }}
cancel-in-progress: false

jobs:
backend:
Expand Down Expand Up @@ -135,11 +145,13 @@ jobs:
echo "DO_APP_ID=${{ secrets.DIGITALOCEAN_STAGING_APP_ID }}" >> "$GITHUB_ENV"
echo "TEST_MODE=false" >> "$GITHUB_ENV"
fi
echo "FORCE_BUILD=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.force_build || 'false' }}" >> "$GITHUB_ENV"

- name: Log Environment Settings
run: |
echo "🚀 Deploying frontend to DigitalOcean App: ${{ env.DO_APP_ID }}"
echo "🧪 Test mode: ${{ env.TEST_MODE }}"
echo "🔨 Force build: ${{ env.FORCE_BUILD }}"

- name: Validate Deployment Configuration (Test Mode)
if: env.TEST_MODE == 'true'
Expand All @@ -154,7 +166,8 @@ jobs:
run: |
RESPONSE=$(curl -s -o response.json -w "%{http_code}" -X POST "https://api.digitalocean.com/v2/apps/$DO_APP_ID/deployments" \
-H "Authorization: Bearer ${{ secrets.DIGITALOCEAN_API_TOKEN }}" \
-H "Content-Type: application/json")
-H "Content-Type: application/json" \
-d "{\"force_build\": $FORCE_BUILD}")

if [ "$RESPONSE" -ne 201 ] && [ "$RESPONSE" -ne 200 ]; then
ERROR_MSG=$(jq -r '.message // "Unknown error occurred."' response.json)
Expand All @@ -176,7 +189,7 @@ jobs:
- name: Poll Deployment Status
if: env.TEST_MODE != 'true'
run: |
MAX_RETRIES=60
MAX_RETRIES=90
SLEEP_TIME=10
COUNTER=0

Expand All @@ -185,18 +198,31 @@ jobs:
-H "Authorization: Bearer ${{ secrets.DIGITALOCEAN_API_TOKEN }}" \
-H "Content-Type: application/json")

STATUS=$(echo "$RESPONSE" | jq -r '.deployment.phase')
STATUS=$(echo "$RESPONSE" | jq -r '.deployment.phase // "UNKNOWN"')

echo "🔄 Deployment Status: $STATUS"

if [ "$STATUS" == "ACTIVE" ]; then
echo "✅ Deployment completed successfully."
exit 0
elif [[ "$STATUS" == "FAILED" || "$STATUS" == "CANCELED" ]]; then
echo "❌ Deployment failed or was cancelled."
exit 1
if [ -z "${DEPLOYMENT_LOGGED:-}" ] && [ "$STATUS" != "UNKNOWN" ]; then
echo "📦 $(echo "$RESPONSE" | jq -r '.deployment | "cause=\(.cause // "unknown") build_reused_from=\(.cloned_from // "none")"')"
echo "🔖 $(echo "$RESPONSE" | jq -r '[.deployment | ((.services//[]) + (.workers//[]) + (.jobs//[]) + (.static_sites//[])) | .[] | "\(.name)@\(.source_commit_hash[0:7] // "?")"] | join(" ") | if . == "" then "(no components reported)" else . end')"
DEPLOYMENT_LOGGED=1
fi

case "$STATUS" in
ACTIVE)
echo "✅ Deployment completed successfully."
exit 0
;;
SUPERSEDED)
echo "⏭️ Superseded by a newer deployment, which is now responsible for this app."
exit 0
;;
ERROR|CANCELED)
echo "❌ Deployment finished in state: $STATUS"
exit 1
;;
esac

COUNTER=$((COUNTER + 1))
echo "⏳ Retrying in $SLEEP_TIME seconds... ($COUNTER/$MAX_RETRIES)"
sleep $SLEEP_TIME
Expand Down
41 changes: 35 additions & 6 deletions frontend/Dockerfile.ssr
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
FROM node:alpine as build-stage
# syntax=docker/dockerfile:1.7

WORKDIR /app
FROM node:22-alpine AS deps

RUN apk add --no-cache yarn
WORKDIR /app

COPY package.json yarn.lock ./

RUN yarn install
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked \
yarn install --frozen-lockfile

FROM node:22-alpine AS build

WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules

COPY . .

RUN yarn build
RUN yarn messages:compile \
&& yarn build:ssr:client \
&& yarn build:ssr:server

FROM node:22-alpine AS prod-deps

WORKDIR /app

COPY package.json yarn.lock ./

RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked \
yarn install --frozen-lockfile --production

FROM node:22-alpine AS runtime

ENV NODE_ENV=production

WORKDIR /app

COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json yarn.lock server.js ./
COPY src ./src
COPY public ./public

EXPOSE 5678

CMD ["yarn", "start"]

6 changes: 6 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
"@lingui/core": "4.14.1",
"@lingui/macro": "^4.7.0",
"@lingui/react": "^4.7.0",
"@mantine/carousel": "^9.2.2",
Expand Down Expand Up @@ -61,18 +62,23 @@
"dayjs": "^1.11.8",
"dotenv": "^16.4.7",
"express": "^4.19.2",
"invariant": "2.2.4",
"lodash": "4.17.21",
"qr-scanner": "^1.4.2",
"query-string": "^8.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-fast-compare": "3.2.2",
"react-helmet-async": "^2.0.4",
"react-qr-code": "^2.0.12",
"react-router": "^7.1.5",
"react-router-dom": "^7.1.5",
"recharts": "^3",
"shallowequal": "1.1.0",
"sirv": "^2.0.4"
},
"devDependencies": {
"@lingui/cli": "4.14.1",
"@lingui/vite-plugin": "^4.14.1",
"@swc/core": "1.11.24",
"@types/express": "^4.17.21",
Expand Down
3 changes: 2 additions & 1 deletion frontend/server.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import express from "express";
import {installGlobals} from "@remix-run/node";
import process from "process";
import {createServer as viteServer} from "vite";
import compression from "compression";
import fs from "node:fs/promises";
import sirv from "sirv";
Expand Down Expand Up @@ -55,6 +54,8 @@ async function main() {
let vite;

if (!isProduction) {
const {createServer: viteServer} = await import("vite");

vite = await viteServer({
server: { middlewareMode: true },
appType: "custom",
Expand Down
8 changes: 4 additions & 4 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3562,7 +3562,7 @@ inquirer@^7.3.3:
resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==

invariant@^2.2.4:
invariant@2.2.4, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
Expand Down Expand Up @@ -3847,7 +3847,7 @@ lodash.sortby@^4.7.0:
resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==

lodash@^4.17.19:
lodash@4.17.21, lodash@^4.17.19:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
Expand Down Expand Up @@ -4551,7 +4551,7 @@ react-dropzone@15.0.0:
file-selector "^2.1.0"
prop-types "^15.8.1"

react-fast-compare@^3.2.2:
react-fast-compare@3.2.2, react-fast-compare@^3.2.2:
version "3.2.2"
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
Expand Down Expand Up @@ -4931,7 +4931,7 @@ setprototypeof@1.2.0:
resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz"
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==

shallowequal@^1.1.0:
shallowequal@1.1.0, shallowequal@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz"
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
Expand Down
Loading