Files

4.8 KiB
Raw Permalink Blame History

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
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

  1. Architecture invariant: All artifacts written to the production registry must have Architecture: amd64. Any arm64 artifact reaching the registry is a pipeline bug.
  2. 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.
  3. DNS invariant: All steps that contact git.wylab.me must run after DNS is confirmed resolvable. Failing fast on DNS errors prevents silent misrouting.
  4. Capacity invariant: Concurrent dotnet job count ≤ 2 on the current hardware configuration to avoid OOM.