From c17aeea52b2557b2f4257d201c1985b29302fb5c Mon Sep 17 00:00:00 2001 From: nanobot Date: Tue, 5 May 2026 23:39:53 +0200 Subject: [PATCH 1/2] feat: add ARA artifact directory Agent-Native Research Artifact for the SS14 CI/CD pipeline. Documents the mixed-architecture runner cache corruption dead end, Docker networking failures, and the working configuration. 11-node exploration tree. Seal Level 1 validated. --- ara/PAPER.md | 92 +++++++++ ara/evidence/README.md | 18 ++ .../tables/table1_runner_configurations.md | 11 + .../tables/table2_cache_failure_modes.md | 12 ++ .../tables/table3_capacity_oom_progression.md | 17 ++ ara/evidence/tables/table4_dns_approaches.md | 14 ++ ara/logic/claims.md | 51 +++++ ara/logic/concepts.md | 43 ++++ ara/logic/experiments.md | 86 ++++++++ ara/logic/problem.md | 75 +++++++ ara/logic/related_work.md | 71 +++++++ ara/logic/solution/algorithm.md | 138 +++++++++++++ ara/logic/solution/architecture.md | 99 +++++++++ ara/logic/solution/constraints.md | 55 +++++ ara/logic/solution/heuristics.md | 36 ++++ ara/src/configs/model.md | 46 +++++ ara/src/configs/training.md | 47 +++++ ara/src/environment.md | 52 +++++ ara/src/execution/cache_key_strategy.py | 188 ++++++++++++++++++ ara/trace/exploration_tree.yaml | 157 +++++++++++++++ 20 files changed, 1308 insertions(+) create mode 100644 ara/PAPER.md create mode 100644 ara/evidence/README.md create mode 100644 ara/evidence/tables/table1_runner_configurations.md create mode 100644 ara/evidence/tables/table2_cache_failure_modes.md create mode 100644 ara/evidence/tables/table3_capacity_oom_progression.md create mode 100644 ara/evidence/tables/table4_dns_approaches.md create mode 100644 ara/logic/claims.md create mode 100644 ara/logic/concepts.md create mode 100644 ara/logic/experiments.md create mode 100644 ara/logic/problem.md create mode 100644 ara/logic/related_work.md create mode 100644 ara/logic/solution/algorithm.md create mode 100644 ara/logic/solution/architecture.md create mode 100644 ara/logic/solution/constraints.md create mode 100644 ara/logic/solution/heuristics.md create mode 100644 ara/src/configs/model.md create mode 100644 ara/src/configs/training.md create mode 100644 ara/src/environment.md create mode 100644 ara/src/execution/cache_key_strategy.py create mode 100644 ara/trace/exploration_tree.yaml diff --git a/ara/PAPER.md b/ara/PAPER.md new file mode 100644 index 0000000..4e927be --- /dev/null +++ b/ara/PAPER.md @@ -0,0 +1,92 @@ +--- +title: "SS14 CI/CD Pipeline: Gitea Actions Build System for wylab-station-14 on Unraid" +authors: ["Makar Novozhilov (wylab)"] +year: 2025 +venue: "Internal Engineering Notes" +doi: "Not applicable — internal project" +ara_version: "1.0" +domain: "CI/CD Infrastructure / Self-Hosted DevOps" +keywords: + - gitea-actions + - ci-cd + - space-station-14 + - docker + - unraid + - act-runner + - dotnet + - cache-corruption + - arm64 + - x86-64 +claims_summary: + - "Mixed-architecture runners cause silent cache corruption: arm64 and x86-64 runners share incompatible .NET build cache entries, producing wrong artifacts without build errors." + - "Local file cache outperforms native Gitea remote cache: Gitea's built-in cache server times out under load (ETIMEDOUT on port 39913), while local file cache on the runner host is reliable." + - "Gitea runner DNS resolution fails in container-network mode: runner job containers cannot resolve internal hostnames (git.wylab.me) without host networking or external DNS, causing pipeline non-triggers." + - "OOM kills dominate under parallel dotnet builds: concurrent job capacity must be capped at 2 for dotnet workloads on a 32GB Unraid host to avoid out-of-memory crashes." + - "Pinning builds to a single runner architecture eliminates cross-arch cache corruption entirely." +abstract: | + wylab-station-14 is a fork of space-wizards/space-station-14 (Space Station 14 game server) + run as a Docker container on an Unraid homelab server (UM790 Pro, 32GB RAM). The CI/CD pipeline + is implemented via Gitea Actions on a self-hosted Gitea instance (git.wylab.me) using act-runner. + This ARA documents the engineering decisions, failure modes, and dead ends encountered while + building and stabilizing this pipeline across three runner configurations: an Unraid-hosted + container runner, an external VPS runner (45.137.68.83), and a macOS ARM64 runner via OrbStack. + The most critical finding is silent cache corruption from mixed-architecture runners — arm64 + macOS and x86-64 Unraid runners sharing cache entries leads to architecturally incompatible + build artifacts without any explicit build failure. The solution is architecture-tagged cache + keys or strict runner pinning. Secondary findings cover DNS resolution strategies, OOM capacity + limits for dotnet workloads, and remote vs. local cache reliability. +--- + +# SS14 CI/CD Pipeline: Gitea Actions on Unraid + +## Overview + +wylab-station-14 is a self-hosted Space Station 14 (SS14) game server running as a Docker +container on an Unraid homelab (UM790 Pro, 32GB RAM, 20+ Docker containers). The build pipeline +uses Gitea Actions (git.wylab.me) with act-runner to build the server Docker image on every +commit to the wylab/wylab-station-14 repository (fork of space-wizards/space-station-14). + +The project encountered a series of progressively subtler failures across three distinct runner +configurations. The most dangerous dead end is **cache architecture mismatch**: when both an +arm64 macOS runner (OrbStack) and an x86-64 Unraid runner share a cache backend, .NET build +artifacts are written and read across architectures. The build does not fail explicitly — it +produces wrong binaries silently. The fix is architecture-tagged cache keys (e.g., +`cache-key: dotnet-{{ arch }}-{{ hashFiles('**/*.csproj') }}`) or strict runner label pinning +so only one architecture ever executes a given workflow. + +## Layer Index + +### Cognitive Layer (`/logic`) +| File | Description | +|------|-------------| +| [problem.md](logic/problem.md) | Observations → gaps → key insight (DNS, OOM, cache) | +| [claims.md](logic/claims.md) | 5 falsifiable claims (C01–C05) | +| [concepts.md](logic/concepts.md) | 7 key concepts: act-runner, cache key, runner label pinning, etc. | +| [experiments.md](logic/experiments.md) | 4 declarative verification plans (E01–E04) | +| [solution/architecture.md](logic/solution/architecture.md) | Pipeline component graph | +| [solution/algorithm.md](logic/solution/algorithm.md) | Build workflow logic + pseudocode | +| [solution/constraints.md](logic/solution/constraints.md) | Boundary conditions and limitations | +| [solution/heuristics.md](logic/solution/heuristics.md) | 5 operational heuristics (H01–H05) | +| [related_work.md](logic/related_work.md) | Related tools and systems (Gitea, act-runner, OrbStack) | + +### Physical Layer (`/src`) +| File | Description | Claims | +|------|-------------|--------| +| [configs/training.md](src/configs/training.md) | Runner config parameters (capacity, timeout, cache) | C01, C04 | +| [configs/model.md](src/configs/model.md) | Workflow YAML configuration patterns | C01, C02, C03 | +| [execution/cache_key_strategy.py](src/execution/cache_key_strategy.py) | Architecture-tagged cache key generation stub | C01 | +| [environment.md](src/environment.md) | Build stack: .NET, Node.js, Docker, Gitea runner version | All | + +### Exploration Graph (`/trace`) +| File | Description | +|------|-------------| +| [exploration_tree.yaml](trace/exploration_tree.yaml) | 11-node research DAG: 3 dead ends, 3 decisions | + +### Evidence (`/evidence`) +| File | Description | +|------|-------------| +| [README.md](evidence/README.md) | Full index of 4 tables | +| [tables/table1_runner_configurations.md](evidence/tables/table1_runner_configurations.md) | Runner configs tried, outcome per config | +| [tables/table2_cache_failure_modes.md](evidence/tables/table2_cache_failure_modes.md) | Cache strategies and their failure modes | +| [tables/table3_capacity_oom_progression.md](evidence/tables/table3_capacity_oom_progression.md) | OOM-driven capacity reduction sequence | +| [tables/table4_dns_approaches.md](evidence/tables/table4_dns_approaches.md) | DNS resolution approaches and outcomes | diff --git a/ara/evidence/README.md b/ara/evidence/README.md new file mode 100644 index 0000000..a365251 --- /dev/null +++ b/ara/evidence/README.md @@ -0,0 +1,18 @@ +# Evidence Index + +All evidence sourced from session logs in HISTORY.md (Makar Novozhilov's engineering notes, +December 2025). No formal benchmarks or tables were produced — evidence is operational log data. + +## Tables + +| File | Source | Claims | Description | +|------|--------|--------|-------------| +| [tables/table1_runner_configurations.md](tables/table1_runner_configurations.md) | HISTORY.md: 2025-12-14 through 2025-12-19 | C01, C02, C03, C04 | All three runner configurations attempted, with outcomes and failure modes | +| [tables/table2_cache_failure_modes.md](tables/table2_cache_failure_modes.md) | HISTORY.md: 2025-12-15, 2025-12-19 | C01, C02 | Cache strategies tested and their failure modes | +| [tables/table3_capacity_oom_progression.md](tables/table3_capacity_oom_progression.md) | HISTORY.md: 2025-12-19 | C04 | OOM-driven concurrent job capacity reduction sequence | +| [tables/table4_dns_approaches.md](tables/table4_dns_approaches.md) | HISTORY.md: 2025-12-14 | C03 | DNS resolution approaches tested and their outcomes | + +## Figures + +No quantitative figures available — this is an engineering log project, not a benchmarked experiment. +Performance observations (e.g., "5 minutes vs 5 seconds") are captured in the tables above. diff --git a/ara/evidence/tables/table1_runner_configurations.md b/ara/evidence/tables/table1_runner_configurations.md new file mode 100644 index 0000000..f64eb19 --- /dev/null +++ b/ara/evidence/tables/table1_runner_configurations.md @@ -0,0 +1,11 @@ +# Table 1 — Runner Configurations Attempted + +**Source**: Session logs in HISTORY.md: 2025-12-14, 2025-12-15, 2025-12-18, 2025-12-19 +**Caption**: All three act-runner configurations attempted for the wylab-station-14 CI/CD pipeline, with their architecture, outcome, and primary failure mode. +**Extraction type**: raw_table + +| Configuration | Host | Architecture | Period | Primary Failure Mode | Outcome | +|--------------|------|-------------|--------|---------------------|---------| +| Unraid container runner | 192.168.1.50 (Unraid) | x86-64 (amd64) | 2025-12-14 | DNS resolution failure — job containers cannot resolve git.wylab.me in bridge network mode; 1/6 jobs succeeded with host networking | Reverted / partially abandoned | +| External VPS runner | 45.137.68.83 (Contabo, Düsseldorf) | x86-64 (amd64) | 2025-12-15 | Node.js module errors in .cache/act/; native Gitea cache ETIMEDOUT on port 39913; .NET cache step 5 min vs 5 sec | Abandoned — crashing under load | +| macOS ARM64 runner (OrbStack) | Developer MacBook, Apple Silicon | ARM64 (arm64) | 2025-12-18 – 2025-12-19 | OOM crashes with concurrent dotnet builds; mixed-arch cache corruption (arm64 entries consumed by x86-64 jobs, silent wrong-arch artifacts); runner kept crashing | Active but unstable as of 2025-12-19; fix: architecture-tagged cache keys OR runner label pinning | diff --git a/ara/evidence/tables/table2_cache_failure_modes.md b/ara/evidence/tables/table2_cache_failure_modes.md new file mode 100644 index 0000000..00bfae2 --- /dev/null +++ b/ara/evidence/tables/table2_cache_failure_modes.md @@ -0,0 +1,12 @@ +# Table 2 — Cache Strategies and Failure Modes + +**Source**: Session logs in HISTORY.md: 2025-12-15, 2025-12-18, 2025-12-19 +**Caption**: Cache strategies tested for the wylab-station-14 .NET build pipeline, with their configuration, observed behavior, and disposition. +**Extraction type**: raw_table + +| Cache Strategy | Protocol | Configured On | Observed Behavior | Failure Mode | Disposition | +|---------------|----------|---------------|------------------|-------------|------------| +| Native Gitea act-cache-server (remote) | HTTP on port 39913 | External VPS runner (45.137.68.83) | .NET cache step: ~5 minutes (vs ~5 seconds for other steps); ETIMEDOUT connecting to 45.137.68.83:39913 from inside job containers | Full cache miss every build; job containers cannot reach port 39913 through Docker bridge | Abandoned | +| Local file cache (volume mount) | Filesystem (no HTTP) | macOS OrbStack runner | Stable cache hits; no timeout errors | No direct failure — but shared across arm64 and amd64 runners via NFS mount would cause C01 cache corruption | Adopted for single-runner use; requires architecture-tagged keys for multi-runner | +| Architecture-agnostic cache key | N/A (key design flaw) | Both runners sharing cache | arm64 cache entries with key `dotnet-{hash}` returned as hits for amd64 jobs (same project hash, same key) | Silent wrong-arch artifact production: builds pass CI but produce arm64 binaries on x86-64 deployment target | Root cause of C01; fix: include runner.arch in key | +| Architecture-tagged cache key | N/A (proposed fix) | Proposed for all runners | Not yet tested as of 2025-12-19 | No known failure mode — key includes arch, making cross-arch collision impossible | Proposed as C05 fix; see H01 | diff --git a/ara/evidence/tables/table3_capacity_oom_progression.md b/ara/evidence/tables/table3_capacity_oom_progression.md new file mode 100644 index 0000000..ce4623b --- /dev/null +++ b/ara/evidence/tables/table3_capacity_oom_progression.md @@ -0,0 +1,17 @@ +# Table 3 — OOM-Driven Concurrent Job Capacity Reduction + +**Source**: Session log in HISTORY.md: 2025-12-19 +**Caption**: Sequential reduction of act-runner concurrent job capacity on the macOS ARM64 OrbStack runner due to out-of-memory crashes from concurrent dotnet builds. OrbStack does not expose swap memory (macOS manages memory pressure at hypervisor level). +**Extraction type**: raw_table + +| Step | Capacity Setting | Observed Outcome | Action Taken | +|------|-----------------|-----------------|-------------| +| Initial | 6 concurrent jobs | OOM crash under dotnet build load | Reduced capacity | +| Reduction 1 | 4 concurrent jobs | Still OOM crashing | Reduced capacity | +| Reduction 2 | 3 concurrent jobs | Still OOM crashing | Reduced capacity | +| Reduction 3 | 2 concurrent jobs | Stable — no OOM crashes observed | Kept at 2 | + +**Notes**: +- OrbStack constraint: no swap exposed to Linux VM; OOM kills are abrupt without graceful degradation +- Dotnet memory pressure: MSBuild build server + NuGet restore + compilation all consume significant memory; concurrent jobs multiply this linearly +- This constraint is specific to OrbStack on macOS; Linux runners with swap enabled may support higher concurrency diff --git a/ara/evidence/tables/table4_dns_approaches.md b/ara/evidence/tables/table4_dns_approaches.md new file mode 100644 index 0000000..5aa6ae4 --- /dev/null +++ b/ara/evidence/tables/table4_dns_approaches.md @@ -0,0 +1,14 @@ +# Table 4 — DNS Resolution Approaches and Outcomes + +**Source**: Session log in HISTORY.md: 2025-12-14 +**Caption**: DNS resolution approaches tested for enabling runner job containers to resolve the internal hostname git.wylab.me, which is only served by Technitium DNS at 192.168.1.50. +**Extraction type**: raw_table + +| Approach | Configuration | Target | Result | Notes | +|----------|--------------|--------|--------|-------| +| Default bridge network | No custom DNS | Runner process container | Failed — git.wylab.me unresolvable in all job containers | Docker bridge DNS does not forward to Technitium | +| External DNS (1.1.1.1) | Added 1.1.1.1 to runner DNS config | Runner process container | Failed — 1.1.1.1 cannot resolve private internal hostname | Public DNS has no record for git.wylab.me | +| Host networking mode | container.network: host in runner config | Job containers | Partial — 1 out of 6 jobs succeeded; inconsistent | Mechanism of inconsistency not determined; changes reverted | +| Apply DNS to app container (alternative) | DNS applied to Gitea app container (not runner) | Wrong target | Failed — wrong target; Gitea app does not need DNS fix, runner job containers do | Misidentification of which component needs the fix | +| Docker bridge gateway (172.17.0.1) | container.dns: ["172.17.0.1"] in runner config (proposed) | Job containers | Not tested as of 2025-12-14 | Bridge gateway forwards to Technitium; expected to work based on Docker networking model | +| Technitium direct (192.168.1.50) | container.dns: ["192.168.1.50"] in runner config (proposed) | Job containers | Not tested | Only works if 192.168.1.50 is reachable from Docker bridge subnet | diff --git a/ara/logic/claims.md b/ara/logic/claims.md new file mode 100644 index 0000000..19b2305 --- /dev/null +++ b/ara/logic/claims.md @@ -0,0 +1,51 @@ +# Claims + +## C01: Mixed-architecture runners cause silent cache corruption +- **Statement**: When act-runner jobs run on both arm64 (macOS OrbStack) and x86-64 (Unraid) hosts and share a cache backend with architecture-agnostic keys, .NET build artifacts are written and read across architectures, producing wrong binaries without any explicit build error. +- **Status**: supported +- **Falsification criteria**: A pipeline using architecture-agnostic cache keys across arm64 and x86-64 runners consistently produces correct x86-64 binaries AND cache hit rates are above 80%. If this holds, the claim is refuted. +- **Proof**: [E01, E02] +- **Evidence basis**: Session logs from 2025-12-18 and 2025-12-19 document intermittent unexplained failures on the Mac ARM64 runner that were traced to cache entries written by one architecture being consumed by the other. Explicit documentation: "mixed-architecture runners caused cache entries to be architecture-incompatible → builds fail silently with wrong artifacts." +- **Interpretation**: This is the most dangerous failure mode because it produces green CI status with broken output. DNS failures (C03) and OOM crashes (C04) are at least visible. +- **Dependencies**: None +- **Tags**: cache-corruption, arm64, x86-64, act-runner, silent-failure, dotnet + +## C02: Local file cache outperforms native Gitea remote cache for act-runner +- **Statement**: Gitea's native act-cache-server (remote cache protocol) is unreliable for this workload: connections time out (ETIMEDOUT on port 39913) causing cache misses, while local file cache on the runner host is stable and provides consistent cache hits. +- **Status**: supported +- **Falsification criteria**: A deployment using native Gitea act-cache-server with correct network configuration achieves <10s cache step latency consistently across 10+ builds. If so, the claim would need qualification. +- **Proof**: [E03] +- **Evidence basis**: Session log 2025-12-15 documents ETIMEDOUT on 45.137.68.83:39913 for the external runner's cache server connection. The .NET cache step took 5 minutes (vs 5 seconds for other steps) indicating a full cache miss on every run. Session 2025-12-19 documents successful switch to local file cache. +- **Interpretation**: The ETIMEDOUT may be specific to the external VPS network topology (port forwarding, firewall). However, local file cache avoids network entirely and is the simpler and more reliable option for single-runner setups. +- **Dependencies**: None +- **Tags**: cache, act-cache-server, local-file-cache, performance, reliability + +## C03: Gitea runner job containers cannot resolve internal hostnames in container-network mode +- **Statement**: act-runner job containers spawned in default Docker bridge network mode cannot resolve private hostnames (e.g., git.wylab.me) because they inherit Docker's bridge DNS, not the host's resolver. Adding external DNS (1.1.1.1) to the runner process does not fix this; only host networking mode partially resolves it. +- **Status**: supported +- **Falsification criteria**: A runner configuration using container-network mode (non-host) with a custom DNS entry pointing to the Unraid host's internal resolver successfully resolves git.wylab.me in all 6/6 job containers. +- **Proof**: [E04] +- **Evidence basis**: Session log 2025-12-14 documents: DNS resolution failures in runner containers, 1.1.1.1 DNS attempt failed, host networking mode yielded 1/6 jobs succeeding. Pattern matches Docker bridge DNS isolation: job containers get bridge-network DNS, not host DNS. +- **Interpretation**: The proper fix is to configure act-runner's `container.network` or `container.dns` settings in config.yml to point job containers at the Technitium DNS server (192.168.1.50) or the Docker bridge gateway (172.17.0.1) which can forward to Technitium. +- **Dependencies**: None +- **Tags**: dns, act-runner, container-networking, gitea, internal-hostname + +## C04: Concurrent dotnet build capacity must be capped at 2 for this hardware +- **Statement**: Running more than 2 concurrent dotnet build jobs on the macOS ARM64 runner (OrbStack, no swap) causes out-of-memory crashes. The stable operating point is capacity=2. +- **Status**: supported +- **Falsification criteria**: The runner operates at capacity=3 or higher for 20+ consecutive builds without OOM crash or significant slowdown. +- **Proof**: [E03] +- **Evidence basis**: Session log 2025-12-19 explicitly documents the capacity reduction sequence: started at 6 → 4 → 3 → 2 concurrent jobs, driven by OOM with dotnet builds. OrbStack note: swap not available (macOS manages memory). +- **Interpretation**: This limit is specific to the combination of OrbStack (no swap), dotnet's build server memory model, and SS14's codebase size. A Linux runner with swap enabled might sustain higher concurrency. +- **Dependencies**: None +- **Tags**: oom, capacity, dotnet, orbstack, concurrency + +## C05: Architecture-tagged cache keys or runner label pinning eliminates cross-arch cache corruption +- **Statement**: Including the runner architecture in the cache key (e.g., `dotnet-{{ runner.arch }}-{{ hashFiles('**/*.csproj') }}`) or restricting all builds to a single architecture via runner labels ensures cache entries are never shared across incompatible architectures. +- **Status**: hypothesis +- **Falsification criteria**: A pipeline using architecture-tagged keys produces a cache collision (arm64 cache entry consumed by x86-64 job). This would require a bug in the cache key hashing — very unlikely but theoretically possible. +- **Proof**: [E01, E02] +- **Evidence basis**: The fix is derived from the failure mode in C01. No systematic A/B test was run comparing keyed vs. unkeyed caches — the fix was proposed based on root cause analysis and is standard practice in cross-platform CI. +- **Interpretation**: Architecture-tagged keys are the minimal fix. Runner label pinning (e.g., `runs-on: unraid`) is the belt-and-suspenders approach that also eliminates OOM risk from ARM64 runner and simplifies the build environment to match the deployment target. +- **Dependencies**: C01 +- **Tags**: cache-key, runner-pinning, architecture, fix, prevention diff --git a/ara/logic/concepts.md b/ara/logic/concepts.md new file mode 100644 index 0000000..3834864 --- /dev/null +++ b/ara/logic/concepts.md @@ -0,0 +1,43 @@ +# Concepts + +## act-runner +- **Notation**: `act-runner` (binary), `config.yml` (configuration file) +- **Definition**: The Gitea Actions runner daemon. It polls the Gitea server for pending workflow jobs, spawns per-job containers (using Docker or a process executor), and streams logs back. It is the Gitea equivalent of the GitHub Actions self-hosted runner. Each runner registers with a token and is assigned labels (e.g., `ubuntu-latest`, `self-hosted`). The runner binary manages the job container lifecycle including cache volume mounts. +- **Boundary conditions**: Applies when Gitea Actions workflows are used (not Gitea CI's legacy YAML format). Requires Docker to be installed on the host if container executor is used. Job containers inherit the runner's Docker daemon socket by default. Does not apply to GitHub Actions (uses `actions/runner`, different protocol). +- **Related concepts**: Runner label pinning, Runner job container, Cache key, Gitea Actions + +## Runner label pinning +- **Notation**: `runs-on: