docs(openspec): archive fpagent-install-upgrade and sync specs

Documents release pinning (--tag), the pinned-tag metadata route,
installed-version tracking with upgrade reporting, and restart of a
managed running service on upgrade; syncs the four requirements into the
fpagent-installer main spec.
This commit is contained in:
2026-10-02 11:41:50 +01:00
parent ff62770bcc
commit 35d895574f
6 changed files with 308 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-02
@@ -0,0 +1,64 @@
# Design
## Context
See proposal.md — Why. The `fpagent-installer` capability (change
`fpagent-release-installer`) installs the newest release and hands off to the
bundle's `deploy/install.sh`, which replaces the binary but only warns when a
daemon is running. The bundle name already embeds the git hash
(`fpagent-<hash>-<platform>.tar.gz`) and `bundle.sh` already builds the binary
with that hash as its embedded `Version`.
## Goals / Non-Goals
**Goals:** install a pinned release; make a rerun a real upgrade that reports
what changed and takes effect; keep both install paths (hosted and direct
bundle) consistent.
**Non-Goals:** a separate version service or a new `--version` CLI flag; signing
or provenance beyond sha256; a downgrade-unsafe rollback.
## Decisions
1. **The version is the asset-name git hash.** It is already the binary's
embedded `Version` and needs no new endpoint or flag. The hosted installer
parses it, and also writes it as `VERSION` in the extracted bundle so the
bundle installer has it even for bundles built before this change.
2. **The bundle installer owns the upgrade reporting and the restart.** It
knows `INSTALL_PATH` and the service; the hosted installer only fetches,
verifies and hands off. `bundle.sh` ships `VERSION` so direct bundle installs
behave the same. The stamp lives at `${INSTALL_PATH}.version`, so a custom
`--install-path` keeps its own record, and uninstall removes it.
3. **Restart only on a managed, already-running service.** `enable --now` starts
a stopped unit; for an upgrade the installer checks `is-active` before
enabling and restarts only if it was active, avoiding a redundant restart on
a fresh install.
4. **`--tag` reuses the release handler.** A second same-origin metadata route
(`/fpagent/releases/tags/:tag`) shares the latest route's handler, so the
filtering and non-disclosure rules are identical; the installer switches only
the metadata URL, then downloads by the resolved tag.
## Risks / Trade-offs
- [Bundles built before this change carry no VERSION and do not restart] → the
hosted installer writes VERSION from the asset name (fixes reporting), but the
restart is in the bundle and appears only after a new `fingerprint-agent`
release. Until then upgrades still need a manual restart.
- [Upgrade without `--enable-service` leaves a running daemon on the old binary]
→ the installer warns; the documented install command uses `--enable-service`.
- [A pinned tag whose assets differ] → the tag's own metadata drives asset
selection, the same as latest.
## Migration Plan
Release a new `fingerprint-agent` bundle (ships VERSION and restarts on
upgrade), then deploy the app. The hosted installer is backward compatible: with
an older bundle it reports the version but the bundle's older installer ignores
the stamp. Rollback = revert the app; `--tag` can pin the previous release.
## Open Questions
None.
@@ -0,0 +1,42 @@
# Proposal: fpagent-install-upgrade
## Why
The installer always took the newest release and left any running `fpagent`
daemon on the old binary, so there was no way to hold a known-good version and
an "upgrade" did not take effect until a reboot. Operators need a controlled
version and a rerun that actually upgrades.
## What Changes
- The installer accepts `--tag TAG` to install a specific release instead of
`latest`, and the app proxies that tag's metadata through a same-origin route.
- The installed version is the git hash carried in the bundle's asset name. The
installer passes it to the bundle installer, which records it beside the
binary and reports `upgrading <old> -> <new>` on the next run.
- When the installer manages the systemd service and the service was already
running, an upgrade restarts it so the new binary takes effect. The version
stamp is removed on uninstall.
## Capabilities
### New Capabilities
(none)
### Modified Capabilities
- `fpagent-installer`: adds release pinning, a pinned-tag metadata endpoint,
installed-version tracking with upgrade reporting, and restart of a managed
service on upgrade.
## Impact
- `tools/server.js`: `/fpagent/releases/tags/:tag` route (shares the release
metadata handler with `/fpagent/releases/latest`).
- `tools/public/fpagent/install.sh`: `--tag`, version parse from the asset name,
and handoff of the version to the bundle installer.
- `fingerprint-agent` `deploy/install.sh` and `deploy/bundle.sh`: read/stamp the
version and restart an already-running service; `bundle.sh` ships `VERSION`.
- Depends on the `fingerprint-agent` bundle naming, which already embeds the
git hash (`fpagent-<hash>-<platform>.tar.gz`).
@@ -0,0 +1,92 @@
# Spec Delta
## ADDED Requirements
### Requirement: Pinned release selection
The installer SHALL accept a release tag and install that release instead of the
latest. Without a tag it SHALL install the latest release.
#### Scenario: A pinned release is installed
- **WHEN** the installer is invoked with a release tag
- **THEN** it fetches that tag's metadata and installs its bundle
#### Scenario: Default is the latest release
- **WHEN** the installer is invoked without a release tag
- **THEN** it installs the latest release
#### Scenario: Unknown pinned release
- **WHEN** the pinned tag does not exist
- **THEN** the installer exits with a failure message and installs nothing
### Requirement: Pinned release metadata endpoint
The app SHALL expose a same-origin endpoint that returns a specific release's
tag and asset names as same-origin URLs, with the same authentication, filtering
and non-disclosure guarantees as the latest-release endpoint. It SHALL reject a
tag that is not a valid release tag without contacting the forge.
#### Scenario: Tagged release is returned
- **WHEN** a client requests the metadata for an existing tag
- **THEN** the response carries that release's tag and its assets as same-origin
URLs
#### Scenario: Invalid tag is rejected
- **WHEN** a client requests metadata for a tag that is not a valid release tag
- **THEN** the app rejects the request without contacting the forge
### Requirement: Installed version is tracked and upgrades are reported
The installer SHALL treat the git hash in the bundle's asset name as the version.
It SHALL hand that version to the bundle installer, which SHALL record it beside
the installed binary and SHALL report an upgrade with the previous and new
versions on the next run. Uninstalling SHALL remove the recorded version.
#### Scenario: Version is derived from the asset name
- **WHEN** the installer selects a bundle named `fpagent-<hash>-<platform>.tar.gz`
- **THEN** it uses `<hash>` as the version
#### Scenario: Upgrade is reported
- **WHEN** the recorded version differs from the version being installed
- **THEN** the bundle installer reports the change from the old to the new
version
#### Scenario: First install records the version
- **WHEN** no version has been recorded yet
- **THEN** the bundle installer records the new version and reports no upgrade
#### Scenario: Uninstall removes the recorded version
- **WHEN** the installer is asked to uninstall
- **THEN** the recorded version is removed
### Requirement: A managed running service is restarted on upgrade
When the installer is asked to manage the systemd service and a service is
already running, the installer SHALL restart it after installing the new binary
so the new binary takes effect. A fresh install SHALL NOT perform a redundant
restart, and the installer SHALL NOT restart the service when service management
is not requested.
#### Scenario: Upgrade restarts the running service
- **WHEN** the running service is managed and a new binary is installed
- **THEN** the service is restarted on the new binary
#### Scenario: Fresh install starts the service
- **WHEN** the service is not running and service management is requested
- **THEN** the service is started and no separate restart is performed
#### Scenario: Service management not requested
- **WHEN** service management is not requested
- **THEN** the installer does not restart the service
@@ -0,0 +1,19 @@
# Tasks
- [x] 1. App: add `/fpagent/releases/tags/:tag`, sharing the release-metadata
handler with `/fpagent/releases/latest`; reject invalid tags without contacting
the forge.
- [x] 2. Hosted installer: parse `--tag`, fetch the pinned tag's metadata,
derive the git hash from the asset name, and write it as `VERSION` into the
extracted bundle before handoff.
- [x] 3. Bundle installer (`fingerprint-agent`): read `VERSION` beside the script
or the binary, report `upgrading <old> -> <new>` from `${INSTALL_PATH}.version`,
stamp the new version, remove it on uninstall.
- [x] 4. Bundle installer: restart an already-running `fpagent.service` after
`enable --now` (via `is-active`, so a fresh install does not restart).
- [x] 5. `bundle.sh`: ship `VERSION` in the bundle.
- [x] 6. Tests: tag-route metadata and invalid-tag rejection; `--tag` makes the
installer request the tag endpoint, not latest. Verified 59 tests pass.
- [x] 7. Verified end to end: a real patched bundle served through a fake gateway
→ hosted installer → bundle installer reported `upgrading fpagent deadbeef ->
cafebabe`, version `cafebabe`, with options forwarded.
+89
View File
@@ -134,3 +134,92 @@ asset.
- **WHEN** the release key is supplied in the environment - **WHEN** the release key is supplied in the environment
- **THEN** the installer and proxy endpoints use it without any change to the - **THEN** the installer and proxy endpoints use it without any change to the
hardcoded configuration hardcoded configuration
### Requirement: Pinned release selection
The installer SHALL accept a release tag and install that release instead of the
latest. Without a tag it SHALL install the latest release.
#### Scenario: A pinned release is installed
- **WHEN** the installer is invoked with a release tag
- **THEN** it fetches that tag's metadata and installs its bundle
#### Scenario: Default is the latest release
- **WHEN** the installer is invoked without a release tag
- **THEN** it installs the latest release
#### Scenario: Unknown pinned release
- **WHEN** the pinned tag does not exist
- **THEN** the installer exits with a failure message and installs nothing
### Requirement: Pinned release metadata endpoint
The app SHALL expose a same-origin endpoint that returns a specific release's
tag and asset names as same-origin URLs, with the same authentication, filtering
and non-disclosure guarantees as the latest-release endpoint. It SHALL reject a
tag that is not a valid release tag without contacting the forge.
#### Scenario: Tagged release is returned
- **WHEN** a client requests the metadata for an existing tag
- **THEN** the response carries that release's tag and its assets as same-origin
URLs
#### Scenario: Invalid tag is rejected
- **WHEN** a client requests metadata for a tag that is not a valid release tag
- **THEN** the app rejects the request without contacting the forge
### Requirement: Installed version is tracked and upgrades are reported
The installer SHALL treat the git hash in the bundle's asset name as the version.
It SHALL hand that version to the bundle installer, which SHALL record it beside
the installed binary and SHALL report an upgrade with the previous and new
versions on the next run. Uninstalling SHALL remove the recorded version.
#### Scenario: Version is derived from the asset name
- **WHEN** the installer selects a bundle named `fpagent-<hash>-<platform>.tar.gz`
- **THEN** it uses `<hash>` as the version
#### Scenario: Upgrade is reported
- **WHEN** the recorded version differs from the version being installed
- **THEN** the bundle installer reports the change from the old to the new
version
#### Scenario: First install records the version
- **WHEN** no version has been recorded yet
- **THEN** the bundle installer records the new version and reports no upgrade
#### Scenario: Uninstall removes the recorded version
- **WHEN** the installer is asked to uninstall
- **THEN** the recorded version is removed
### Requirement: A managed running service is restarted on upgrade
When the installer is asked to manage the systemd service and a service is
already running, the installer SHALL restart it after installing the new binary
so the new binary takes effect. A fresh install SHALL NOT perform a redundant
restart, and the installer SHALL NOT restart the service when service management
is not requested.
#### Scenario: Upgrade restarts the running service
- **WHEN** the running service is managed and a new binary is installed
- **THEN** the service is restarted on the new binary
#### Scenario: Fresh install starts the service
- **WHEN** the service is not running and service management is requested
- **THEN** the service is started and no separate restart is performed
#### Scenario: Service management not requested
- **WHEN** service management is not requested
- **THEN** the installer does not restart the service