Compare commits

..

10 Commits

Author SHA1 Message Date
Andrew Stoltz
ec78175526 tests: add bluejay-ws runner-exclusion lint + fix 3 stale runner-fleet assertions
Adds Runners_MustNotPinToOperatorWorkstationHosts lint test enforcing
operator directive 2026-05-26: BLUEJAY-WS / iamworkin-ws must never be
a fleet GitHub Actions runner. 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). Scans every github-runner
Deployment for forbidden nodeName, nodeSelector, nodeAffinity match
expressions, and toleration key/value pinning. See CLAUDE.md "Common
Mistakes" entry and feedback_bluejay_ws_never_public_runner.md.

Also fixes 3 pre-existing GitHubRunnerFleet_* lint failures that broke
when the runner image bumped to v20260525-ruby3.3.11-stepca (added a
setup-runner-home initContainer):

  * Add MainContainerMappings() helper (containers only, excludes
    initContainers) and switch
    GitHubRunnerFleet_MustRegisterRequiredReposAsRepoScopedDeployments
    + GitHubRunnerFleet_MustSetWritableNonRootDotnetAndCachePaths
    over to it. Without this, ContainerMappings().Should().ContainSingle()
    found the initContainer + runner = 2 containers and failed.

  * Loosen GitHubRunnerFleet_MustAvoidRwoMultiAttachForScaledDeployments
    ReplicaCount assertion from Be(2) to BeGreaterOrEqualTo(2). The
    semantic invariant is "at least 2 replicas so no single-pod
    bottleneck"; deployments tuned upward per 14d CI activity (e.g.
    github-runner-print-web at replicas: 3, see commit 1f1f682 PR #24)
    are valid.

Lint baseline: 6 failed -> 3 failed (the 3 remaining are unrelated:
PublicReadWriteIngressRoutes_* lives in FlowerCore.Updater/k8s/
ingressroute.yaml — separate PR; FcDeviceManagement_* needs operator
domain decision on the missing apps/fc-devicemgmt/argocd-application.yaml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:41:00 -05:00
Andrew Stoltz
2cc91b6df0 runners: bump tts-reader memory limit 4Gi -> 8Gi
The github-runner-tts-reader pod was being OOMKilled (exit 137)
mid-`dotnet test` on the TtsReader 1000+ test suite. PR #21 CI
(the Windows -> Linux runner migration) flapped twice with the
"self-hosted runner lost communication" annotation before the
K8s-side symptoms surfaced via kubectl describe pod.

Requests bumped 1Gi -> 2Gi, limits 4Gi -> 8Gi. Comment added
inline so future fleet runs don't trip the same wall.

Unblocks PR #21 + the 9 other open TtsReader PRs that all rebase
through it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:31:48 -05:00
0d2090fe81 runners: add github-runner-updater Deployment (#29)
Close runner-fleet gap for FlowerCore.Updater. Matches Sprint 32 long-tail pattern; registers entry in fleet-lint required-set.
2026-05-26 03:24:13 +00:00
Andrew Stoltz
bc3548e715 runners: add github-runner-pimanager Deployment
FlowerCore.PiManager build run 26417714843 sat queued 5h with zero
self-hosted runners registered to the repo. PiManager was missed in
the Sprint 32 long-tail sweep — every other FC repo got a dedicated
repo-scoped Deployment with its own ACCESS_TOKEN registration, but
PiManager fell through the cracks.

Adds a 2-replica ephemeral runner Deployment matching the Signage /
DMS / Print.Web pattern (per-pod emptyDir caches, no shared PVC,
labels `self-hosted,linux,fc-build-linux`, shared github-runner-token
PAT). Once ArgoCD syncs, the queued job will pick up automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:33:44 -05:00
74333cc26b selenium: right-size hub + chrome + edge memory limits (#28) 2026-05-26 01:12:15 +00:00
Andrew Stoltz
7310fb88c2 selenium: right-size hub + chrome + edge memory limits
Edge node has been OOMKilled 51 times in 5 days (~1 every 2.4h) on a
1Gi memory limit. Chrome runs maxSessions=2 on the same 1Gi cap and
was idling at 684Mi — first concurrent session pushing the node to
~900Mi+ would be the next OOM. Hub was running at 766Mi against a 1Gi
limit (75%); no recent restarts but no headroom either.

Firefox node has been running at 2Gi memory limit for 9 days with
zero restarts — that is the right size for a Selenium 4.27 browser
node under our session profile (screen recording sidecar + 1080p
rendering + page captures). Match it.

Changes:
- Hub:    limit 1Gi -> 1.5Gi, request 512Mi -> 1Gi
- Chrome: limit 1Gi -> 2Gi,   request 512Mi -> 1Gi
- Edge:   limit 1Gi -> 2Gi,   request 512Mi -> 1Gi

CPU left alone on all three — observed utilization is well under the
existing limits (hub 54m / 500m, chrome 185m / 1, edge 11m / 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:11:41 -05:00
148bc87b9a runners: bake step-ca root CA into image (v20260525-stepca) (#27) 2026-05-26 01:04:14 +00:00
Andrew Stoltz
2a1e842100 runners: bake step-ca root CA into image (v20260525-stepca)
Without the IAmWorkin step-ca root CA in the runner image's system
trust store, .NET HttpClient calls from CI tests against
`*.iamworkin.lan` (e.g. `https://selenium.iamworkin.lan/session`) fail
with `The remote certificate is invalid because of errors in the
certificate chain: PartialChain`. FlowerCore.Print.Web's
`WebScreenshotService` unit tests hit this on every build.

Drop the step-ca root PEM into `/usr/local/share/ca-certificates/`,
run `update-ca-certificates` once during apt install, and let OpenSSL +
.NET-on-Linux read the regenerated `/etc/ssl/certs/ca-certificates.crt`
automatically — no `SSL_CERT_FILE` env var, no per-Deployment volume
mount.

Image rebuilt + saved + imported on all 3 schedulable RKE2 nodes
(rke2-server, rke2-agent1, rke2-agent2) before this PR — verified with
`ctr images list -q | grep stepca` on each node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:55:38 -05:00
bc28430d24 selenium: allow github-runner namespace ingress on 4444 (#26) 2026-05-26 00:44:23 +00:00
Andrew Stoltz
cc92272217 selenium: allow github-runner namespace ingress on 4444
Unblocks CI jobs running in github-runner pods (e.g. FlowerCore.Print.Web
`help-screenshots`) from reaching selenium-hub. Previously the session
POST was DNAT'd to the hub pod IP then dropped at the Calico ingress
hook, surfacing as a 60s timeout against
http://selenium-hub.selenium.svc.cluster.local:4444 while the Selenium
UI showed 0/4 sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 19:43:12 -05:00
7 changed files with 538 additions and 78 deletions

View File

@@ -12,6 +12,15 @@ ENV PATH="/home/runner/_tool/Ruby/${RUBY_MINOR}/x64/bin:/opt/runner-toolcache/Ru
USER root
# Bake the IAmWorkin step-ca root CA into the system trust store. Without
# this, .NET HttpClient calls from CI tests against *.iamworkin.lan
# (e.g. https://selenium.iamworkin.lan/session) fail with `PartialChain`
# because the runner image's default Ubuntu trust bundle doesn't include
# our internal Root CA. update-ca-certificates regenerates
# /etc/ssl/certs/ca-certificates.crt, which OpenSSL + .NET on Linux read
# automatically — no SSL_CERT_FILE env var needed.
COPY step-ca-root.crt /usr/local/share/ca-certificates/iamworkin-step-ca-root.crt
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
autoconf \
@@ -31,6 +40,7 @@ RUN apt-get update \
pkg-config \
uuid-dev \
zlib1g-dev \
&& update-ca-certificates \
&& curl -fsSL "https://github.com/rbenv/ruby-build/archive/refs/tags/${RUBY_BUILD_VERSION}.tar.gz" -o /tmp/ruby-build.tar.gz \
&& mkdir -p /tmp/ruby-build \
&& tar -xzf /tmp/ruby-build.tar.gz --strip-components=1 -C /tmp/ruby-build \

View File

@@ -7,7 +7,7 @@ Deployments with `kubectl`; update this manifest and let ArgoCD reconcile.
All repo-scoped Linux runners use:
- `localhost/fc-github-runner:v20260520-ruby3.3.11`, derived from
- `localhost/fc-github-runner:v20260525-ruby3.3.11-stepca`, derived from
`myoung34/github-runner:latest`
- `ACCESS_TOKEN` from the `github-runner-token` Secret
- `RUN_AS_ROOT=false`
@@ -40,14 +40,26 @@ still mounts an `emptyDir` over `/home/runner`, so the `setup-runner-home` init
container copies the baked toolcache from `/opt/runner-toolcache/Ruby` into
`/home/runner/_tool/Ruby` before the runner container starts.
The IAmWorkin step-ca root CA is also baked into the system trust store
(`/usr/local/share/ca-certificates/iamworkin-step-ca-root.crt`, registered by
`update-ca-certificates`). Without it, .NET HttpClient calls from CI tests
against `*.iamworkin.lan` (e.g. `https://selenium.iamworkin.lan/session`)
fail with `PartialChain`. To refresh the bundled cert when the root rotates,
re-extract from the cluster and overwrite `step-ca-root.crt`:
```bash
kubectl get secret -n cert-manager step-ca-root \
-o jsonpath='{.data.ca\.crt}' | base64 -d > step-ca-root.crt
```
```bash
cd apps/github-runner
podman build -t localhost/fc-github-runner:v20260520-ruby3.3.11 .
podman run --rm localhost/fc-github-runner:v20260520-ruby3.3.11 ruby -v
podman run --rm localhost/fc-github-runner:v20260520-ruby3.3.11 \
podman build -t localhost/fc-github-runner:v20260525-ruby3.3.11-stepca .
podman run --rm localhost/fc-github-runner:v20260525-ruby3.3.11-stepca ruby -v
podman run --rm localhost/fc-github-runner:v20260525-ruby3.3.11-stepca \
test -f /opt/runner-toolcache/Ruby/3.3/x64.complete
podman save localhost/fc-github-runner:v20260520-ruby3.3.11 \
-o fc-github-runner-v20260520-ruby3.3.11.tar
podman save localhost/fc-github-runner:v20260525-ruby3.3.11-stepca \
-o fc-github-runner-v20260525-ruby3.3.11-stepca.tar
```
Import the saved image on every schedulable RKE2 node before ArgoCD rolls the
@@ -55,9 +67,9 @@ Deployments:
```bash
for node in rke2-server rke2-agent1 rke2-agent2; do
scp fc-github-runner-v20260520-ruby3.3.11.tar "$node:/tmp/"
ssh "$node" 'sudo ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images rm localhost/fc-github-runner:v20260520-ruby3.3.11 || true'
ssh "$node" 'sudo ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /tmp/fc-github-runner-v20260520-ruby3.3.11.tar'
scp fc-github-runner-v20260525-ruby3.3.11-stepca.tar "$node:/tmp/"
ssh "$node" 'sudo ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images rm localhost/fc-github-runner:v20260525-ruby3.3.11-stepca || true'
ssh "$node" 'sudo ctr -a /run/k3s/containerd/containerd.sock -n k8s.io images import /tmp/fc-github-runner-v20260525-ruby3.3.11-stepca.tar'
done
```

View File

@@ -22,7 +22,7 @@
# NUGET_PACKAGES, XDG_CACHE_HOME, and RUNNER_TOOL_CACHE are all pointed at
# writable mounted paths under /home/runner so actions/setup-dotnet does not
# attempt to install into /usr/share/dotnet.
# Ruby 3.3.11 is baked into localhost/fc-github-runner:v20260520-ruby3.3.11
# Ruby 3.3.11 is baked into localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
# under /opt/runner-toolcache; setup-runner-home copies it into
# /home/runner/_tool because the runner-home emptyDir masks image content
# under /home/runner at runtime.
@@ -157,7 +157,7 @@ spec:
# honors the deeper mount.
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -178,7 +178,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
# GitHub org/repo targeting.
@@ -334,7 +334,7 @@ spec:
# rather than re-applied per repo as flipped lanes land.
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -355,7 +355,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -472,7 +472,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -493,7 +493,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -604,7 +604,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -625,7 +625,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -736,7 +736,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -757,7 +757,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -868,7 +868,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -889,7 +889,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -1003,7 +1003,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -1024,7 +1024,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -1135,7 +1135,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -1156,7 +1156,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -1267,7 +1267,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -1288,7 +1288,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -1399,7 +1399,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -1420,7 +1420,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -1533,7 +1533,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -1554,7 +1554,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -1667,7 +1667,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -1688,7 +1688,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -1726,13 +1726,17 @@ spec:
key: credential
- name: RUN_AS_ROOT
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:
requests:
cpu: "500m"
memory: "1Gi"
memory: "2Gi"
limits:
cpu: "2000m"
memory: "4Gi"
memory: "8Gi"
volumeMounts:
- name: runner-home
mountPath: /home/runner
@@ -1802,7 +1806,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -1823,7 +1827,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -1936,7 +1940,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -1957,7 +1961,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -2070,7 +2074,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -2091,7 +2095,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -2204,7 +2208,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -2225,7 +2229,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -2337,7 +2341,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -2358,7 +2362,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -2471,7 +2475,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -2492,7 +2496,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -2604,7 +2608,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -2625,7 +2629,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -2737,7 +2741,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -2758,7 +2762,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -2870,7 +2874,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -2891,7 +2895,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -3003,7 +3007,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -3024,7 +3028,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -3136,7 +3140,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -3157,7 +3161,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -3270,7 +3274,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -3291,7 +3295,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -3404,7 +3408,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -3425,7 +3429,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -3538,7 +3542,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -3559,7 +3563,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -3672,7 +3676,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -3693,7 +3697,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -3806,7 +3810,7 @@ spec:
fsGroup: 1001
initContainers:
- name: setup-runner-home
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
command:
- sh
@@ -3827,7 +3831,7 @@ spec:
mountPath: /home/runner
containers:
- name: runner
image: localhost/fc-github-runner:v20260520-ruby3.3.11
image: localhost/fc-github-runner:v20260525-ruby3.3.11-stepca
imagePullPolicy: Never
env:
- name: REPO_URL
@@ -3897,9 +3901,277 @@ spec:
- name: tmp
emptyDir: {}
restartPolicy: Always
---
# Runner for FlowerCore.PiManager. Two replicas use per-pod emptyDir caches, so
# backlog can drain without sharing a ReadWriteOnce PVC. Added 2026-05-25 to
# close the runner-fleet gap that left run 26417714843 queued for 5h.
apiVersion: apps/v1
kind: Deployment
metadata:
name: github-runner-pimanager
namespace: github-runner
labels:
app.kubernetes.io/name: github-runner-pimanager
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: pimanager
flowercore.io/github-repo: FlowerCore.PiManager
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: github-runner-pimanager
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: github-runner-pimanager
app.kubernetes.io/component: runner
app.kubernetes.io/part-of: flowercore
flowercore.io/created-by: argocd
flowercore.io/runner-repo: pimanager
flowercore.io/github-repo: FlowerCore.PiManager
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.PiManager"
- name: RUNNER_NAME_PREFIX
value: "rke2-linux-pimanager"
- 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: "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.Updater. Two replicas use per-pod emptyDir caches, so
# backlog can drain without sharing a ReadWriteOnce PVC. Added 2026-05-26 to
# close the runner-fleet gap that left the repo with only the offline
# windows-sandbox runner and no Linux PR-CI capacity for future workflows.
apiVersion: apps/v1
kind: Deployment
metadata:
name: github-runner-updater
namespace: github-runner
labels:
app.kubernetes.io/name: github-runner-updater
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: updater
flowercore.io/github-repo: FlowerCore.Updater
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: github-runner-updater
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: github-runner-updater
app.kubernetes.io/component: runner
app.kubernetes.io/part-of: flowercore
flowercore.io/created-by: argocd
flowercore.io/runner-repo: updater
flowercore.io/github-repo: FlowerCore.Updater
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.Updater"
- name: RUNNER_NAME_PREFIX
value: "rke2-linux-updater"
- 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: "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
# Long-tail runner pattern:
#
# Sprint 32 added the final 16 long-tail repo-scoped Deployments above. Keep
# 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.
# 2026-05-25: PiManager added (was missed in the Sprint 32 long-tail sweep).

View File

@@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBxDCCAWqgAwIBAgIRAPY357G6ow6zMAL5+4bS2kkwCgYIKoZIzj0EAwIwQDEa
MBgGA1UEChMRSUFtV29ya2luIEFDTUUgQ0ExIjAgBgNVBAMTGUlBbVdvcmtpbiBB
Q01FIENBIFJvb3QgQ0EwHhcNMjYwMzA4MTgwNzExWhcNMzYwMzA1MTgwNzExWjBA
MRowGAYDVQQKExFJQW1Xb3JraW4gQUNNRSBDQTEiMCAGA1UEAxMZSUFtV29ya2lu
IEFDTUUgQ0EgUm9vdCBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJ2n04X1
JZo5Zdq/i1Idv8+fqwZyAzBh7whbqj0SWsJL8UWRabCMqYCs7+dXO0xRSzqkwFDL
x+vooOai8RgRNhajRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/
AgEBMB0GA1UdDgQWBBRnuPPQR6iM/H6vOluiU3Sygayz8jAKBggqhkjOPQQDAgNI
ADBFAiEArQK9dYPGmAZsdYnjziuFVVE5NKZUcceYvGfGC+tLXUsCIAudF2zJrCRq
3mK50ZZET/fwTkJwiEF4824mjP8p1CKM
-----END CERTIFICATE-----

View File

@@ -24,7 +24,16 @@
# (10.0.57.16:5200), public internet 80/443 (excluding RFC1918), and
# fc-signage:5190 for the signage AAT lane.
# - Ingress: Traefik (4444 + 8089 ACME-solver-style), intra-pod,
# telephony / gitea / fc-system / fc-signage namespaces on 4444.
# telephony / gitea / fc-system / fc-signage / github-runner namespaces
# on 4444.
#
# 2026-05-25: added github-runner ingress on 4444 so CI jobs running in
# self-hosted runner pods (e.g. FlowerCore.Print.Web `help-screenshots`)
# can reach the grid. Without this allow, the session POST to
# `selenium-hub.selenium.svc.cluster.local:4444` was DNAT'd to the hub
# pod IP and then dropped at the Calico ingress hook — Selenium UI showed
# 0/4 sessions while the .NET HTTP client timed out at 60s. Same family
# as `feedback_netpol_dnat_backend_port`, wrong-source-namespace flavor.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
@@ -203,6 +212,13 @@ spec:
ports:
- port: 4444
protocol: TCP
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: github-runner
ports:
- port: 4444
protocol: TCP
podSelector: {}
policyTypes:
- Ingress

View File

@@ -132,13 +132,18 @@ spec:
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 5
# Hub baseline working set ~766Mi on 2026-05-25 (75% of prior 1Gi
# limit). Bump to 1.5Gi / 1Gi to keep ~50% headroom; matches the
# stampede-buffer pattern documented for multus
# (feedback_k8s_cni_multus_sizing). CPU left alone — observed 54m
# against a 500m limit, no contention.
resources:
limits:
cpu: 500m
memory: 1Gi
memory: 1536Mi
requests:
cpu: 250m
memory: 512Mi
memory: 1Gi
---
apiVersion: apps/v1
kind: Deployment
@@ -198,13 +203,18 @@ spec:
port: 5555
initialDelaySeconds: 15
periodSeconds: 5
# Chromium-based browser node. Bumped from 1Gi -> 2Gi (req 512Mi
# -> 1Gi) on 2026-05-25 — Edge had 51 OOMKills in 5d on the
# original 1Gi cap (~1 OOM every 2.4h), and Chrome at maxSessions=2
# was running 684Mi idle on the same cap. Matches the Firefox node's
# tested-stable 2Gi limit. CPU unchanged.
resources:
limits:
cpu: '1'
memory: 1Gi
memory: 2Gi
requests:
cpu: 500m
memory: 512Mi
memory: 1Gi
volumeMounts:
- mountPath: /dev/shm
name: dshm
@@ -378,13 +388,18 @@ spec:
port: 5555
initialDelaySeconds: 15
periodSeconds: 5
# Chromium-based browser node. Bumped from 1Gi -> 2Gi (req 512Mi
# -> 1Gi) on 2026-05-25 — Edge had 51 OOMKills in 5d on the
# original 1Gi cap (~1 OOM every 2.4h), and Chrome at maxSessions=2
# was running 684Mi idle on the same cap. Matches the Firefox node's
# tested-stable 2Gi limit. CPU unchanged.
resources:
limits:
cpu: '1'
memory: 1Gi
memory: 2Gi
requests:
cpu: 500m
memory: 512Mi
memory: 1Gi
volumeMounts:
- mountPath: /dev/shm
name: dshm

View File

@@ -67,6 +67,7 @@ public sealed class FleetManifestLintTests
["github-runner-chat"] = "https://github.com/astoltz/FlowerCore.Chat",
["github-runner-mysql"] = "https://github.com/astoltz/FlowerCore.MySQL",
["github-runner-kiosk-linux"] = "https://github.com/astoltz/FlowerCore.Kiosk.Linux",
["github-runner-updater"] = "https://github.com/astoltz/FlowerCore.Updater",
};
private static readonly HashSet<string> ScaledLinuxRunnerDeployments = new(StringComparer.Ordinal)
@@ -80,6 +81,7 @@ public sealed class FleetManifestLintTests
"github-runner-chat",
"github-runner-mysql",
"github-runner-kiosk-linux",
"github-runner-updater",
};
private static readonly IReadOnlyDictionary<string, string> WritableRunnerEnv = new Dictionary<string, string>(StringComparer.Ordinal)
@@ -234,7 +236,7 @@ public sealed class FleetManifestLintTests
{
deployments.Should().ContainKey(expectedRunner.Key);
var container = deployments[expectedRunner.Key].ContainerMappings().Should().ContainSingle().Subject;
var container = deployments[expectedRunner.Key].MainContainerMappings().Should().ContainSingle().Subject;
EnvValue(container, "REPO_URL").Should().Be(expectedRunner.Value);
EnvValue(container, "EPHEMERAL").Should().Be("true");
EnvValue(container, "LABELS").Should().Be("self-hosted,linux,fc-build-linux");
@@ -250,7 +252,7 @@ public sealed class FleetManifestLintTests
{
foreach (var deployment in GitHubRunnerDeployments().Values)
{
var container = deployment.ContainerMappings().Should().ContainSingle().Subject;
var container = deployment.MainContainerMappings().Should().ContainSingle().Subject;
foreach (var expectedEnv in WritableRunnerEnv)
{
@@ -277,7 +279,10 @@ public sealed class FleetManifestLintTests
foreach (var deploymentName in ScaledLinuxRunnerDeployments)
{
var deployment = deployments[deploymentName];
ReplicaCount(deployment).Should().Be(2);
// Scaled runners must have >= 2 replicas (avoid single-pod bottleneck).
// 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 claimNames = volumes
@@ -303,6 +308,108 @@ public sealed class FleetManifestLintTests
.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]
public void Monitoring_MustAlertWhenLinuxRunnerDeploymentIsUnavailable()
{
@@ -890,6 +997,22 @@ internal sealed record ManifestDocument(
.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()
{
return ContainerMappings()