Skip to content
Open
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
128 changes: 128 additions & 0 deletions .github/workflows/build-base-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: Build Base Docker Images

on:
workflow_call:
inputs:
force-rebuild:
description: 'Force rebuild even if images exist'
required: false
type: boolean
default: false
outputs:
build-base-image:
description: 'Full build-base image reference'
value: ${{ jobs.build-base.outputs.image }}
runtime-base-image:
description: 'Full runtime-base image reference'
value: ${{ jobs.build-base.outputs.runtime-image }}
workflow_dispatch:
inputs:
force-rebuild:
description: 'Force rebuild even if images exist'
required: false
type: boolean
default: false

permissions:
contents: read
packages: write

env:
REGISTRY: ghcr.io
IMAGE_NAME: ghcr.io/opengovsg/formsg

jobs:
build-base:
name: Build base images
runs-on: ubuntu-latest
outputs:
image: ${{ steps.set-outputs.outputs.build-image }}
runtime-image: ${{ steps.set-outputs.outputs.runtime-image }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Compute dependency hash
id: deps-hash
run: |
HASH=$(cat \
pnpm-lock.yaml \
package.json \
pnpm-workspace.yaml \
Dockerfile.base \
apps/backend/package.json \
apps/frontend/package.json \
packages/shared/package.json \
packages/sdk/package.json \
| sha256sum | cut -c1-16)
echo "hash=$HASH" >> "$GITHUB_OUTPUT"
echo "Dependency hash: $HASH"

- name: Log in to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Check if images already exist
id: check-exists
if: ${{ !inputs.force-rebuild }}
run: |
BUILD_EXISTS=false
RUNTIME_EXISTS=false

if docker manifest inspect "${{ env.IMAGE_NAME }}:build-${{ steps.deps-hash.outputs.hash }}" > /dev/null 2>&1; then
BUILD_EXISTS=true
echo "build-base image already exists"
fi

if docker manifest inspect "${{ env.IMAGE_NAME }}:runtime-${{ steps.deps-hash.outputs.hash }}" > /dev/null 2>&1; then
RUNTIME_EXISTS=true
echo "runtime-base image already exists"
fi

if [[ "$BUILD_EXISTS" == "true" && "$RUNTIME_EXISTS" == "true" ]]; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Both images exist, skipping build"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Set up Docker Buildx
if: steps.check-exists.outputs.skip != 'true'
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Build and push build-base image
if: steps.check-exists.outputs.skip != 'true'
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: Dockerfile.base
target: build-base
push: true
tags: |
${{ env.IMAGE_NAME }}:build-${{ steps.deps-hash.outputs.hash }}
${{ env.IMAGE_NAME }}:build-latest
cache-from: |
type=registry,ref=${{ env.IMAGE_NAME }}:build-latest

- name: Build and push runtime-base image
if: steps.check-exists.outputs.skip != 'true'
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: Dockerfile.base
target: runtime-base
push: true
tags: |
${{ env.IMAGE_NAME }}:runtime-${{ steps.deps-hash.outputs.hash }}
${{ env.IMAGE_NAME }}:runtime-latest
cache-from: |
type=registry,ref=${{ env.IMAGE_NAME }}:runtime-latest

- name: Set outputs
id: set-outputs
run: |
echo "build-image=${{ env.IMAGE_NAME }}:build-${{ steps.deps-hash.outputs.hash }}" >> "$GITHUB_OUTPUT"
echo "runtime-image=${{ env.IMAGE_NAME }}:runtime-${{ steps.deps-hash.outputs.hash }}" >> "$GITHUB_OUTPUT"
84 changes: 84 additions & 0 deletions .github/workflows/build-release-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Build Release Image

on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: 'Version tag to build (e.g., v7.4.5)'
required: true
type: string

permissions:
contents: read
packages: write

env:
IMAGE_NAME: ghcr.io/opengovsg/formsg

jobs:
ensure-base-images:
name: Ensure base images
uses: ./.github/workflows/build-base-images.yml
permissions:
contents: read
packages: write

build-release:
name: Build release image
needs: ensure-base-images
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Determine version tag
id: version
run: |
if [[ "${{ github.event_name }}" == "push" ]]; then
TAG="${GITHUB_REF#refs/tags/}"
else
TAG="${{ inputs.tag }}"
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "Building release image for $TAG"

- name: Log in to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Check if release image already exists
id: check-exists
run: |
IMAGE="${{ env.IMAGE_NAME }}:release-${{ steps.version.outputs.tag }}"
if docker manifest inspect "$IMAGE" > /dev/null 2>&1; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Release image already exists: $IMAGE"
else
echo "skip=false" >> "$GITHUB_OUTPUT"
fi

- name: Set up Docker Buildx
if: steps.check-exists.outputs.skip != 'true'
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Build and push release image
if: steps.check-exists.outputs.skip != 'true'
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: Dockerfile.production
push: true
tags: |
${{ env.IMAGE_NAME }}:release-${{ steps.version.outputs.tag }}
build-args: |
BUILD_BASE_IMAGE=${{ needs.ensure-base-images.outputs.build-base-image }}
APP_VERSION=${{ steps.version.outputs.tag }}
DD_RUM_APP_ID=${{ secrets.DD_RUM_APP_ID }}
DD_RUM_CLIENT_TOKEN=${{ secrets.DD_RUM_CLIENT_TOKEN }}
cache-from: |
type=registry,ref=${{ env.IMAGE_NAME }}:build-latest
87 changes: 62 additions & 25 deletions .github/workflows/deploy-ecs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,19 @@ on:
permissions:
id-token: write
contents: read
packages: write

jobs:
ensure-base-images:
name: Ensure base images
uses: ./.github/workflows/build-base-images.yml
permissions:
contents: read
packages: write

deploy:
name: Deploy to ECS
needs: ensure-base-images
runs-on: ubuntu-latest
environment: ${{ inputs.gha-environment }}
env:
Expand All @@ -133,24 +142,6 @@ jobs:
run: |
echo "APP_VERSION=$(jq -r .version package.json)-$(echo ${GITHUB_REF##*/})-$(echo ${GITHUB_SHA} | cut -c1-8)" >> $GITHUB_ENV

- name: Inject frontend build env vars
env:
VITE_APP_DD_RUM_APP_ID: ${{ secrets.dd-rum-app-id }}
VITE_APP_DD_RUM_CLIENT_TOKEN: ${{ secrets.dd-rum-client-token }}
VITE_APP_DD_RUM_ENV: ${{ inputs.dd-env }}
VITE_APP_DD_SAMPLE_RATE: ${{ secrets.dd-sample-rate }}
VITE_APP_GA_TRACKING_ID: ${{ secrets.ga-tracking-id }}
VITE_APP_FORMSG_SDK_MODE: ${{ inputs.react-app-form-sg-sdk-mode }}
VITE_APP_URL: ${{ inputs.app-url }}
run: |
sed -i -e "s|@VITE_APP_URL|${{ env.VITE_APP_URL }}|g" -e "s/@VITE_APP_DD_RUM_APP_ID/$VITE_APP_DD_RUM_APP_ID/g" -e "s/@VITE_APP_DD_RUM_CLIENT_TOKEN/$VITE_APP_DD_RUM_CLIENT_TOKEN/g" -e "s/@VITE_APP_DD_RUM_ENV/$VITE_APP_DD_RUM_ENV/g" -e "s/@VITE_APP_VERSION/${{env.APP_VERSION}}/g" -e "s/@VITE_APP_DD_SAMPLE_RATE/$VITE_APP_DD_SAMPLE_RATE/g" apps/frontend/datadog-chunk.ts
echo VITE_APP_VERSION=${{ inputs.app-version }} > apps/frontend/.env
echo VITE_APP_URL=$VITE_APP_URL > apps/frontend/.env
echo VITE_APP_GA_TRACKING_ID=$VITE_APP_GA_TRACKING_ID >> apps/frontend/.env
echo VITE_APP_FORMSG_SDK_MODE=$VITE_APP_FORMSG_SDK_MODE >> apps/frontend/.env
echo VITE_APP_DD_RUM_CLIENT_TOKEN=$VITE_APP_DD_RUM_CLIENT_TOKEN >> apps/frontend/.env
echo VITE_APP_DD_RUM_ENV=$VITE_APP_DD_RUM_ENV >> apps/frontend/.env

- name: Grant runner AWS credentials
uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1
with:
Expand All @@ -161,27 +152,61 @@ jobs:
id: login-ecr
uses: aws-actions/amazon-ecr-login@f2e9fc6c2b355c1890b65e6f6f0e2ac3e6e22f78 # v2.1.2

- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Check for pre-built release image
id: check-release-image
run: |
VERSION="v$(jq -r .version package.json)"
IMAGE="ghcr.io/opengovsg/formsg:release-${VERSION}"
echo "Looking for pre-built release image: $IMAGE"
if docker manifest inspect "$IMAGE" > /dev/null 2>&1; then
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
echo "Found pre-built release image, will skip Docker build"
else
echo "image=" >> "$GITHUB_OUTPUT"
echo "No pre-built release image found, will build from scratch"
fi

- name: Pull and push pre-built image to ECR
if: steps.check-release-image.outputs.image != ''
env:
ECR_REPOSITORY: ${{ inputs.ecr-repository }}
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
run: |
docker pull ${{ steps.check-release-image.outputs.image }}
docker tag ${{ steps.check-release-image.outputs.image }} $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

- name: Set up Docker Buildx
if: steps.check-release-image.outputs.image == ''
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Build and push Docker image
if: steps.check-release-image.outputs.image == ''
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
env:
DD_API_KEY: ${{ secrets.DD_API_KEY }}
DD_ENV: ${{ inputs.dd-env }}
ECR_REPOSITORY: ${{ inputs.ecr-repository }}
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
with:
context: .
file: Dockerfile.production
push: true
load: true
tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}
build-args: |
APP_VERSION=${{ inputs.app-version }}
APP_URL=${{ inputs.app-url }}
REPO_URL=${{ github.server_url }}/${{ github.repository }}
secrets: |
"dd_api_key=${{ secrets.DD_API_KEY }}"
BUILD_BASE_IMAGE=${{ needs.ensure-base-images.outputs.build-base-image }}
RUNTIME_BASE_IMAGE=${{ needs.ensure-base-images.outputs.runtime-base-image }}
APP_VERSION=${{ env.APP_VERSION }}
DD_RUM_APP_ID=${{ secrets.dd-rum-app-id }}
DD_RUM_CLIENT_TOKEN=${{ secrets.dd-rum-client-token }}
Comment thread
eliotlim marked this conversation as resolved.
DD_SAMPLE_RATE=${{ secrets.dd-sample-rate }}
GA_TRACKING_ID=${{ secrets.ga-tracking-id }}

- name: Replace variables in task definition file
id: replace-variables
Expand Down Expand Up @@ -220,6 +245,18 @@ jobs:
docker run --rm -v /tmp/s3static:/tmp/s3static $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG cp -r ./apps/frontend/dist/. /tmp/s3static
aws s3 sync /tmp/s3static s3://$S3_STATIC_ASSETS_BUCKET_NAME

- name: Upload Datadog sourcemaps
env:
DATADOG_API_KEY: ${{ secrets.DD_API_KEY }}
run: |
curl -L --fail "https://github.com/DataDog/datadog-ci/releases/download/v5.11.0/datadog-ci_linux-x64" --output /usr/local/bin/datadog-ci
chmod +x /usr/local/bin/datadog-ci
datadog-ci sourcemaps upload /tmp/s3static \
Comment thread
eliotlim marked this conversation as resolved.
--service=formsg-react \
--release-version=${{ env.APP_VERSION }} \
--minified-path-prefix=${{ inputs.app-url }} \
--repository-url=${{ github.server_url }}/${{ github.repository }}

- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@df9643053eda01f169e64a0e60233aacca83799a # v1.4.11
with:
Expand Down
52 changes: 52 additions & 0 deletions Dockerfile.base
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# syntax=docker/dockerfile:1

# =============================================================================
# build-base: Full build toolchain with pre-installed node_modules
# =============================================================================
FROM node:22.22.2-alpine3.22 AS build-base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@10.30.3 --activate

# Install build dependencies (same as Dockerfile.production build stage)
RUN apk upgrade --no-cache && \
apk --no-cache add --virtual native-deps \
g++ gcc libgcc libstdc++ linux-headers autoconf automake make nasm python3 git curl \
build-base cairo-dev pango-dev giflib-dev libjpeg-turbo-dev librsvg-dev && \
pnpm add -g node-gyp@12.2.0

WORKDIR /build

# Copy only dependency-related files for layer caching
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/backend/package.json ./apps/backend/
COPY packages/shared/package.json ./packages/shared/
COPY packages/sdk/package.json ./packages/sdk/
COPY apps/frontend/package.json ./apps/frontend/

RUN pnpm install --frozen-lockfile --filter "!services/*" --filter "!packages/react-email-preview/*"

# =============================================================================
# runtime-base: Minimal runtime image with system deps and non-root user
# =============================================================================
FROM node:22.22.2-alpine3.22 AS runtime-base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable && corepack prepare pnpm@10.30.3 --activate

RUN apk upgrade --no-cache && \
apk add --no-cache \
nss \
freetype \
freetype-dev \
harfbuzz \
ca-certificates \
ttf-freefont \
tini \
cairo \
pango \
librsvg \
giflib

# Pre-create non-root user
RUN addgroup -S formsguser && adduser -S -g formsguser formsguser
Loading
Loading