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:
@@ -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`).
|
||||||
+92
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user