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
133 changes: 133 additions & 0 deletions .github/workflows/release_web_app.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
name: Build & Publish Web App (Docker + Helm)

on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Release tag in the form appName@x.y.z"
required: true
type: string

permissions:
contents: read
packages: write

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
parse-tag:
name: Parse app name and version
runs-on: ubuntu-latest
outputs:
app: ${{ steps.parse.outputs.app }}
version: ${{ steps.parse.outputs.version }}
steps:
- name: Determine tag
id: tag
run: |
if [ "${{ github.event_name }}" = "release" ]; then
echo "TAG=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
else
echo "TAG=${{ inputs.tag }}" >> $GITHUB_ENV
fi

- name: Parse tag
id: parse
run: |
TAG="${TAG}"

if [[ ! "$TAG" =~ ^[^@]+@(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$ ]]; then
echo "Invalid tag format: $TAG"
exit 1
fi

APP="${TAG%@*}" # remove shortest match of @* from the end of $TAG
VERSION="${TAG#*@}" # remove the shortest match *@ from the start of $TAG

echo "app=$APP" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT

docker:
name: Build & Push Docker image
runs-on: ubuntu-latest
needs: parse-tag

steps:
- uses: actions/checkout@v4

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_PAT }}

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.web-app
push: true
build-args: |
APP_NAME=${{ needs.parse-tag.outputs.app }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ needs.parse-tag.outputs.app }}:${{ needs.parse-tag.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${{ needs.parse-tag.outputs.app }}:latest

helm:
name: Package & Push Helm chart
runs-on: ubuntu-latest
needs: parse-tag

steps:
- uses: actions/checkout@v4

- name: Install Helm
uses: azure/setup-helm@v4
with:
version: v3.14.0

- name: Log in to GHCR (Helm OCI)
run: |
echo "${{ secrets.GHCR_PAT }}" | \
helm registry login ghcr.io \
--username ${{ github.actor }} \
--password-stdin

- name: Sync versions in Chart and Values
run: |
CHART_DIR="apps/${{ needs.parse-tag.outputs.app }}/helm"
VERSION="${{ needs.parse-tag.outputs.version }}"

# 1. Update Chart.yaml
yq -i ".version = \"$VERSION\" | .appVersion = \"$VERSION\"" "$CHART_DIR/Chart.yaml"

# 2. Update the image tag in values.yaml
yq -i ".ui-base.image.tag = \"$VERSION\"" "$CHART_DIR/values.yaml"

- name: Build base chart dependencies
run: |
helm dependency build ./helm

- name: Build chart dependencies
run: |
CHART_DIR="apps/${{ needs.parse-tag.outputs.app }}/helm"
helm dependency build "$CHART_DIR"

- name: Package chart
run: |
CHART_DIR="apps/${{ needs.parse-tag.outputs.app }}/helm"
helm package "$CHART_DIR" \
--version "${{ needs.parse-tag.outputs.version }}" \
--app-version "${{ needs.parse-tag.outputs.version }}"

- name: Push chart
run: |
CHART_PACKAGE=$(ls *.tgz)

helm push "$CHART_PACKAGE" \
oci://${{ env.REGISTRY }}/${{ github.repository }}/charts
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ dist-ssr

# Turborepo
.turbo

# Helm dependency build artifacts
/helm/charts/
/apps/*/helm/charts/
*.tgz
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Atlas - Diamond II Science UI Monorepo

**Atlas** is the monorepo for Diamond II science-facing web user interfaces.

It provides a shared home for multiple applications built with TypeScript + React, using the sci-react-ui component library for consistent look, feel, and interaction patterns across beamlines and services.

The goal is to keep UI development:

- Centralised – all science UIs live in one repository
- Visible – teams can see, learn from, and contribute to each other’s work
- Reusable – shared components, patterns, and infrastructure
- Consistent – common authentication, layout, and deployment model

## Tech stack

- Frontend: React + Typescript
- UI components: `sci-react-ui`/`mui`
- Deployment: Kubernetes via Helm
- Authentication: OIDC (Keycloak) via `oauth2-proxy`

## Repository structure

```
apps/ # Individual UI applications
helm/ # Shared base Helm chart (ui-base)
packages/ # Shared libraries/utilities (if applicable)
```

Each application under `apps/<name>` is independently buildable and releasable, but follows shared conventions for layout, auth, and deployment.

## Testing

Frontend testing across the monorepo is standardised via [packages/vitest-conf](packages/vitest-conf/README.md). This package centralises shared test setup, including, Vitest configuration, jsdom test environment, Testing Library matchers and common setup.

Applications and shared packages extend this configuration rather than redefining their own, which keeps test setup minimal, consistent, and easy to maintain across the repository.

## Common deployment model

All UIs are deployed behind a shared authentication and routing layer provided by the `ui-base` Helm chart.

Application charts depend on this base chart and only need to declare:

1. Which container image to run
2. Which backend services (“upstreams”) the UI talks to

The base chart handles:

- Ingress
- Service wiring
- OAuth2/OIDC login via Keycloak
- Reverse proxying to backend APIs

This keeps app charts small, declarative, and consistent across the platform.

## Releasing applications

Applications are released using Git tags. Each app is versioned and released independently.

### Tag format

```
<appName>@<semver>
```

### Examples

```
visr@0.1.8
swift@1.2.0-beta.2
```

### What happens on release

When a tag matching this format is pushed (or a release workflow is manually triggered with the same inputs), CI will:

1. Parse the tag into application name and version
2. Build and publish the application using repo-level Dockerfile and nginx configuration
3. Update the app’s Helm chart:
- `Chart.yaml`: `version` and `appVersion`
- `values.yaml`: container image tag
4. Build Helm dependencies
5. Package application chart
6. Push the chart to the GitHub Container Registry (OCI)

This ensures a single Git tag produces a versioned **container image** and a matching **Helm chart**.

## Authentication

All production deployments use OIDC authentication via Diamond’s Keycloak realms, enforced at the ingress layer using `oauth2-proxy`. Applications themselves do not need to implement login flows directly.

## Contributing

When building a new UI:

- Follow existing app structure under apps/
- Use components from sci-react-ui wherever possible
- Keep app-specific logic in the app, and reusable logic in shared packages

Consistency across UIs is a core goal of this repository.
44 changes: 44 additions & 0 deletions docker/Dockerfile.web-app
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
FROM node:22-alpine AS base

# Use a consistent working directory across all stages
WORKDIR /app

# 1) Install dependencies
FROM base AS deps

ARG APP_NAME

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc* ./
COPY packages ./packages
COPY apps/${APP_NAME}/package.json ./apps/${APP_NAME}/

# Install dependencies
RUN corepack enable pnpm && pnpm i --frozen-lockfile --filter=@atlas/${APP_NAME}...

# 2) Run the build
FROM base AS builder

ARG APP_NAME

# Copy all node_modules from deps stage
COPY --from=deps /app ./

# Copy source files
COPY apps/${APP_NAME} ./apps/${APP_NAME}
COPY packages ./packages
COPY turbo.json package.json pnpm-workspace.yaml tsconfig.json ./
RUN corepack enable pnpm && pnpm --filter=@atlas/${APP_NAME}... build

# 3) Create minimal image to serve the app
FROM nginxinc/nginx-unprivileged:1.25-alpine AS runner

ARG APP_NAME

# Copy built files to nginx web root
COPY --from=builder /app/apps/${APP_NAME}/dist /usr/share/nginx/html

# Copy your custom nginx config
COPY docker/nginx.conf /etc/nginx/nginx.conf

EXPOSE 8080
CMD ["nginx", "-g", "daemon off;"]
61 changes: 61 additions & 0 deletions docker/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
worker_processes auto; # Let Nginx decide based on CPU cores
pid /tmp/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

# Logging to stdout/stderr is standard for Docker
access_log /dev/stdout;
error_log /dev/stderr warn;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

# Gzip Compression - Critical for web app performance
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml image/svg+xml;

# Temp paths for unprivileged user
client_body_temp_path /tmp/client_temp;
proxy_temp_path /tmp/proxy_temp_path;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;

server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;

# SPA routing: try file, then directory, then fall back to index.html
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-store" always;
}

# Cache hashed assets (JS/CSS/Images) for a long time
# This assumes your build tool (Vite/Webpack) uses hashes in filenames
location ~* \.(?:js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
11 changes: 11 additions & 0 deletions helm/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v2
name: ui-base
description: Web UI behind oauth2-proxy
type: application
version: 0.1.0
appVersion: "0.1.0"

dependencies:
- name: oauth2-proxy
repository: https://oauth2-proxy.github.io/manifests
version: 10.1.0
Loading