feat: deploy-freshness check -- catch stale containers automatically (#200/#194) - #35
Merged
Merged
Conversation
… hits the exact regression (#200/#194) Hit this pattern 3x in one session: a container running stale code relative to what's merged/available upstream, invisible until someone happens to test the exact behavior that changed -- - celery-worker: rename regression (rebuilt after the fact) - workbench: DAG-count regression (same) - toolserver: ghcr.io image 91 days stale, no local build fallback at all Adds a one-shot deploy-verify compose service, wired via depends_on: condition: service_completed_successfully on every service it covers (workbench, celery-worker, tes, toolserver) -- runs automatically on every docker compose up, including 'docker compose up -d <one-service>' (the pattern used all session), no new command for anyone to learn or remember. Always exits 0 -- a loud warning system, not a startup gate, so a broken upstream CI (toolserver) can't take down the whole stack. Two checks, same underlying question (is this image older than what it should reflect): - build-from-source (workbench/celery-worker/tes): image Created vs origin/main's latest commit, via one lightweight GitHub REST API call (not git fetch -- no object transfer). Also reads refs/heads/main directly (plain-text file, no git binary needed) to distinguish 'forgot to pull' from 'forgot to rebuild'. - pull-from-registry (toolserver): age threshold (default 14d, overridable per-service via a deploy-verify.max-age-days label). Same threshold used as build-type's fallback if the GitHub API call fails. Output goes to both stdout (docker compose up's own combined output) and STALE_SERVICES.txt at repo root (overwritten each run) -- redundant channels so a finding is harder to scroll past. Tested against the real running stack, not just docker compose config --quiet: found and fixed 2 real bugs along the way (dropped 'Z' in a regex substitution producing a naive datetime; tes's explicit 'image: omnibioai-tes-local' override not matching the assumed <project>-<service> auto-derived tag). Final run correctly identified celery-worker and tes as genuinely stale right now, and toolserver's already-known 91-day staleness -- real, current findings, not synthetic test data. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a deploy-freshness check: catches the pattern hit 3x in one session -- a container running stale code relative to what's merged/available upstream, invisible until someone happens to test the exact behavior that changed.
celery-worker-- rename regression, image rebuilt only after the factworkbench-- DAG-count regression, sametoolserver--ghcr.ioimage 91 days stale, no local build fallback exists at allDesign
Proposed and approved before any code was written (see #200 discussion). Two checks, one underlying question -- "is this image older than what it should reflect":
workbench,celery-worker,tes): imageCreatedtimestamp vsorigin/main's latest commit, via one lightweight GitHub REST API call (GET /repos/{owner}/{repo}/commits/main) -- notgit fetch, no object transfer, just commit metadata. Also readsrefs/heads/maindirectly (plain-text file inside the mounted.gitdir, nogitbinary needed) to distinguish "forgot to pull" (local ref also behind origin) from "forgot to rebuild" (local ref matches origin; image just predates it).toolserver): no source repo to compare against by definition -- that's the actual gap. Falls back to an age threshold (default 14 days). Same threshold is also build-type's fallback if the GitHub API call fails for any reason (rate limit, network, token issue) -- never crashes, never blocks, degrades to best-available signal.Where it runs
A one-shot
deploy-verifycompose service, wired viadepends_on: condition: service_completed_successfullyon every service it covers. Runs automatically on everydocker compose up, includingdocker compose up -d <one-service>(the exact single-service pattern used throughout this session) -- no new command for anyone to learn or remember. This environment has no CI/CD and no wrapper deploy script (confirmed before designing this --dev_up.shdoesn't touch Docker at all), so this had to fit inside the plaindocker composecommands people already type, not assume infrastructure that doesn't exist here.Always exits 0. This is a loud warning system, not a startup gate -- a hard block would let
toolserver's already-broken upstream CI take down the entire stack for a reason that has nothing to do with today's deploy.Output -- two redundant channels
docker compose up's own combined output /docker compose logs deploy-verifySTALE_SERVICES.txtat repo root, overwritten every runso a finding is harder to scroll past than a single log line would be.
Per-service
max_age_daysoverrideA
deploy-verify.max-age-dayslabel in that service'slabels:block (default 14 if absent) -- not a single global constant. Supports both list-style (- deploy-verify.max-age-days=30) and dict-style compose label syntax, and${VAR:-default}interpolation inside the label value. Tested in isolation for all four shapes before committing.Scope: 4 services today, not all ~30
workbench,celery-worker,tes,toolserver-- the ones actually hit this session. Several other build-from-source services (tes,model-registry) share a broad multi-repo build context where "which subdirectory is the real source repo" gets ambiguous to auto-derive reliably; rather than guess wrong for services nobody's hit an incident on yet, scoped this to an explicit, short list. Extending coverage is a straightforward follow-up once this is validated in practice, not a blocker.Testing -- against the real running stack, not just
docker compose config --quietFound and fixed 2 real bugs along the way:
Createdtimestamps dropped the trailingZin its replacement string, producing a naivedatetimeand crashing every comparison (TypeError: can't compare offset-naive and offset-aware datetimes). Fixed by relying on Python 3.11+'sfromisoformat, which handlesZand arbitrary fractional-second precision natively -- deleted the regex entirely rather than fixing it, simpler and correct.teshas an explicitimage: omnibioai-tes-localoverride indocker-compose.yml, not Compose's auto-derived<project>-<service>tag every other build-from-source service uses -- the script assumed the auto-derived pattern unconditionally and silently skippedtes([SKIP] image omnibioai-studio-tes:latest not built yet) until this was caught and fixed to check for an explicitimage:override first.Final real run against the actual live stack (not synthetic test data):
celery-workerandtesare genuinely stale right now (this PR itself surfaced that) -- real, current findings, not test fixtures.toolserver's known 91-day staleness (see #200) is correctly caught by the age-threshold path.workbenchcorrectly shows OK.Also confirmed the never-blocks guarantee for real:
docker compose up -d workbenchrandeploy-verifyautomatically, it reported the stale findings above, andworkbenchstill started normally.Known limitations (stated in the script's own docstring too)
origin/mainfetched live, but the local ahead/behind classification only distinguishes "matches origin" vs "doesn't" via a single SHA comparison -- not a full ahead/behind commit count (would require agit fetch, deliberately avoided per the design discussion -- a lightweightGET /commits/mainAPI call was chosen instead).Createdtimestamp is a reasonable but not perfectly bulletproof proxy for "when the code was actually built."Part of #200 (does not close it).
🤖 Generated with Claude Code