Files
local-ci-cd-system/config.example.toml
T
Simone af7dee60e5 feat(web): Executions card replaces Live VMs
The dashboard could only show what was powered on this instant, so it could
not answer "did the last build pass?" — and no local execution history exists
to answer it from (`report job` reads a PowerShell-era JSONL path that nothing
writes on this host, and the recent artifact dirs are empty).

Recent runs now come from Gitea's Actions API (`operations/executions.py`):
`/api/v1/admin/actions/runs` for every repo on the instance, falling back to
`/api/v1/user/actions/runs` when the token is not admin-scoped. One list call
per refresh, cached 10s, so the card's 5s htmx poll costs Gitea at most one
call per window. Live VMs are grouped under the run that spawned them — the
job id is `{run_id}-{run_attempt}[-suffix]` and the clone name carries it — so
a matrix run collects several VMs on one row. Every failure mode (no token,
401, unreachable Gitea, malformed payload) degrades to a notice plus the plain
VM rows, each keeping its Remove button, so the card is never less useful than
the table it replaces. Orphans stay where they were: the sweep in Maintenance,
the count in the status-header tile.

Auth reuses the access token from the operator's own OIDC login, which
`auth.py` previously discarded. SECURITY: that token is kept server-side only
(`app.state.gitea_tokens`, dropped on logout) and never written into the
session — `SessionMiddleware` signs the session cookie but does not encrypt
it, and this service is deployed over plain HTTP on the LAN, so a bearer token
in there would be readable in the browser and on the wire. An expired token is
refreshed once; `[web] gitea_token_cred` names an optional keyring PAT for the
cases a login token cannot cover (service restart, non-admin operator).

`GET /vms` and `partials/vms.html` are gone; `POST /vms/{id}/remove` and
`POST /vms/cleanup` are untouched.

Design: docs/superpowers/specs/2026-07-25-executions-card-design.md
2026-07-25 14:55:27 +02:00

145 lines
6.4 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ci-orchestrator config example.
#
# Copy to $CI_ROOT/config.toml or set $CI_CONFIG to the file path.
# Environment variables (CI_ROOT, CI_TEMPLATES, CI_BUILD_VMS, CI_ARTIFACTS,
# CI_KEYS, CI_VMRUN_PATH, CI_SSH_KEY_PATH, CI_GUEST_CRED_TARGET) override
# anything set here.
#
# IMPORTANT (TOML rule): top-level keys must appear BEFORE any [section] header.
# Keys placed after a [section] belong to that section, not to the root table.
# Top-level orchestrator settings.
vmrun_path = "C:/Program Files (x86)/VMware/VMware Workstation/vmrun.exe"
ssh_key_path = "F:/CI/keys/ci_linux"
guest_cred_target = "BuildVMGuest"
# --------------------------------------------------------------- Windows host
[paths]
root = "F:/CI"
templates = "F:/CI/Templates"
build_vms = "F:/CI/BuildVMs"
artifacts = "F:/CI/Artifacts"
keys = "F:/CI/keys"
# Phase B (Linux host) — uncomment after migration.
# [paths]
# root = "/var/lib/ci"
# templates = "/var/lib/ci/templates"
# build_vms = "/var/lib/ci/build-vms"
# artifacts = "/var/lib/ci/artifacts"
# keys = "/etc/ci/keys"
# Phase C hook: backend selector. Only "workstation" is implemented in Phase A.
[backend]
type = "workstation"
# Optional: pre-assign static IPs to build VMs via VMX guestinfo injection.
# Eliminates VMware-Tools IP-detect polling (20–180 s variance) — the host
# injects guestinfo.ip-assignment into each cloned VMX before vmrun start,
# and the guest boot script applies the static IP immediately.
#
# Prerequisite: install the guest-side startup script from
# template/guest-setup/linux/ci-report-ip.sh (Linux guests)
# template/guest-setup/windows/ci-static-ip.ps1 (Windows guests)
# and re-take the BaseClean / BaseClean-Linux snapshot.
#
# Pool size should equal runner capacity (default 4).
# Addresses must be outside the VMnet8 DHCP range (check
# /etc/vmware/vmnet8/dhcpd/dhcpd.conf — typically .128–.254).
#
# [ip_pool]
# addresses = ["192.168.79.201", "192.168.79.202",
# "192.168.79.203", "192.168.79.204"]
# netmask = "255.255.255.0"
# gateway = "192.168.79.2" # VMnet8 gateway; leave empty to skip default route
# lease_file = "/var/lib/ci/ip-pool.json"
# ------------------------------------------------- cargo dependency cache
# Compiled Rust dependency artifacts, reused between jobs. Engages only for
# Linux guests whose build workdir contains a Cargo.lock, so non-Rust jobs
# need no flag. Workspace crates are stripped before an entry is stored and
# the job refuses to publish if any survive, so a restored entry cannot carry
# the code under test.
#
# One immutable file per key under `root`; a key is
# sha256(Cargo.lock) + rustc version + host triple. Deleting entries by hand
# is always safe -- each one costs exactly one cold build to regenerate.
#
# `root` defaults to <paths.root>/cache/cargo and follows CI_ROOT unless set
# here explicitly. Measured on LinuxRust2404: an entry is ~645 MB and a warm
# job pays ~8s to skip ~120s of dependency compilation.
[cargo_cache]
enabled = true
# root = "/var/lib/ci/cache/cargo"
retention_days = 30
# --------------------------------------------------------------------- web UI
# Optional management web UI (`ci web serve`). Disabled unless `enabled = true`.
# Server-rendered dashboard for VM/host management, auth'd via Gitea OIDC.
#
# SECURITY:
# * `bind_host` defaults to 127.0.0.1 (localhost only). Only set it to a
# LAN IP explicitly and deliberately -- never "0.0.0.0" (that exposes the
# dashboard, which can kill VMs and purge caches, to every network the
# host is reachable from).
# * `session_secret_cred` and `[web.oidc] client_secret_cred` below are
# KEYRING KEY NAMES, not the secrets themselves. The actual secret
# values are never written to this file -- set them with:
# ci creds set WebSessionSecret
# ci creds set WebOidcClientSecret
# (the `_cred` values here just tell the app which keyring entry to read
# at request time).
#
# [web]
# enabled = true
# bind_host = "192.168.1.50" # explicit LAN IP; never 0.0.0.0
# bind_port = 8800
# audit_log = "/var/lib/ci/logs/web-audit.jsonl"
# allowed_users = ["simone"] # Gitea usernames allowed to log in
# session_secret_cred = "WebSessionSecret" # keyring KEY NAME, not the secret
#
# Executions card (recent Gitea Actions runs, live ones included). It normally
# reuses the access token from the operator's own OIDC login, held server-side
# only -- so neither key below is required. Set them to cover the cases that
# token cannot: a service restart (which forgets the logged-in tokens) or a
# non-admin operator.
# executions_limit = 15 # how many runs to list (1..100)
# gitea_token_cred = "GiteaApiToken" # keyring KEY NAME of a fallback PAT
#
# Gitea OIDC client registration: Gitea -> Settings -> Applications -> OAuth2,
# redirect URI must exactly match `redirect_url` below (host+port+path).
#
# `scopes` needs an API read scope on top of the OIDC ones for the Executions
# card: `read:repository` lists the operator's own repos, `read:admin` (admin
# accounts only) lists every repo on the instance. Adding a scope requires one
# fresh login to re-consent.
#
# [web.oidc]
# issuer = "https://gitea.lan"
# client_id = "ci-web"
# redirect_url = "http://192.168.1.50:8800/auth/callback"
# client_secret_cred = "WebOidcClientSecret" # keyring KEY NAME, not the secret
# scopes = ["openid", "profile", "read:repository"]
#
# Optional: override the default systemd-unit allow-list the dashboard's
# service-management panel (Stage D) is allowed to start/stop/restart. Leave
# unset to use the built-in default (act-runner, act-runner-rust,
# vmware-host, ci-web -- see operations/services.py::MANAGED_SERVICES).
# A malformed row (missing/wrong-typed key) is skipped, never crashes
# startup. Every command reaching sudo is an exact-match systemctl/systemd-run
# invocation approved in deploy/sudoers.d/ci-web -- adding a unit here without
# also adding its NOPASSWD entries there just means every action for it fails
# (sudo prompts for a password that is never supplied, exit non-zero).
#
# [[web.managed_services]]
# id = "act-runner"
# unit = "act-runner.service"
# label = "Runner"
# allow_stop = true
#
# [[web.managed_services]]
# id = "ci-web"
# unit = "ci-web.service"
# label = "Web UI"
# allow_stop = false # restart-only -- stopping it would lock the operator out