• v0.25.0 5e2f858048

    v0.25.0
    All checks were successful
    CI / gate (push) Successful in 51s
    CI / image (push) Successful in 17s
    Stable

    joeyr released this 2026-08-07 21:42:54 -05:00 | 668 commits to main since this release

    Added

    • The query API proves its dependencies at boot instead of announcing its
      configuration.
      Phase 4 of docs/query-api-plan.md.

      Until now the service said one thing about itself when it started:

      INF api/config > lol-companion-data api started | version=0.24.1 commit=7ef8062
        listen=0.0.0.0:8080 database=postgres:5432/lolc_damage rate=120/min burst 60
      

      Every field on that line is configuration read back. Not one is evidence that
      anything works. The crawler's block has been the opposite since 0.19.0 —
      server=, entries=205, patch=16.15 are all answers something gave it —
      and the argument for one here is stronger, not weaker: the crawler's bad
      moment is an unattended window at 02:00, this one's is a 500 served into champ
      select, to a client on a draft timer.

      Six checks, one round trip each, run concurrently:

      INF preflight/db         | ok  server=PostgreSQL 18.4
      INF preflight/role       | ok  role=lolc_api
      INF preflight/schema     | ok  tables=4
      INF preflight/grants     | ok  select=true update=last_used_at
      INF preflight/keys       | ok  active=1 revoked=0
      INF preflight/artifacts  | ok  count=10 expected=10 patch=16.15 oldest=19h
      

      preflight/role is the one that matters most, and the crawler has no need
      for it.
      The whole read-only design rests on this process connecting as
      lolc_api rather than the crawler's lolc — that is what makes
      participant_stat unreadable rather than merely unqueried. Nothing already
      logged can tell you which it got: databaseLabel strips the credentials from
      the DSN on purpose, so a stack file edited in a hurry could point this process
      at the crawler's role and every line would look exactly as it does now. It has
      to be asked, so it is: SELECT current_user, compared against the new
      API_DB_ROLE, which defaults to lolc_api so a stack file that omits it
      is still checked. Running against a development database as lolc means
      setting it deliberately.

      preflight/keys is the cheapest real outage this catches. An API with
      zero active keys boots clean, answers /v1/health, and 401s every real
      request — which from the client's side is indistinguishable from a client bug.

      preflight/grants probes the write it never makes at boot. The
      last_used_at UPDATE happens on a flush timer minutes later, so a missing
      grant would otherwise surface as a background failure long after the deploy
      that caused it. UPDATE api_key SET last_used_at = last_used_at WHERE false
      matches no rows and writes nothing; Postgres checks table and column
      privileges when the statement starts regardless, so the refusal is identical
      to the one a real flush would get.

      Two of the six are fatal, and that is the one way this differs from the
      crawler's block.
      preflight.ts is emphatic that nothing may stop the
      crawler starting — a dead Forgejo token must not cost a night of Riot data a
      fixed token could publish tomorrow. A read service has no equivalent argument:
      it holds no work in progress, so refusing to start costs nothing that starting
      broken would not cost more of. schema is fatal because a read service
      against an unmigrated database 500s forever and cannot fix itself; role is
      fatal because it is a security boundary rather than a dependency, and
      degrading gracefully into an HTTP surface with SELECT on participant_stat is
      not degrading gracefully. Grants, keys and artifacts stay loud and non-fatal —
      each is fixable while the process runs.

    Changed

    • assertSchema stopped being the reporter for faults it knows nothing
      about.
      It was the first statement to touch the database, so every
      connection-level failure surfaced there regardless of cause — which is how
      0.24.0's malformed DATABASE_URL came out as a TypeError from four frames
      inside pg while its careful "the crawler owns these tables" message was
      never reached. It is now one check of six, behind preflight/db, and only
      ever speaks about tables.

    • The shape of a boot check moved to src/check.ts, shared by both
      processes. CheckOutcome, CheckStatus, the 10-second per-check timeout and
      the preflight/summary line are now defined once. The two blocks are read in
      the same Portainer viewer, often one above the other, so a parallel structure
      would have drifted into two timeouts, two summary lines and two spellings of
      "failed". What is deliberately not shared is fatality — that is a policy
      about the checks rather than their shape, so logCheckBlock takes the summary
      wording instead of deciding it.

    • ApiStore.ping() returns the server version rather than nothing, matching
      Store.ping(), so both satisfy one check interface and the boot line can say
      server=PostgreSQL 18.4 instead of proving a query returned.

    Downloads