4.8 KiB
Algorithm
Mathematical Formulation
Cache Key Correctness Condition
Let K be the set of cache keys, A ∈ {arm64, amd64} be the runner architecture, and
H = hash(*.csproj) be the content hash of project dependency files.
A cache key k ∈ K is architecture-safe if and only if:
k = f(A, H, ...) where f is injective in A
That is: for any two keys k₁ = f(A₁, H, ...) and k₂ = f(A₂, H, ...) with A₁ ≠ A₂, we have k₁ ≠ k₂. This guarantees no cache collision across architectures.
An architecture-agnostic key k = f(H) (not injective in A) violates this condition:
two runners with different architectures but the same H produce the same key, causing
cross-architecture cache pollution.
Runner Routing Condition
Let L(r) be the label set of runner r, and J be a job requiring label l.
Runner r is eligible for J iff l ∈ L(r).
For strict architecture pinning: assign unique architecture labels:
L(unraid_runner) = {unraid, self-hosted, linux, amd64}
L(mac_runner) = {mac, self-hosted, darwin, arm64}
Job YAML:
runs-on: unraid # Only dispatched to unraid_runner
This guarantees A = amd64 for all builds of this job, making architecture-tagged
cache keys redundant (but still good practice as defense-in-depth).
Build Workflow Pseudocode
PROCEDURE build_ss14_image(commit_sha):
# Step 1: Checkout
git clone git.wylab.me/wylab/wylab-station-14 --depth=1 --ref=commit_sha
# Step 2: Restore cache (architecture-safe)
arch = runner.arch # "amd64" or "arm64"
project_hash = sha256(glob("**/*.csproj"))
cache_key = f"dotnet-{arch}-{project_hash}"
restore cache(key=cache_key, path="~/.nuget/packages")
# Step 3: Build .NET server
dotnet restore
dotnet build --configuration Release --no-restore
dotnet publish --configuration Release --output ./publish/
# Step 4: Save cache
save cache(key=cache_key, path="~/.nuget/packages")
# Step 5: Build Docker image
docker build -t ss14-server:commit_sha -f Dockerfile ./publish/
docker tag ss14-server:commit_sha registry/wylab/ss14-server:latest
# Step 6: Push image
docker push registry/wylab/ss14-server:latest
# Step 7: Report status
report status to Gitea (success/failure)
PROCEDURE select_runner(job):
eligible = {r for r in runners if job.runs_on ⊆ labels(r)}
if |eligible| == 0:
queue job indefinitely # DANGER: silent queue, no error
else:
dispatch to eligible runner with lowest load
Gitea Actions Workflow YAML (Recommended Pattern)
name: Build SS14 Server
on:
push:
branches: [main, master]
jobs:
build:
runs-on: unraid # CRITICAL: pin to x86-64 runner
container:
image: mcr.microsoft.com/dotnet/sdk:7.0
options: --dns 172.17.0.1 # Docker bridge gateway → Technitium → git.wylab.me
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Cache .NET packages
uses: actions/cache@v3
with:
path: ~/.nuget/packages
# Architecture encoded in key — eliminates cross-arch pollution
key: dotnet-${{ runner.arch }}-${{ hashFiles('**/*.csproj') }}
restore-keys: |
dotnet-${{ runner.arch }}-
- name: Build
run: dotnet build --configuration Release
- name: Publish
run: dotnet publish --configuration Release --output ./publish
- name: Build Docker image
run: docker build -t ss14-server:${{ github.sha }} ./publish
- name: Push Docker image
run: docker push registry/wylab/ss14-server:latest
Complexity Analysis
| Operation | Complexity | Notes |
|---|---|---|
| Cache restore (hit) | O(n) where n = package size | Bounded by disk I/O; ~5s local |
| Cache restore (miss/ETIMEDOUT) | O(n × download_time) | ~5 min observed |
| dotnet build (cold) | O(source_lines × compile_factor) | Dominates build time; OOM risk |
| dotnet build (warm) | O(changed_files) | Incremental; much faster |
| docker build | O(layer_count) | Layer caching reduces to O(changed_layers) |
| Runner job dispatch | O(1) | Poll-based; latency = poll interval |
Key Invariants
- Architecture invariant: All artifacts written to the production registry must have
Architecture: amd64. Any arm64 artifact reaching the registry is a pipeline bug. - Cache isolation invariant: No cache entry written by an arm64 runner may be consumed by an amd64 runner (and vice versa). Enforced by architecture-tagged keys.
- DNS invariant: All steps that contact git.wylab.me must run after DNS is confirmed resolvable. Failing fast on DNS errors prevents silent misrouting.
- Capacity invariant: Concurrent dotnet job count ≤ 2 on the current hardware configuration to avoid OOM.