Skip to content

feat: support manifest for local development - #2892

Merged
Noroth merged 11 commits into
mainfrom
ludwig/eng-9620-local-split-engine-config-files-for-router
May 29, 2026
Merged

feat: support manifest for local development#2892
Noroth merged 11 commits into
mainfrom
ludwig/eng-9620-local-split-engine-config-files-for-router

Conversation

@Noroth

@Noroth Noroth commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Router can load execution config from a local manifest directory and optionally hot-reload it; supports skipping missing or ignoring specific feature flags and configurable watch intervals.
  • Chores

    • Added tooling/CLI to generate manifest directory layouts and Make targets to run them.
    • Updated ignore rules to exclude manifest directories.
  • Documentation

    • Added docs and examples for manifest-based config and hot-reload behavior.
  • Tests

    • Added tests covering manifest hot-reload scenarios.

Review Change Stack

Checklist

Open Source AI Manifesto

This project follows the principles of the Open Source AI Manifesto. Please ensure your contribution aligns with its principles.

@Noroth
Noroth requested review from a team as code owners May 26, 2026 12:53

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Router now loads and hot-reloads execution configuration from a "manifest" directory (split into mapper.json and per-feature-flag configs) as an alternative to single-file configs. Changes include schema updates, router wiring and assembly helpers, runtime watchers, demo tooling, docs, and integration tests.

Changes

Manifest Config Loading and Hot-Reload

Layer / File(s) Summary
Configuration schema and data contracts
router/pkg/config/config.go, router/pkg/config/config.schema.json, router/pkg/config/json_schema.go, router/pkg/config/testdata/config_*.json, router/pkg/config/config_test.go
New ExecutionConfigManifest struct embeds into ExecutionConfig with manifest path, skip/ignore feature-flag controls, and watch settings. JSON schema adds directory-path validation format and manifest as an execution-config loading alternative with required path and optional feature-flag/watch options; validation tests and defaults updated.
Router configuration wiring and manifest option
router/core/router.go, router/core/router_config.go, router/core/supervisor_instance.go
ManifestConfig struct and WithManifestConfig option introduced; Config gains manifestConfig field; Usage() reports manifest status; supervisor's optionFromExecutionConfig helper routes between WithExecutionConfig and WithManifestConfig; bootstrap assembles static config from manifest when configured.
Manifest utilities and config assembly
router/pkg/routerconfig/routerconfig.go
Adds IsValidManifestPath, ReadMapperFile, AssembleConfigRules, and AssembleConfig to validate a manifest directory and build a *nodev1.RouterConfig from latest.json plus per-feature feature-flags/*.json, honoring skip/ignored rules.
Router bootstrap and runtime hot-reload
router/core/router.go, router/pkg/watcher/watcher.go
startWithStaticExecutionConfig centralizes static-config startup, PQL startup, and readiness, and conditionally wires file watchers for execution config file and manifest directory; manifest watcher re-assembles config on mapper.json changes. WatcherFunc type formalized for watcher callbacks.
Demo manifest generation tooling
demo/Makefile, demo/build_manifest.py, demo/.gitignore, router-tests/testenv/testdata/manifest/mapper.json
ManifestBuilder Python class reads combined router config JSON and splits it into manifest directory layout (mapper.json, latest.json, feature-flags/<name>.json); Makefile targets invoke the script for local and integration testdata; demo .gitignore and test fixture added.
Integration testing for manifest hot-reload
router-tests/protocol/config_hot_reload_test.go
TestConfigHotReloadManifest validates watch-enabled/disabled reload, concurrent traffic preservation, feature-flag additions, and SkipMissingFeatureFlags behavior; helper functions create test manifest directory structure on disk; minor test refactors applied.
Documentation
docs-website/router/configuration.mdx
Docs updated with a Local manifest YAML example, manifest directory layout description, hot-reload semantics, and env-var mapping table additions.
Repository housekeeping
demo/.gitignore, router/.gitignore
Manifest build output directories and plugin manifest schema directory ignored.

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • wundergraph/cosmo#2844: Split-config poller implementation that pairs with this manifest-assembly PR, providing the upstream dependency for mapper-based feature-flag config aggregation and skip/ignore controls.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support manifest for local development' accurately reflects the main changeset, which introduces manifest directory support for local development workflows in the router.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown

Router image scan passed

✅ No security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-e9f11f20bf814b1b881416823ed786c5ff54e42c

@codecov

codecov Bot commented May 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 54.54545% with 95 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.18%. Comparing base (42b307b) to head (a8fc3f5).

Files with missing lines Patch % Lines
router/pkg/routerconfig/routerconfig.go 51.42% 19 Missing and 15 partials ⚠️
router/core/router.go 68.57% 19 Missing and 14 partials ⚠️
router/core/supervisor_instance.go 0.00% 21 Missing ⚠️
router/pkg/config/json_schema.go 36.36% 7 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2892       +/-   ##
===========================================
+ Coverage   40.86%   66.18%   +25.32%     
===========================================
  Files        1037      258      -779     
  Lines      131332    27221   -104111     
  Branches     6176        0     -6176     
===========================================
- Hits        53672    18017    -35655     
+ Misses      75914     7769    -68145     
+ Partials     1746     1435      -311     
Files with missing lines Coverage Δ
router/core/router_config.go 93.82% <100.00%> (+0.03%) ⬆️
router/pkg/config/config.go 81.92% <ø> (ø)
router/pkg/watcher/watcher.go 94.00% <100.00%> (ø)
router/pkg/config/json_schema.go 61.91% <36.36%> (-0.91%) ⬇️
router/core/supervisor_instance.go 0.00% <0.00%> (ø)
router/core/router.go 70.38% <68.57%> (-0.08%) ⬇️
router/pkg/routerconfig/routerconfig.go 55.26% <51.42%> (-44.74%) ⬇️

... and 782 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
router/pkg/config/config.go (1)

1049-1050: ⚡ Quick win

Namespace manifest env vars consistently with the other manifest fields.

SKIP_MISSING_FEATURE_FLAGS / IGNORED_FEATURE_FLAGS are currently unscoped, while the rest of this block uses EXECUTION_CONFIG_MANIFEST_*. Scoping these two as well improves predictability and avoids accidental global env overrides.

Proposed diff
 type ExecutionConfigManifest struct {
 	Path                    string        `yaml:"path,omitempty" env:"EXECUTION_CONFIG_MANIFEST_PATH"`
-	SkipMissingFeatureFlags bool          `yaml:"skip_missing_feature_flags" envDefault:"false" env:"SKIP_MISSING_FEATURE_FLAGS"`
-	IgnoredFeatureFlags     []string      `yaml:"ignored_feature_flags,omitempty" env:"IGNORED_FEATURE_FLAGS"`
+	SkipMissingFeatureFlags bool          `yaml:"skip_missing_feature_flags" envDefault:"false" env:"EXECUTION_CONFIG_MANIFEST_SKIP_MISSING_FEATURE_FLAGS"`
+	IgnoredFeatureFlags     []string      `yaml:"ignored_feature_flags,omitempty" env:"EXECUTION_CONFIG_MANIFEST_IGNORED_FEATURE_FLAGS"`
 	Watch                   bool          `yaml:"watch,omitempty" envDefault:"false" env:"EXECUTION_CONFIG_MANIFEST_WATCH"`
 	WatchInterval           time.Duration `yaml:"watch_interval,omitempty" envDefault:"1s" env:"EXECUTION_CONFIG_MANIFEST_WATCH_INTERVAL"`
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/pkg/config/config.go` around lines 1049 - 1050, The two struct fields
SkipMissingFeatureFlags and IgnoredFeatureFlags use unscoped env tags; update
their `env` tags to match the rest of the manifest block by prefixing with
EXECUTION_CONFIG_MANIFEST_, i.e. change env:"SKIP_MISSING_FEATURE_FLAGS" to
env:"EXECUTION_CONFIG_MANIFEST_SKIP_MISSING_FEATURE_FLAGS" and
env:"IGNORED_FEATURE_FLAGS" to
env:"EXECUTION_CONFIG_MANIFEST_IGNORED_FEATURE_FLAGS" on the
SkipMissingFeatureFlags and IgnoredFeatureFlags fields respectively (leave the
yaml tags and envDefault values as-is).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@demo/build_manifest.py`:
- Around line 146-153: The mapper.json is written before other config files
which can trigger the manifest watcher on an incomplete state; change the write
order so that self.latest_config (and any other config files) are written first
via write_content_to_file (referencing latest_path and self.latest_config), then
write mapper.json (mapper_path with self.mapper) last so mapper.json mtime is
updated only after all other configs are persisted.

In `@router-tests/protocol/config_hot_reload_test.go`:
- Around line 515-551: The test uses time.Sleep and an atomic counter (done
atomic.Uint32) with two goroutines calling xEnv.MakeGraphQLRequestOK to wait for
both requests, which is flaky; replace this pattern by installing and using the
SyncReporter (inject into the router at test setup) and the existing wait
helpers (call e.syncReporter() or the appropriate WaitFor* helper) to await both
requests and config reload instead of sleep+atomic, and keep the
writeTestManifest("updated", manifestDir) and the require.EventuallyWithT reload
assertion; remove the atomic done, the two manual goroutines synchronization,
and use SyncReporter/Wait helpers to assert both requests completed and the
config version switched to "updated".
- Around line 970-972: The test currently writes mapper.json before the base
config, causing the watcher to see the mapper mtime update before latest.json
and miss the new manifest; move the writeTestMapper(...) call to run last so
mapper.json is written after writeBaseGraphConfig(...) (and after any
writeTestManifest(...) that updates latest.json), i.e. ensure
writeBaseGraphConfig and/or writeTestManifest are called first and then call
writeTestMapper so the mapper mtime reflects the final state.

In `@router/core/router.go`:
- Around line 1631-1640: The deferred call that sets readiness and emits the
"Server initialized" log (r.httpServer.healthcheck.SetReady(true) and
r.logger.Info(...)) is executed even when subsequent startup steps (watcher
creation/initialization) fail; move or conditionalize readiness/logging so it
only runs after all startup steps succeed: either remove the defer and place the
SetReady(true)/logger.Info(...) at the end of Start after watcher creation
succeeds, or change the defer to check the function's final error (use a named
return err and only set ready/log when err == nil). Update references in Start
that create watchers/initialize components so readiness is set only when those
succeed.
- Around line 1731-1733: The manifest watcher currently only watches
mapper.json; update the Paths slice construction (where Logger: ll, Paths:
[]string{filepath.Join(r.manifestConfig.Path, "mapper.json")}, Interval:
r.manifestConfig.WatchInterval) to include filepath.Join(r.manifestConfig.Path,
"latest.json") and the feature-flags patterns (e.g.
filepath.Join(r.manifestConfig.Path, "feature-flags", "*")) so changes to
latest.json or any file under feature-flags/ will trigger the hot-reload.

In `@router/pkg/routerconfig/routerconfig.go`:
- Line 77: AssembleConfig currently dereferences the incoming rules parameter
without a nil guard which causes a panic for callers passing nil; inside
AssembleConfig check if rules == nil and either return a clear error (e.g.,
fmt.Errorf("rules is nil")) or initialize a default AssembleConfigRules and
continue, ensuring subsequent uses (the code paths that read fields from rules)
never dereference a nil pointer; update AssembleConfig's error return path
accordingly so exported API returns an error instead of panicking.
- Around line 119-129: The code calling fs.ReadFile(fsys, key+".json") currently
only handles os.IsNotExist errors and lets other I/O errors fall through leaving
fileBytes invalid; update the error handling in the ReadFile block so that when
err != nil and os.IsNotExist(err) is false you immediately return the error
(e.g. wrap and return fmt.Errorf("reading feature flag %s: %w", key, err)); keep
the existing branch that continues on missing files when
rules.SkipMissingFeatureFlags is true. Reference: fs.ReadFile, fileBytes, err,
and rules.SkipMissingFeatureFlags.

---

Nitpick comments:
In `@router/pkg/config/config.go`:
- Around line 1049-1050: The two struct fields SkipMissingFeatureFlags and
IgnoredFeatureFlags use unscoped env tags; update their `env` tags to match the
rest of the manifest block by prefixing with EXECUTION_CONFIG_MANIFEST_, i.e.
change env:"SKIP_MISSING_FEATURE_FLAGS" to
env:"EXECUTION_CONFIG_MANIFEST_SKIP_MISSING_FEATURE_FLAGS" and
env:"IGNORED_FEATURE_FLAGS" to
env:"EXECUTION_CONFIG_MANIFEST_IGNORED_FEATURE_FLAGS" on the
SkipMissingFeatureFlags and IgnoredFeatureFlags fields respectively (leave the
yaml tags and envDefault values as-is).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8855fbf-8f2f-4df7-8250-2ca75acc46de

📥 Commits

Reviewing files that changed from the base of the PR and between b96296a and c1303b3.

📒 Files selected for processing (19)
  • demo/.gitignore
  • demo/Makefile
  • demo/build_manifest.py
  • router-tests/protocol/config_hot_reload_test.go
  • router-tests/testenv/testdata/manifest/feature-flags/myff.json
  • router-tests/testenv/testdata/manifest/latest.json
  • router-tests/testenv/testdata/manifest/mapper.json
  • router/.gitignore
  • router/core/router.go
  • router/core/router_config.go
  • router/core/supervisor_instance.go
  • router/pkg/config/config.go
  • router/pkg/config/config.schema.json
  • router/pkg/config/config_test.go
  • router/pkg/config/json_schema.go
  • router/pkg/config/testdata/config_defaults.json
  • router/pkg/config/testdata/config_full.json
  • router/pkg/routerconfig/routerconfig.go
  • router/pkg/watcher/watcher.go

Comment thread demo/build_manifest.py Outdated
Comment thread router-tests/protocol/config_hot_reload_test.go Outdated
Comment thread router-tests/protocol/config_hot_reload_test.go Outdated
Comment thread router/core/router.go Outdated
Comment thread router/core/router.go
Comment thread router/pkg/routerconfig/routerconfig.go Outdated
Comment thread router/pkg/routerconfig/routerconfig.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs-website/router/configuration.mdx`:
- Line 1266: The sentence combines multiple clauses and has a typo: change the
verb "stats" to "watches" and split into two shorter sentences—one describing
what the manifest directory contains (mentioning mapper.json, latest.json, and
feature-flags/<name>.json) and a second stating that when watch is enabled the
router watches mapper.json and reloads the assembled config without downtime
whenever the file's mtime changes; update the sentence referencing "watch" and
"mapper.json" accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 029630ee-0e7b-4d2d-ae9c-dc48c6787f74

📥 Commits

Reviewing files that changed from the base of the PR and between c1303b3 and 61d84ff.

📒 Files selected for processing (2)
  • docs-website/router/configuration.mdx
  • router/pkg/config/config.go

Comment thread docs-website/router/configuration.mdx Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
router-tests/protocol/config_hot_reload_test.go (1)

132-147: ⚡ Quick win

Prefer sync.WaitGroup.Go for these test goroutines
In router-tests/protocol/config_hot_reload_test.go (around 132-147, 375-390, 514-529), switch from wg.Add(2) + defer wg.Done() + go func() to wg.Go(func() { ... }) for consistency with the repo’s existing sync.WaitGroup.Go usage (Go 1.25.0).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router-tests/protocol/config_hot_reload_test.go` around lines 132 - 147,
Replace the manual goroutine pattern (wg.Add(2) + go func() { defer wg.Done();
... }) with the repo's sync.WaitGroup.Go helper: remove wg.Add and defer
wg.Done, and call wg.Go(func() { ... }) for each concurrent test routine; apply
this change to the blocks that call MakeGraphQLRequestOK and assert on
RouterConfigVersionMain()/testutils.EmployeesIDData so the goroutines use wg.Go
consistently (symbols to update: wg, wg.Go, MakeGraphQLRequestOK,
RouterConfigVersionMain, testutils.EmployeesIDData).
router/pkg/routerconfig/routerconfig.go (1)

41-41: 💤 Low value

Use filepath.Join for path construction.

String concatenation with / is not portable. The rest of this file uses filepath.Join consistently (lines 82, 94).

♻️ Suggested fix
-	mapperStat, err := os.Lstat(path + "/mapper.json")
+	mapperStat, err := os.Lstat(filepath.Join(path, "mapper.json"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@router/pkg/routerconfig/routerconfig.go` at line 41, Replace the
string-concatenation path used when calling os.Lstat on "mapper.json" with
filepath.Join to follow the file's existing portable path handling; locate the
call to os.Lstat(path + "/mapper.json") (referencing mapperStat and err) and
change it to build the target file path via filepath.Join(path, "mapper.json")
before passing it to os.Lstat so it matches other uses like the calls around
lines using filepath.Join.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@router-tests/protocol/config_hot_reload_test.go`:
- Around line 132-147: Replace the manual goroutine pattern (wg.Add(2) + go
func() { defer wg.Done(); ... }) with the repo's sync.WaitGroup.Go helper:
remove wg.Add and defer wg.Done, and call wg.Go(func() { ... }) for each
concurrent test routine; apply this change to the blocks that call
MakeGraphQLRequestOK and assert on
RouterConfigVersionMain()/testutils.EmployeesIDData so the goroutines use wg.Go
consistently (symbols to update: wg, wg.Go, MakeGraphQLRequestOK,
RouterConfigVersionMain, testutils.EmployeesIDData).

In `@router/pkg/routerconfig/routerconfig.go`:
- Line 41: Replace the string-concatenation path used when calling os.Lstat on
"mapper.json" with filepath.Join to follow the file's existing portable path
handling; locate the call to os.Lstat(path + "/mapper.json") (referencing
mapperStat and err) and change it to build the target file path via
filepath.Join(path, "mapper.json") before passing it to os.Lstat so it matches
other uses like the calls around lines using filepath.Join.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2182c885-3224-479d-b8c4-eac9be27d139

📥 Commits

Reviewing files that changed from the base of the PR and between 61d84ff and 3d96ad3.

📒 Files selected for processing (5)
  • demo/build_manifest.py
  • docs-website/router/configuration.mdx
  • router-tests/protocol/config_hot_reload_test.go
  • router/core/router.go
  • router/pkg/routerconfig/routerconfig.go

@dkorittki dkorittki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with some minor improvement ideas, feel free to implement them at will.

In general I feel a bit confused about the manifest name for the new config type. One is called "static execution config" and the other is called "manifest execution config". To me both are static and manifests, so these do not distinguish them for me. Maybe we can elaborate on the name again or describe it somewhere central in a godoc.

Also it would be great to have a place in our docs where we explain this concept to users. Currently we only have the new parameters listed on the routers config page.

Comment thread router/pkg/routerconfig/routerconfig.go Outdated
Comment thread router/pkg/routerconfig/routerconfig.go
Comment thread router/core/router.go Outdated
Comment thread router/pkg/config/config.go
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

Actionable comments posted: 0

@Noroth
Noroth merged commit b852f91 into main May 29, 2026
40 checks passed
@Noroth
Noroth deleted the ludwig/eng-9620-local-split-engine-config-files-for-router branch May 29, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants