Skip to content

Sprint 11: config-as-code — portable campaign definitions, export/import, and the promocean CLI - #28

Merged
hynding merged 12 commits into
mainfrom
sprint-11-config-as-code
Jul 16, 2026
Merged

Sprint 11: config-as-code — portable campaign definitions, export/import, and the promocean CLI#28
hynding merged 12 commits into
mainfrom
sprint-11-config-as-code

Conversation

@hynding

@hynding hynding commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the Sprint 11 plan (docs/superpowers/plans/2026-07-16-sprint-11-config-as-code.md) — campaign definitions become git-committable JSON, delivering the "config-as-code: JSON export/import of definitions + CLI (also the Strapi-exit migration path)" v1.x slice:

  • Slugs everywhere: achievements, offers, and timed events gain required slugs (rewards/placements already had them) with the reward-pattern uniqueness lifecycles — the stable, instance-portable identity that files carry instead of documentIds
  • Export: GET /config-plane/projects/:id/export emits the formatVersion: 1 file exactly (slug-sorted for determinism, explicit nulls, slug cross-refs); any legacy row missing a slug fails loudly with EVERY offender named
  • Import: POST /config-plane/projects/:id/import — slug-matched upsert with field-level diffing (idempotent), opt-in prune, dryRun returning the plan with zero writes; every write goes through the Document Service so all Sprint 8–10 lifecycle validations fire; mid-run rejection returns 422 with a plan RECOMPUTED from the database, never the intended one
  • CLI: new MIT @promocean/cli (promocean export|import) — secret via PROMOCEAN_CONFIG_SECRET env only; exit code 2 on dry-run-found-changes makes promocean import --dry-run a zero-tooling CI drift check
  • Proven flows: config-sync e2e drives the CLI as a real subprocess against the compose stack; cross-instance simulation hand-verified (export → import into an empty project → re-export → diff identical); nine-scenario live import-verification script checked in (pnpm --filter cms verify:config-sync)

Behavior notes for existing consumers

  • Three content types gain a required slug — existing dev volumes need a reseed (docker compose --profile stack down -v) or manual slug backfill; the export endpoint's loud failure names every offender.
  • The config plane gains its first WRITE endpoint — same x-config-secret trust model, operator-only. This is a posture change from the previously read-only plane.
  • cms Node support floor raised to 20.19 — the cms now require()s the ESM-only @promocean/contracts (its first coupling to a workspace package); runtime images and CI are already Node 22.
  • Prune-time cross-reference strictness: with --prune, an import is rejected up front (400, before any write) if a kept offer references a placement/timed event the file omits — that target would be deleted, orphaning the offer. (Documented in both READMEs; a deliberate tightening of the spec's cross-ref rule.)
  • Runtime surfaces untouched: no changes under packages/core, adapter-db, adapter-strapi, sdk, widgets, or apps/api — slugs exist for the config plane only; adapters strip the additive field.
  • Runtime-history caveat: update-in-place preserves documentIds; prune + recreate gets new ones — continuity across delete/recreate is explicitly not promised.
  • CI workflow change: the e2e job now builds @promocean/cli on the runner before Playwright (the spec drives the real binary).

Test plan

  • Full turbo 29–33/33 green (contracts 93, cli 27 incl. a real tmp-symlink bin regression test, existing suites untouched)
  • Fresh-volume compose stack: 9/9 Playwright specs green incl. the new config-sync loop (export → scripted edit → dry-run exit 2 with the exact plan → import → api-visible within the 30s TTL → re-import all-unchanged)
  • Live: nine-scenario verify-config-sync (round-trip invariant, dry-run≡apply, prune bounds incl. webhook survival, 422 recompute vs DB state, prune-ref guard both directions) — 0 findings
  • Cross-instance diff transcript in the task report

Review notes

Six tasks through the full gate cycle (three task fix waves: contracts null-coverage, cms verification-script gap, export gate-before-sort with differential live repro) plus the final whole-branch review — verdict ready to merge after fix wave 43c5ed1, which closed two Criticals the whole-branch read caught: the npm-installed promocean bin silently no-oping (isMain vs bin symlinks — now realpath-based with a symlink subprocess regression test) and the CI e2e job never building the CLI. Also tightened configFileSchema (pointRules keys / registeredEventTypes / duplicate slugs) while formatVersion: 1 is still free to change. Task 6 additionally caught the cms Docker image missing contracts/dist and export nondeterminism. Follow-up issue filed alongside this PR: config-plane polish bundle.

One v1.x roadmap item remains: the React Native SDK.

🤖 Generated with Claude Code

hynding and others added 12 commits July 16, 2026 00:21
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ync verification script

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sort

sortBySlug's compare called slug.localeCompare unconditionally, which throws
on a null slug. Since sortBySlug ran before the findings gate, a legacy
slug === null row — exactly the case the gate exists to catch — crashed the
sort into a generic 500 instead of the designed
{ error: 'unexported definitions missing slugs', findings: [...] } response.
Move the sort after the gate (every slug is a non-null string by then) and
make the compare null-safe as defense in depth.
…nes floor, prune ref guard, cli polish

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hynding
hynding merged commit dbf3fe5 into main Jul 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant