Skip to content
Merged
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
116 changes: 115 additions & 1 deletion .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,118 @@ jobs:
push: true
cache-from: type=gha

dashboard-test:
name: dashboard-test
runs-on: ubuntu-24.04
timeout-minutes: 10
defaults:
run:
working-directory: dashboard
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# The dashboard runs on Node and npm, not Bun: decision 0005. The version comes
# from dashboard/.nvmrc, so CI, Docker and the laptop read it from one file.
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: dashboard/.nvmrc
cache: npm
cache-dependency-path: dashboard/package-lock.json
# "ci", never "install": it installs exactly what package-lock.json names, so CI
# can never quietly resolve a version the laptop never tested.
- run: npm ci
# The runner has no browser. Playwright installs the exact build its own version
# expects, plus the system libraries Chromium needs.
- name: Install Chromium for Playwright
run: npx playwright install --with-deps chromium
- run: npm run lint
# "build" is tsc -b && vite build, so the type check fails here as its own step
# instead of being buried in a Docker build log.
- run: npm run build
# Playwright starts Vite itself (webServer in playwright.config.ts), so there is
# no separate "start the app" step.
- name: End-to-end tests
run: npm run test:e2e

dashboard-image:
name: dashboard-image
needs: dashboard-test # never pack an image out of code that failed its tests
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
packages: write # only on main, in the push step below
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to GHCR
if: github.event_name == 'push'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Every PR and every merge: build into the runner's own Docker, so the image can be
# tested before anything is pushed. VITE_API_URL is not passed: the Dockerfile's
# default is already https://api.myrelay.tech.
# scope=dashboard: the gha cache scope is "buildkit" for everyone by default, so
# without this the API build and this one would overwrite each other's cache.
- name: Build the image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: dashboard
platforms: linux/amd64 # the VPS is amd64; GitHub's runners build that natively
tags: relay-dashboard:smoke
load: true
cache-from: type=gha,scope=dashboard
cache-to: type=gha,mode=max,scope=dashboard
# Kubernetes decides a pod is ready by asking for /index.html. An image that
# cannot answer that would never become ready: the rollout would time out after
# 180 s and roll the API back with it. So we ask the same question here, on the PR.
- name: Smoke-test the image
shell: bash # GitHub then adds -e and pipefail
run: |
docker run -d --name smoke-dash -p 127.0.0.1:8099:8080 relay-dashboard:smoke
# nginx is not up the instant the container is: wait, but not forever.
for _ in $(seq 1 30); do
# -fs, not -fsS: a refused connection while nginx starts is expected here,
# and printing it would look like a failure in a run that went fine.
curl -fs -o /dev/null http://127.0.0.1:8099/index.html && break
sleep 1
done
# curl -s exits 0 on a 404 too, so every check compares the code itself.
code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8099/index.html)
if [ "$code" != 200 ]; then
echo "::error::the image did not serve /index.html (HTTP $code)"
exit 1
fi
# Every unknown path returns index.html (the single-page fallback in nginx.conf),
# so a 200 there proves little on its own. location /assets/ has try_files =404 and
# no fallback, so asking for the bundle index.html names is what proves the built
# files are really in the image: an image with an empty assets/ answers 404 here
# while still answering 200 above.
asset=$(curl -s http://127.0.0.1:8099/index.html | grep -oE '/assets/[^"]+\.js' | head -n 1)
if [ -z "$asset" ]; then
echo "::error::index.html names no built bundle"
exit 1
fi
code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:8099$asset")
if [ "$code" != 200 ]; then
echo "::error::$asset answered $code: the build is not in the image"
exit 1
fi
echo "the image serves /index.html and $asset"
# Only after a merge to main: push the same image. Its layers come from the build
# above (the builder's cache), so this takes seconds and pushes what was tested.
- name: Push the image (main only)
if: github.event_name == 'push'
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: dashboard
platforms: linux/amd64
tags: ghcr.io/${{ github.repository_owner }}/relay-dashboard:${{ github.sha }}
push: true
cache-from: type=gha,scope=dashboard

deploy-script:
name: deploy-script-test
runs-on: ubuntu-24.04
Expand All @@ -143,7 +255,9 @@ jobs:

deploy:
name: deploy
needs: [image, deploy-script] # a broken deploy script never reaches the server
# Every image this commit's manifests ask for must be on the shelf before the
# server is told to pull them, so deploy waits for both image jobs.
needs: [image, dashboard-image, deploy-script] # a broken deploy script never reaches the server
if: github.event_name == 'push' # only after a merge to main, never on a PR
runs-on: ubuntu-24.04
# The environment hands over DEPLOY_SSH_KEY, and only to workflows running on main.
Expand Down
Loading