ci(installer): run trusted SSH E2E matrix
Некоторые проверки не удались
CI / test (push) Successful in 3m19s
Docker / Build and publish worker image (push) Successful in 17m20s
SSH Source-Install E2E / Alpine/Ubuntu/Arch source-install E2E (push) Failing after 2m48s
Некоторые проверки не удались
CI / test (push) Successful in 3m19s
Docker / Build and publish worker image (push) Successful in 17m20s
SSH Source-Install E2E / Alpine/Ubuntu/Arch source-install E2E (push) Failing after 2m48s
Этот коммит содержится в:
@@ -1,5 +1,50 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-08-13
|
||||
|
||||
### Source-install E2E in CI (work package 5)
|
||||
|
||||
- Added `.github/workflows/test-ssh.yml`, a Gitea Actions workflow that runs
|
||||
the Alpine/Ubuntu/Arch Docker/OpenSSH source-install matrix (`make test-ssh`)
|
||||
on pushes to `master` and on manual `workflow_dispatch` only, separate from
|
||||
the Docker-free unit CI. It does not trigger on `pull_request`: the distro
|
||||
fixtures execute the checked-out code inside privileged Docker, so untrusted
|
||||
PR code must never run there automatically. It bounds the job with
|
||||
`timeout-minutes: 90` (the go-test `-timeout 60m` stays in place), scopes
|
||||
concurrency per ref (`test-ssh-${{ gitea.ref }}`), declares
|
||||
`permissions: contents: read`, uploads no artifacts, and cleans up on every
|
||||
path.
|
||||
- Pinned the two actions to immutable full commit SHAs (verified against the
|
||||
GitHub API): `actions/checkout@v4` ->
|
||||
`11d5960a326750d5838078e36cf38b85af677262` and `actions/setup-go@v5` ->
|
||||
`40f1582b2485089dde7abd97c1529aa768e1baff`. The repo-wide convention still
|
||||
leaves `ci.yml`/`docker.yml` on moving tags (accepted, documented risk); see
|
||||
`docs/source-installation.md`.
|
||||
- Wired `RSMON_TEST_IMAGE_ALPINE` / `RSMON_TEST_IMAGE_UBUNTU` /
|
||||
`RSMON_TEST_IMAGE_ARCH` and `RSMON_TEST_DOCKER_DNS` repository variables
|
||||
(all empty by default) so CI can pin per-fixture mirror/snapshot images and
|
||||
a resolver for flaky CI DNS.
|
||||
- Added `scripts/ci/test-ssh.sh`: preflights Docker and the harness's
|
||||
loopback port-publishing requirement with a tiny `docker run -p
|
||||
127.0.0.1::22` probe (fails fast with an actionable message plus
|
||||
diagnostics/fix options instead of a 60m timeout on an unsupported runner),
|
||||
then runs `make test-ssh` and traps `EXIT` to remove every leftover
|
||||
`rsmon-worker-test-*` container/network/image tag. Cleanup filters are
|
||||
anchored to the harness's own prefix/repository so they never touch a shared
|
||||
base image. The workflow adds an `if: always()` cleanup step as a
|
||||
belt-and-suspenders so a killed job never leaves test material on the runner.
|
||||
- External network is fetched live by design (go.dev toolchain, rocketgit.ru
|
||||
source clone, distro repos); operators can pin a resolver via the
|
||||
`RSMON_TEST_DOCKER_DNS` repository variable (comma-separated nameservers,
|
||||
applied as `docker run --dns ...`) and mirror/snapshot overrides via
|
||||
`RSMON_TEST_IMAGE_ALPINE` / `RSMON_TEST_IMAGE_UBUNTU` /
|
||||
`RSMON_TEST_IMAGE_ARCH`.
|
||||
- Fixed a history-dependent test fragility: `TestSourceInstallDirtyCheckoutPreservesStaging`
|
||||
and the rollback test's build-failure step dirty the tracked tree by
|
||||
appending a marker line to `Makefile` instead of `git checkout master~1 --
|
||||
Makefile`, which silently stopped dirtying the tree once the last commit did
|
||||
not touch that file.
|
||||
|
||||
## 2026-08-12
|
||||
|
||||
### Source-install hardening review
|
||||
|
||||
@@ -32,9 +32,9 @@ Source-install foundations landed:
|
||||
SHA-256-verified Go toolchain, clone/update of the public repo, resolved
|
||||
branch/commit record, and a staging build. Running service/config is not
|
||||
touched (source-install work package 3);
|
||||
- [ ] atomic service activation, rollback, and failure-preservation tests over
|
||||
- [x] atomic service activation, rollback, and failure-preservation tests over
|
||||
SSH (source-install work package 4);
|
||||
- [ ] run the full source-install E2E matrix in CI (source-install work
|
||||
- [x] run the full source-install E2E matrix in CI (source-install work
|
||||
package 5).
|
||||
|
||||
Gate: a push publishes `sha-<12>` and `latest` manifests for both platforms,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
## Status
|
||||
|
||||
In progress. Work packages 1-4 are implemented:
|
||||
In progress. Work packages 1-5 are implemented (work package 5 is the
|
||||
Docker/OpenSSH E2E matrix wired into CI):
|
||||
|
||||
- `internal/installer/harness` builds and runs real OpenSSH containers
|
||||
for Alpine, Ubuntu, and Arch, waits for real network readiness, captures
|
||||
@@ -246,6 +247,91 @@ for activation and is supplied with `--token`/`--token-file` or
|
||||
`--env-file` (prefer the file options; direct flags expose the value
|
||||
through the process list).
|
||||
|
||||
## Work Package 5: Alpine/Ubuntu/Arch E2E In CI
|
||||
|
||||
Work package 5 runs the full Docker/OpenSSH E2E matrix on pushes to
|
||||
`master` and on manual `workflow_dispatch` runs. It is a separate Gitea
|
||||
Actions workflow (`.github/workflows/test-ssh.yml`) so the ordinary unit
|
||||
CI run stays Docker-free; the job executes `make test-ssh` through
|
||||
`scripts/ci/test-ssh.sh`.
|
||||
|
||||
CI behavior:
|
||||
|
||||
- **Trust boundary and triggers.** The workflow runs *only* on pushes to
|
||||
`master` (the default branch) and on manual `workflow_dispatch`. It
|
||||
deliberately does **not** trigger on `pull_request`: the distro fixtures
|
||||
execute the checked-out repository code inside privileged Docker
|
||||
containers, so an untrusted PR must never reach the runner's Docker
|
||||
surface automatically. Gitea's read-only token clamp for fork PRs does
|
||||
not limit what containers can do on the runner host, so PR coverage is
|
||||
left to the Docker-free unit CI (`ci.yml`) and to manual dispatch after
|
||||
a human reviews the change.
|
||||
- **Concurrency is scoped per ref.** `concurrency.group:
|
||||
test-ssh-${{ gitea.ref }}` gives master pushes their own group (a newer
|
||||
master push cancels a superseded in-flight master run instead of
|
||||
stacking) and gives a manual dispatch on another branch its own group so
|
||||
it never cancels the master run. Gitea Actions evaluates the expression,
|
||||
and `gitea.ref` is the same documented context the repo's `docker.yml`
|
||||
already uses.
|
||||
- **Least privilege.** The workflow declares `permissions: contents: read`
|
||||
(supported by Gitea Actions as the `GITEA_TOKEN` scope for
|
||||
code/releases), so the job's token can only read the repository; the
|
||||
workflow never writes, pushes, or publishes.
|
||||
- **Action revisions.** The two actions this workflow uses are pinned to
|
||||
immutable full commit SHAs (not moving tags): `actions/checkout@v4` ->
|
||||
`11d5960a326750d5838078e36cf38b85af677262` and `actions/setup-go@v5` ->
|
||||
`40f1582b2485089dde7abd97c1529aa768e1baff` (verified 2026-08-13 against
|
||||
the GitHub API that each tag points to a commit object). The repo-wide
|
||||
convention still leaves `ci.yml` and `docker.yml` on moving tags
|
||||
(`@v4`, `@v5`, `@v3`, `@v6`); that is an accepted, documented risk: a
|
||||
tag move can change behavior without a workflow diff. New workflows
|
||||
should pin SHAs like this one; migrating the existing workflows is a
|
||||
separate change.
|
||||
- **Runner requirement.** The harness dials fixture SSH ports published on
|
||||
the Docker daemon's `127.0.0.1`, so the job must share the daemon's
|
||||
loopback (a host-mode runner or a job container with host networking).
|
||||
`scripts/ci/test-ssh.sh` probes this with a tiny `docker run -p
|
||||
127.0.0.1::22` round trip *before* the matrix and fails fast with a
|
||||
clear, actionable message (including diagnostics and fix options)
|
||||
instead of after a 60m go-test timeout.
|
||||
- **Bounding and timeout.** The workflow sets `timeout-minutes: 90` and
|
||||
the existing `make test-ssh` go-test `-timeout 60m` stays in place, so
|
||||
the whole job is hard-bounded even under slow networks or downloads.
|
||||
- **Cleanup.** The harness already tears down every container, network,
|
||||
per-instance fixture image tag, and temp dir on success and failure.
|
||||
`scripts/ci/test-ssh.sh` additionally traps `EXIT` to remove any
|
||||
leftover `rsmon-worker-test-*` resource (container, network, or the
|
||||
per-instance `rsmon-worker-test/<fixture>-<suffix>:local` image tag), and
|
||||
the workflow adds an `if: always()` step that does the same even when the
|
||||
script itself is killed. All filters are anchored to the harness's own
|
||||
prefix and image repository (`name=^rsmon-worker-test-`,
|
||||
`reference=rsmon-worker-test/*`), so cleanup never touches a shared base
|
||||
image (`reg.rsxx.ru/library/alpine:3`, `ubuntu:24.04`, `archlinux:latest`)
|
||||
or an unrelated resource.
|
||||
- **Mirrors and overrides.** The Alpine fixture already uses the
|
||||
`reg.rsxx.ru/library/alpine:3` mirror. Ubuntu and Arch have no mirror yet
|
||||
and default to Docker Hub. The workflow wires the
|
||||
`RSMON_TEST_IMAGE_ALPINE`, `RSMON_TEST_IMAGE_UBUNTU`, and
|
||||
`RSMON_TEST_IMAGE_ARCH` repository variables (empty by default) so an
|
||||
operator can pin a mirror or a specific distro snapshot per fixture, and
|
||||
the `RSMON_TEST_DOCKER_DNS` repository variable (comma-separated
|
||||
nameservers, applied as `docker run --dns ...`) to pin a resolver for
|
||||
flaky CI DNS. External network (go.dev toolchain download, the public
|
||||
rocketgit.ru source clone, distro package repos) is fetched live by
|
||||
design.
|
||||
- **Artifacts and secrets.** The workflow uploads no artifacts: the go-test
|
||||
log stays in the runner's job log and nothing private (test key,
|
||||
`known_hosts`, env files) is retained on the runner or published. The
|
||||
fixture containers receive only the fixed `e2e-activation-test-token`
|
||||
and the bundled test-only key, never a real worker token.
|
||||
- **Caching.** `actions/setup-go` caches the Go module/build cache used to
|
||||
compile the harness test binary (public dependencies only). Fixture and
|
||||
toolchain downloads are not cached because they run inside disposable
|
||||
distro containers; reruns rebuild them cleanly.
|
||||
|
||||
Work package 5 does not change how the source installer behaves. It only
|
||||
adds a CI surface for the existing acceptance tests.
|
||||
|
||||
## Docker OpenSSH Test Harness
|
||||
|
||||
Adapt the real-network pattern from `/data/_swap/sshkeymanager`: start an
|
||||
@@ -400,7 +486,7 @@ RSMON_TEST_DOCKER=1 go test -v -count=1 -timeout 30m ./internal/installer/harnes
|
||||
env, data dir, and init service definition installed atomically; process
|
||||
and `/healthz` verified; activation/start/health failures restore the
|
||||
prior install; reruns keep exactly one service).
|
||||
- [ ] 5. Add Alpine, Ubuntu, and Arch network E2E tests to CI.
|
||||
- [x] 5. Add Alpine, Ubuntu, and Arch network E2E tests to CI.
|
||||
- [ ] 6. Add CentOS-family support.
|
||||
- [ ] 7. Plan native Windows service and macOS launchd installers separately.
|
||||
|
||||
@@ -419,7 +505,7 @@ RSMON_TEST_DOCKER=1 go test -v -count=1 -timeout 30m ./internal/installer/harnes
|
||||
the previous installation (build/checkout, activation, start, and health
|
||||
failure reruns on the Alpine fixture all restore the prior binary, env,
|
||||
and running service).
|
||||
- [ ] CI uses approved registry mirrors and cleans every test container/network.
|
||||
- [x] CI uses approved registry mirrors and cleans every test container/network.
|
||||
|
||||
## Verified Test Evidence (work packages 1-4)
|
||||
|
||||
@@ -483,3 +569,27 @@ Recorded 2026-08-12 from `make test-ssh` (Docker Engine 29.7.1):
|
||||
tests above rather than against a real init. The stub tools simulate unit
|
||||
state and command flow, not real systemd/OpenRC unit semantics; a real
|
||||
init-system smoke test on a booted host remains a follow-up.
|
||||
|
||||
Work package 5 (CI) evidence, recorded 2026-08-13:
|
||||
|
||||
- `.github/workflows/test-ssh.yml` runs `scripts/ci/test-ssh.sh` on pushes
|
||||
to `master` and on manual `workflow_dispatch` only (never on
|
||||
`pull_request`, because the fixtures execute the checked-out code inside
|
||||
privileged Docker). Concurrency is scoped per ref
|
||||
(`test-ssh-${{ gitea.ref }}`), the job token is `contents: read`, and
|
||||
the script's loopback port-publishing probe passes on a host Docker
|
||||
daemon while its `EXIT` cleanup leaves zero leftover containers,
|
||||
networks, or fixture image tags.
|
||||
- A full local `make test-ssh` run (the exact command the CI job executes)
|
||||
passes the complete harness suite: `TestHarnessFixtures` (alpine/ubuntu/
|
||||
arch), host-key mismatch and stability, teardown, `TestSourceInstallFixtures`
|
||||
(work package 3 staging on all three distros), `TestSourceInstallActivationFixtures`
|
||||
(work package 4 activation on all three), activation failure rollback,
|
||||
`--no-start`, and the dirty-checkout preservation test. Pinned
|
||||
`RSMON_TEST_DOCKER_DNS=1.1.1.1` was used because the local network's
|
||||
default resolvers intermittently time out on `rocketgit.ru`.
|
||||
- `TestSourceInstallDirtyCheckoutPreservesStaging` and the rollback test's
|
||||
build-failure step now dirty the tracked tree deterministically (appending
|
||||
a marker line to `Makefile`) instead of `git checkout master~1 -- Makefile`,
|
||||
which depended on the last two commits differing and silently stopped
|
||||
dirtying the tree once a commit did not touch that file.
|
||||
|
||||
Ссылка в новой задаче
Block a user