Compare commits

..

1 Commits

Author SHA1 Message Date
Andrew Stoltz
59543016c0 runners: add github-runner-updater Deployment
FlowerCore.Updater had only the offline bluejay-ws-sandbox-1 Windows
runner registered (the specialized fcsetup E2E target) and no Linux
self-hosted runner, leaving the repo with no Linux PR-CI capacity for
any future workflow. Modeled on github-runner-pimanager (Sprint 32
long-tail final entry, 2026-05-25); two replicas with per-pod emptyDir
caches to keep ReadWriteOnce PVC contention out of the picture.

Also registers github-runner-updater in the LinuxRunnerRepos +
ScaledLinuxRunnerDeployments fleet-lint sets so future suite repairs
treat the entry as canonically required (the 6 pre-existing lint
failures on this file family are orthogonal: initContainer single-
container count assertion + fc-updater IngressRoute POST allowlist
+ DM ApplicationSet convention drift).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:22:41 -05:00
8 changed files with 75 additions and 612 deletions

View File

@@ -532,7 +532,7 @@ spec:
fsGroupChangePolicy: OnRootMismatch fsGroupChangePolicy: OnRootMismatch
containers: containers:
- name: web - name: web
image: localhost/fc-ttsreader-web:v20260603-s54cx14-pr29-live image: localhost/fc-ttsreader-web:v20260518-sprint36-demo-finish-b132cbf
imagePullPolicy: Never imagePullPolicy: Never
ports: ports:
- containerPort: 5217 - containerPort: 5217
@@ -554,8 +554,6 @@ spec:
value: "/data/chapter-context.db" value: "/data/chapter-context.db"
- name: TtsReader__Jobs__Root - name: TtsReader__Jobs__Root
value: "/data/jobs" value: "/data/jobs"
- name: TtsReader__Export__LocalCasRoot
value: "/data/bundles/cas"
- name: TtsReader__Piper__Host - name: TtsReader__Piper__Host
value: "10.0.57.17" value: "10.0.57.17"
- name: TtsReader__Piper__Port - name: TtsReader__Piper__Port

View File

@@ -58,7 +58,7 @@ spec:
nodeName: rke2-server nodeName: rke2-server
containers: containers:
- name: web - name: web
image: localhost/fc-updater-web:v202605310029-7974fc4 image: localhost/fc-updater-web:v20260509-4162dca-authgate
imagePullPolicy: Never imagePullPolicy: Never
ports: ports:
- containerPort: 8080 - containerPort: 8080
@@ -88,8 +88,6 @@ spec:
value: Faith AI Mike Edition value: Faith AI Mike Edition
- name: FlowerCore__Updater__PublicShares__Links__0__Description - name: FlowerCore__Updater__PublicShares__Links__0__Description
value: Private release link for Mike's Faith AI bundle. value: Private release link for Mike's Faith AI bundle.
- name: FlowerCore__Audit__Sinks__Loki__Enabled
value: "false"
- name: FlowerCore__Updater__Auth__Bootstrap__Enabled - name: FlowerCore__Updater__Auth__Bootstrap__Enabled
value: "true" value: "true"
- name: FlowerCore__Updater__Auth__Bootstrap__Username - name: FlowerCore__Updater__Auth__Bootstrap__Username

View File

@@ -241,7 +241,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -306,7 +306,7 @@ spec:
# UN-PARKED 2026-05-21: Shared.Pos #5 fixed the non-root setup-dotnet path # UN-PARKED 2026-05-21: Shared.Pos #5 fixed the non-root setup-dotnet path
# (DOTNET_INSTALL_DIR step-scoped). Sprint 30 Cl-8 capacity Q-CI-52: raised # (DOTNET_INSTALL_DIR step-scoped). Sprint 30 Cl-8 capacity Q-CI-52: raised
# to replicas: 2 to absorb top-8 burst load per substrate-recommended default. # to replicas: 2 to absorb top-8 burst load per substrate-recommended default.
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-sharedpos app.kubernetes.io/name: github-runner-sharedpos
@@ -397,7 +397,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -448,7 +448,7 @@ metadata:
flowercore.io/runner-repo: puppet flowercore.io/runner-repo: puppet
flowercore.io/github-repo: FlowerCore.Puppet flowercore.io/github-repo: FlowerCore.Puppet
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-puppet app.kubernetes.io/name: github-runner-puppet
@@ -533,7 +533,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -580,7 +580,7 @@ metadata:
flowercore.io/runner-repo: signage flowercore.io/runner-repo: signage
flowercore.io/github-repo: FlowerCore.Signage flowercore.io/github-repo: FlowerCore.Signage
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-signage app.kubernetes.io/name: github-runner-signage
@@ -665,7 +665,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -712,7 +712,7 @@ metadata:
flowercore.io/runner-repo: dms flowercore.io/runner-repo: dms
flowercore.io/github-repo: FlowerCore.DMS flowercore.io/github-repo: FlowerCore.DMS
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-dms app.kubernetes.io/name: github-runner-dms
@@ -797,7 +797,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -844,7 +844,7 @@ metadata:
flowercore.io/runner-repo: telephony flowercore.io/runner-repo: telephony
flowercore.io/github-repo: FlowerCore.Telephony flowercore.io/github-repo: FlowerCore.Telephony
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-telephony app.kubernetes.io/name: github-runner-telephony
@@ -929,7 +929,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -979,7 +979,7 @@ spec:
# Sprint 33 morning-routine (2026-05-25): bumped 2 → 3 because help-screenshots # Sprint 33 morning-routine (2026-05-25): bumped 2 → 3 because help-screenshots
# AAT job holds a runner 30+ min, causing head-of-line blocking on parallel PRs. # AAT job holds a runner 30+ min, causing head-of-line blocking on parallel PRs.
# 12 runs in trailing 5d. # 12 runs in trailing 5d.
replicas: 1 replicas: 3
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-print-web app.kubernetes.io/name: github-runner-print-web
@@ -1064,7 +1064,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -1111,7 +1111,7 @@ metadata:
flowercore.io/runner-repo: chat flowercore.io/runner-repo: chat
flowercore.io/github-repo: FlowerCore.Chat flowercore.io/github-repo: FlowerCore.Chat
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-chat app.kubernetes.io/name: github-runner-chat
@@ -1196,7 +1196,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -1243,7 +1243,7 @@ metadata:
flowercore.io/runner-repo: mysql flowercore.io/runner-repo: mysql
flowercore.io/github-repo: FlowerCore.MySQL flowercore.io/github-repo: FlowerCore.MySQL
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-mysql app.kubernetes.io/name: github-runner-mysql
@@ -1328,7 +1328,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -1375,7 +1375,7 @@ metadata:
flowercore.io/runner-repo: kiosk-linux flowercore.io/runner-repo: kiosk-linux
flowercore.io/github-repo: FlowerCore.Kiosk.Linux flowercore.io/github-repo: FlowerCore.Kiosk.Linux
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-kiosk-linux app.kubernetes.io/name: github-runner-kiosk-linux
@@ -1460,7 +1460,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -1509,7 +1509,7 @@ metadata:
flowercore.io/runner-repo: marquee flowercore.io/runner-repo: marquee
flowercore.io/github-repo: FlowerCore.Marquee flowercore.io/github-repo: FlowerCore.Marquee
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-marquee app.kubernetes.io/name: github-runner-marquee
@@ -1594,7 +1594,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -1643,7 +1643,7 @@ metadata:
flowercore.io/runner-repo: tts-reader flowercore.io/runner-repo: tts-reader
flowercore.io/github-repo: FlowerCore.TtsReader flowercore.io/github-repo: FlowerCore.TtsReader
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-tts-reader app.kubernetes.io/name: github-runner-tts-reader
@@ -1726,17 +1726,13 @@ spec:
key: credential key: credential
- name: RUN_AS_ROOT - name: RUN_AS_ROOT
value: "false" value: "false"
# Bumped 2026-05-25: previous 4Gi limit caused OOMKill (exit 137)
# mid-`dotnet test` on TtsReader's 1000+ test suite. PR #21 CI flapped
# twice with "runner lost communication" before the K8s side
# symptoms surfaced. 8Gi gives ~30% headroom over peak observed.
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "2Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
memory: "8Gi" memory: "4Gi"
volumeMounts: volumeMounts:
- name: runner-home - name: runner-home
mountPath: /home/runner mountPath: /home/runner
@@ -1867,7 +1863,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -2001,7 +1997,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -2135,7 +2131,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -2269,7 +2265,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -2317,7 +2313,7 @@ metadata:
flowercore.io/runner-repo: remote-desktop flowercore.io/runner-repo: remote-desktop
flowercore.io/github-repo: FlowerCore.RemoteDesktop flowercore.io/github-repo: FlowerCore.RemoteDesktop
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-remote-desktop app.kubernetes.io/name: github-runner-remote-desktop
@@ -2402,7 +2398,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -2536,7 +2532,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -2584,7 +2580,7 @@ metadata:
flowercore.io/runner-repo: distribution flowercore.io/runner-repo: distribution
flowercore.io/github-repo: FlowerCore.Distribution flowercore.io/github-repo: FlowerCore.Distribution
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-distribution app.kubernetes.io/name: github-runner-distribution
@@ -2669,7 +2665,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -2717,7 +2713,7 @@ metadata:
flowercore.io/runner-repo: scoreboard flowercore.io/runner-repo: scoreboard
flowercore.io/github-repo: FlowerCore.Scoreboard flowercore.io/github-repo: FlowerCore.Scoreboard
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-scoreboard app.kubernetes.io/name: github-runner-scoreboard
@@ -2802,7 +2798,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -2850,7 +2846,7 @@ metadata:
flowercore.io/runner-repo: segment-display flowercore.io/runner-repo: segment-display
flowercore.io/github-repo: FlowerCore.SegmentDisplay flowercore.io/github-repo: FlowerCore.SegmentDisplay
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-segment-display app.kubernetes.io/name: github-runner-segment-display
@@ -2935,7 +2931,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -2983,7 +2979,7 @@ metadata:
flowercore.io/runner-repo: signage-contracts flowercore.io/runner-repo: signage-contracts
flowercore.io/github-repo: FlowerCore.Signage.Contracts flowercore.io/github-repo: FlowerCore.Signage.Contracts
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-signage-contracts app.kubernetes.io/name: github-runner-signage-contracts
@@ -3068,7 +3064,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -3116,7 +3112,7 @@ metadata:
flowercore.io/runner-repo: signal-control flowercore.io/runner-repo: signal-control
flowercore.io/github-repo: FlowerCore.SignalControl flowercore.io/github-repo: FlowerCore.SignalControl
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-signal-control app.kubernetes.io/name: github-runner-signal-control
@@ -3201,7 +3197,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -3335,7 +3331,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -3469,7 +3465,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -3603,7 +3599,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -3737,7 +3733,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -3871,7 +3867,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -3919,7 +3915,7 @@ metadata:
flowercore.io/runner-repo: pimanager flowercore.io/runner-repo: pimanager
flowercore.io/github-repo: FlowerCore.PiManager flowercore.io/github-repo: FlowerCore.PiManager
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-pimanager app.kubernetes.io/name: github-runner-pimanager
@@ -4004,7 +4000,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -4053,7 +4049,7 @@ metadata:
flowercore.io/runner-repo: updater flowercore.io/runner-repo: updater
flowercore.io/github-repo: FlowerCore.Updater flowercore.io/github-repo: FlowerCore.Updater
spec: spec:
replicas: 1 replicas: 2
selector: selector:
matchLabels: matchLabels:
app.kubernetes.io/name: github-runner-updater app.kubernetes.io/name: github-runner-updater
@@ -4138,419 +4134,7 @@ spec:
value: "false" value: "false"
resources: resources:
requests: requests:
cpu: "100m" cpu: "500m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
volumeMounts:
- name: runner-home
mountPath: /home/runner
- name: nuget-cache
mountPath: /home/runner/.nuget/packages
- name: tmp
mountPath: /tmp
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "pgrep -f Runner.Listener > /dev/null"
initialDelaySeconds: 30
periodSeconds: 30
failureThreshold: 3
volumes:
- name: runner-home
emptyDir: {}
- name: nuget-cache
emptyDir:
sizeLimit: 2Gi
- name: tmp
emptyDir: {}
restartPolicy: Always
---
# Runner for FlowerCore.DeviceManagement. Two replicas use per-pod emptyDir
# caches, so backlog can drain without sharing a ReadWriteOnce PVC. Added
# 2026-05-26 morning-routine — DM had ZERO registered runners while Sprint 37
# Cx-1 PRs #20 (CI-to-Linux migration), #21 (WDAC), and #22 (AppLocker) were
# all queued indefinitely. Chicken-and-egg: the migration PRs need Linux
# runners that the migration creates.
apiVersion: apps/v1
kind: Deployment
metadata:
name: github-runner-device-management
namespace: github-runner
labels:
app.kubernetes.io/name: github-runner-device-management
app.kubernetes.io/component: runner
app.kubernetes.io/part-of: flowercore
app.kubernetes.io/managed-by: argocd
flowercore.io/created-by: argocd
flowercore.io/runner-repo: device-management
flowercore.io/github-repo: FlowerCore.DeviceManagement
spec:
# Single replica until cluster CPU pressure resolves; the fleet-wide
# request right-sizing pass is queued for a future sweep.
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: github-runner-device-management
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: github-runner-device-management
app.kubernetes.io/component: runner
app.kubernetes.io/part-of: flowercore
flowercore.io/created-by: argocd
flowercore.io/runner-repo: device-management
flowercore.io/github-repo: FlowerCore.DeviceManagement
spec:
serviceAccountName: github-runner
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
- -c
- |
set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext:
runAsUser: 0
runAsNonRoot: false
volumeMounts:
- name: runner-home
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.DeviceManagement"
- name: RUNNER_NAME_PREFIX
value: "rke2-linux-device-management"
- name: RUNNER_WORKDIR
value: "/tmp/runner/work"
- name: EPHEMERAL
value: "true"
- name: LABELS
value: "self-hosted,linux,fc-build-linux"
- name: HOME
value: "/home/runner"
- name: DOTNET_INSTALL_DIR
value: "/home/runner/.dotnet"
- name: DOTNET_CLI_TELEMETRY_OPTOUT
value: "1"
- name: DOTNET_NOLOGO
value: "1"
- name: DOTNET_GENERATE_ASPNET_CERTIFICATE
value: "false"
- name: DOTNET_CLI_HOME
value: "/home/runner"
- name: NUGET_PACKAGES
value: "/home/runner/.nuget/packages"
- name: XDG_CACHE_HOME
value: "/home/runner/.cache"
- name: RUNNER_TOOL_CACHE
value: "/home/runner/_tool"
- name: ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: github-runner-token
key: credential
- name: RUN_AS_ROOT
value: "false"
resources:
# Reduced from 500m → 100m 2026-05-26 because cluster CPU
# requests were at 99% across all 3 nodes; idle runners use ~1m.
# Burst headroom preserved by limits.cpu: 2000m.
requests:
cpu: "100m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
volumeMounts:
- name: runner-home
mountPath: /home/runner
- name: nuget-cache
mountPath: /home/runner/.nuget/packages
- name: tmp
mountPath: /tmp
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "pgrep -f Runner.Listener > /dev/null"
initialDelaySeconds: 30
periodSeconds: 30
failureThreshold: 3
volumes:
- name: runner-home
emptyDir: {}
- name: nuget-cache
emptyDir:
sizeLimit: 2Gi
- name: tmp
emptyDir: {}
restartPolicy: Always
---
# Runner for FlowerCore.AiStation.Linux. Two replicas use per-pod emptyDir
# caches. Added 2026-05-26 — #13 master-CI-fix PR was queued indefinitely
# (Linux job has no runner; Windows job remains queued until the Windows
# runner host substrate lands per Sprint 36 v2 Cl-2 / ADR-174).
apiVersion: apps/v1
kind: Deployment
metadata:
name: github-runner-aistation-linux
namespace: github-runner
labels:
app.kubernetes.io/name: github-runner-aistation-linux
app.kubernetes.io/component: runner
app.kubernetes.io/part-of: flowercore
app.kubernetes.io/managed-by: argocd
flowercore.io/created-by: argocd
flowercore.io/runner-repo: aistation-linux
flowercore.io/github-repo: FlowerCore.AiStation.Linux
spec:
# Single replica until cluster CPU pressure resolves; the fleet-wide
# request right-sizing pass is queued for a future sweep.
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: github-runner-aistation-linux
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: github-runner-aistation-linux
app.kubernetes.io/component: runner
app.kubernetes.io/part-of: flowercore
flowercore.io/created-by: argocd
flowercore.io/runner-repo: aistation-linux
flowercore.io/github-repo: FlowerCore.AiStation.Linux
spec:
serviceAccountName: github-runner
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
- -c
- |
set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext:
runAsUser: 0
runAsNonRoot: false
volumeMounts:
- name: runner-home
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.AiStation.Linux"
- name: RUNNER_NAME_PREFIX
value: "rke2-linux-aistation-linux"
- name: RUNNER_WORKDIR
value: "/tmp/runner/work"
- name: EPHEMERAL
value: "true"
- name: LABELS
value: "self-hosted,linux,fc-build-linux"
- name: HOME
value: "/home/runner"
- name: DOTNET_INSTALL_DIR
value: "/home/runner/.dotnet"
- name: DOTNET_CLI_TELEMETRY_OPTOUT
value: "1"
- name: DOTNET_NOLOGO
value: "1"
- name: DOTNET_GENERATE_ASPNET_CERTIFICATE
value: "false"
- name: DOTNET_CLI_HOME
value: "/home/runner"
- name: NUGET_PACKAGES
value: "/home/runner/.nuget/packages"
- name: XDG_CACHE_HOME
value: "/home/runner/.cache"
- name: RUNNER_TOOL_CACHE
value: "/home/runner/_tool"
- name: ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: github-runner-token
key: credential
- name: RUN_AS_ROOT
value: "false"
resources:
requests:
cpu: "100m"
memory: "1Gi"
limits:
cpu: "2000m"
memory: "4Gi"
volumeMounts:
- name: runner-home
mountPath: /home/runner
- name: nuget-cache
mountPath: /home/runner/.nuget/packages
- name: tmp
mountPath: /tmp
livenessProbe:
exec:
command:
- /bin/sh
- -c
- "pgrep -f Runner.Listener > /dev/null"
initialDelaySeconds: 30
periodSeconds: 30
failureThreshold: 3
volumes:
- name: runner-home
emptyDir: {}
- name: nuget-cache
emptyDir:
sizeLimit: 2Gi
- name: tmp
emptyDir: {}
restartPolicy: Always
---
# Runner for FlowerCore.WorldBuilder. Two replicas use per-pod emptyDir
# caches. Added 2026-05-26 — #3 and #4 Linux migration PRs queued
# indefinitely with one stale offline runner registered against the repo.
apiVersion: apps/v1
kind: Deployment
metadata:
name: github-runner-worldbuilder
namespace: github-runner
labels:
app.kubernetes.io/name: github-runner-worldbuilder
app.kubernetes.io/component: runner
app.kubernetes.io/part-of: flowercore
app.kubernetes.io/managed-by: argocd
flowercore.io/created-by: argocd
flowercore.io/runner-repo: worldbuilder
flowercore.io/github-repo: FlowerCore.WorldBuilder
spec:
# Single replica until cluster CPU pressure resolves; the fleet-wide
# request right-sizing pass is queued for a future sweep.
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: github-runner-worldbuilder
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: github-runner-worldbuilder
app.kubernetes.io/component: runner
app.kubernetes.io/part-of: flowercore
flowercore.io/created-by: argocd
flowercore.io/runner-repo: worldbuilder
flowercore.io/github-repo: FlowerCore.WorldBuilder
spec:
serviceAccountName: github-runner
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
- -c
- |
set -e
mkdir -p /home/runner/.dotnet /home/runner/.nuget/packages /home/runner/.nuget/NuGet /home/runner/.cache /home/runner/_tool
if [ -d /opt/runner-toolcache/Ruby ] && [ ! -d /home/runner/_tool/Ruby ]; then
cp -a /opt/runner-toolcache/Ruby /home/runner/_tool/
fi
chown -R 1001:1001 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
chmod -R 755 /home/runner/.dotnet /home/runner/.nuget /home/runner/.cache /home/runner/_tool
securityContext:
runAsUser: 0
runAsNonRoot: false
volumeMounts:
- name: runner-home
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
value: "https://github.com/astoltz/FlowerCore.WorldBuilder"
- name: RUNNER_NAME_PREFIX
value: "rke2-linux-worldbuilder"
- name: RUNNER_WORKDIR
value: "/tmp/runner/work"
- name: EPHEMERAL
value: "true"
- name: LABELS
value: "self-hosted,linux,fc-build-linux"
- name: HOME
value: "/home/runner"
- name: DOTNET_INSTALL_DIR
value: "/home/runner/.dotnet"
- name: DOTNET_CLI_TELEMETRY_OPTOUT
value: "1"
- name: DOTNET_NOLOGO
value: "1"
- name: DOTNET_GENERATE_ASPNET_CERTIFICATE
value: "false"
- name: DOTNET_CLI_HOME
value: "/home/runner"
- name: NUGET_PACKAGES
value: "/home/runner/.nuget/packages"
- name: XDG_CACHE_HOME
value: "/home/runner/.cache"
- name: RUNNER_TOOL_CACHE
value: "/home/runner/_tool"
- name: ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: github-runner-token
key: credential
- name: RUN_AS_ROOT
value: "false"
resources:
requests:
cpu: "100m"
memory: "1Gi" memory: "1Gi"
limits: limits:
cpu: "2000m" cpu: "2000m"
@@ -4587,6 +4171,3 @@ spec:
# Common as the only PVC-backed runner at replicas: 1. Any future multi-replica # Common as the only PVC-backed runner at replicas: 1. Any future multi-replica
# runner must use per-pod emptyDir caches, not a shared ReadWriteOnce PVC. # runner must use per-pod emptyDir caches, not a shared ReadWriteOnce PVC.
# 2026-05-25: PiManager added (was missed in the Sprint 32 long-tail sweep). # 2026-05-25: PiManager added (was missed in the Sprint 32 long-tail sweep).
# 2026-05-26: Updater + DeviceManagement + AiStation.Linux + WorldBuilder
# added by the morning-routine sweep — those repos had had ZERO online Linux
# PR-CI capacity, blocking the Sprint 37 Cx-1 Linux-CI-migration PRs.

View File

@@ -46,7 +46,7 @@ spec:
spec: spec:
containers: containers:
- name: intranet-web - name: intranet-web
image: localhost/fc-intranet-web:v20260531-ttsreader-bridge image: localhost/fc-intranet-web:v20260508-brochure-w1
imagePullPolicy: Never imagePullPolicy: Never
ports: ports:
- containerPort: 5300 - containerPort: 5300

View File

@@ -25,7 +25,7 @@ metadata:
role: github-actions-runner role: github-actions-runner
flowercore.io/managed-by: bluejay-infra flowercore.io/managed-by: bluejay-infra
spec: spec:
runStrategy: Halted runStrategy: Always
template: template:
metadata: metadata:
labels: labels:

View File

@@ -207,13 +207,20 @@ spec:
- port: 993 - port: 993
targetPort: 993 targetPort: 993
name: imaps name: imaps
# --- mail-tls Certificate REMOVED 2026-06-01 --- ---
# mail-tls is now managed OUTSIDE cert-manager: issued from step-ca's JWK 'admin' # TLS Certificate via cert-manager
# provisioner and auto-renewed by a systemd timer on noc1 (step ca renew), which apiVersion: cert-manager.io/v1
# writes the mail-tls secret directly. step-ca-acme only has an HTTP-01 (Traefik) kind: Certificate
# solver, but mail.iamworkin.lan must resolve to the dedicated MetalLB IP 10.0.56.202 metadata:
# (SMTP/IMAP), so HTTP-01 cannot validate. Do NOT re-add a cert-manager Certificate name: mail-tls
# here unless a DNS-01 solver is deployed for step-ca-acme. namespace: mail
spec:
secretName: mail-tls
issuerRef:
name: step-ca-acme
kind: ClusterIssuer
dnsNames:
- mail.iamworkin.lan
--- ---
# Traefik IngressRoute - Webmail placeholder # Traefik IngressRoute - Webmail placeholder
apiVersion: traefik.io/v1alpha1 apiVersion: traefik.io/v1alpha1

View File

@@ -479,11 +479,11 @@ data:
- "https://gitea.iamworkin.lan/" - "https://gitea.iamworkin.lan/"
- "https://argocd.iamworkin.lan/" - "https://argocd.iamworkin.lan/"
- "https://intranet.iamworkin.lan/" - "https://intranet.iamworkin.lan/"
- "https://signage.iamworkin.lan/healthz" # root 401 auth-gated 2026-06-01; /healthz anon 200 - "https://signage.iamworkin.lan/"
- "https://kiosk.iamworkin.lan/" - "https://kiosk.iamworkin.lan/"
- "https://media.iamworkin.lan/" - "https://media.iamworkin.lan/"
- "https://mysql.iamworkin.lan/healthz" # root 401 auth-gated 2026-06-01; /healthz anon 200 - "https://mysql.iamworkin.lan/"
- "https://php.iamworkin.lan/healthz" # root 401 auth-gated 2026-06-01; /healthz anon 200 - "https://php.iamworkin.lan/"
- "https://zabbix.iamworkin.lan/" - "https://zabbix.iamworkin.lan/"
- "https://desktop.iamworkin.lan/" - "https://desktop.iamworkin.lan/"
- "https://print.iamworkin.lan/" - "https://print.iamworkin.lan/"

View File

@@ -236,7 +236,7 @@ public sealed class FleetManifestLintTests
{ {
deployments.Should().ContainKey(expectedRunner.Key); deployments.Should().ContainKey(expectedRunner.Key);
var container = deployments[expectedRunner.Key].MainContainerMappings().Should().ContainSingle().Subject; var container = deployments[expectedRunner.Key].ContainerMappings().Should().ContainSingle().Subject;
EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value); EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value);
EnvValue(container, "EPHEMERAL").Should().Be("true"); EnvValue(container, "EPHEMERAL").Should().Be("true");
EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux"); EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux");
@@ -252,7 +252,7 @@ public sealed class FleetManifestLintTests
{ {
foreach (var deployment in GitHubRunnerDeployments().Values) foreach (var deployment in GitHubRunnerDeployments().Values)
{ {
var container = deployment.MainContainerMappings().Should().ContainSingle().Subject; var container = deployment.ContainerMappings().Should().ContainSingle().Subject;
foreach (var expectedEnv in WritableRunnerEnv) foreach (var expectedEnv in WritableRunnerEnv)
{ {
@@ -279,10 +279,7 @@ public sealed class FleetManifestLintTests
foreach (var deploymentName in ScaledLinuxRunnerDeployments) foreach (var deploymentName in ScaledLinuxRunnerDeployments)
{ {
var deployment = deployments[deploymentName]; var deployment = deployments[deploymentName];
// Scaled runners must have >= 2 replicas (avoid single-pod bottleneck). ReplicaCount(deployment).Should().Be(2);
// Individual deployments may be tuned upward per CI activity — see
// "runners: right-size replica counts per 14d CI activity (#24)".
ReplicaCount(deployment).Should().BeGreaterOrEqualTo(2, $"{deploymentName} is in the scaled set and must run with at least 2 replicas");
var volumes = deployment.MappingSequence("spec", "template", "spec", "volumes"); var volumes = deployment.MappingSequence("spec", "template", "spec", "volumes");
var claimNames = volumes var claimNames = volumes
@@ -308,108 +305,6 @@ public sealed class FleetManifestLintTests
.Be("github-runner-nuget-cache"); .Be("github-runner-nuget-cache");
} }
[Fact]
public void Runners_MustNotPinToOperatorWorkstationHosts()
{
// CRITICAL SAFETY (operator directive 2026-05-26): BLUEJAY-WS is the
// operator's primary workstation — host of the 1Password Connect
// bearer token, fcadmin SSH keys to noc1, signing CA private keys,
// and source for every FC repo. A self-hosted GitHub Actions runner
// there would execute arbitrary PR code with that local access.
// Build-side analog of the Sprint 9 NEW safe-account exclusion gate
// (Puppet GPO/AppLocker/WDAC/audit-forwarder modules refuse to apply
// on BLUEJAY-WS). This lint asserts no GitHub-runner Deployment in
// apps/github-runner/ pins to a forbidden operator-workstation host
// via nodeName, nodeSelector, nodeAffinity, or tolerations.
// Existing legacy `bluejay-ws-sandbox-1` GitHub-registered runner is
// out of scope here (it's a runtime registration, not a K8s
// Deployment) — see CLAUDE.md "Common Mistakes" entry and
// feedback_bluejay_ws_never_public_runner.md.
var forbiddenHostPatterns = new[]
{
"bluejay-ws",
"BLUEJAY-WS",
"bluejay-ws.iamworkin.lan",
"iamworkin-ws",
};
bool ContainsForbidden(string? value) =>
!string.IsNullOrWhiteSpace(value)
&& forbiddenHostPatterns.Any(pattern => value!.Contains(pattern, StringComparison.OrdinalIgnoreCase));
var violations = GitHubRunnerDeployments().Values.SelectMany(deployment =>
{
var local = new List<string>();
var podSpec = ManifestNodeExtensions.Mapping(deployment.Root, "spec", "template", "spec");
if (podSpec is null)
{
return local;
}
// nodeName: pins the pod to a specific node by name.
var nodeName = ManifestNodeExtensions.Scalar(podSpec, "nodeName");
if (ContainsForbidden(nodeName))
{
local.Add($"{deployment.Name} sets nodeName='{nodeName}' which targets a forbidden operator-workstation host.");
}
// nodeSelector: dict of label → value pinning the pod to nodes
// carrying matching labels. Examples that would trip this:
// kubernetes.io/hostname: bluejay-ws
// flowercore.io/host: bluejay-ws.iamworkin.lan
var nodeSelector = ManifestNodeExtensions.Mapping(podSpec, "nodeSelector");
if (nodeSelector is not null)
{
foreach (var entry in nodeSelector.Children)
{
var key = entry.Key is YamlScalarNode keyScalar ? keyScalar.Value : null;
var value = entry.Value is YamlScalarNode valueScalar ? valueScalar.Value : null;
if (ContainsForbidden(value))
{
local.Add($"{deployment.Name} has nodeSelector entry '{key}: {value}' which targets a forbidden operator-workstation host.");
}
}
}
// nodeAffinity: matchExpressions over node labels.
foreach (var term in ManifestNodeExtensions.MappingSequence(podSpec, "affinity", "nodeAffinity", "requiredDuringSchedulingIgnoredDuringExecution", "nodeSelectorTerms"))
{
foreach (var expr in ManifestNodeExtensions.MappingSequence(term, "matchExpressions"))
{
var key = ManifestNodeExtensions.Scalar(expr, "key");
foreach (var valueNode in ManifestNodeExtensions.ScalarSequence(expr, "values"))
{
if (ContainsForbidden(valueNode))
{
local.Add($"{deployment.Name} has nodeAffinity matchExpression '{key}' value '{valueNode}' which targets a forbidden operator-workstation host.");
}
}
}
}
// tolerations: scheduling onto a tainted operator-workstation
// node would let the runner run there. Forbid any toleration
// value that names the workstation.
foreach (var toleration in ManifestNodeExtensions.MappingSequence(podSpec, "tolerations"))
{
var key = ManifestNodeExtensions.Scalar(toleration, "key");
var value = ManifestNodeExtensions.Scalar(toleration, "value");
if (ContainsForbidden(key))
{
local.Add($"{deployment.Name} has toleration key '{key}' which targets a forbidden operator-workstation host.");
}
if (ContainsForbidden(value))
{
local.Add($"{deployment.Name} has toleration value '{value}' which targets a forbidden operator-workstation host.");
}
}
return local;
}).ToList();
violations.Should().BeEmpty("BLUEJAY-WS / iamworkin-ws must never host a fleet GitHub Actions runner; see CLAUDE.md 'Registering BLUEJAY-WS as a fleet GitHub Actions runner' and feedback_bluejay_ws_never_public_runner.md");
}
[Fact] [Fact]
public void Monitoring_MustAlertWhenLinuxRunnerDeploymentIsUnavailable() public void Monitoring_MustAlertWhenLinuxRunnerDeploymentIsUnavailable()
{ {
@@ -997,22 +892,6 @@ internal sealed record ManifestDocument(
.ToList(); .ToList();
} }
// MainContainerMappings excludes initContainers. Use this when asserting
// properties of the primary container (env, image, volumeMounts) where an
// initContainer would be a false-positive match — e.g. the GitHub runner
// image's `setup-runner-home` initContainer should not count toward the
// single-container assertions on the runner deployments.
public IReadOnlyList<YamlMappingNode> MainContainerMappings()
{
var podSpec = PodSpec();
if (podSpec is null)
{
return Array.Empty<YamlMappingNode>();
}
return ManifestNodeExtensions.MappingSequence(podSpec, "containers").ToList();
}
public IReadOnlyList<ContainerSpec> ContainerSpecs() public IReadOnlyList<ContainerSpec> ContainerSpecs()
{ {
return ContainerMappings() return ContainerMappings()