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
47 changes: 47 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Dependabot configuration.
#
# This repo had NO dependency watching until 2026-09-09, which is the
# same gap that produced the Sentinel AI agent's three production
# breakages in two days: mcp 2.x renaming a symbol, Starlette removing
# on_startup, and a ceiling added to stop the second one silently
# pinning the service to a Starlette line with 7 advisories. All three
# surfaced on a rebuild of a repo nobody had rebuilt since June.
#
# `pip-audit --strict` in the Test workflow catches a *known advisory*
# in what is already pinned. It does nothing about a lockfile quietly
# ageing out of support. That is what this is for.
#
# NO AUTO-MERGE WORKFLOW HERE, DELIBERATELY.
#
# This repo now deploys to Fly on push to master. GitHub does not
# trigger `on: push` workflows for commits pushed with GITHUB_TOKEN, so
# an auto-merge action would land a bump on master WITHOUT deploying it
# β€” master and production drift apart, and nothing reports an error.
# That is a real, observed bug in Sentinel-Command (four frontend bumps
# merged and never shipped on 2026-09-09). A human merge triggers the
# deploy normally, so these PRs are reviewed and merged by hand until
# that repo's PAT fix is proven.
version: 2
updates:
# Python (uv) β€” pyproject.toml + uv.lock live at the repo root.
- package-ecosystem: "uv"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
commit-message:
prefix: "build(deps)"
labels:
- "dependencies"

# GitHub Actions β€” pinned action versions age out the same way.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "ci"
50 changes: 44 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
name: Test
name: Test & Deploy

# Deploy automation is deferred deliberately (see the Sentinel Command
# plan doc, Phase 1: "fine to start with a manual `fly deploy` for the
# very first cut given a human should be watching the first-ever deploy
# of new infrastructure anyway; automate once phase 1 is stable"). This
# workflow only covers lint + dependency audit + tests.
# Tests on every push and PR; deploys to Fly on pushes to master.
#
# Deploy automation was deferred so a human could watch the first-ever
# deploy of new infrastructure, with a note to "automate once phase 1 is
# stable". Phase 1 is stable, and leaving it deferred created a worse
# problem than it avoided: fly.toml became a file that did nothing. A
# scale-to-zero change merged with CI green on 2026-09-09 and never
# reached Fly β€” the app kept running always-on until someone noticed and
# deployed by hand. Config that silently doesn't apply is more dangerous
# than no config.

on:
push:
Expand Down Expand Up @@ -68,3 +73,36 @@ jobs:
env:
TEST_DATABASE_URL: ${{ matrix.test_database_url }}
run: uv run pytest -v

deploy:
name: Deploy to Fly.io
runs-on: ubuntu-latest
# Waits on BOTH matrix legs (sqlite and postgres) β€” this service runs
# the same code against either dialect, so a green sqlite run alone
# is not evidence the deploy is safe.
needs: test
# Push-only: a PR runs the tests above but never ships.
if: github.event_name == 'push'
# Serialize so two quick pushes don't race on the machine update.
concurrency:
group: deploy-sentinel-license
cancel-in-progress: true
steps:
- uses: actions/checkout@v7

- uses: superfly/flyctl-actions/setup-flyctl@master

# --strategy immediate because this app mounts a volume
# (sentinel_license_data). The default rolling strategy tries to
# stand up a parallel machine first and errors on the volume's
# single attachment slot. Sentinel-Sync has no volume and so needs
# no override; Command Center has the same constraint and the same
# flag.
#
# --ha=false because Fly otherwise provisions TWO machines, which
# this service does not need and which the volume cannot serve
# anyway.
- name: flyctl deploy
run: flyctl deploy --remote-only --strategy immediate --ha=false --yes
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ uv run pytest

## Deploy

Single-stage `Dockerfile` (no frontend build β€” this service has no UI), `fly.toml` targets a much smaller VM than Command Center's (no video workload, tiny check-in traffic). First deploy is manual (`fly deploy`) by design β€” see `.github/workflows/test.yml`'s comment for why deploy automation is deferred.
Single-stage `Dockerfile` (no frontend build β€” this service has no UI). `fly.toml` targets a much smaller VM than Command Center's: 256 MB, no video workload, tiny check-in traffic.

**Deploys from CI.** Every push to `master` runs the tests against both SQLite and Postgres, then `flyctl deploy`. Deploy automation was deferred while this was new infrastructure; that turned out worse than what it avoided, because `fly.toml` became a file that did nothing β€” a scale-to-zero change merged with CI fully green on 2026-09-09 and never reached Fly, and it *looked* applied because the commit was on master. Config that silently doesn't apply is more dangerous than no config.

Two flags, each for a reason: `--strategy immediate` because this app mounts `sentinel_license_data` and the default rolling strategy errors on the volume's single attachment slot; `--ha=false` because Fly otherwise provisions two machines, which one volume can't serve anyway.

**Scales to zero.** Self-hosted installs check in on a ~15-minute background tick, so this is idle by default. Safe because boot is ~4s β€” inside the ~8s Fly's proxy waits for an auto-started machine to bind β€” the caller's timeout is 10s, and a missed check-in is a *designed* path: Command Center treats network/5xx as "unreachable" and applies a 72-hour grace window. This flips if a licence check ever moves onto a user-blocking path; then a 4s cold start becomes a 4s page stall.

## Status

Expand Down