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/Chart.yaml b/charts/github-app/Chart.yaml index 9f97c61..f91c5f6 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,12 @@ 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: "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 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..481341c 100644 --- a/charts/github-app/templates/_helpers.tpl +++ b/charts/github-app/templates/_helpers.tpl @@ -73,6 +73,23 @@ otherwise the chart-managed name -secret. {{- end }} {{- end }} +{{/* +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 (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 }} +{{- 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 fd6fcb1..1e67712 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,38 @@ 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) --- + # 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. + {{- 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 +165,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..db96e7d --- /dev/null +++ b/charts/github-app/templates/controller-secret.yaml @@ -0,0 +1,26 @@ +{{- if not .Values.secrets.existingControllerSecret }} +{{- $s := .Values.secrets }} +{{- $name := printf "%s-controller-secret" (include "github-app.fullname" .) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + 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. + # + # 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 }} +{{- end }} diff --git a/charts/github-app/templates/deployment.yaml b/charts/github-app/templates/deployment.yaml index 2d7e51e..df7a76d 100644 --- a/charts/github-app/templates/deployment.yaml +++ b/charts/github-app/templates/deployment.yaml @@ -55,6 +55,8 @@ spec: name: {{ include "github-app.fullname" . }}-config - secretRef: name: {{ include "github-app.secretName" . }} + - secretRef: + name: {{ include "github-app.controllerSecretName" . }} {{- with .Values.extraEnv }} env: {{- toYaml . | nindent 12 }} diff --git a/charts/github-app/templates/secret.yaml b/charts/github-app/templates/secret.yaml index e9e8e62..3787e2b 100644 --- a/charts/github-app/templates/secret.yaml +++ b/charts/github-app/templates/secret.yaml @@ -69,4 +69,7 @@ data: {{- if $s.daemonAuthTokenPrevious }} DAEMON_AUTH_TOKEN_PREVIOUS: {{ $s.daemonAuthTokenPrevious | 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 b71f8c7..9cd873f 100644 --- a/charts/github-app/values.yaml +++ b/charts/github-app/values.yaml @@ -264,7 +264,14 @@ 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. + # 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) --- appId: "" # Required in server mode (orchestratorUrl unset). @@ -292,6 +299,10 @@ 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. + # 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. 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 ─────────────────────── # `config:` is a 1:1 mirror of the non-secret fields of the Zod schema in @@ -325,6 +336,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 +348,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 +367,34 @@ 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) --- + # 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. + 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 +464,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: "" # 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..a479c97 100755 --- a/scripts/check-github-app-render.sh +++ b/scripts/check-github-app-render.sh @@ -69,4 +69,68 @@ if helm template "$chart" \ exit 1 fi -echo "github-app render matrix: 9/9 cases rendered" +# 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 + 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 + +# 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: 13/13 cases rendered"