Zheng Feng d57f695769
fix(helm): default sandbox Services to ClusterIP (#3929) (#4190)
* fix(helm): default sandbox Services to ClusterIP (#3929)

The K8s sandbox provisioner supports both NodePort and ClusterIP via
SANDBOX_SERVICE_TYPE (added in #4016), but the Helm chart never set it,
so real-cluster installs inherited the NodePort default. That bound the
code-execution sandbox on every node's interfaces - including externally
reachable ones on GKE/EKS/AKS - and pinned every sandbox URL to one node
IP (SPOF on node reboot/drain/ephemeral-IP).

Default the chart to ClusterIP: the provisioner returns a cluster-DNS URL
(http://sandbox-<id>-svc.<ns>.svc.cluster.local:8080) so the gateway->
sandbox hop stays inside the cluster network - no node IP, no 30xxx port,
no external exposure. The chart always runs the gateway in-cluster, so
ClusterIP is always correct there.

NodePort remains an opt-in (provisioner.sandboxServiceType: NodePort +
nodeHost) for the Docker-Compose/hybrid path where the gateway is not in
K8s and cannot resolve .svc.cluster.local; the provisioner code default
stays NodePort for that path.

- values.yaml: add provisioner.sandboxServiceType ("ClusterIP")
- provisioner-deployment.yaml: emit SANDBOX_SERVICE_TYPE; gate the
  NODE_HOST block on NodePort mode (default "ClusterIP" for upgrade safety)
- NOTES.txt + README.md: document ClusterIP default + NodePort opt-in

No change to docker/provisioner/app.py (already mode-aware since #4016)
or RBAC (services verbs already cover ClusterIP).

* test(helm): assert sandbox Service-type gating + CHANGELOG the default flip (#3929)

Address review on #4190:

- Add scripts/check_chart_sandbox_service.sh: renders the chart for the
  default (ClusterIP, no NODE_HOST), the NodePort opt-in (both emitted),
  and NodePort+nodeHost (literal value, not downward API). Locks in the
  #3929 gating so a regression (e.g. re-adding an unconditional NODE_HOST,
  or dropping the `default "ClusterIP"` upgrade-safety fallback) fails CI.
  Wired into .github/workflows/chart.yaml validate-chart job. (#2)
- CHANGELOG [Unreleased] -> Changed: note the NodePort->ClusterIP default
  flip on upgrade + the `sandboxServiceType: NodePort` opt-back-in. (#4)

No chart template changes (the gating itself landed in the first commit).

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-07-16 14:30:05 +08:00

95 lines
3.8 KiB
YAML

name: Publish Helm Chart
# Publishes the DeerFlow Helm chart as an OCI artifact to GHCR alongside the
# container images (see container.yaml). Triggers on the same `v*` tags.
#
# On pull requests touching the chart or config.example.yaml, `validate-chart`
# runs lint + template render + a sandbox Service-type gating check + a
# config_version drift check (the chart's embedded config_version must not lag
# config.example.yaml) so a broken or stale chart fails the PR, not the release.
#
# Users then install with:
# helm install deer-flow oci://ghcr.io/${{ owner }}/charts/deer-flow --version <ver>
on:
push:
tags:
- "v*"
pull_request:
paths:
- "deploy/helm/deer-flow/**"
- "config.example.yaml"
- ".github/workflows/chart.yaml"
- "scripts/check_config_version.sh"
- "scripts/check_chart_sandbox_service.sh"
jobs:
validate-chart:
# Runs on PRs and release tags: catch a broken render or a stale
# config_version before merge / publish. A broken chart published under a
# vX.Y.Z tag is an immutable OCI artifact (GHCR won't let you overwrite
# --version), so a regression must fail here, not on install.
if: github.event_name == 'pull_request' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
# ubuntu-latest ships with helm 3 preinstalled - no setup-helm action needed.
- name: Lint chart
run: helm lint deploy/helm/deer-flow
- name: Validate templates render
run: helm template deer-flow deploy/helm/deer-flow --include-crds >/dev/null
# Assert the sandbox Service-type gating: default emits
# SANDBOX_SERVICE_TYPE=ClusterIP and no NODE_HOST; the NodePort opt-in
# emits both. Guards the #3929 default flip against regressions.
- name: Validate sandbox Service-type gating
run: bash scripts/check_chart_sandbox_service.sh
# The chart's `config:` block embeds a config_version that must not fall
# behind config.example.yaml. A stale version is silent in-cluster (the
# image ships no example to compare against, so _check_config_version
# never warns) but means the chart's config is authored against an older
# schema. Bump it in values.yaml and the README example. config_version
# gates no runtime behavior - it only drives the outdated-warning - so a
# bare version bump needs no field changes. Logic lives in
# scripts/check_config_version.sh (shared with nightly.yaml).
- name: config_version drift check
run: bash scripts/check_config_version.sh
verify-versions:
# Gate the release: every version source must match the v* tag. A forgotten
# bump in Chart.yaml, pyproject.toml, or package.json fails here and skips
# the publish. See scripts/verify_versions.sh.
if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/verify-versions.yml
publish-chart:
if: startsWith(github.ref, 'refs/tags/v')
needs: [verify-versions, validate-chart]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
- name: Log in to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Package chart
run: helm package deploy/helm/deer-flow --destination ./packages
- name: Push chart to GHCR
run: |
for pkg in ./packages/*.tgz; do
echo "--- pushing $pkg"
helm push "$pkg" oci://ghcr.io/${{ github.repository_owner }}/charts
done