Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
2c2448f
feat: add isolated tester host installer
Aug 16, 2026
6196867
fix: make tester lifecycle recoverable and scoped
Aug 16, 2026
de515af
test: cover tester cleanup failures
Aug 16, 2026
4866a84
docs: clarify tester credential boundary
Aug 16, 2026
50905e0
test: support read-only releases as non-root
Aug 16, 2026
beff43c
fix: close remaining tester isolation channels
Aug 16, 2026
b9242fb
fix: make tester lifecycle repairable
Aug 16, 2026
25fbac1
fix: serialize tester lifecycle updates
Aug 16, 2026
3e5c7e4
fix: contain tester recovery failures
Aug 16, 2026
9b09cfa
fix: quiesce tester timers during activation
Aug 16, 2026
e4fca5b
fix: close tester isolation gaps
Aug 16, 2026
e2c33af
fix: route tester lifecycle through stable launcher
Aug 16, 2026
7db5f25
test: follow dedicated tester lifecycle lock
Aug 16, 2026
034bf0d
fix: make tester uninstall idempotent
Aug 17, 2026
9e8dc3c
Merge remote-tracking branch 'origin/main' into pr-76
Aug 17, 2026
d003568
fix: close final tester lifecycle gaps
Aug 17, 2026
4be273e
fix: restore tester when rollback record fails
Aug 17, 2026
25b4306
fix: repair PR #76 rewrite corruption and resolve 8 Codex findings
Nickfost Aug 28, 2026
a3793b7
Merge origin/main into pr76 repair (no history rewrite)
Nickfost Aug 28, 2026
c661e72
test: reproduce archive symlink dereference
Nickfost Aug 30, 2026
3ef9fbf
fix: reject symlinked tester release members
Nickfost Aug 30, 2026
6fcdb67
test: reproduce escaped explicit include key
Nickfost Aug 30, 2026
c1650ac
fix: reject explicit Compose mapping keys
Nickfost Aug 30, 2026
054d159
test: reproduce source label file loading
Nickfost Aug 30, 2026
1269edd
fix: reject Compose label files before rendering
Nickfost Aug 30, 2026
5113982
test: reproduce broken deployed tester launcher
Nickfost Aug 30, 2026
e3f425b
fix: validate activation through deployed launcher
Nickfost Aug 30, 2026
bfa705b
fix: preserve inherited tester lifecycle lock
Nickfost Aug 30, 2026
df14fa5
test: require activation service execution
Nickfost Aug 30, 2026
d3cc48d
fix: exercise tester maintenance services on activation
Nickfost Aug 30, 2026
7516fa5
fix: close tester maintenance review findings
Nickfost Aug 30, 2026
08be87d
fix: avoid tester maintenance lock deadlock
Nickfost Aug 30, 2026
9654ace
fix: harden tester Compose policy validation
Nickfost Aug 30, 2026
55db13a
fix: close tester activation policy gaps
Nickfost Aug 30, 2026
e8a517a
fix: close tester verification gaps
Nickfost Aug 30, 2026
1892638
fix: close tester lifecycle verification gaps
Nickfost Aug 30, 2026
818b780
fix: close remaining tester policy gaps
Nickfost Aug 30, 2026
8087c37
fix: close tester lifecycle verification gaps
Nickfost Aug 30, 2026
f183587
fix: close tester policy and health gaps
Nickfost Aug 30, 2026
5428783
fix: close tester maintenance policy gaps
Nickfost Aug 30, 2026
a91697b
fix: harden tester maintenance validation
Nickfost Aug 30, 2026
589fc4e
test: preserve rollback route fixture
Nickfost Aug 30, 2026
57db054
fix: permit tester health probe sockets
Nickfost Aug 30, 2026
de02123
fix: scope tester runtime prerequisites to each action
Nickfost Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/OPERATOR-WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Use this page to choose an outcome, confirm the responsible role, and either fol
| Authorize and onboard an application project | Current, experimental | Application repository, private configuration, and GitHub runner-group policy | Yes; no host mutation | Follow [adding a project](ADDING-A-PROJECT.md), then the [migration procedure](MIGRATING-EXISTING-CI.md). |
| Create controller GitHub credentials and runner-group prerequisites | Current manual procedure; automation in review | GitHub web UI and the target host | Yes | Follow [GitHub App setup](GITHUB-APP-SETUP.md) and the [runner-group procedure](LIVE-PILOT.md#2-create-the-organization-runner-group). [Issue #27](https://github.com/RandomDevelopment/ci-fleet/issues/27) and [PR #75](https://github.com/RandomDevelopment/ci-fleet/pull/75) track automated bootstrap and its required organization-owner evidence; do not use it from the default branch yet. |
| Run an isolated first-job proof | Prototype only | GitHub Actions and one isolated worker host | Yes, transiently | Follow the [live pilot](LIVE-PILOT.md). Stop if the matching job queue is not proven empty. |
| Install a persistent test/staging environment host | In review | Separate test host | N/A on the default branch | [Issue #23](https://github.com/RandomDevelopment/ci-fleet/issues/23) and [PR #76](https://github.com/RandomDevelopment/ci-fleet/pull/76) track the installer and its required real-host evidence. Do not install it from the default branch yet. |
| Install a persistent test/staging environment host | In review | Separate test host | N/A on the default branch | Follow [test-environment host](TESTER-HOST.md); [issue #23](https://github.com/RandomDevelopment/ci-fleet/issues/23) and [PR #76](https://github.com/RandomDevelopment/ci-fleet/pull/76) track the installer and prepared-host evidence. Do not install it from the default branch yet. |
| Install a deployment host | In review | Separate deployment host | N/A on the default branch | [Issue #22](https://github.com/RandomDevelopment/ci-fleet/issues/22) and [PR #69](https://github.com/RandomDevelopment/ci-fleet/pull/69) track the installer and its required real-host evidence. Do not install it from the default branch yet. |
| Deploy to production | Not production-ready | Separate production boundary | N/A | Stop. The project status and production evidence do not authorize this workflow. |
| Publish or update from the standalone configuration template | Planned release path | Management workstation and a private configuration repository | Yes | Use the template vendored in the exact reviewed engine commit today. A standalone immutable release/compatibility signal remains [open](https://github.com/RandomDevelopment/ci-fleet-config-template/issues/12). |
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ New operator? Use [Operator workflows](OPERATOR-WORKFLOWS.md) to select a suppor
| Decide whether it fits my infrastructure | [Architecture](ARCHITECTURE.md) and the root [README](../README.md) |
| Try the experimental implementation safely | [Live pilot runbook](LIVE-PILOT.md) |
| Add another Docker host, VM, computer, or VPS | [Adding a host](ADDING-A-HOST.md) |
| Install an isolated deployed-application test host | [Test-environment host](TESTER-HOST.md) |
| Manage controller configuration from Git | [Git-authored controller desired state](DESIRED-STATE.md) |
| Create the controller GitHub App identity | [GitHub App setup](GITHUB-APP-SETUP.md) |
| Add a private project to the shared runner pool | [Adding a project](ADDING-A-PROJECT.md) |
Expand Down
110 changes: 110 additions & 0 deletions docs/TESTER-HOST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Test-environment host

Status: repository implementation complete; prepared isolated-host acceptance remains required.

This role runs persistent or expiring deployed application test environments. It is deliberately separate from ephemeral CI workers and production deployers. It cannot register ordinary CI runners or promote production releases.

## Boundary

A tester host accepts only:

- a reviewed `ci-fleet` source commit for the tester service;
- root-owned host configuration and environment declarations;
- application images addressed by an immutable `sha256` digest;
- environment secrets stored below that environment's fixed host-local secret directory.

It rejects mutable images, public port binds, host bind mounts, external/unscoped Docker resources, custom volume drivers/options, privileged containers, added capabilities, host or shared namespaces, Docker API access, global container names, and credentials outside the environment secret boundary. Compose environment variables, env files, and configs are forbidden credential channels; use only fixed mode-`0600` Compose secrets. Every service must define positive CPU, memory, and PID limits, remain OOM-killable, be read-only, drop all capabilities, and set `no-new-privileges=true`. The validated rendered Compose model is copied into protected runtime state before activation, so partial starts remain tracked and later cleanup does not depend on a mutable or deleted source definition. Test identity, networks, storage, routes, domains, data, and credentials must have no production authority. Host/network isolation is an external acceptance gate, not something this repository-only change can prove.

## Prepare host-local configuration

The installer supports only `/etc/ci-fleet-tester/tester.env`. Create it and all protected directories as root; never commit them:

```bash
sudo install -d -m 0700 \
/etc/ci-fleet-tester \
/etc/ci-fleet-tester/environments \
/etc/ci-fleet-tester/definitions \
/etc/ci-fleet-tester/secrets
sudo install -m 0600 /dev/null /etc/ci-fleet-tester/tester.env
```

Example non-secret settings:

```text
CI_FLEET_TESTER_DEFAULT_TTL_SECONDS=86400
CI_FLEET_TESTER_MAX_ENVIRONMENTS=20
CI_FLEET_TESTER_DISK_WARN_PERCENT=80
CI_FLEET_TESTER_NETWORK_PROBE_HOST=tester-probe.invalid
CI_FLEET_TESTER_HTTPS_PROBE_URL=https://tester-probe.invalid/health
CI_FLEET_TESTER_ISOLATION_ACK=test-only-no-production-authority
```

Set both probe values to a test-only host whose DNS resolution and HTTPS HEAD response exercise the intended local proxy path without carrying credentials. The acknowledgement is required but is not proof: an authorized operator must still verify that the prepared host has no production identity or network authority.

For environment `example-preview`, create root-owned mode-`0600` `/etc/ci-fleet-tester/environments/example-preview.env`:

```text
CI_FLEET_TESTER_PROJECT=example-project
CI_FLEET_TESTER_OWNER=example-owner
CI_FLEET_TESTER_COMPOSE_FILE=/etc/ci-fleet-tester/definitions/example-preview.yaml
CI_FLEET_TESTER_EXPIRES_AT=REVIEWED_FUTURE_UNIX_TIME
CI_FLEET_TESTER_ROUTE_SERVICE=web
CI_FLEET_TESTER_ROUTE_PORT=18080
```

The Compose file is root-owned mode `0644` and may contain no credential value. Each image must use `registry/path@sha256:REVIEWED_64_HEX_DIGEST`, and each service must set positive `cpus`, `mem_limit`, and `pids_limit` values. Exactly one route is published, on loopback only, at the declared port. Compose-generated network and volume names must remain below `ci-fleet-test-<environment>_...`; explicit external names are rejected.

If credentials are required, create `/etc/ci-fleet-tester/secrets/example-preview` as root-owned mode `0700`, put only test-scope regular files there as root-owned mode `0600`, and reference them through Compose `secrets.file`. Symlinks, external secrets, production credentials, environment-variable secret transport, and files outside that exact directory are unsupported.

## Fresh install or repair

Use a clean reviewed checkout at the exact commit:

```bash
ref=$(git rev-parse 'HEAD^{commit}')
sudo ./scripts/install-tester.sh --install \
--config /etc/ci-fleet-tester/tester.env \
--ref "$ref"
```

The command fails before mutation unless it sees Debian 12 or newer, root, the local default Docker context/socket/root, Compose v2, required generic tools, protected paths, and Docker storage below 80%. It stages an immutable source release, validates it, switches the `current` symlink, installs health and expiration timers, and verifies both configuration and active environments. Repeating the same command is idempotent.

## Environment lifecycle

The runtime command is the one interface for create/update, inspect, reset, and removal:

```bash
sudo /opt/ci-fleet-tester/tester-runtime --converge --environment example-preview
sudo /opt/ci-fleet-tester/tester-runtime --inspect --environment example-preview
sudo /opt/ci-fleet-tester/tester-runtime --reset --environment example-preview
sudo /opt/ci-fleet-tester/tester-runtime --remove --environment example-preview
```

`--converge` validates the full resolved Compose model before `up --wait`. `--reset` removes only that exact Compose project and its volumes, then recreates it from the approved definition and digest. `--remove` uses the same scoped `compose down --volumes`; no global Docker prune is used. State reports only environment/project/owner, loopback route, expiry, source revision, timestamps, and health—not Compose environment values or secret content.

`ci-fleet-tester-health.timer` checks every five minutes. `ci-fleet-tester-cleanup.timer` checks expiration every fifteen minutes. Expired environments are removed through the same scoped path. Disposable environment data is intentionally removed on reset/expiry. Reviewed definitions and fixture sources belong outside runtime state and need ordinary configuration backups; credentials and disposable volumes are never backed up by this service.

## Upgrade, validation, and rollback

```bash
ref=$(git rev-parse 'HEAD^{commit}')
sudo ./scripts/install-tester.sh --upgrade --config /etc/ci-fleet-tester/tester.env --ref "$ref"
sudo ./scripts/install-tester.sh --check --config /etc/ci-fleet-tester/tester.env
sudo ./scripts/install-tester.sh --rollback --config /etc/ci-fleet-tester/tester.env
```

Upgrade validates the candidate before activation and restores the complete previous release if post-switch checks fail. A successful switch records only the previous complete source revision as last known good. Rollback changes the tester service release; it does not rewrite an application's immutable image digest or reset environment data.

## Removal

Remove every environment explicitly, verify no state remains, then uninstall:

```bash
sudo ./scripts/install-tester.sh --uninstall --config /etc/ci-fleet-tester/tester.env
```

Uninstall fails while any managed environment exists. It removes only tester units and immutable service releases. Host configuration, definitions, and secrets remain for explicit operator disposition; the script never guesses which credential may be deleted.

## External acceptance gate

Before this draft can merge, an authorized operator must use a prepared isolated Debian Docker host with test-only identity/network/storage/domain boundaries and provide evidence for: fresh install, unchanged second install, immutable application converge, health, scoped reset, expiration cleanup, upgrade, failed-upgrade restoration, rollback, and removal. No production credentials, database, network authority, ordinary CI runner, or live production system may participate.
20 changes: 20 additions & 0 deletions host/systemd/ci-fleet-tester-cleanup.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[Unit]
Description=Remove expired ci-fleet test environments
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
Comment thread
Nickfost marked this conversation as resolved.
ExecStart=/opt/ci-fleet-tester/tester-runtime --cleanup
User=root
Group=root
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/ci-fleet-tester -/run/lock/ci-fleet-tester
Comment thread
Nickfost marked this conversation as resolved.
TimeoutStartSec=300
RestrictAddressFamilies=AF_UNIX
LockPersonality=yes
MemoryDenyWriteExecute=yes
12 changes: 12 additions & 0 deletions host/systemd/ci-fleet-tester-cleanup.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Unit]
Description=Expire abandoned ci-fleet test environments

[Timer]
OnBootSec=5m
OnUnitActiveSec=15m
Persistent=true
RandomizedDelaySec=1m
Unit=ci-fleet-tester-cleanup.service

[Install]
WantedBy=timers.target
20 changes: 20 additions & 0 deletions host/systemd/ci-fleet-tester-health.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[Unit]
Description=Validate ci-fleet test environments
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/opt/ci-fleet-tester/tester-runtime --health
User=root
Group=root
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict
ReadWritePaths=/var/lib/ci-fleet-tester -/run/lock/ci-fleet-tester
TimeoutStartSec=300
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
LockPersonality=yes
MemoryDenyWriteExecute=yes
12 changes: 12 additions & 0 deletions host/systemd/ci-fleet-tester-health.timer
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Unit]
Description=Check ci-fleet test environments every five minutes

[Timer]
OnBootSec=2m
OnUnitActiveSec=5m
Persistent=true
RandomizedDelaySec=30s
Unit=ci-fleet-tester-health.service

[Install]
WantedBy=timers.target
4 changes: 4 additions & 0 deletions host/systemd/ci-fleet-tester-lock.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Recreate the volatile tester lifecycle lock directory at boot and on
# tmpfiles --create, so the maintenance units and stable launcher can open it
# after a reboot even though /run is cleared.
d /run/lock/ci-fleet-tester 0755 root root -
Loading