Skip to content

ci: deploy only what the box runs - #88

Merged
dimslaev merged 4 commits into
ci/backend-import-layersfrom
ci/deploy-app-only
Oct 19, 2026
Merged

dimslaev merged 4 commits into
ci/backend-import-layersfrom
ci/deploy-app-only

Conversation

@dimslaev

Copy link
Copy Markdown
Owner

This changes what lands on the production box. The first deploy after merge deletes every file in /opt/agentique that the new filter doesn't list. That includes old pipeline/, docs/, scripts/, frontend/src, backend/baml_src, backend/conftest.py, the test_*.py files and anything else from earlier deploys. It keeps .env, .venv and .git.

After the first deploy, check:

  • systemctl status agentique-backend is active, and curl -s 127.0.0.1:8000/api/v1/utils/health-check/ answers
  • systemctl list-timers 'agentique-*' lists backup 03:30, pipeline 04:00 and report 06:00
  • ls -A /opt/agentique shows only .env .git .python-version .venv backend compose.yml deploy frontend pyproject.toml uv.lock, ls /opt/agentique/backend shows .python-version app baml_client migrations pyproject.toml, and ls /opt/agentique/frontend shows dist
  • optionally sudo systemctl start agentique-report to run one unit end to end

Stacked on #87 (step 7 of the backend migration plan).

What changed

  • deploy/rsync-filter lists what the box runs: backend/app without test_*.py, __pycache__ or app/core/db/factories.py, backend/migrations, backend/baml_client, backend/pyproject.toml, backend/.python-version, root pyproject.toml, uv.lock, .python-version, compose.yml, deploy/ and frontend/dist. Everything else is excluded.
  • The deploy job runs rsync -a --delete --delete-excluded --filter='merge deploy/rsync-filter' ./ /opt/agentique/.
  • The frontend artifact now downloads straight into frontend/dist, so one sync ships it. The separate dist rsync step is gone. The job checks for frontend/dist/index.html before syncing, so an empty or missing artifact fails the job and doesn't delete the live site.
  • deploy/README.md describes the filter, which paths it protects, and how to install units by hand.

Deletion: why --delete-excluded

  • With an include list and a final - *, plain --delete would never delete excluded paths. Old files would stay on the box forever.
  • --delete-excluded deletes them. The P rules protect .env, .venv and .git from deletion, and rsync never scans inside them.
  • The set of protected paths is the same as today's excludes, minus frontend/dist and frontend-dist. Those are now shipped from the artifact. Today's --delete already removed anything on the box that wasn't in the checkout, so files the box creates at runtime (such as __pycache__ under app/) are handled the same as before. The only new deletions are tracked files that are now outside the list.
  • .git is protected even though the deploy never sends it. Today's excludes protected it, and keeping it costs nothing.

Verified against what runs on the box

  • Units: fastapi run ... app/main.py, python -m app.entry.cli.collect, python -m app.entry.cli.report, python app/entry/cli/backup_db.py, all from /opt/agentique/backend. EnvironmentFile is /opt/agentique/.env.
  • prestart.sh runs app.entry.cli.{wait_for_db,create_superuser,seed_tags,seed_articles} and alembic upgrade head. [tool.alembic] script_location = "%(here)s/migrations".
  • uv sync --frozen --package app needs the workspace root's pyproject.toml, uv.lock and every member's pyproject.toml.
  • backup_db.py passes -f <repo root>/compose.yml (parents[4]), so compose.yml has to be at the root.
  • Non-.py files in app/: sign_in.html, welcome.html, weekly_issue.html, tags.json, prestart.sh. The filter ships all of them.
  • baml_client imports from inlinedbaml.py. It doesn't read baml_src at runtime.
  • Only conftest.py imports factories.py, and neither ships.

Simulation

These are the exact commands I ran in a scratch dir. The workspace was a copy of git ls-files. The fake box had an old tree in it: .env, .venv/ holding __pycache__ and test_x.py, .git/, pipeline/, docs/, backend/scripts/, frontend/src/, a stale frontend/dist/old.js and backend/conftest.py.

  • With no dist, test -f frontend/dist/index.html exits 1 and nothing syncs.
  • The workflow's rsync line with the destination swapped: the stale files are gone, .env is unchanged and .venv contents are untouched.
  • uv sync --frozen --package app succeeds on a fresh .venv.
  • uv run --env-file ../.env bash app/entry/cli/prestart.sh with ENVIRONMENT=production against a fresh Postgres 16 + pgvector db succeeds. uv.lock is unchanged afterwards.
  • alembic check reports no new upgrade operations.
  • python -c "import app.main" passes, and so does importing each app.entry.cli.* module and all 112 modules under app.
  • fastapi run --workers 1 ... app/main.py returns 200 on the health check and /docs.
  • alembic upgrade head --sql fails in offline mode on a3b4c5d6e7f8_passwordless_user.py, which reads rows with conn.execute(...).all(). This was already true before this PR and doesn't affect the deploy, which runs migrations online.

Tests added

backend/app/entry/cli/test_deploy_files.py takes the rsync line from the workflow and runs it against the checkout's tracked files. Then it checks:

  • every unit's WorkingDirectory, ExecStart script or -m module, and the box-owned EnvironmentFile and venv
  • every working-directory: in the deploy job and the bash script it runs
  • every module prestart.sh runs
  • the uv workspace files, migrations and compose.yml
  • every data file in app/
  • that no test code ships
  • that every shipped module imports from the shipped tree and nothing loads from the checkout through the editable install

I broke the filter on purpose to check the tests catch it. Dropping compose.yml, migrations or baml_client fails them, and so does a prod module importing factories. The test needs rsync 3 and skips without it. ubuntu-latest has 3.2.7.

Bug fixes

docs: point the deploy README's env table at app/features

The newsletter env table still named app/newsletter/broadcast.py and app/newsletter/routes.py. They are now app/features/newsletter/broadcast.py and service.py.

Review

I ran the code-review skill at high effort on the diff. Fixed:

  • The import check could load a dropped package (baml_client) from the checkout through the editable .pth and still pass. It now runs from the shipped tree and fails if any module came from the checkout.
  • Folding dist into the one --delete-excluded sync meant a missing artifact would delete the live site. The job now checks for index.html first.
  • The README only mentioned the timers. An ExecStart path change also breaks the backend at the deploy's own restart. The README now says so.
  • The test used its own copy of the rsync command. It now reads the command from the workflow.
  • The test synced untracked files. It now syncs git ls-files only.
  • The unit parser broke on -/@/+ prefixes, ExecStart with no arguments and -m packages. It handles them now.
  • The data-file check used a suffix allowlist. It now covers every non-.py file.
  • The test now skips when rsync isn't version 3.

Units: installed by hand, on purpose

  • The deploy job's only sudo command is systemctl restart agentique-backend.
  • Letting the job install units would mean sudo cp from /opt/agentique/deploy or the workspace. The runner user can write to both, so it could install a root unit with any ExecStart, which gives it root. This is the same reason agentique-sql lives in /usr/local/bin. I don't propose it.
  • Something safe would need a root-owned installer that checks each unit against a fixed allowlist of contents, and that's more than this PR should take on.

Follow-ups

  • Report unit alert: no unit uses OnFailure= today. The pipeline reports through the 06:00 mail, so a failed report sends nothing and nobody notices. A clean fix is a small agentique-alert@.service that mails through app.core.email, wired in with OnFailure=agentique-alert@%n.service. I left it out because it adds a unit and a CLI module.
  • uv sync --frozen --package app installs the dev groups on the box (pytest, mypy, prek, zizmor). --no-dev would drop them. Check that nothing in prod needs a dev dependency first.
  • .claude/skills/curate/regression.md has two typos hits (syste, generat, from truncated titles). They're not in this diff, and pre-commit CI only checks changed files.
  • Locally, one backend test (test_read_endpoints_are_public[/search?q=agents]) fails because the sandbox proxy blocks the model2vec download (403). The other 699 pass.

Generated by Claude Code

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
@dimslaev
dimslaev merged commit b0ddf95 into ci/backend-import-layers Oct 19, 2026
5 checks passed
@dimslaev
dimslaev deleted the ci/deploy-app-only branch October 19, 2026 22:37
dimslaev added a commit that referenced this pull request Oct 19, 2026
* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
dimslaev added a commit that referenced this pull request Oct 19, 2026
* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
dimslaev added a commit that referenced this pull request Oct 19, 2026
* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
dimslaev added a commit that referenced this pull request Oct 19, 2026
* refactor: split backend domains into app/features

* refactor: type app/features for mypy strict

* fix: keep links on a shared platform's subdomains with the aggregator

* fix: drop a newsletter product whose picked link is past the search results

* test: cover approve and reject against the schema, and publisher resolution edges

* chore: point the end-of-file exclude at the moved email templates

* refactor: tidy comments and test helpers from the features split

* refactor: move mcp tools and scripts into app/entry (#85)

* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
dimslaev added a commit that referenced this pull request Oct 19, 2026
* fix: percent-encode database credentials in the dsn

* fix: declare the indexes and column types the migrations built

* refactor: move tables into app/core/db

* test: cover the schema against migrations, the engine and ledger writes

* refactor: split backend domains into app/features (#84)

* refactor: split backend domains into app/features

* refactor: type app/features for mypy strict

* fix: keep links on a shared platform's subdomains with the aggregator

* fix: drop a newsletter product whose picked link is past the search results

* test: cover approve and reject against the schema, and publisher resolution edges

* chore: point the end-of-file exclude at the moved email templates

* refactor: tidy comments and test helpers from the features split

* refactor: move mcp tools and scripts into app/entry (#85)

* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
dimslaev added a commit that referenced this pull request Oct 19, 2026
* refactor: move shared backend mechanism into app/core

* refactor: reword the check_link module docstring

* fix: read a feed date with no timezone as utc

* test: cover token rejection, the daily mail send and github lookups

* test: spell unparsable the way typos expects

* refactor: move tables into app/core/db (#83)

* fix: percent-encode database credentials in the dsn

* fix: declare the indexes and column types the migrations built

* refactor: move tables into app/core/db

* test: cover the schema against migrations, the engine and ledger writes

* refactor: split backend domains into app/features (#84)

* refactor: split backend domains into app/features

* refactor: type app/features for mypy strict

* fix: keep links on a shared platform's subdomains with the aggregator

* fix: drop a newsletter product whose picked link is past the search results

* test: cover approve and reject against the schema, and publisher resolution edges

* chore: point the end-of-file exclude at the moved email templates

* refactor: tidy comments and test helpers from the features split

* refactor: move mcp tools and scripts into app/entry (#85)

* refactor: expose what entry needs through each feature's service

* refactor: split the mcp tools into app/entry/mcp and fold the router into main

* refactor: move scripts and the timer entrypoints into app/entry/cli

* test: cover the mcp token gate end to end over http

* test: cover the cli entrypoints' targets and exit codes

* fix: fail the backup when pg_dump writes nothing

* refactor: make app/entry pass strict mypy

* fix: keep sentry off in development

* chore: name the seed module in restore-db's hint without backticks

* docs: say what tests-start.sh does with its argument

* test: return the tool result's error flag and text from the gate tests' helper

* fix: stop importing the email module from configuring the root logger

* fix: require the alembic that reads its config from pyproject

* test: check the collect run leaves the root logger alone

* docs: say where command-line entrypoints live

* refactor: name the newsletter service alike in every mcp tool

* test: colocate backend tests beside their modules (#86)

* test: colocate backend tests beside their modules

* test: cover the curation tools, sign-in refusals, publisher lookups and the known-URL filter

* fix: fail the daily report when its mail does not send

* test: pin that the report reaches the send, and clean up the filter's publisher

* ci: enforce backend import layers (#87)

* fix: run mypy and ty in pre-commit with the backend config

* ci: enforce backend import layers

* fix: name the exact claude-code-action tag in its pin comments

* ci: deploy only what the box runs (#88)

* docs: point the deploy README's env table at app/features

* ci: deploy only what the box runs

* test: check the deploy filter ships every path the units and the job use

* docs: record the layered backend (#89)

* docs: record the layered backend

* fix: address the backend migration review (#91)

* fix: configure logging for the api process

* fix: let a publisher's link claim a platform subdomain

* fix: fail a link loudly on a non-numeric jev answer

* docs: point stale docstrings at where the code moved

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants