Add setup-aws.sh: idempotent AWS provisioning for the EKS MVP (#309) … #659
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |