From c1afd6e7628013e4087d16b5d0b720273c20f1ca Mon Sep 17 00:00:00 2001 From: chrisleekr-bot Date: Wed, 2 Sep 2026 22:32:44 +0000 Subject: [PATCH 1/5] chore(github-app): sync chart to v1.17.0 --- charts/github-app/Chart.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/charts/github-app/Chart.yaml b/charts/github-app/Chart.yaml index 9f97c61..d8f234f 100644 --- a/charts/github-app/Chart.yaml +++ b/charts/github-app/Chart.yaml @@ -13,12 +13,12 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.18.0 +version: 0.19.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.16.1" +appVersion: "1.17.0" kubeVersion: ">= 1.31.0" maintainers: - name: Chris Lee @@ -36,6 +36,8 @@ sources: annotations: # https://artifacthub.io/docs/topics/annotations/helm/ artifacthub.io/changes: | + - kind: changed + description: "Sync to github-app v1.17.0: **core:** extract HTTP router and add output-safety primitives (#285); **repo-config:** add per-repo .github-app.yaml control surface (#286); **runner:** add the config surface and process boundary (#290); **runner:** isolate structured workflows in one-attempt Kubernetes Pods (#292); **runner:** move structured workflows onto a leased, durable run rail (#291). Release notes: https://github.com/chrisleekr/github-app/releases/tag/v1.17.0" - kind: security description: "Sync to github-app v1.16.1: **deps:** pin transitive adm-zip to 0.6.0 for CVE-2026-39244 (#268). Release notes: https://github.com/chrisleekr/github-app/releases/tag/v1.16.1" - kind: security @@ -98,4 +100,3 @@ annotations: description: "Orchestrator bearer-token check is now constant-time across all branches (upstream #76, #103)." - kind: changed description: "Default `config.triageModel` flipped from `haiku-4-5` to `sonnet-4-6` to align with upstream's new Zod default. Operators pinning to a Haiku alias for cost reasons must now set `config.triageModel` explicitly." - artifacthub.io/containsSecurityUpdates: "true" From 10c276dd56b14ffb4136ceb667ff87de59cd9ec5 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:38:10 +0000 Subject: [PATCH 2/5] chore(github-app): reconcile env surface for v1.17.0 --- charts/github-app/.env-contract-ignore | 6 ++++++ charts/github-app/templates/configmap.yaml | 25 ++++++++++++++++++++++ charts/github-app/templates/secret.yaml | 6 ++++++ charts/github-app/values.yaml | 21 +++++++++++++++++- 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/charts/github-app/.env-contract-ignore b/charts/github-app/.env-contract-ignore index 6cbe799..667184e 100644 --- a/charts/github-app/.env-contract-ignore +++ b/charts/github-app/.env-contract-ignore @@ -20,3 +20,9 @@ SOCKET_HEALTH_LEAK_SAMPLES SOCKET_HEALTH_SELF_HEAL_SAMPLES SOCKET_HEALTH_CPU_PERCENT SOCKET_HEALTH_SELF_HEAL_ENABLED +# Isolated-workflow-runner startup flag. Spawner-injected by +# src/k8s/workflow-runner-spawner.ts onto each runner Pod; upstream docs state +# operators must NOT set it on the controller or shared daemon Deployment. The +# operator-facing half of the feature is surfaced via config.workflowRunner.* and +# secrets.workflowRunnerCapabilitySecret. +WORKFLOW_RUNNER diff --git a/charts/github-app/templates/configmap.yaml b/charts/github-app/templates/configmap.yaml index fd6fcb1..07bf9f0 100644 --- a/charts/github-app/templates/configmap.yaml +++ b/charts/github-app/templates/configmap.yaml @@ -51,6 +51,9 @@ data: {{- if $c.allowedOwners }} ALLOWED_OWNERS: {{ $c.allowedOwners | quote }} {{- end }} + {{- if $c.autoReviewUsers }} + AUTO_REVIEW_USERS: {{ $c.autoReviewUsers | quote }} + {{- end }} # --- 6. Orchestrator --- WS_PORT: {{ $c.wsPort | quote }} @@ -64,6 +67,7 @@ data: STALE_EXECUTION_THRESHOLD_MS: {{ int $c.staleExecutionThresholdMs | quote }} DAEMON_DRAIN_TIMEOUT_MS: {{ int $c.daemonDrainTimeoutMs | quote }} JOB_MAX_RETRIES: {{ $c.jobMaxRetries | quote }} + WORKFLOW_DISPATCH_TIMEOUT_MS: {{ int $c.workflowDispatchTimeoutMs | quote }} OFFER_TIMEOUT_MS: {{ $c.offerTimeoutMs | quote }} QUEUE_WORKER_BACKOFF_MAX_MS: {{ int $c.queueWorkerBackoffMaxMs | quote }} LIVENESS_REAPER_INTERVAL_MS: {{ int $c.livenessReaperIntervalMs | quote }} @@ -80,11 +84,31 @@ data: EPHEMERAL_DAEMON_SPAWN_COOLDOWN_MS: {{ $c.ephemeralDaemon.spawnCooldownMs | quote }} EPHEMERAL_DAEMON_SPAWN_QUEUE_THRESHOLD: {{ $c.ephemeralDaemon.spawnQueueThreshold | quote }} EPHEMERAL_DAEMON_NAMESPACE: {{ default .Release.Namespace $c.ephemeralDaemon.namespace | quote }} + {{- if $c.ephemeralDaemon.secretName }} + EPHEMERAL_DAEMON_SECRET_NAME: {{ $c.ephemeralDaemon.secretName | quote }} + {{- end }} DAEMON_IMAGE: {{ default (include "github-app.image.daemon" (dict "root" .)) $c.ephemeralDaemon.image | quote }} {{- if $c.ephemeralDaemon.orchestratorPublicUrl }} ORCHESTRATOR_PUBLIC_URL: {{ $c.ephemeralDaemon.orchestratorPublicUrl | quote }} {{- end }} + # --- 8a. Isolated workflow runners (K8s-spawned per-attempt Pods) --- + # Operator half of the isolated workflow-runner feature (gated by + # secrets.workflowRunnerCapabilitySecret). Each key is gated so empty → app + # default. The internal WORKFLOW_RUNNER flag is spawner-injected, never set here. + {{- if $c.workflowRunner.namespace }} + WORKFLOW_RUNNER_NAMESPACE: {{ $c.workflowRunner.namespace | quote }} + {{- end }} + {{- if $c.workflowRunner.nodeLabel }} + WORKFLOW_RUNNER_NODE_LABEL: {{ $c.workflowRunner.nodeLabel | quote }} + {{- end }} + {{- if $c.workflowRunner.nodeValue }} + WORKFLOW_RUNNER_NODE_VALUE: {{ $c.workflowRunner.nodeValue | quote }} + {{- end }} + {{- if $c.workflowRunner.imagePullSecret }} + WORKFLOW_RUNNER_IMAGE_PULL_SECRET: {{ $c.workflowRunner.imagePullSecret | quote }} + {{- end }} + # --- 9. Triage (binary heavy classifier) --- TRIAGE_ENABLED: {{ $c.triageEnabled | quote }} TRIAGE_TOOLS_ENABLED: {{ $c.triageToolsEnabled | quote }} @@ -134,6 +158,7 @@ data: SCHEDULER_ENABLED: {{ $c.schedulerEnabled | quote }} SCHEDULER_SCAN_INTERVAL_MS: {{ int $c.schedulerScanIntervalMs | quote }} SCHEDULER_ALLOW_AUTO_MERGE: {{ $c.schedulerAllowAutoMerge | quote }} + REPO_CONFIG_FILE: {{ $c.repoConfigFile | quote }} SCHEDULER_CONFIG_FILE: {{ $c.schedulerConfigFile | quote }} REVIEW_LEARNINGS_ENABLED: {{ $c.reviewLearningsEnabled | quote }} REVIEW_LEARNINGS_RAG_ENABLED: {{ $c.reviewLearningsRagEnabled | quote }} diff --git a/charts/github-app/templates/secret.yaml b/charts/github-app/templates/secret.yaml index e9e8e62..3a4cc4e 100644 --- a/charts/github-app/templates/secret.yaml +++ b/charts/github-app/templates/secret.yaml @@ -69,4 +69,10 @@ data: {{- if $s.daemonAuthTokenPrevious }} DAEMON_AUTH_TOKEN_PREVIOUS: {{ $s.daemonAuthTokenPrevious | b64enc | quote }} {{- end }} + {{- if $s.workflowRunnerCapabilitySecret }} + WORKFLOW_RUNNER_CAPABILITY_SECRET: {{ $s.workflowRunnerCapabilitySecret | b64enc | quote }} + {{- end }} + {{- if $s.workflowRunnerCapabilitySecretPrevious }} + WORKFLOW_RUNNER_CAPABILITY_SECRET_PREVIOUS: {{ $s.workflowRunnerCapabilitySecretPrevious | b64enc | quote }} + {{- end }} {{- end }} diff --git a/charts/github-app/values.yaml b/charts/github-app/values.yaml index b71f8c7..910aa92 100644 --- a/charts/github-app/values.yaml +++ b/charts/github-app/values.yaml @@ -292,6 +292,8 @@ secrets: # --- 8. Daemon / Orchestrator WebSocket (auto-generated, persisted across upgrades) --- daemonAuthToken: "" # Leave empty to auto-generate and persist. daemonAuthTokenPrevious: "" # Optional rotation-window predecessor of daemonAuthToken. When set, the orchestrator accepts daemons whose Bearer header matches EITHER the primary or this previous value (constant-time). Set to the old token during a rolling rotation, then unset once every daemon has restarted on the new primary. Daemon-side ignored — daemons always send the primary daemonAuthToken. + workflowRunnerCapabilitySecret: "" # Controller-only HMAC root for deadline-bound, per-attempt isolated-workflow-runner capabilities. Min 32 chars. Setting it enables the isolated workflow runner feature (see config.workflowRunner). Must differ from daemonAuthToken and daemonAuthTokenPrevious. Rejected at boot if set on a daemon/worker. + workflowRunnerCapabilitySecretPrevious: "" # Optional controller-only rotation predecessor of workflowRunnerCapabilitySecret. Accepted only for capabilities whose signed expiry has not elapsed. Must also differ from both daemon-auth slots. # ─────────────────────────── APPLICATION CONFIG ─────────────────────── # `config:` is a 1:1 mirror of the non-secret fields of the Zod schema in @@ -325,6 +327,7 @@ config: agentMaxTurns: "" # Leave empty for no turn cap (recommended). Set to a positive int only when ops needs a hard ceiling; overrides defaultMaxTurns when both are set. claudeCodePath: "" # Required only when claude-code is installed globally (e.g. Docker). allowedOwners: "" # Comma-separated. Required when secrets.claudeCodeOauthToken is set. + autoReviewUsers: "" # Comma-separated GitHub logins whose pushes to an open PR (pull_request.synchronize) auto-trigger the review workflow, matched case-insensitively. Empty disables the feature. Requires config.allowedOwners set and workflows.review.auto=true on the repo. Under a PAT, do NOT list the PAT owner (self-pushes are skipped). # --- 6. Orchestrator --- wsPort: 3002 # Must differ from config.port. @@ -336,6 +339,7 @@ config: staleExecutionThresholdMs: 3660000 # Must exceed agentTimeoutMs (3600000); 60s margin. daemonDrainTimeoutMs: 3600000 # Set >= agentTimeoutMs to avoid mid-run kills on SIGTERM. Inflates daemon Pod terminationGracePeriodSeconds (drain/1000 + 30s) — confirm cluster tolerates ~60min graceful shutdown. jobMaxRetries: 3 + workflowDispatchTimeoutMs: 4200000 # Maximum age of an unclaimed structured-workflow dispatch. On retry-budget or age expiry the workflow fails, releases its in-flight lock, fails its execution receipt, and queues a public failure projection. offerTimeoutMs: 5000 queueWorkerBackoffMaxMs: 5000 # Caps doubling backoff when a leased job has no locally-capable daemon and is re-pushed for another instance to claim. livenessReaperIntervalMs: 30000 # Cadence of the heartbeat-based liveness reaper. Min 20000 (orchestrator heartbeat refresh interval) to avoid false reaps. @@ -354,9 +358,23 @@ config: spawnCooldownMs: 30000 # Minimum interval between spawns. Prevents burst storms. spawnQueueThreshold: 3 # Queue depth that triggers overflow spawn when persistent pool is saturated. namespace: "" # Leave empty to use the release namespace. Sets EPHEMERAL_DAEMON_NAMESPACE. + secretName: "" # Existing Secret in the ephemeral-daemon namespace that spawned Pods mount via envFrom. Leave empty to use the app default (daemon-secrets). Every key in it becomes an env var on Pods that run agent-authored code. Sets EPHEMERAL_DAEMON_SECRET_NAME. image: "" # Leave empty to use the daemon image helper. Sets DAEMON_IMAGE. orchestratorPublicUrl: "" # Optional ws:// or wss:// URL ephemeral daemons dial back. Unset OK when spawned Pods can reach the in-cluster orchestrator Service. + # --- 8a. Isolated workflow runners (K8s-spawned per-attempt Pods) --- + # Structured `workflow-run` jobs each run in one bare Pod per attempt, in a + # dedicated namespace, gated by secrets.workflowRunnerCapabilitySecret (unset + # → feature off). Requires rbac.create=true so the orchestrator can manage + # runner Pods. Every knob has an app default; leave empty to inherit it. The + # internal WORKFLOW_RUNNER flag is spawner-injected and never set here. See + # upstream src/k8s/workflow-runner-spawner.ts. + workflowRunner: + namespace: "" # Dedicated namespace for runner Pods, per-attempt Secrets, PSA, and the runner ValidatingAdmissionPolicy. Must differ from ephemeralDaemon.namespace. Empty → app default (github-app-runners). Sets WORKFLOW_RUNNER_NAMESPACE. + nodeLabel: "" # Node label key the runner Pod's nodeSelector and NoSchedule toleration are both built from. Empty → app default (github-app.node-restriction.kubernetes.io/workflow-runner). Sets WORKFLOW_RUNNER_NODE_LABEL. + nodeValue: "" # Value paired with nodeLabel. Empty → app default (true). Sets WORKFLOW_RUNNER_NODE_VALUE. + imagePullSecret: "" # Name of an existing kubernetes.io/dockerconfigjson Secret in the runner namespace that runner Pods may reference. Empty emits no imagePullSecrets (anonymous-pull registries only). Sets WORKFLOW_RUNNER_IMAGE_PULL_SECRET. + # --- 9. Triage (binary heavy classifier) --- triageEnabled: true # Kill-switch for the triage LLM call. triageToolsEnabled: true # Kill-switch for tool-driven triage (upstream #117). When false, falls back to snapshot-only triage classifier. @@ -426,7 +444,8 @@ config: schedulerEnabled: true # Chart default ON (upstream defaults OFF). Master kill-switch for the scheduler. When false the scheduler never starts. schedulerScanIntervalMs: 300000 # Tick cadence (5m). Zod range [60000, 3600000]. Below 60s the per-tick GitHub API cost is not worth it; above 1h cron precision degrades. schedulerAllowAutoMerge: false # Privilege gate for the `merge_readiness` MCP tool. When true (and an action sets `auto_merge: true`), the tool can merge PRs. Does NOT sandbox the agent from merging by other means: `allowed_tools` is owner-trusted config. Default OFF — opt-in per deployment. - schedulerConfigFile: ".github-app.yaml" # Filename the scheduler reads from each installed repo's default-branch root. + repoConfigFile: ".github-app.yaml" # Filename read from each installed repo's default-branch root, carrying the repo-wide per-repo policy (workflow toggles, agent knobs, scheduled actions, review-learnings). Trimmed at load — a stray space 404s on every repo and silently disables the surface. Sets REPO_CONFIG_FILE. + schedulerConfigFile: ".github-app.yaml" # DEPRECATED former name for repoConfigFile; still honoured as a fallback (logs a one-shot boot warning) so upgrades don't silently change which file is read. Sets SCHEDULER_CONFIG_FILE. reviewLearningsEnabled: true # Master kill-switch for review-learnings. When false the orchestrator never loads learnings into job:payload, the prompt block stays empty, and save/delete MCP calls are dropped. Additive feature — safe to leave ON. reviewLearningsRagEnabled: true # Chart default ON (upstream defaults OFF). Stage 2 pgvector RAG path: orchestrator embeds each directive at save time + each PR's changed-file paths at handleAccept, then runs pgvector top-K against `review_learnings.embedding`. REQUIRES Postgres migration 015 (pgvector extension); if missing or migration not applied, falls back to legacy file-glob filter. From 37d80bf667e706f1e127e2ce56292f7c9ccc12a7 Mon Sep 17 00:00:00 2001 From: Chris Lee Date: Sat, 5 Sep 2026 14:16:15 +1000 Subject: [PATCH 3/5] fix(github-app): split controller-only secret and honour legacy config-file override Address CodeRabbit review on #67. - Move WORKFLOW_RUNNER_CAPABILITY_SECRET[_PREVIOUS] out of the shared app Secret, which every daemon pool envFrom-mounts, into a new -controller-secret mounted on the orchestrator only. Upstream's K8s Secret split (docs/operate/configuration.md) keeps the capability signing root off workers. Add secrets.existingControllerSecret for externally managed Secrets. - Gate REPO_CONFIG_FILE and SCHEDULER_CONFIG_FILE on non-empty values and default both to empty. The app prefers REPO_CONFIG_FILE, so rendering it with a chart default silently shadowed a pre-1.17 schedulerConfigFile override. - Render matrix: add case 10 (secret split) and case 11 (legacy-only config-file override), both with assertions. Parity gate and its self-test scan the new template. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01UUbJFVvqyJu8VnEFjJaQMd --- charts/github-app/Chart.yaml | 4 ++ charts/github-app/templates/_helpers.tpl | 13 ++++++ charts/github-app/templates/configmap.yaml | 7 +++ .../templates/controller-secret.yaml | 22 ++++++++++ charts/github-app/templates/deployment.yaml | 4 ++ charts/github-app/templates/secret.yaml | 9 ++-- charts/github-app/values.yaml | 13 ++++-- scripts/check-env-parity.sh | 1 + scripts/check-env-parity.test.sh | 1 + scripts/check-github-app-render.sh | 44 ++++++++++++++++++- 10 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 charts/github-app/templates/controller-secret.yaml diff --git a/charts/github-app/Chart.yaml b/charts/github-app/Chart.yaml index d8f234f..67300a4 100644 --- a/charts/github-app/Chart.yaml +++ b/charts/github-app/Chart.yaml @@ -38,6 +38,10 @@ annotations: artifacthub.io/changes: | - kind: changed description: "Sync to github-app v1.17.0: **core:** extract HTTP router and add output-safety primitives (#285); **repo-config:** add per-repo .github-app.yaml control surface (#286); **runner:** add the config surface and process boundary (#290); **runner:** isolate structured workflows in one-attempt Kubernetes Pods (#292); **runner:** move structured workflows onto a leased, durable run rail (#291). Release notes: https://github.com/chrisleekr/github-app/releases/tag/v1.17.0" + - kind: security + description: "Move `secrets.workflowRunnerCapabilitySecret[Previous]` out of the shared app Secret (envFrom-mounted on every daemon pool) into a new `-controller-secret` mounted on the orchestrator only, per upstream's K8s Secret split. Adds `secrets.existingControllerSecret` for externally managed Secrets." + - kind: fixed + description: "`config.repoConfigFile` and the deprecated `config.schedulerConfigFile` now default to empty and render only when set, so a pre-1.17 `schedulerConfigFile` override keeps reading the same file after upgrade instead of being shadowed by a chart-default `REPO_CONFIG_FILE`." - kind: security description: "Sync to github-app v1.16.1: **deps:** pin transitive adm-zip to 0.6.0 for CVE-2026-39244 (#268). Release notes: https://github.com/chrisleekr/github-app/releases/tag/v1.16.1" - kind: security diff --git a/charts/github-app/templates/_helpers.tpl b/charts/github-app/templates/_helpers.tpl index 6fbf3ab..77a5b49 100644 --- a/charts/github-app/templates/_helpers.tpl +++ b/charts/github-app/templates/_helpers.tpl @@ -73,6 +73,19 @@ otherwise the chart-managed name -secret. {{- end }} {{- end }} +{{/* +Name of the controller-only Secret (workflow-runner capability roots), or empty +when the feature is off. Empty means the orchestrator mounts nothing extra, so +enabling the feature changes the pod template and triggers a rollout. +*/}} +{{- define "github-app.controllerSecretName" -}} +{{- if .Values.secrets.existingControllerSecret }} +{{- .Values.secrets.existingControllerSecret }} +{{- else if or .Values.secrets.workflowRunnerCapabilitySecret .Values.secrets.workflowRunnerCapabilitySecretPrevious }} +{{- printf "%s-controller-secret" (include "github-app.fullname" .) }} +{{- end }} +{{- end }} + {{/* Workspace PVC name. Uses existingClaim when set, otherwise -workspace. */}} diff --git a/charts/github-app/templates/configmap.yaml b/charts/github-app/templates/configmap.yaml index 07bf9f0..40ad0a6 100644 --- a/charts/github-app/templates/configmap.yaml +++ b/charts/github-app/templates/configmap.yaml @@ -158,7 +158,14 @@ data: SCHEDULER_ENABLED: {{ $c.schedulerEnabled | quote }} SCHEDULER_SCAN_INTERVAL_MS: {{ int $c.schedulerScanIntervalMs | quote }} SCHEDULER_ALLOW_AUTO_MERGE: {{ $c.schedulerAllowAutoMerge | quote }} + # Both gated: the app reads REPO_CONFIG_FILE first and falls back to the + # deprecated SCHEDULER_CONFIG_FILE, so rendering REPO_CONFIG_FILE with a chart + # default would silently override a pre-1.17 schedulerConfigFile override. + {{- if $c.repoConfigFile }} REPO_CONFIG_FILE: {{ $c.repoConfigFile | quote }} + {{- end }} + {{- if $c.schedulerConfigFile }} SCHEDULER_CONFIG_FILE: {{ $c.schedulerConfigFile | quote }} + {{- end }} REVIEW_LEARNINGS_ENABLED: {{ $c.reviewLearningsEnabled | quote }} REVIEW_LEARNINGS_RAG_ENABLED: {{ $c.reviewLearningsRagEnabled | quote }} diff --git a/charts/github-app/templates/controller-secret.yaml b/charts/github-app/templates/controller-secret.yaml new file mode 100644 index 0000000..a3349b3 --- /dev/null +++ b/charts/github-app/templates/controller-secret.yaml @@ -0,0 +1,22 @@ +{{- $s := .Values.secrets }} +{{- if and (not $s.existingControllerSecret) (or $s.workflowRunnerCapabilitySecret $s.workflowRunnerCapabilitySecretPrevious) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "github-app.fullname" . }}-controller-secret + labels: + {{- include "github-app.labels" . | nindent 4 }} +type: Opaque +data: + # Controller-only keys, mounted on the orchestrator Deployment and nothing + # else. The shared app Secret (secret.yaml) is envFrom-mounted on every daemon + # pool, and upstream's K8s Secret split (docs/operate/configuration.md) keeps + # the runner-capability signing root off workers: a shared daemon that sees it + # logs a leak warning, an isolated runner refuses to start. + {{- if $s.workflowRunnerCapabilitySecret }} + WORKFLOW_RUNNER_CAPABILITY_SECRET: {{ $s.workflowRunnerCapabilitySecret | b64enc | quote }} + {{- end }} + {{- if $s.workflowRunnerCapabilitySecretPrevious }} + WORKFLOW_RUNNER_CAPABILITY_SECRET_PREVIOUS: {{ $s.workflowRunnerCapabilitySecretPrevious | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/charts/github-app/templates/deployment.yaml b/charts/github-app/templates/deployment.yaml index 2d7e51e..5b49ac3 100644 --- a/charts/github-app/templates/deployment.yaml +++ b/charts/github-app/templates/deployment.yaml @@ -55,6 +55,10 @@ spec: name: {{ include "github-app.fullname" . }}-config - secretRef: name: {{ include "github-app.secretName" . }} + {{- with include "github-app.controllerSecretName" . }} + - secretRef: + name: {{ . }} + {{- end }} {{- with .Values.extraEnv }} env: {{- toYaml . | nindent 12 }} diff --git a/charts/github-app/templates/secret.yaml b/charts/github-app/templates/secret.yaml index 3a4cc4e..3787e2b 100644 --- a/charts/github-app/templates/secret.yaml +++ b/charts/github-app/templates/secret.yaml @@ -69,10 +69,7 @@ data: {{- if $s.daemonAuthTokenPrevious }} DAEMON_AUTH_TOKEN_PREVIOUS: {{ $s.daemonAuthTokenPrevious | b64enc | quote }} {{- end }} - {{- if $s.workflowRunnerCapabilitySecret }} - WORKFLOW_RUNNER_CAPABILITY_SECRET: {{ $s.workflowRunnerCapabilitySecret | b64enc | quote }} - {{- end }} - {{- if $s.workflowRunnerCapabilitySecretPrevious }} - WORKFLOW_RUNNER_CAPABILITY_SECRET_PREVIOUS: {{ $s.workflowRunnerCapabilitySecretPrevious | b64enc | quote }} - {{- end }} + # WORKFLOW_RUNNER_CAPABILITY_SECRET[_PREVIOUS] live in controller-secret.yaml: + # this Secret is envFrom-mounted on daemon pools too, and the capability root + # must never reach a worker (upstream docs/operate/configuration.md, K8s Secret split). {{- end }} diff --git a/charts/github-app/values.yaml b/charts/github-app/values.yaml index 910aa92..09a8fd4 100644 --- a/charts/github-app/values.yaml +++ b/charts/github-app/values.yaml @@ -264,7 +264,12 @@ secrets: # Chart escape hatch. When set, the chart-managed Secret is NOT rendered # and the Deployment consumes the named Secret directly. All-or-nothing: # the named Secret must contain every key the app expects (see secret.yaml). + # It is mounted on the orchestrator AND every daemon pool, so keep + # controller-only keys out of it and use existingControllerSecret instead. existingSecret: "" + # Same escape hatch for the controller-only Secret (see controller-secret.yaml): + # WORKFLOW_RUNNER_CAPABILITY_SECRET[_PREVIOUS]. Mounted on the orchestrator only. + existingControllerSecret: "" # --- 1. GitHub App credentials (server mode) --- appId: "" # Required in server mode (orchestratorUrl unset). @@ -292,7 +297,9 @@ secrets: # --- 8. Daemon / Orchestrator WebSocket (auto-generated, persisted across upgrades) --- daemonAuthToken: "" # Leave empty to auto-generate and persist. daemonAuthTokenPrevious: "" # Optional rotation-window predecessor of daemonAuthToken. When set, the orchestrator accepts daemons whose Bearer header matches EITHER the primary or this previous value (constant-time). Set to the old token during a rolling rotation, then unset once every daemon has restarted on the new primary. Daemon-side ignored — daemons always send the primary daemonAuthToken. - workflowRunnerCapabilitySecret: "" # Controller-only HMAC root for deadline-bound, per-attempt isolated-workflow-runner capabilities. Min 32 chars. Setting it enables the isolated workflow runner feature (see config.workflowRunner). Must differ from daemonAuthToken and daemonAuthTokenPrevious. Rejected at boot if set on a daemon/worker. + # Both keys below render into a separate -controller-secret mounted + # on the orchestrator only, never on daemon pools (upstream K8s Secret split). + workflowRunnerCapabilitySecret: "" # Controller-only HMAC root for deadline-bound, per-attempt isolated-workflow-runner capabilities. Min 32 chars. Setting it enables the isolated workflow runner feature (see config.workflowRunner). Must differ from daemonAuthToken and daemonAuthTokenPrevious. A shared daemon that sees it logs a leak warning; an isolated runner refuses to start. workflowRunnerCapabilitySecretPrevious: "" # Optional controller-only rotation predecessor of workflowRunnerCapabilitySecret. Accepted only for capabilities whose signed expiry has not elapsed. Must also differ from both daemon-auth slots. # ─────────────────────────── APPLICATION CONFIG ─────────────────────── @@ -444,8 +451,8 @@ config: schedulerEnabled: true # Chart default ON (upstream defaults OFF). Master kill-switch for the scheduler. When false the scheduler never starts. schedulerScanIntervalMs: 300000 # Tick cadence (5m). Zod range [60000, 3600000]. Below 60s the per-tick GitHub API cost is not worth it; above 1h cron precision degrades. schedulerAllowAutoMerge: false # Privilege gate for the `merge_readiness` MCP tool. When true (and an action sets `auto_merge: true`), the tool can merge PRs. Does NOT sandbox the agent from merging by other means: `allowed_tools` is owner-trusted config. Default OFF — opt-in per deployment. - repoConfigFile: ".github-app.yaml" # Filename read from each installed repo's default-branch root, carrying the repo-wide per-repo policy (workflow toggles, agent knobs, scheduled actions, review-learnings). Trimmed at load — a stray space 404s on every repo and silently disables the surface. Sets REPO_CONFIG_FILE. - schedulerConfigFile: ".github-app.yaml" # DEPRECATED former name for repoConfigFile; still honoured as a fallback (logs a one-shot boot warning) so upgrades don't silently change which file is read. Sets SCHEDULER_CONFIG_FILE. + repoConfigFile: "" # Empty → app default (.github-app.yaml). Filename read from each installed repo's default-branch root, carrying the repo-wide per-repo policy (workflow toggles, agent knobs, scheduled actions, review-learnings). Trimmed at load — a stray space 404s on every repo and silently disables the surface. Sets REPO_CONFIG_FILE. + schedulerConfigFile: "" # DEPRECATED former name for repoConfigFile; rename to repoConfigFile. Rendered only when set. The app honours it while repoConfigFile is empty (one-shot boot warning), so a pre-1.17 override keeps reading the same file after upgrade. Sets SCHEDULER_CONFIG_FILE. reviewLearningsEnabled: true # Master kill-switch for review-learnings. When false the orchestrator never loads learnings into job:payload, the prompt block stays empty, and save/delete MCP calls are dropped. Additive feature — safe to leave ON. reviewLearningsRagEnabled: true # Chart default ON (upstream defaults OFF). Stage 2 pgvector RAG path: orchestrator embeds each directive at save time + each PR's changed-file paths at handleAccept, then runs pgvector top-K against `review_learnings.embedding`. REQUIRES Postgres migration 015 (pgvector extension); if missing or migration not applied, falls back to legacy file-glob filter. diff --git a/scripts/check-env-parity.sh b/scripts/check-env-parity.sh index 4014e11..96b43be 100755 --- a/scripts/check-env-parity.sh +++ b/scripts/check-env-parity.sh @@ -51,6 +51,7 @@ contract=$(jq -r '.[].env' "$contract_json" | sort -u) chart_keys=$(grep -rhoE '(^[[:space:]]*[A-Z][A-Z0-9_]{2,}:)|(name: [A-Z][A-Z0-9_]{2,})' \ "$chart/templates/configmap.yaml" \ "$chart/templates/secret.yaml" \ + "$chart/templates/controller-secret.yaml" \ "$chart/templates/daemon-deployment.yaml" \ | sed -E 's/name: //; s/[[:space:]:]//g' | sort -u) ignore="" diff --git a/scripts/check-env-parity.test.sh b/scripts/check-env-parity.test.sh index 9d5dbfb..adcd5bd 100755 --- a/scripts/check-env-parity.test.sh +++ b/scripts/check-env-parity.test.sh @@ -22,6 +22,7 @@ unmirrored="$tmp/unmirrored.json" keys=$(grep -rhoE '(^[[:space:]]*[A-Z][A-Z0-9_]{2,}:)|(name: [A-Z][A-Z0-9_]{2,})' \ "$chart/templates/configmap.yaml" \ "$chart/templates/secret.yaml" \ + "$chart/templates/controller-secret.yaml" \ "$chart/templates/daemon-deployment.yaml" \ | sed -E 's/name: //; s/[[:space:]:]//g' | sort -u | grep -vx DOCKER_HOST) printf '%s\n' "$keys" | jq -R 'select(length > 0) | {env: ., group: "test", kind: "config"}' | jq -s . > "$contract" diff --git a/scripts/check-github-app-render.sh b/scripts/check-github-app-render.sh index 1005926..a656b68 100755 --- a/scripts/check-github-app-render.sh +++ b/scripts/check-github-app-render.sh @@ -69,4 +69,46 @@ if helm template "$chart" \ exit 1 fi -echo "github-app render matrix: 9/9 cases rendered" +# 10. Isolated workflow runners on: the capability root must land in the +# controller-only Secret (mounted on the orchestrator alone) and never in +# the shared app Secret, which every daemon pool envFrom-mounts. +out=$(helm template "$chart" \ + --set rbac.create=true \ + --set secrets.workflowRunnerCapabilitySecret=0123456789abcdef0123456789abcdef \ + --values "$chart/ci/daemon-pools-values.yaml") +# Secret document that carries the key vs. the Secret documents that must not. +if ! printf '%s\n' "$out" | awk '/^# Source: github-app\/templates\/controller-secret.yaml/,/^---/' \ + | grep -q '^ WORKFLOW_RUNNER_CAPABILITY_SECRET:'; then + echo "::error file=charts/github-app/templates/controller-secret.yaml::WORKFLOW_RUNNER_CAPABILITY_SECRET missing from the controller-only Secret" >&2 + exit 1 +fi +if printf '%s\n' "$out" | awk '/^# Source: github-app\/templates\/secret.yaml/,/^---/' \ + | grep -q '^ WORKFLOW_RUNNER_CAPABILITY_SECRET'; then + echo "::error file=charts/github-app/templates/secret.yaml::WORKFLOW_RUNNER_CAPABILITY_SECRET leaked into the shared app Secret that daemon pools mount" >&2 + exit 1 +fi +if printf '%s\n' "$out" | awk '/^# Source: github-app\/templates\/daemon-deployment.yaml/,/^---/' \ + | grep -q 'controller-secret'; then + echo "::error file=charts/github-app/templates/daemon-deployment.yaml::daemon Deployment references the controller-only Secret" >&2 + exit 1 +fi +if ! printf '%s\n' "$out" | awk '/^# Source: github-app\/templates\/deployment.yaml/,/^---/' \ + | grep -q 'controller-secret'; then + echo "::error file=charts/github-app/templates/deployment.yaml::orchestrator Deployment does not mount the controller-only Secret" >&2 + exit 1 +fi +# 11. Legacy-only repo-config override: a pre-1.17 `config.schedulerConfigFile` +# override must still reach the app. The app prefers REPO_CONFIG_FILE, so +# the chart must not render that key with a default alongside it. +out=$(helm template "$chart" --set config.schedulerConfigFile=legacy.yaml \ + | awk '/^# Source: github-app\/templates\/configmap.yaml/,/^---/') +if ! printf '%s\n' "$out" | grep -q '^ SCHEDULER_CONFIG_FILE: "legacy.yaml"$'; then + echo "::error file=charts/github-app/templates/configmap.yaml::config.schedulerConfigFile=legacy.yaml did not render SCHEDULER_CONFIG_FILE" >&2 + exit 1 +fi +if printf '%s\n' "$out" | grep -q '^ REPO_CONFIG_FILE:'; then + echo "::error file=charts/github-app/templates/configmap.yaml::REPO_CONFIG_FILE rendered alongside a legacy-only schedulerConfigFile override; the app would ignore the override" >&2 + exit 1 +fi + +echo "github-app render matrix: 11/11 cases rendered" From bf33b58528b389be1854abca20b955ae106de13f Mon Sep 17 00:00:00 2001 From: Chris Lee Date: Sat, 5 Sep 2026 14:25:09 +1000 Subject: [PATCH 4/5] fix(github-app): guard same-name external Secrets and restore security annotation Address second CodeRabbit round on #67. - Fail rendering when secrets.existingControllerSecret equals secrets.existingSecret: the app Secret is mounted on daemon pools, so a shared name would undo the controller-only split. Render matrix case 12 is the negative test for the guard. - Restore artifacthub.io/containsSecurityUpdates, dropped by the v1.17.0 sync commit; this chart version again carries a kind: security change. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01UUbJFVvqyJu8VnEFjJaQMd --- charts/github-app/Chart.yaml | 1 + charts/github-app/templates/_helpers.tpl | 3 +++ scripts/check-github-app-render.sh | 12 +++++++++++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/charts/github-app/Chart.yaml b/charts/github-app/Chart.yaml index 67300a4..e44a689 100644 --- a/charts/github-app/Chart.yaml +++ b/charts/github-app/Chart.yaml @@ -104,3 +104,4 @@ annotations: description: "Orchestrator bearer-token check is now constant-time across all branches (upstream #76, #103)." - kind: changed description: "Default `config.triageModel` flipped from `haiku-4-5` to `sonnet-4-6` to align with upstream's new Zod default. Operators pinning to a Haiku alias for cost reasons must now set `config.triageModel` explicitly." + artifacthub.io/containsSecurityUpdates: "true" diff --git a/charts/github-app/templates/_helpers.tpl b/charts/github-app/templates/_helpers.tpl index 77a5b49..27a4144 100644 --- a/charts/github-app/templates/_helpers.tpl +++ b/charts/github-app/templates/_helpers.tpl @@ -80,6 +80,9 @@ enabling the feature changes the pod template and triggers a rollout. */}} {{- define "github-app.controllerSecretName" -}} {{- if .Values.secrets.existingControllerSecret }} +{{- if eq .Values.secrets.existingControllerSecret .Values.secrets.existingSecret }} +{{- fail "secrets.existingControllerSecret must differ from secrets.existingSecret: the app Secret is mounted on daemon pools, so sharing it would expose the controller-only capability root to workers" }} +{{- end }} {{- .Values.secrets.existingControllerSecret }} {{- else if or .Values.secrets.workflowRunnerCapabilitySecret .Values.secrets.workflowRunnerCapabilitySecretPrevious }} {{- printf "%s-controller-secret" (include "github-app.fullname" .) }} diff --git a/scripts/check-github-app-render.sh b/scripts/check-github-app-render.sh index a656b68..568c687 100755 --- a/scripts/check-github-app-render.sh +++ b/scripts/check-github-app-render.sh @@ -111,4 +111,14 @@ if printf '%s\n' "$out" | grep -q '^ REPO_CONFIG_FILE:'; then exit 1 fi -echo "github-app render matrix: 11/11 cases rendered" +# 12. Negative: the two external Secret names must differ, otherwise the +# controller-only keys ride the Secret every daemon pool mounts and the +# split in case 10 is undone by configuration. +if helm template "$chart" \ + --set secrets.existingSecret=shared \ + --set secrets.existingControllerSecret=shared > /dev/null 2>&1; then + echo "::error file=charts/github-app/templates/_helpers.tpl::secrets.existingControllerSecret equal to secrets.existingSecret rendered instead of failing; the same-name guard is gone" >&2 + exit 1 +fi + +echo "github-app render matrix: 12/12 cases rendered" From addd5b3e886e6efd8d4db884078986bfdf433e2d Mon Sep 17 00:00:00 2001 From: Chris Lee Date: Sat, 5 Sep 2026 17:09:59 +1000 Subject: [PATCH 5/5] fix(github-app): always render the controller capability root; tighten guards Address third review round on #67. - WORKFLOW_RUNNER_CAPABILITY_SECRET is required on the v1.17.0 controller (config.ts validateDataLayerConfig), so a default install crash-looped. The controller-only Secret is now always rendered and the root is auto-generated and persisted with github-app.stableToken, like DAEMON_AUTH_TOKEN. Render case 10 asserts a default render carries it. - Same-name guard compares existingControllerSecret against the resolved app Secret name, closing the chart-managed-name alias. Case 12 pins both routes. - Fail rendering when the runner namespace equals the ephemeral-daemon namespace, which upstream rejects at boot. Case 13 is the negative test. - values.yaml: correct the config.workflowRunner block. rbac.create covers ephemeral-daemon Pods in the release namespace only; the runner namespace RBAC, provider Secret, admission boundary and dedicated nodes are provisioned outside this chart (upstream docs/operate/deployment.md). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01UUbJFVvqyJu8VnEFjJaQMd --- charts/github-app/Chart.yaml | 2 +- charts/github-app/templates/_helpers.tpl | 13 +++---- charts/github-app/templates/configmap.yaml | 7 ++++ .../templates/controller-secret.yaml | 14 +++++--- charts/github-app/templates/deployment.yaml | 4 +-- charts/github-app/values.yaml | 27 +++++++++++---- scripts/check-github-app-render.sh | 34 +++++++++++++------ 7 files changed, 68 insertions(+), 33 deletions(-) diff --git a/charts/github-app/Chart.yaml b/charts/github-app/Chart.yaml index e44a689..f91c5f6 100644 --- a/charts/github-app/Chart.yaml +++ b/charts/github-app/Chart.yaml @@ -39,7 +39,7 @@ annotations: - kind: changed description: "Sync to github-app v1.17.0: **core:** extract HTTP router and add output-safety primitives (#285); **repo-config:** add per-repo .github-app.yaml control surface (#286); **runner:** add the config surface and process boundary (#290); **runner:** isolate structured workflows in one-attempt Kubernetes Pods (#292); **runner:** move structured workflows onto a leased, durable run rail (#291). Release notes: https://github.com/chrisleekr/github-app/releases/tag/v1.17.0" - kind: security - description: "Move `secrets.workflowRunnerCapabilitySecret[Previous]` out of the shared app Secret (envFrom-mounted on every daemon pool) into a new `-controller-secret` mounted on the orchestrator only, per upstream's K8s Secret split. Adds `secrets.existingControllerSecret` for externally managed Secrets." + description: "Move `secrets.workflowRunnerCapabilitySecret[Previous]` out of the shared app Secret (envFrom-mounted on every daemon pool) into a new `-controller-secret` mounted on the orchestrator only, per upstream's K8s Secret split. The capability root is required on the v1.17.0 controller, so it is auto-generated and persisted across upgrades (same `lookup` treatment as `DAEMON_AUTH_TOKEN`) when `secrets.workflowRunnerCapabilitySecret` is empty. Adds `secrets.existingControllerSecret` for externally managed Secrets, rejected at render time when it names the app Secret. Render fails when the runner and ephemeral-daemon namespaces collide, which upstream refuses at boot." - kind: fixed description: "`config.repoConfigFile` and the deprecated `config.schedulerConfigFile` now default to empty and render only when set, so a pre-1.17 `schedulerConfigFile` override keeps reading the same file after upgrade instead of being shadowed by a chart-default `REPO_CONFIG_FILE`." - kind: security diff --git a/charts/github-app/templates/_helpers.tpl b/charts/github-app/templates/_helpers.tpl index 27a4144..481341c 100644 --- a/charts/github-app/templates/_helpers.tpl +++ b/charts/github-app/templates/_helpers.tpl @@ -74,17 +74,18 @@ otherwise the chart-managed name -secret. {{- end }} {{/* -Name of the controller-only Secret (workflow-runner capability roots), or empty -when the feature is off. Empty means the orchestrator mounts nothing extra, so -enabling the feature changes the pod template and triggers a rollout. +Name of the controller-only Secret (workflow-runner capability roots). Uses +secrets.existingControllerSecret when set, otherwise -controller-secret. +Compared against the resolved app Secret name, not the raw existingSecret value, +so pointing it at the chart-managed app Secret is rejected too. */}} {{- define "github-app.controllerSecretName" -}} {{- if .Values.secrets.existingControllerSecret }} -{{- if eq .Values.secrets.existingControllerSecret .Values.secrets.existingSecret }} -{{- fail "secrets.existingControllerSecret must differ from secrets.existingSecret: the app Secret is mounted on daemon pools, so sharing it would expose the controller-only capability root to workers" }} +{{- if eq .Values.secrets.existingControllerSecret (include "github-app.secretName" .) }} +{{- fail (printf "secrets.existingControllerSecret=%q is the app Secret that every daemon pool mounts; it must name a separate Secret so the controller-only capability root never reaches workers" .Values.secrets.existingControllerSecret) }} {{- end }} {{- .Values.secrets.existingControllerSecret }} -{{- else if or .Values.secrets.workflowRunnerCapabilitySecret .Values.secrets.workflowRunnerCapabilitySecretPrevious }} +{{- else }} {{- printf "%s-controller-secret" (include "github-app.fullname" .) }} {{- end }} {{- end }} diff --git a/charts/github-app/templates/configmap.yaml b/charts/github-app/templates/configmap.yaml index 40ad0a6..1e67712 100644 --- a/charts/github-app/templates/configmap.yaml +++ b/charts/github-app/templates/configmap.yaml @@ -93,6 +93,13 @@ data: {{- end }} # --- 8a. Isolated workflow runners (K8s-spawned per-attempt Pods) --- + # config.ts validateWorkerNamespaces rejects controller boot when the runner + # and ephemeral-daemon namespaces match; both are known here, so fail early. + {{- $runnerNs := $c.workflowRunner.namespace | default "github-app-runners" }} + {{- $daemonNs := $c.ephemeralDaemon.namespace | default .Release.Namespace }} + {{- if eq $runnerNs $daemonNs }} + {{- fail (printf "config.workflowRunner.namespace resolves to %q, the same as the ephemeral-daemon namespace. Upstream refuses to boot when WORKFLOW_RUNNER_NAMESPACE equals EPHEMERAL_DAEMON_NAMESPACE. Set config.workflowRunner.namespace to a dedicated namespace, or install the release into a different namespace." $runnerNs) }} + {{- end }} # Operator half of the isolated workflow-runner feature (gated by # secrets.workflowRunnerCapabilitySecret). Each key is gated so empty → app # default. The internal WORKFLOW_RUNNER flag is spawner-injected, never set here. diff --git a/charts/github-app/templates/controller-secret.yaml b/charts/github-app/templates/controller-secret.yaml index a3349b3..db96e7d 100644 --- a/charts/github-app/templates/controller-secret.yaml +++ b/charts/github-app/templates/controller-secret.yaml @@ -1,9 +1,10 @@ +{{- if not .Values.secrets.existingControllerSecret }} {{- $s := .Values.secrets }} -{{- if and (not $s.existingControllerSecret) (or $s.workflowRunnerCapabilitySecret $s.workflowRunnerCapabilitySecretPrevious) }} +{{- $name := printf "%s-controller-secret" (include "github-app.fullname" .) }} apiVersion: v1 kind: Secret metadata: - name: {{ include "github-app.fullname" . }}-controller-secret + name: {{ $name }} labels: {{- include "github-app.labels" . | nindent 4 }} type: Opaque @@ -13,9 +14,12 @@ data: # pool, and upstream's K8s Secret split (docs/operate/configuration.md) keeps # the runner-capability signing root off workers: a shared daemon that sees it # logs a leak warning, an isolated runner refuses to start. - {{- if $s.workflowRunnerCapabilitySecret }} - WORKFLOW_RUNNER_CAPABILITY_SECRET: {{ $s.workflowRunnerCapabilitySecret | b64enc | quote }} - {{- end }} + # + # Always rendered: v1.17.0 config.ts rejects controller boot without + # WORKFLOW_RUNNER_CAPABILITY_SECRET, so it gets the same lookup-backed + # generation as DAEMON_AUTH_TOKEN. Independent randoms also satisfy the + # upstream rule that capability roots differ from daemon-auth roots. + WORKFLOW_RUNNER_CAPABILITY_SECRET: {{ include "github-app.stableToken" (dict "key" "WORKFLOW_RUNNER_CAPABILITY_SECRET" "secretName" $name "override" $s.workflowRunnerCapabilitySecret "root" .) | b64enc | quote }} {{- if $s.workflowRunnerCapabilitySecretPrevious }} WORKFLOW_RUNNER_CAPABILITY_SECRET_PREVIOUS: {{ $s.workflowRunnerCapabilitySecretPrevious | b64enc | quote }} {{- end }} diff --git a/charts/github-app/templates/deployment.yaml b/charts/github-app/templates/deployment.yaml index 5b49ac3..df7a76d 100644 --- a/charts/github-app/templates/deployment.yaml +++ b/charts/github-app/templates/deployment.yaml @@ -55,10 +55,8 @@ spec: name: {{ include "github-app.fullname" . }}-config - secretRef: name: {{ include "github-app.secretName" . }} - {{- with include "github-app.controllerSecretName" . }} - secretRef: - name: {{ . }} - {{- end }} + name: {{ include "github-app.controllerSecretName" . }} {{- with .Values.extraEnv }} env: {{- toYaml . | nindent 12 }} diff --git a/charts/github-app/values.yaml b/charts/github-app/values.yaml index 09a8fd4..9cd873f 100644 --- a/charts/github-app/values.yaml +++ b/charts/github-app/values.yaml @@ -269,6 +269,8 @@ secrets: existingSecret: "" # Same escape hatch for the controller-only Secret (see controller-secret.yaml): # WORKFLOW_RUNNER_CAPABILITY_SECRET[_PREVIOUS]. Mounted on the orchestrator only. + # Must name a Secret other than the app Secret; the named Secret must carry + # WORKFLOW_RUNNER_CAPABILITY_SECRET (required on the controller since v1.17.0). existingControllerSecret: "" # --- 1. GitHub App credentials (server mode) --- @@ -299,7 +301,7 @@ secrets: daemonAuthTokenPrevious: "" # Optional rotation-window predecessor of daemonAuthToken. When set, the orchestrator accepts daemons whose Bearer header matches EITHER the primary or this previous value (constant-time). Set to the old token during a rolling rotation, then unset once every daemon has restarted on the new primary. Daemon-side ignored — daemons always send the primary daemonAuthToken. # Both keys below render into a separate -controller-secret mounted # on the orchestrator only, never on daemon pools (upstream K8s Secret split). - workflowRunnerCapabilitySecret: "" # Controller-only HMAC root for deadline-bound, per-attempt isolated-workflow-runner capabilities. Min 32 chars. Setting it enables the isolated workflow runner feature (see config.workflowRunner). Must differ from daemonAuthToken and daemonAuthTokenPrevious. A shared daemon that sees it logs a leak warning; an isolated runner refuses to start. + workflowRunnerCapabilitySecret: "" # Controller-only HMAC root for deadline-bound, per-attempt isolated-workflow-runner capabilities. Required on the controller since v1.17.0 (boot fails without it). Leave empty to auto-generate and persist across upgrades, like daemonAuthToken. Min 32 chars when set. Must differ from daemonAuthToken and daemonAuthTokenPrevious. A shared daemon that sees it logs a leak warning; an isolated runner refuses to start. workflowRunnerCapabilitySecretPrevious: "" # Optional controller-only rotation predecessor of workflowRunnerCapabilitySecret. Accepted only for capabilities whose signed expiry has not elapsed. Must also differ from both daemon-auth slots. # ─────────────────────────── APPLICATION CONFIG ─────────────────────── @@ -370,12 +372,23 @@ config: orchestratorPublicUrl: "" # Optional ws:// or wss:// URL ephemeral daemons dial back. Unset OK when spawned Pods can reach the in-cluster orchestrator Service. # --- 8a. Isolated workflow runners (K8s-spawned per-attempt Pods) --- - # Structured `workflow-run` jobs each run in one bare Pod per attempt, in a - # dedicated namespace, gated by secrets.workflowRunnerCapabilitySecret (unset - # → feature off). Requires rbac.create=true so the orchestrator can manage - # runner Pods. Every knob has an app default; leave empty to inherit it. The - # internal WORKFLOW_RUNNER flag is spawner-injected and never set here. See - # upstream src/k8s/workflow-runner-spawner.ts. + # Since v1.17.0 every structured `workflow-run` job runs in one bare Pod per + # attempt in a dedicated namespace; there is no daemon fallback. The chart + # only supplies the controller side (capability root, these knobs). The + # runner namespace itself is NOT provisioned here and needs, per upstream + # docs/operate/deployment.md "Kubernetes worker requirements": + # - a Role+RoleBinding for the orchestrator ServiceAccount in that + # namespace: pods create/get/delete, secrets create/get/update/delete. + # rbac.create=true does NOT cover this; it grants Pod verbs in the + # release namespace for ephemeral daemons only, and the runner namespace + # must differ from the ephemeral-daemon namespace. + # - the `workflow-runner-secrets` provider-only Secret, + # - the runner admission boundary (ValidatingAdmissionPolicy + ConfigMap), + # - labelled and tainted dedicated runner nodes. + # Until those exist, structured workflow attempts fail at dispatch with a + # notification; the rest of the app is unaffected. Every knob has an app + # default; leave empty to inherit it. The internal WORKFLOW_RUNNER flag is + # spawner-injected and never set here. workflowRunner: namespace: "" # Dedicated namespace for runner Pods, per-attempt Secrets, PSA, and the runner ValidatingAdmissionPolicy. Must differ from ephemeralDaemon.namespace. Empty → app default (github-app-runners). Sets WORKFLOW_RUNNER_NAMESPACE. nodeLabel: "" # Node label key the runner Pod's nodeSelector and NoSchedule toleration are both built from. Empty → app default (github-app.node-restriction.kubernetes.io/workflow-runner). Sets WORKFLOW_RUNNER_NODE_LABEL. diff --git a/scripts/check-github-app-render.sh b/scripts/check-github-app-render.sh index 568c687..a479c97 100755 --- a/scripts/check-github-app-render.sh +++ b/scripts/check-github-app-render.sh @@ -69,13 +69,12 @@ if helm template "$chart" \ exit 1 fi -# 10. Isolated workflow runners on: the capability root must land in the -# controller-only Secret (mounted on the orchestrator alone) and never in -# the shared app Secret, which every daemon pool envFrom-mounts. -out=$(helm template "$chart" \ - --set rbac.create=true \ - --set secrets.workflowRunnerCapabilitySecret=0123456789abcdef0123456789abcdef \ - --values "$chart/ci/daemon-pools-values.yaml") +# 10. Controller-only Secret split, on a DEFAULT render (no capability value +# supplied): v1.17.0 refuses controller boot without the capability root, +# so the chart must generate it, and it must land in the controller-only +# Secret (mounted on the orchestrator alone), never in the shared app +# Secret that every daemon pool envFrom-mounts. +out=$(helm template "$chart" --values "$chart/ci/daemon-pools-values.yaml") # Secret document that carries the key vs. the Secret documents that must not. if ! printf '%s\n' "$out" | awk '/^# Source: github-app\/templates\/controller-secret.yaml/,/^---/' \ | grep -q '^ WORKFLOW_RUNNER_CAPABILITY_SECRET:'; then @@ -111,14 +110,27 @@ if printf '%s\n' "$out" | grep -q '^ REPO_CONFIG_FILE:'; then exit 1 fi -# 12. Negative: the two external Secret names must differ, otherwise the -# controller-only keys ride the Secret every daemon pool mounts and the -# split in case 10 is undone by configuration. +# 12. Negative: existingControllerSecret must not alias the app Secret by +# either route (an operator-supplied existingSecret, or the chart-managed +# name), otherwise the controller-only keys ride the Secret every daemon +# pool mounts and the split in case 10 is undone by configuration. if helm template "$chart" \ --set secrets.existingSecret=shared \ --set secrets.existingControllerSecret=shared > /dev/null 2>&1; then echo "::error file=charts/github-app/templates/_helpers.tpl::secrets.existingControllerSecret equal to secrets.existingSecret rendered instead of failing; the same-name guard is gone" >&2 exit 1 fi +if helm template rel "$chart" \ + --set secrets.existingControllerSecret=rel-github-app-secret > /dev/null 2>&1; then + echo "::error file=charts/github-app/templates/_helpers.tpl::secrets.existingControllerSecret pointing at the chart-managed app Secret rendered instead of failing; the guard compares the raw existingSecret value, not the resolved name" >&2 + exit 1 +fi +# 13. Negative: upstream refuses to boot when the runner namespace equals the +# ephemeral-daemon namespace. Installing into the runner default namespace +# is the easiest way to hit it, so the chart must fail at render time. +if helm template "$chart" --namespace github-app-runners > /dev/null 2>&1; then + echo "::error file=charts/github-app/templates/configmap.yaml::release namespace equal to the default runner namespace rendered instead of failing; the namespace-collision guard is gone" >&2 + exit 1 +fi -echo "github-app render matrix: 12/12 cases rendered" +echo "github-app render matrix: 13/13 cases rendered"