Skip to content

Implement: helm-upgrade-smoke CI job (Phase 13a §6.3 deferral, #284) … #653

Implement: helm-upgrade-smoke CI job (Phase 13a §6.3 deferral, #284) …

Implement: helm-upgrade-smoke CI job (Phase 13a §6.3 deferral, #284) … #653

Workflow file for this run

name: ci
on:
push:
branches: [main]
pull_request:
branches: [main]
merge_group:
# Per-PR job gating: a single `changes` detector job classifies the PR's
# touched paths into coarse buckets, and every downstream job carries an
# `if:` gate keyed off those buckets. This lets docs-only / spec-only /
# conformance-only PRs skip the expensive compose smokes, e2e, postgres,
# and conformance jobs they don't exercise.
#
# Why this works with branch protection: a job skipped via a job-level
# `if:` reports a "skipped" conclusion, which branch protection's required
# status checks treat as PASSING (unlike a workflow-level `paths:` filter,
# which never creates the check and leaves it Pending forever, deadlocking
# the merge). So gating here is safe for the 12 required contexts.
#
# Every downstream gate has the shape:
#
# if: >-
# !cancelled() && (
# needs.changes.result != 'success' ||
# needs.changes.outputs.run_all == 'true' ||
# needs.changes.outputs.<bucket> == 'true' )
#
# The three OR clauses, in order, are the safety design:
#
# 1. `needs.changes.result != 'success'` — FAIL-SAFE. If the detector
# job errors, every gated job runs the FULL suite rather than
# green-skipping. A false negative (skipping validation a change
# needed) is the dangerous mode here, because PR checks are the only
# pre-merge gate (no merge queue, 0 required reviews). Issue #225
# additionally makes `changes` a required check; this clause is the
# in-workflow belt to that branch-protection suspenders.
# 2. `run_all` — true when the event is NOT a pull_request (push-to-main
# / merge_group get the full suite as a post-merge safety net) OR a
# rootcfg file changed (the workflow file — a PR editing a job
# definition must run that job to validate the edit — or the root
# pyproject.toml / uv.lock, which every uv-sync job depends on).
# Computed in the `gate` step below so it survives a skipped/failed
# filter step.
# 3. the per-job bucket(s) — the actual path-based gating.
#
# `!cancelled()` is required because using `needs.changes.result` forces
# the gate to opt out of the default "skip on failed dependency" behavior;
# without it a manual cancel wouldn't propagate. Filters are biased toward
# over-inclusion: a false positive wastes minutes, a false negative skips
# validation.
jobs:
changes:
name: changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
docs: ${{ steps.filter.outputs.docs }}
spec: ${{ steps.filter.outputs.spec }}
schemas: ${{ steps.filter.outputs.schemas }}
python: ${{ steps.filter.outputs.python }}
contracts: ${{ steps.filter.outputs.contracts }}
compose: ${{ steps.filter.outputs.compose }}
conformance: ${{ steps.filter.outputs.conformance }}
helm: ${{ steps.filter.outputs.helm }}
run_all: ${{ steps.gate.outputs.run_all }}
steps:
# No checkout needed: on pull_request, dorny/paths-filter lists changed
# files via the GitHub API (needs pull-requests: read). The step is
# skipped on push / merge_group, where `run_all` is true regardless of
# these outputs.
- uses: dorny/paths-filter@v3
id: filter
if: github.event_name == 'pull_request'
with:
filters: |
docs:
- '*.md'
- '**/*.md'
- 'docs/**'
# docs-lint auto-discovers this config (no --config override);
# the ISSUE_TEMPLATE dir holds the .yml that rename-discipline
# scans (its .md siblings already match '**/*.md').
- '.markdownlint.json'
- '.github/ISSUE_TEMPLATE/**'
spec:
- 'spec/**'
schemas:
- 'spec/v0/schemas/**'
- 'tests/fixtures/experiment/.eden/config.yaml'
python:
- 'reference/packages/**'
- 'reference/services/**'
- 'conformance/**'
- 'scripts/**'
- 'tests/**'
contracts:
- 'reference/packages/eden-contracts/**'
- 'spec/v0/schemas/**'
compose:
- 'reference/compose/**'
- 'reference/scripts/**'
# The compose image build context is the repo root (`../..`),
# so the root .dockerignore directly shapes every compose
# smoke / e2e image build.
- '.dockerignore'
conformance:
- 'conformance/**'
- 'reference/packages/**'
- 'reference/services/**'
- 'spec/**'
# helm-smoke builds the reference image and drives it through the
# chart, so it must run on chart changes AND on any reference
# package/service change (the image + orchestrator code it embeds).
helm:
- 'reference/helm/**'
- 'reference/scripts/setup-experiment-helm.sh'
- 'reference/packages/**'
- 'reference/services/**'
# helm-smoke builds the image from this Dockerfile (context = repo
# root), so the build inputs gate the job too.
- 'reference/compose/Dockerfile'
- '.dockerignore'
# helm-smoke drives the fixture experiment; a fixture change can
# alter bootstrap behavior or the expected event counts.
- 'tests/fixtures/**'
# rootcfg = workspace-global build config. The workflow file
# (any job definition) and the root pyproject.toml / uv.lock
# (consumed by every `uv sync --frozen` job: python-lint,
# python-typecheck, python-test, schema-parity, conformance,
# python-test-postgres, the compose image build). A change here
# forces the full suite via run_all, because no single bucket
# captures all the jobs these files affect.
rootcfg:
- '.github/workflows/ci.yml'
- 'pyproject.toml'
- 'uv.lock'
- '.python-version'
# `run_all` collapses the "run the whole suite" triggers into one
# output: (a) the event is not a pull_request — push-to-main and
# merge_group get the full post-merge safety net; (b) a rootcfg file
# changed — the workflow file (a PR editing a job definition must run
# that job to validate the edit) or the root pyproject.toml / uv.lock
# (affects every uv-sync job, which no single bucket captures).
# This step has no `if:`, so it runs on every event; on non-PR events
# the filter step is skipped and its `rootcfg` output is empty, which
# the event_name check below already covers.
- id: gate
run: |
if [ "${{ github.event_name }}" != "pull_request" ] \
|| [ "${{ steps.filter.outputs.rootcfg }}" = "true" ]; then
echo "run_all=true" >> "$GITHUB_OUTPUT"
else
echo "run_all=false" >> "$GITHUB_OUTPUT"
fi
docs-lint:
name: docs-lint
runs-on: ubuntu-latest
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.docs == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install markdownlint-cli2
run: npm install -g markdownlint-cli2@0.14.0
- name: Lint markdown
run: |
markdownlint-cli2 "**/*.md" "#node_modules" "#.venv" "#docs/archive/**" "#docs/plans/review/**"
rename-discipline:
name: rename-discipline
runs-on: ubuntu-latest
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.docs == 'true' ||
needs.changes.outputs.spec == 'true' ||
needs.changes.outputs.python == 'true' ||
needs.changes.outputs.compose == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Check for pre-rename EDEN vocabulary
run: python3 scripts/check-rename-discipline.py
complexity-gate:
name: complexity-gate
runs-on: ubuntu-latest
needs: changes
# `compose` is included because radon scans all of reference/ — which
# includes reference/compose/healthcheck/e2e_drive.py, a .py file that
# lives in the compose bucket, not the python bucket.
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.python == 'true' ||
needs.changes.outputs.compose == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install radon
run: python3 -m pip install 'radon==6.0.1'
- name: Run Tier-1 complexity gate
run: python3 scripts/check-complexity.py
schema-validity:
name: schema-validity
runs-on: ubuntu-latest
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.schemas == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install check-jsonschema
run: pip install 'check-jsonschema==0.29.4'
- name: Validate each schema against Draft 2020-12 meta-schema
run: |
check-jsonschema --check-metaschema spec/v0/schemas/*.schema.json
- name: Validate experiment fixture against experiment-config schema
run: |
check-jsonschema \
--schemafile spec/v0/schemas/experiment-config.schema.json \
tests/fixtures/experiment/.eden/config.yaml
python-lint:
name: python-lint
runs-on: ubuntu-latest
needs: changes
# `compose` is included because `ruff check .` lints the whole repo —
# including reference/compose/healthcheck/e2e_drive.py, a .py file that
# lives in the compose bucket, not the python bucket. (pyright's
# explicit include list excludes reference/compose, so python-typecheck
# does not need this.)
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.python == 'true' ||
needs.changes.outputs.compose == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: '0.11.2'
enable-cache: true
- name: Sync workspace
run: uv sync --frozen
- name: Ruff
run: uv run ruff check .
python-typecheck:
name: python-typecheck
runs-on: ubuntu-latest
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: '0.11.2'
enable-cache: true
- name: Sync workspace
run: uv sync --frozen
- name: Pyright
run: uv run pyright
python-test:
name: python-test
runs-on: ubuntu-latest
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.python == 'true' ||
needs.changes.outputs.spec == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: '0.11.2'
enable-cache: true
- name: Sync workspace
run: uv sync --frozen
- name: Pytest
# Skip the `docker` marker — those tests need a docker daemon
# and are run by the dedicated `compose-smoke-subprocess-docker`
# job. github-hosted runners have docker installed so without
# this filter the tests would run twice and slow this job down.
run: uv run pytest -q -m "not docker"
schema-parity:
name: schema-parity
runs-on: ubuntu-latest
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.spec == 'true' ||
needs.changes.outputs.contracts == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: '0.11.2'
enable-cache: true
- name: Sync workspace
run: uv sync --frozen
- name: Schema ↔ model parity
run: |
uv run pytest -q \
reference/packages/eden-contracts/tests/test_schema_parity.py \
reference/packages/eden-contracts/tests/test_roundtrip.py
compose-smoke:
name: compose-smoke
runs-on: ubuntu-latest
timeout-minutes: 15
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.compose == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Run compose smoke
working-directory: reference/compose
run: bash healthcheck/smoke.sh
compose-smoke-subprocess:
name: compose-smoke-subprocess
runs-on: ubuntu-latest
timeout-minutes: 15
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.compose == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Run compose smoke (subprocess overlay)
working-directory: reference/compose
run: bash healthcheck/smoke-subprocess.sh
# TODO(eden#38): bump branch protection to require this job once it
# has stayed clean on main for ~2 weeks since 2026-05-01. If this
# job flakes on main between now and then, leave a comment on
# issue #38 with the run URL and the failure mode so the bump can
# account for it.
compose-smoke-subprocess-docker:
name: compose-smoke-subprocess-docker
runs-on: ubuntu-latest
timeout-minutes: 25
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.compose == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
# Same PEP 668 posture as compose-e2e — docker-backed pytest
# markers may need pip installs in future expansions; setting
# this up now keeps the path consistent.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: '0.11.2'
enable-cache: true
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Sync workspace
run: uv sync --frozen
- name: Run docker-backed unit tests
run: uv run pytest -q -m docker reference/services/_common/tests
- name: Run compose smoke (subprocess + docker exec mode)
working-directory: reference/compose
run: bash healthcheck/smoke-subprocess-docker.sh
# 12a-2 wave 7 — new smoke jobs (not required by branch protection in
# this PR; same posture chunks 10c / 10d / 10e took for newly-added
# jobs). Bump to required-status after staying clean on main for
# ~2 weeks.
compose-smoke-manual-mode:
name: compose-smoke-manual-mode
runs-on: ubuntu-latest
timeout-minutes: 15
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.compose == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Run compose smoke (manual-mode drill)
working-directory: reference/compose
run: bash healthcheck/smoke-manual-mode.sh
compose-smoke-multi-orchestrator:
name: compose-smoke-multi-orchestrator
runs-on: ubuntu-latest
timeout-minutes: 20
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.compose == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Run compose smoke (multi-orchestrator drill)
working-directory: reference/compose
run: bash healthcheck/smoke-multi-orchestrator.sh
# Backfill of the Phase 12b CHANGELOG-narrated deferral (issue #152):
# exercises the portable-checkpoint export/import round-trip
# end-to-end via the Compose stack. Not required by branch
# protection in this PR; same posture as the other newly-added
# smoke jobs — bump to required-status after staying clean on main
# for ~2 weeks.
compose-smoke-checkpoint:
name: compose-smoke-checkpoint
runs-on: ubuntu-latest
timeout-minutes: 20
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.compose == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Run compose smoke (checkpoint round-trip)
working-directory: reference/compose
run: bash healthcheck/smoke-checkpoint.sh
compose-smoke-auto-checkpoint:
name: compose-smoke-auto-checkpoint
runs-on: ubuntu-latest
timeout-minutes: 20
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.compose == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Run compose smoke (auto-checkpoint cadence + terminal)
working-directory: reference/compose
run: bash healthcheck/smoke-auto-checkpoint.sh
# Backfill of the Phase 12c CHANGELOG-narrated deferral (issue #147),
# re-scoped: the reference impl cannot host >1 experiment per
# deployment (single-experiment task-store-server; cross-experiment
# isolation deferred to #254), so this exercises the control-plane as
# a first-class Compose service + the chapter-11 lease lifecycle and
# lease-handoff chaos drill on the deployed stack. Not required by
# branch protection in this PR; same posture as the other newly-added
# smoke jobs — bump to required-status after staying clean on main
# for ~2 weeks.
#
# TEMPORARILY DISABLED via `if: false` for the #128 identity rename:
# the smoke (and #147's compose.multi-experiment.yaml) assume the
# pre-#128 model where control-plane and task-store share a caller-
# supplied experiment_id. Under #128's spec-correct minting, the
# control-plane mints its own `exp_*` and lease-mode orchestrator
# drives a non-existent task-store experiment. Tracked in issue #281
# for the architectural reconciliation; re-enable after #281 lands.
compose-smoke-multi-experiment:
name: compose-smoke-multi-experiment
runs-on: ubuntu-latest
timeout-minutes: 20
needs: changes
if: false # #281: re-enable after the control-plane/task-store exp_id reconciliation
steps:
- uses: actions/checkout@v4
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Run compose smoke (control-plane + lease-handoff drill)
working-directory: reference/compose
run: bash healthcheck/smoke-multi-experiment.sh
# Issue #110: exercises the opt-in Loki + Alloy + Grafana log-search
# overlay (compose.logging.yaml) end-to-end — brings up base +
# subprocess + logging, asserts Loki ingests EDEN lines, Grafana is
# healthy with the datasource + dashboard provisioned, and (since the
# runner has a docker socket) the optional compose.logging-infra.yaml
# overlay captures postgres stdout. Path-gated on the `compose`
# bucket like every other compose-smoke-* job. Not required by branch
# protection in this PR; same posture as compose-smoke-checkpoint —
# bump to required-status after staying clean on main for ~2 weeks.
compose-smoke-logging:
name: compose-smoke-logging
runs-on: ubuntu-latest
timeout-minutes: 20
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.compose == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Run compose smoke (log-search overlay)
working-directory: reference/compose
run: bash healthcheck/smoke-logging.sh
compose-e2e:
name: compose-e2e
runs-on: ubuntu-latest
timeout-minutes: 20
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.compose == 'true' ||
needs.changes.outputs.python == 'true' )
steps:
- uses: actions/checkout@v4
# ubuntu-latest is now 24.04 with a system Python that
# rejects bare `pip install` per PEP 668. setup-python provides
# an isolated interpreter whose pip is unrestricted, and adds it
# to PATH so the bash wrapper's `python3` resolves to it.
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Verify docker compose + jq + python3 are available
run: |
docker compose version
jq --version
python3 --version
- name: Install httpx for the python driver
run: python3 -m pip install 'httpx>=0.27,<1'
- name: Run compose e2e
working-directory: reference/compose
run: bash healthcheck/e2e.sh
python-test-postgres:
name: python-test-postgres
runs-on: ubuntu-latest
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.python == 'true' ||
needs.changes.outputs.spec == 'true' )
services:
postgres:
image: postgres:16.6-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 3s
--health-retries 10
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: '0.11.2'
enable-cache: true
- name: Sync workspace
run: uv sync --frozen
- name: Run parametrized backend tests against Postgres
env:
EDEN_TEST_POSTGRES_DSN: postgresql://postgres:postgres@localhost:5432/postgres
run: uv run pytest -q reference/packages/eden-storage/tests
# TODO(eden#38): bump branch protection to require this job once it
# has stayed clean on main for ~2 weeks since 2026-05-01. If this
# job flakes on main between now and then, leave a comment on
# issue #38 with the run URL and the failure mode so the bump can
# account for it.
conformance:
name: conformance
runs-on: ubuntu-latest
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.conformance == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: '0.11.2'
enable-cache: true
- name: Sync workspace
run: uv sync --frozen
- name: Run conformance suite
# `-n auto` parallelizes the per-scenario subprocess spawn across
# the runner's CPUs (each scenario boots its own in-memory
# task-store-server + control-plane subprocess on random ports).
# The chapter 07 §7 vocabulary-closure assertion spans the whole
# run, so it is aggregated across workers in the harness plugin.
run: uv run pytest -q conformance/ -n auto
- name: Verify directionality (no reference imports outside adapters/reference/)
run: |
set -euo pipefail
if grep -RnE '^(from|import) (reference|eden_)' \
conformance/src/conformance/harness \
conformance/src/conformance/_meta \
conformance/scenarios; then
echo "FAIL: conformance/harness, conformance/_meta, or conformance/scenarios import reference packages."
exit 1
fi
- name: Verify scenario citations
run: uv run python conformance/src/conformance/tools/check_citations.py
# Fast structural gate for the Helm chart: lint + render + offline validate.
# Runs on every push (seconds); the heavier helm-smoke (below) spins up kind.
helm-lint:
name: helm-lint
runs-on: ubuntu-latest
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.helm == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: 'v3.16.2'
# helm lint + helm template both run the chart through values.schema.json
# and the template engine, failing on schema violations or render errors.
- name: helm lint (infra tier)
run: helm lint reference/helm/eden -f reference/helm/eden/ci-values.yaml
- name: helm template renders the full single-experiment stack
# baseCommitSha enables the store tier; the identity.* values enable the
# identity-consuming app tier (orchestrator, worker hosts, web-ui) the
# way setup-experiment-helm.sh's phase 3 does. Default lease mode off →
# no control-plane.
run: |
helm template eden reference/helm/eden \
-f reference/helm/eden/ci-values.yaml \
--set experiment.baseCommitSha=0123456789abcdef0123456789abcdef01234567 \
--set-string identity.orchestrator.workerId=wkr_template,identity.orchestrator.token=t \
--set-string identity.webUi.workerId=wkr_template,identity.webUi.token=t \
--set-string identity.ideatorHost.workerId=wkr_template,identity.ideatorHost.token=t \
--set-string identity.executorHost.workerId=wkr_template,identity.executorHost.token=t \
--set-string identity.evaluatorHost.workerId=wkr_template,identity.evaluatorHost.token=t \
> /dev/null
- name: helm template renders the lease-mode stack (control-plane on)
# Opt-in lease mode (deferred #281) must still render cleanly: it adds the
# control-plane Deployment, which the default single-experiment path omits.
run: |
helm template eden reference/helm/eden \
-f reference/helm/eden/ci-values.yaml \
--set experiment.baseCommitSha=0123456789abcdef0123456789abcdef01234567 \
--set orchestrator.leaseMode.enabled=true \
--set-string identity.orchestrator.workerId=wkr_template,identity.orchestrator.token=t \
--set-string identity.webUi.workerId=wkr_template,identity.webUi.token=t \
--set-string identity.ideatorHost.workerId=wkr_template,identity.ideatorHost.token=t \
--set-string identity.executorHost.workerId=wkr_template,identity.executorHost.token=t \
--set-string identity.evaluatorHost.workerId=wkr_template,identity.evaluatorHost.token=t \
| grep -q 'eden-control-plane'
- name: helm template fails closed without a required image
run: |
if helm template eden reference/helm/eden \
--set image.repository="" --set image.tag=x \
--set experiment.id=e >/dev/null 2>&1; then
echo "expected helm template to fail with empty image.repository" >&2
exit 1
fi
# Issue #174: the s3 / gcs blob-backend modes must render (positive)
# and must fail closed when the operator-required bucket or auth
# config is missing (negative). The kind-based helm-smoke stays on
# the default file mode — exercising a real S3 wire needs an
# in-cluster MinIO (deferred; see issue filed at chunk completion).
- name: helm template renders the s3 blob-backend mode
run: |
helm template eden reference/helm/eden \
-f reference/helm/eden/ci-values.yaml \
--set experiment.baseCommitSha=0123456789abcdef0123456789abcdef01234567 \
--set blob.backend=s3 \
--set blob.s3.bucket=ci-bucket \
--set blob.s3.region=us-east-1 \
--set blob.s3.irsa.enabled=true \
--set blob.s3.irsa.roleArn=arn:aws:iam::000000000000:role/eden-ci \
| grep -q -- '--blob-s3-bucket'
- name: helm template renders the gcs blob-backend mode
run: |
helm template eden reference/helm/eden \
-f reference/helm/eden/ci-values.yaml \
--set experiment.baseCommitSha=0123456789abcdef0123456789abcdef01234567 \
--set blob.backend=gcs \
--set blob.gcs.bucket=ci-bucket \
--set blob.gcs.existingSecret=eden-gcs-key \
| grep -q -- '--blob-gcs-bucket'
- name: helm template fails closed on a cloud blob backend without bucket/auth
run: |
if helm template eden reference/helm/eden \
-f reference/helm/eden/ci-values.yaml \
--set blob.backend=s3 >/dev/null 2>&1; then
echo "expected helm template to fail with blob.backend=s3 and no bucket" >&2
exit 1
fi
if helm template eden reference/helm/eden \
-f reference/helm/eden/ci-values.yaml \
--set blob.backend=gcs --set blob.gcs.bucket=b >/dev/null 2>&1; then
echo "expected helm template to fail with blob.backend=gcs and no auth path" >&2
exit 1
fi
# --- Phase 13c: managed (external) Postgres mode ---------------------
- name: helm template renders mode=external (no in-cluster Postgres)
# mode=external must omit the Postgres StatefulSet/Service and carry the
# operator DSN as EDEN_STORE_URL. Render with a seeded SHA so the
# task-store-server (the DSN consumer) renders too.
run: |
out="$(helm template eden reference/helm/eden \
-f reference/helm/eden/ci-values.yaml \
--set experiment.baseCommitSha=0123456789abcdef0123456789abcdef01234567 \
--set postgres.mode=external \
--set-string postgres.external.connectionString=postgresql://u:pw@db.example.com:5432/eden)"
if echo "$out" | grep -q 'name: eden-postgres'; then echo "external mode rendered a postgres resource" >&2; exit 1; fi
echo "$out" | grep -q 'EDEN_STORE_URL: "postgresql://u:pw@db.example.com:5432/eden"' || { echo "external DSN not written to Secret" >&2; exit 1; }
- name: helm template renders mode=external + TLS verify-full (CA mount + sslmode suffix)
run: |
out="$(helm template eden reference/helm/eden \
-f reference/helm/eden/ci-values.yaml \
--set experiment.baseCommitSha=0123456789abcdef0123456789abcdef01234567 \
--set postgres.mode=external \
--set-string postgres.external.connectionString=postgresql://u:pw@db.example.com:5432/eden \
--set postgres.tls.enabled=true \
--set postgres.tls.mode=verify-full \
--set-string postgres.tls.caBundleSecret=eden-rds-ca)"
echo "$out" | grep -q 'sslmode=verify-full&sslrootcert=/etc/eden/postgres-ca/ca.crt' || { echo "TLS suffix not appended to DSN" >&2; exit 1; }
echo "$out" | grep -q 'name: postgres-ca' || { echo "CA bundle volume not mounted" >&2; exit 1; }
- name: helm template renders mode=external + existingSecret (env override, no DSN in chart Secret)
run: |
out="$(helm template eden reference/helm/eden \
-f reference/helm/eden/ci-values.yaml \
--set experiment.baseCommitSha=0123456789abcdef0123456789abcdef01234567 \
--set postgres.mode=external \
--set-string postgres.external.existingSecret=eden-managed-pg)"
echo "$out" | grep -q 'eden-managed-pg' || { echo "external existingSecret not referenced via secretKeyRef" >&2; exit 1; }
if echo "$out" | grep -q '^ EDEN_STORE_URL:'; then echo "chart-managed Secret should omit EDEN_STORE_URL when external.existingSecret is set" >&2; exit 1; fi
- name: helm template fails closed on invalid external/TLS values
# Each of these MUST be rejected by values.schema.json at template time —
# the AGENTS.md substrate-plan pitfall on operator-required values.
run: |
base="helm template eden reference/helm/eden -f reference/helm/eden/ci-values.yaml"
assert_reject() {
local desc="$1"; shift
if $base "$@" >/dev/null 2>&1; then
echo "expected rejection: ${desc}" >&2
exit 1
fi
echo "rejected (ok): ${desc}"
}
assert_reject "mode=external without a DSN" --set postgres.mode=external
assert_reject "mode=external with a non-URL connectionString" --set postgres.mode=external --set-string postgres.external.connectionString=foo
assert_reject "mode=external with a libpq keyword DSN" --set postgres.mode=external --set-string 'postgres.external.connectionString=host=h dbname=eden user=eden'
assert_reject "tls.mode=disable (weak mode)" --set postgres.tls.mode=disable
assert_reject "tls verify-full without a caBundleSecret" --set postgres.mode=external --set-string postgres.external.connectionString=postgresql://u:p@h/eden --set postgres.tls.enabled=true
assert_reject "external+existingSecret+tls without tlsAlreadyEncodedInSecret" --set postgres.mode=external --set-string postgres.external.existingSecret=s --set-string postgres.external.connectionStringKey=K --set postgres.tls.enabled=true --set postgres.tls.mode=require
assert_reject "external + whole-chart secrets.existingSecret + inline connectionString (silently dropped)" --set postgres.mode=external --set-string secrets.existingSecret=my-secret --set-string postgres.external.connectionString=postgresql://u:p@h/eden
assert_reject "tls.enabled with mode=embedded (no server-side TLS on the embedded StatefulSet)" --set postgres.tls.enabled=true --set postgres.tls.mode=require
assert_reject "external + whole-chart secrets.existingSecret + tls without tlsAlreadyEncodedInSecret" --set postgres.mode=external --set-string secrets.existingSecret=my-secret --set postgres.tls.enabled=true --set postgres.tls.mode=require
assert_reject "external + lease mode (unsupported in v0)" --set postgres.mode=external --set-string postgres.external.connectionString=postgresql://u:p@h/eden --set orchestrator.leaseMode.enabled=true
# helm-smoke: the Helm analogue of compose-smoke. Spins up kind, builds +
# loads the reference image, runs setup-experiment-helm.sh against the
# fixture, and asserts the same integration end-state. Not required by branch
# protection in this PR (same posture newly-added smoke jobs took in 10c/10d);
# bump to required after it stays clean on main for ~2 weeks.
helm-smoke:
name: helm-smoke
runs-on: ubuntu-latest
timeout-minutes: 30
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.helm == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: 'v3.16.2'
- name: Set up kind
uses: helm/kind-action@v1
with:
install_only: true
# The setup script + ci-smoke.sh use only stdlib python3 (json, string)
# plus jq — no pip install needed.
- name: Verify tooling
run: |
kind version
kubectl version --client
helm version
jq --version
python3 --version
- name: Run helm smoke
run: bash reference/helm/eden/ci-smoke.sh
# helm-smoke-managed-postgres (13c): the external-Postgres analogue of
# helm-smoke. Stands up postgres:16.6-alpine as a sibling container on the kind
# Docker network (NOT chart-managed), installs the chart with
# postgres.mode=external pointing at it by IP, and asserts the SAME integration
# end-state — proving the managed-Postgres path drives an experiment to
# quiescence identically. Does NOT replace helm-smoke (embedded); 13c runs
# both. Not required by branch protection yet (same posture newly-added smoke
# jobs took); bump after it stays clean on main for ~2 weeks.
helm-smoke-managed-postgres:
name: helm-smoke-managed-postgres
runs-on: ubuntu-latest
timeout-minutes: 30
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.helm == 'true' )
steps:
- uses: actions/checkout@v4
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: 'v3.16.2'
- name: Set up kind
uses: helm/kind-action@v1
with:
install_only: true
- name: Verify tooling
run: |
kind version
kubectl version --client
helm version
jq --version
python3 --version
- name: Run helm smoke (managed external Postgres)
run: bash reference/helm/eden/ci-smoke-managed-postgres.sh
# helm-upgrade-smoke: issue #284 (Phase 13a §6.3 deferral). Installs the
# chart at the merge-base with origin/main (on a PR: main's chart; falls
# back to HEAD~1 when the merge-base is HEAD itself), drives a
# doubled-total fixture derivative to a first integration, then
# `helm upgrade`s in place to the PR's chart per the documented operator
# procedure (docs/deployment/helm.md §7) and asserts no immutable-field
# errors, no experiment-state loss (event counts never regress), strict
# post-upgrade progress, and the 6-variant end-state. Catches "chart
# change breaks upgrade-in-place" (immutable StatefulSet fields, PVC
# reclaim mistakes, render breaks against a live release's values). A PR
# that deliberately breaks upgrade-in-place (legal pre-user) can pin
# EDEN_UPGRADE_BASELINE_REF=HEAD on the run step below — in the same PR,
# with the reason called out in the PR body. Not required by branch
# protection (same posture helm-smoke took in 13a); bump to required
# after it stays clean on main for ~2 weeks.
helm-upgrade-smoke:
name: helm-upgrade-smoke
runs-on: ubuntu-latest
timeout-minutes: 40
needs: changes
if: >-
!cancelled() && (
needs.changes.result != 'success' ||
needs.changes.outputs.run_all == 'true' ||
needs.changes.outputs.helm == 'true' )
steps:
- uses: actions/checkout@v4
with:
# The upgrade baseline is resolved via merge-base with origin/main
# and extracted via git archive, so full history is needed.
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4
with:
version: 'v3.16.2'
- name: Set up kind
uses: helm/kind-action@v1
with:
install_only: true
# The setup script + ci-upgrade-smoke.sh use only stdlib python3 (json,
# string) plus jq — no pip install needed.
- name: Verify tooling
run: |
kind version
kubectl version --client
helm version
jq --version
python3 --version
- name: Run helm upgrade smoke
run: bash reference/helm/eden/ci-upgrade-smoke.sh