ci: stop close-external-prs from closing your own PRs and bots #193
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | |
| # | |
| # Note: `--depot=false` is intentional — depot.dev timed out | |
| # twice in a row on 2026-04-28 (5 min each), wedging CI for | |
| # ~10 min before failing. Standard remote builder is slower | |
| # (~3 min cached vs ~30s depot) but reliable. | |
| - name: Build + push image to Fly registry | |
| id: build | |
| run: | | |
| set -e | |
| # --build-only --push: build the image and push to Fly's | |
| # registry, but do NOT touch any machines. The image tag is | |
| # printed on a line like: | |
| # image: registry.fly.io/opensentry-command:deployment-XXXX | |
| # Capture it so the next step can target it explicitly. | |
| OUT=$(flyctl deploy --remote-only --depot=false --build-only --push 2>&1 | tee /dev/stderr) | |
| IMAGE=$(echo "$OUT" | grep -oP 'image:\s+\K[^\s]+' | tail -1) | |
| if [ -z "$IMAGE" ]; then | |
| echo "::error::Could not find image tag in flyctl output" | |
| exit 1 | |
| fi | |
| echo "Captured image: $IMAGE" | |
| echo "image=$IMAGE" >> "$GITHUB_OUTPUT" | |
| env: | |
| FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} | |
| - 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: ${{ steps.build.outputs.image }} |