2026-10-02 09:53:35 +01:00
|
|
|
# fpagent-installer Specification
|
|
|
|
|
|
|
|
|
|
## Purpose
|
|
|
|
|
|
|
|
|
|
Lets a field laptop install the latest private `fpagent` release with a single
|
|
|
|
|
public command, without ever handling the forge credential — the tools app
|
|
|
|
|
proxies the private release and serves the installer.
|
|
|
|
|
|
|
|
|
|
## Requirements
|
|
|
|
|
|
|
|
|
|
### Requirement: Public installer endpoint
|
|
|
|
|
|
|
|
|
|
The app SHALL serve an installer script at `/fpagent/install.sh` that is
|
2026-10-06 19:19:23 +01:00
|
|
|
reachable without authentication. When run, the script SHALL determine the
|
|
|
|
|
host's architecture, fetch the latest release metadata, choose the bundle for
|
|
|
|
|
the host architecture, verify the bundle's published sha256 checksum, extract
|
|
|
|
|
it, and hand off to the extracted bundle's own installer. It SHALL prefer a
|
|
|
|
|
bundle built for the host's distro codename when one exists for that
|
|
|
|
|
architecture, and otherwise use the generic bundle for that architecture. The
|
|
|
|
|
script SHALL be served uncached.
|
2026-10-02 09:53:35 +01:00
|
|
|
|
|
|
|
|
#### Scenario: Installer is fetched anonymously
|
|
|
|
|
|
|
|
|
|
- **WHEN** a client requests `/fpagent/install.sh` without a session
|
|
|
|
|
- **THEN** the app returns the installer script and does not redirect to login
|
|
|
|
|
|
|
|
|
|
#### Scenario: Host-appropriate bundle is chosen
|
|
|
|
|
|
|
|
|
|
- **WHEN** the latest release offers both a bundle for the host's Ubuntu
|
|
|
|
|
codename and a generic bundle
|
|
|
|
|
- **THEN** the installer uses the codename bundle
|
|
|
|
|
|
|
|
|
|
#### Scenario: Generic bundle is the fallback
|
|
|
|
|
|
|
|
|
|
- **WHEN** the latest release offers no bundle matching the host's Ubuntu
|
|
|
|
|
codename
|
|
|
|
|
- **THEN** the installer uses the generic bundle
|
|
|
|
|
|
2026-10-06 19:19:23 +01:00
|
|
|
#### Scenario: Bundle matches the host architecture
|
|
|
|
|
|
|
|
|
|
- **WHEN** the latest release offers a bundle for the host architecture and a
|
|
|
|
|
bundle for another architecture
|
|
|
|
|
- **THEN** the installer chooses the bundle for the host architecture
|
|
|
|
|
- **AND** it never chooses a bundle for another architecture
|
|
|
|
|
|
|
|
|
|
#### Scenario: 32-bit host uses the 32-bit bundle
|
|
|
|
|
|
|
|
|
|
- **WHEN** the installer runs on an i386 host and the release offers only a
|
|
|
|
|
generic 32-bit bundle
|
|
|
|
|
- **THEN** the installer uses that generic 32-bit bundle
|
|
|
|
|
|
2026-10-02 09:53:35 +01:00
|
|
|
#### Scenario: Checksum is verified before extraction
|
|
|
|
|
|
|
|
|
|
- **WHEN** the installer downloads a bundle and its published checksum
|
|
|
|
|
- **THEN** it verifies the checksum before extracting and refuses to install
|
|
|
|
|
when the checksum does not match
|
|
|
|
|
|
|
|
|
|
#### Scenario: Options are forwarded to the bundled installer
|
|
|
|
|
|
2026-10-06 19:19:23 +01:00
|
|
|
- **WHEN** the installer is invoked with options such as `--no-service` or
|
2026-10-02 09:53:35 +01:00
|
|
|
`--check`
|
|
|
|
|
- **THEN** it passes those options through to the extracted bundle's installer
|
|
|
|
|
|
|
|
|
|
#### Scenario: No compatible bundle
|
|
|
|
|
|
|
|
|
|
- **WHEN** the latest release contains no bundle for the host architecture
|
2026-10-06 19:19:23 +01:00
|
|
|
- **THEN** the installer exits with a failure message that names the host
|
|
|
|
|
architecture and installs nothing
|
|
|
|
|
|
|
|
|
|
#### Scenario: Unsupported architecture
|
|
|
|
|
|
|
|
|
|
- **WHEN** the installer runs on a machine whose architecture is neither
|
|
|
|
|
x86_64 nor i386
|
|
|
|
|
- **THEN** it exits with a failure message and installs nothing
|
2026-10-02 09:53:35 +01:00
|
|
|
|
|
|
|
|
### Requirement: Release metadata endpoint
|
|
|
|
|
|
|
|
|
|
The app SHALL expose a same-origin endpoint that returns the latest release's
|
|
|
|
|
tag and the names of its downloadable assets as same-origin URLs. The app SHALL
|
|
|
|
|
authenticate to the private forge using the configured release key, SHALL omit
|
|
|
|
|
asset names that are not valid asset names, and SHALL NOT reveal the release
|
|
|
|
|
key or the forge host to clients.
|
|
|
|
|
|
|
|
|
|
#### Scenario: Latest release is returned
|
|
|
|
|
|
|
|
|
|
- **WHEN** a client requests the latest-release endpoint and the forge returns
|
|
|
|
|
a release
|
|
|
|
|
- **THEN** the response carries the release tag and each asset's name and a
|
|
|
|
|
same-origin URL under `/fpagent/releases/download/`
|
|
|
|
|
|
|
|
|
|
#### Scenario: Unsafe asset names are omitted
|
|
|
|
|
|
|
|
|
|
- **WHEN** the forge release lists an asset whose name is not a valid asset
|
|
|
|
|
name
|
|
|
|
|
- **THEN** that asset does not appear in the response
|
|
|
|
|
|
|
|
|
|
#### Scenario: Release key is not exposed
|
|
|
|
|
|
|
|
|
|
- **WHEN** any client fetches the latest-release endpoint
|
|
|
|
|
- **THEN** the response body contains neither the release key nor the forge host
|
|
|
|
|
|
|
|
|
|
#### Scenario: Installer not configured
|
|
|
|
|
|
|
|
|
|
- **WHEN** the release key is not configured
|
|
|
|
|
- **THEN** the endpoint answers that the installer is not configured
|
|
|
|
|
|
|
|
|
|
#### Scenario: Forge unavailable
|
|
|
|
|
|
|
|
|
|
- **WHEN** the forge cannot be reached or returns an error for the latest
|
|
|
|
|
release
|
|
|
|
|
- **THEN** the endpoint answers with a gateway error
|
|
|
|
|
|
|
|
|
|
### Requirement: Release asset download proxy
|
|
|
|
|
|
|
|
|
|
The app SHALL stream a requested release asset to the client through a
|
|
|
|
|
same-origin route, authenticating the upstream request with the release key so
|
|
|
|
|
the client sends no credentials. It SHALL reject asset names that are not valid
|
|
|
|
|
asset names and SHALL report a missing asset as not found.
|
|
|
|
|
|
|
|
|
|
#### Scenario: Asset is streamed without client credentials
|
|
|
|
|
|
|
|
|
|
- **WHEN** a client requests a release asset by tag and name without any
|
|
|
|
|
credential
|
|
|
|
|
- **THEN** the app fetches it from the forge with the release key and streams
|
|
|
|
|
its bytes to the client
|
|
|
|
|
|
|
|
|
|
#### Scenario: Invalid asset name is rejected
|
|
|
|
|
|
|
|
|
|
- **WHEN** a client requests an asset whose tag or name is not a valid asset
|
|
|
|
|
name
|
|
|
|
|
- **THEN** the app rejects the request without contacting the forge
|
|
|
|
|
|
|
|
|
|
#### Scenario: Missing asset
|
|
|
|
|
|
|
|
|
|
- **WHEN** the forge reports the requested asset as not found
|
|
|
|
|
- **THEN** the app answers with not found
|
|
|
|
|
|
|
|
|
|
#### Scenario: Installer not configured
|
|
|
|
|
|
|
|
|
|
- **WHEN** the release key is not configured
|
|
|
|
|
- **THEN** the endpoint answers that the installer is not configured
|
|
|
|
|
|
|
|
|
|
### Requirement: Installer credential stays server-side
|
|
|
|
|
|
|
|
|
|
The app SHALL read the private-forge release key from the
|
|
|
|
|
`GITEA_RELEASE_INSTALL_KEY` environment variable. The key SHALL NOT appear in
|
|
|
|
|
the installer script, in any release metadata response, or in any streamed
|
|
|
|
|
asset.
|
|
|
|
|
|
|
|
|
|
#### Scenario: Served installer carries no credential
|
|
|
|
|
|
|
|
|
|
- **WHEN** a client fetches `/fpagent/install.sh`
|
|
|
|
|
- **THEN** the response contains no release key
|
|
|
|
|
|
|
|
|
|
#### Scenario: Credential supplied by the environment
|
|
|
|
|
|
|
|
|
|
- **WHEN** the release key is supplied in the environment
|
|
|
|
|
- **THEN** the installer and proxy endpoints use it without any change to the
|
|
|
|
|
hardcoded configuration
|
2026-10-02 11:41:50 +01:00
|
|
|
|
|
|
|
|
### 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
|
|
|
|
|
|
2026-10-06 19:19:23 +01:00
|
|
|
The installer SHALL treat the git hash in the bundle's asset name as the
|
|
|
|
|
version, for every architecture it can install. 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.
|
2026-10-02 11:41:50 +01:00
|
|
|
|
|
|
|
|
#### 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
|
|
|
|
|
|
2026-10-06 19:19:23 +01:00
|
|
|
#### Scenario: Version is derived for a 32-bit bundle
|
|
|
|
|
|
|
|
|
|
- **WHEN** the installer selects a generic bundle named
|
|
|
|
|
`fpagent-<hash>-linux-i386.tar.gz`
|
|
|
|
|
- **THEN** it uses `<hash>` as the version
|
|
|
|
|
|
2026-10-02 11:41:50 +01:00
|
|
|
#### 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
|