Skip to content

ui(mcp): stop Revoke button from stretching across the API Keys row #225

ui(mcp): stop Revoke button from stretching across the API Keys row

ui(mcp): stop Revoke button from stretching across the API Keys row #225

Workflow file for this run

name: Test & Deploy
on:
push:
branches:
- master
jobs:
test:
name: Backend tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v4
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
working-directory: backend
run: uv sync --extra dev
- name: Run tests
working-directory: backend
run: uv run pytest -v
frontend:
name: Frontend audit + build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
working-directory: frontend
run: npm ci
# `npm audit` flags vulnerabilities in production dependencies.
# Threshold = high so low/moderate findings (transitive dev-only
# CVEs that don't reach prod, advisories with no fix yet, etc.)
# don't block legitimate deploys. High + critical findings DO
# block — those are real and need a fix or a documented
# `--omit=optional` / override / waiver.
#
# `--omit=dev` skips devDependencies because they don't ship
# to production; the prod bundle is what reaches a user.
- name: npm audit (production deps only, high+critical)
working-directory: frontend
run: npm audit --audit-level=high --omit=dev
# Build now so a syntax or type error fails CI here rather than
# mid-deploy. Catches the same class of bug as backend pytest.
- name: Build production bundle
working-directory: frontend
run: npm run build
deploy:
name: Deploy to Fly.io
runs-on: ubuntu-latest
needs: [test, frontend]
concurrency:
group: deploy
cancel-in-progress: true
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
# Why two-step (build → machine update) instead of plain `fly deploy`:
#
# We run a single Fly Machine with a single persistent volume
# (`opensentry_data` at /data, holding the SQLite DB). `fly deploy`
# for this topology is non-deterministic: sometimes it sees the
# existing machine and updates it in place, sometimes it decides
# the image config has "drifted enough" and tries to provision a
# NEW machine alongside the old. The new-machine path errors
# immediately because the volume only has one attachment slot:
# "creating a new machine in group 'app' requires an
# unattached 'opensentry_data' volume."
# We hit this on consecutive runs 2026-04-28 with strategy=rolling
# AND strategy=immediate, and `max_unavailable` is rolling-only so
# it didn't help either.
#
# `fly machine update --image …` is the explicit in-place API.
# It targets a specific machine ID, restarts it on the new image,
# and the volume stays attached throughout. It cannot try to
# create a new machine. ~30-60s of downtime per deploy (same as
# `strategy = "immediate"` on a good day) but reliably works.
#
# Builder choice has flipped THREE times now:
# - Original: --depot=true (depot.dev managed builder).
# - 2026-04-28: depot.dev timed out 5 min × 2 in a row
# (~10 min wasted per deploy). Switched to --depot=false
# (Fly's standard remote builder). ~100 deploys worked.
# - 2026-05-04 morning: Fly's standard remote builder started
# returning `unauthorized` on the WireGuard heartbeat for
# valid deploy tokens (Request ID 01KQTE4AHWKB2PAAS8A372EKNP).
# Swapped tokens — same failure. Server-side scope change
# or platform incident; either way, CI was wedged. Tried
# --depot=true again briefly: depot built fine but tagged
# the manifest under its own internal namespace
# (vo4x1o84n7ozql5y), so the subsequent `fly machine update`
# got MANIFEST_UNKNOWN looking for the image at the
# opensentry-command path. Mismatch between depot's push
# and the two-step pattern we use.
# - 2026-05-04 afternoon (current): build locally on the
# GitHub runner with docker/build-push-action and push
# directly to registry.fly.io. No third-party builders.
# No WireGuard. Same FLY_API_TOKEN works for the registry
# push (proven on the failed depot run — depot's push to
# registry.fly.io itself succeeded; the namespace mismatch
# was on its side, not the registry's). Dockerfile is a
# standard multi-stage build (node:20-alpine for frontend,
# uv:python3.12-bookworm-slim for backend) — no special
# build hardware needed.
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Fly registry
uses: docker/login-action@v3
with:
registry: registry.fly.io
# Fly's registry ignores the username; only the token matters.
username: x
password: ${{ secrets.FLY_API_TOKEN }}
- name: Build + push image to Fly registry
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
# Tag with the commit SHA so each deploy is uniquely
# addressable + grep-friendly. The previous flyctl-managed
# format was deployment-<ULID> — a SHA is more useful for
# cross-referencing the image to the source revision when
# debugging.
tags: registry.fly.io/opensentry-command:deployment-${{ github.sha }}
# GitHub Actions cache for layers — ~30s saved on warm
# builds vs. cold. scope=deploy keeps it isolated from
# any future workflows that might also use buildx.
cache-from: type=gha,scope=deploy
cache-to: type=gha,scope=deploy,mode=max
- name: Export image tag for next step
id: image
run: |
IMAGE="registry.fly.io/opensentry-command:deployment-${{ github.sha }}"
echo "Captured image: $IMAGE"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
- name: Update machine in place
run: |
set -e
# We currently run exactly one machine. If we ever scale to
# multiple machines (or migrate to LiteFS / Postgres so we
# don't need a single volume), this script needs to loop.
MACHINE=$(flyctl machines list -a opensentry-command --json | jq -r '.[0].id')
if [ -z "$MACHINE" ] || [ "$MACHINE" = "null" ]; then
echo "::error::No machines found for opensentry-command"
exit 1
fi
echo "Updating machine $MACHINE to $IMAGE"
flyctl machine update "$MACHINE" --image "$IMAGE" --yes -a opensentry-command
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
# Image tag comes from the `image` step (the export step
# after docker/build-push-action), not the build step.
IMAGE: ${{ steps.image.outputs.image }}