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
71 changes: 71 additions & 0 deletions .github/workflows/build-and-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Build and Push Jazz Bands

on:
push:
branches: [master, develop]
tags: ["v*"]
pull_request:
branches: [master, develop]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Build ${{ github.head_ref || github.ref_name }}
runs-on: ubuntu-latest
if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v')) || github.event_name == 'pull_request'

permissions:
contents: read
packages: write
id-token: write # For OIDC authentication with GHCR

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
install: true
version: latest

- name: Log in to Container Registry (using OIDC)
uses: docker/login-action@v3
with:
registry: ghcr.io
# GitHub token for OIDC authentication (no password needed)
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository_owner }}/jazz-bands
flavor: |
latest=${{ github.ref == 'refs/heads/master' }}
tags: |
# Main branch only
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
# Branch names (main, develop)
type=ref,event=branch,enable=${{ github.event_name != 'pull_request' }}
# Version tags
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
# PR builds: pr-42-branch-name
type=raw,value=pr-${{ github.event.pull_request.number }}-${{ github.head_ref }},enable=${{ github.event_name == 'pull_request' }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: apps/jazz-bands
file: apps/jazz-bands/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Pi4 = ARM64, else = AMD64 (builds as multi-arch manifest with QEMU)
platforms: linux/amd64,linux/arm64
14 changes: 13 additions & 1 deletion apps/jazz-bands/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@ SANITY_DATASET=production
SANITY_API_READ_TOKEN=your-read-token
# SANITY_API_WRITE_TOKEN is NOT used in frontend app (read-only)

# Sanity CMS Client Variables (Vite/client-side, prefixed with VITE_)
# PRODUCTION UMAMI WEBSITE IDs (per band)
# Analytics tracking IDs for each prod band (docker-compose.jazzbands.yml)
# Create these in Umami dashboard after deploying
# ============================================================================
UMAMI_BOHEME_ID=generated-id
UMAMI_CANTO_ID=generated-id
UMAMI_JAZZOLA_ID=generated-id
UMAMI_SWING_FAMILY_ID=generated-id
UMAMI_TRIO_RSH_ID=generated-id
UMAMI_WEST_SIDE_TRIO_ID=generated-id

# ============================================================================
# SANITY CMS CLIENT VARIABLES (Vite/client-side, prefixed with VITE_)
# These are required for client-side hydration and rendering
VITE_SANITY_PROJECT_ID=your-project-id
VITE_SANITY_DATASET=production
Expand Down
64 changes: 42 additions & 22 deletions apps/jazz-bands/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,44 +1,64 @@
# Multi-stage build
# ============================================================================
# Stage 1: Builder - Compile React Router SSR app
# ============================================================================
FROM node:24-slim AS builder

RUN apk add --no-cache python3 make g++
# Install build dependencies (Debian slim, not Alpine—better ARM64 natively)
RUN apt-get update && apt-get install -y --no-install-recommends \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Vite requires VITE_ prefix for client-side environment variables
ARG VITE_SANITY_STUDIO_PROJECT_ID
ARG VITE_SANITY_STUDIO_DATASET
ARG VITE_SANITY_API_READ_TOKEN
ENV VITE_SANITY_STUDIO_PROJECT_ID=$VITE_SANITY_STUDIO_PROJECT_ID
ENV VITE_SANITY_STUDIO_DATASET=$VITE_SANITY_STUDIO_DATASET
ENV VITE_SANITY_API_READ_TOKEN=$VITE_SANITY_API_READ_TOKEN

# Copy package files first (Docker layer caching)
COPY package*.json ./

# Production dependencies only (no dev deps)
RUN npm ci

# Copy source code
COPY . .

ARG BAND_SLUG
ENV BAND_SLUG=${BAND_SLUG}
ARG UMAMI_WEBSITE_ID
ENV UMAMI_WEBSITE_ID=${UMAMI_WEBSITE_ID}
# Build to ./dist
RUN npm run build

# Production runtime
# ============================================================================
# Stage 2: Runner - Production container (Debian slim, ~140MB base)
# Note: Slim is larger than Alpine but has native ARM64 glibc support
# ============================================================================
FROM node:24-slim AS runner

ARG UID=1001
ARG GID=1001
RUN addgroup -g ${GID} -S nodejs && \
adduser -S nodejs -u ${UID}

# Set working directory
WORKDIR /app

COPY --from=builder /app/dist ./dist
# Copy build artifacts from builder stage (as root, then chown to node)
COPY --from=builder /app/build ./build
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# Switch to non-root user
USER nodejs
COPY --from=builder /app/package*.json .
RUN chown -R node:node /app

EXPOSE 3000
# Drop privileges (node-slim uses 'node' user/group)
USER node

ENV NODE_ENV=production
ENV PORT=3000
ENV PORT=5173
ENV NODE_OPTIONS="--max-old-space-size=180"

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:${process.env.PORT}/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Healthcheck using curl (default in node-slim)
# Port 5173 = React Router v7 default, respects PORT env var
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD ["curl", "-f", "http://localhost:5173/", "-s"]

CMD ["npm", "run", "start"]
# Zero-config production server with @react-router/serve
# 180MB max heap for Pi 4; PORT=5173
CMD ["npm", "start"]
5 changes: 2 additions & 3 deletions apps/jazz-bands/app/components/shared/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ function NavLink({ to, children, primaryColor }: NavLinkProps) {
return (
<Link
to={to}
className={`focus-ring hover:opacity-80 relative px-1 ${
isActive ? 'text-white' : 'text-gray-300'
}`}
className={`focus-ring hover:opacity-80 relative px-1 ${isActive ? 'text-white' : 'text-gray-300'
}`}
>
{children}
{isActive && (
Expand Down
11 changes: 3 additions & 8 deletions apps/jazz-bands/app/lib/sanity.settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,6 @@ if (!dataset) {
'Missing required environment variable: SANITY_STUDIO_DATASET',
)
}
if (!apiReadToken) {
throw new Error(
'Missing required environment variable: SANITY_API_READ_TOKEN',
)
}

// Base configuration
const baseConfig: ClientConfig = {
Expand All @@ -88,9 +83,9 @@ const baseConfig: ClientConfig = {
export const sanityClient =
typeof window === 'undefined'
? createClient({
...baseConfig,
useCdn: false,
})
...baseConfig,
useCdn: false,
})
: (undefined as never) // Type guard for client-side

/**
Expand Down
Loading
Loading