# Setu 0.1.5: followed manifest URLs and setu upgrade

Published 2026-10-21 · Canonical: https://setucp.com/docs/specs/10-21-setu-0-1-5-design

---

Two changes that remove manual steps from keeping a Setu install current. Today a new app version reaches the Updates tab only after an operator publishes its manifest again, and Setu itself is upgraded by re-running `install.sh` with the flags of the first install.

## 1. Apps follow their manifest URL

An app published from a URL **follows** that URL: Setu fetches it again on a schedule and publishes any new version it finds. Nothing is installed or updated on a site; a new version only appears in the site's Updates tab.

- **Schema.** Migration `0004_app_sources.sql` adds a table `app_sources` with one row per app:

A separate table keeps every existing `apps` query unchanged.
  - `app_id`, the primary key, references `apps (id)` with `ON DELETE CASCADE`;
  - `url`, the manifest URL;
  - `checked_at`, when the last check finished, and `last_error`, its error or NULL.
- **Publish.** `POST /api/v1/apps` with `url` stores the URL as the app's source (replacing an earlier one). Publishing pasted TOML leaves the source as it is.
- **Check.** One check of one app:

A check never replaces a version that is already published, even when the manifest at the URL changed: a manifest names binaries and their checksums, and an unattended re-publish must not swap what a running version installs. Re-publishing by hand still replaces it, as today.
  1. fetches the URL with the publish guards (public addresses only, no redirects, 10 s, 1 MiB);
  2. parses and validates the manifest;
  3. refuses a manifest whose `app.id` is not the followed app's;
  4. when the version is **not yet published**, publishes it, audited as `app.publish` with no operator and the detail `<app>/<version> (followed)`;
  5. records `checked_at` and `last_error`.
- **Schedule.** The control plane checks every followed app every 6 hours (on the scheduler tick, the first check one tick after start). Failures are recorded and retried at the next round; they are not errors of the panel.
- **API.**
  - `AppOut` gains `source: { url, checked_at, last_error } | null`.
  - `POST /api/v1/apps/{id}/check` (operator and above) runs a check now and returns `{ published: <version> | null, source }`; 404 when the app follows no URL.
  - `DELETE /api/v1/apps/{id}/source` (operator and above) stops following; 404 when there is none. Audited as `app.source_clear`.
- **UI.** The Apps page shows, per app, "Follows <url> · checked <time>" (or the last error) with **Check now** and **Stop following**.

## 2. `setu upgrade`

`setu upgrade [--version <ver>]`, run as root on any Setu server, moves the installed binary to a release:

1. Resolves the version: `--version`, else the latest release of `setucp/setu` (GitHub API). Already on it → prints so and exits 0.
2. Downloads `setu-<ver>-<target>.tar.gz` and `SHA256SUMS` from the release, refuses an archive whose checksum does not match, extracts it (`tar`) and checks the new binary reports `setu <ver>`.
3. On a control plane (`setu.service` enabled): takes a control-plane backup into the self-backup directory (the same dump and rotation as the nightly one), and stops if it fails.
4. Installs the binary at `/opt/setu/setu` (write `.setu.new`, rename).
5. Restarts `setu.service` (migrations run on boot) and, when enabled, `setu-agent.service`.
6. On a control plane, waits up to 90 s for `/healthz` on the configured listener and fails loudly if it never answers.

The config (`setu.toml`) is read from `/etc/setu` when the current directory has none, so `setu upgrade` and `setu backup` work from any directory. Downgrades are refused (migrations are forward-only) unless `--allow-downgrade`.

## 3. Docs

`docs/OPERATIONS.md` and `docs/INSTALL.md` describe `setu upgrade`; the backup command no longer depends on the current directory.

## Not in scope

Automatic upgrades of Setu itself, upgrading other servers from the panel, and following URLs of private repositories (no auth headers).

## Tests

- **Follow:** publishing from a URL stores the source; a check publishes a new version, leaves an existing one unchanged, refuses another app's id and records errors; stop following clears it. URL fetches in tests go through an injectable fetcher (the real one refuses local addresses).
- **Upgrade:** checksum parsing and matching, version comparison and the downgrade refusal, archive name per target; the install-and-restart path is covered by `scripts/test-install.sh`.
