Skip to content

fix: load orchestrator through the plugin mechanism, not a static import - #121

Merged
frostebite merged 1 commit into
mainfrom
fix/orchestrator-dynamic-plugin-load
Aug 24, 2026
Merged

frostebite merged 1 commit into
mainfrom
fix/orchestrator-dynamic-plugin-load

Conversation

@frostebite

Copy link
Copy Markdown
Member

Summary

src/cli.ts (core) statically imported orchestratorPlugin straight from plugins/orchestrator's private internals via a relative path (../plugins/orchestrator/src/cli-plugin/index.ts) to make orchestrate work without requiring --plugin @game-ci/orchestrator-plugin. That's core depending directly on a plugin's source at compile time — every other plugin (including this same orchestrator, when loaded explicitly via --plugin) goes through PluginLoader's dynamic import instead, resolved by public package name, never a relative path into internals. This came up while reviewing the repo's own architecture boundary: plugins must never be a direct/compile-time dependency of core, only loaded "using a mechanism."

What changed

Orchestrator now goes through the same PluginLoader.load() mechanism, by its public package name (@game-ci/orchestrator/cli-plugin) — it's just always in the default load list, alongside unity/godot/unreal. "Built-in" now means "always loaded", not "statically imported".

Two things had to be fixed to make this work correctly, not just compile:

  1. Root package.json never declared a dependency on @game-ci/orchestrator at all — which is why the code had to fall back to a raw relative path in the first place; there was no package-manager-created symlink to resolve the name through. Added it as a real workspace dependency ("@game-ci/orchestrator": "workspace:*").
  2. @game-ci/orchestrator's package.json "exports" pointed only at its compiled dist/ output. Today's static import bundled straight from orchestrator's TypeScript source with zero separate build step — switching to the published dist/ path would have silently introduced a new build-order dependency (plugins/orchestrator's own tsc build would need to run before the root CLI's bun build) that isn't wired into any script or the release workflow today. Instead, added a "bun" export condition pointing at the TypeScript source directly — Bun's module resolver picks this over "default" automatically, so bundling behavior is unchanged (verified byte-identical: 1756 modules, 10.52MB, both with and without this fix, with zero pre-built orchestrator dist/ present) while non-Bun consumers of the published package still resolve to compiled dist/ as before.

Verification

  • Functional, not just structural: orchestrate <path> --targetPlatform=... --providerStrategy=local produces byte-identical output on both the original static-import code and this fix (down to the exact same downstream git error for a nonexistent project path) — confirms the plugin's actual command/option wiring is unaffected, not just that it type-checks.
  • tsc --noEmit: 736 errors vs 737 on baseline (one fewer — removed a .ts-extension-import error, not a new one).
  • bun test ./src: 199 pass, 0 fail, including a new regression test confirming orchestrator is registered via PluginRegistry during default setup(), and the existing "does not register built-in plugins more than once" test still passes for it.
  • bun run build: succeeds, byte-identical module count/bundle size to baseline.
  • oxfmt --check: clean.

Test plan

  • New test: orchestrator is registered by name ('orchestrator') via PluginRegistry during default Cli.setup(), with no --plugin flag needed
  • Existing "does not register built-in plugins more than once" test still covers orchestrator's dedup behavior
  • Manual functional comparison: identical CLI output for a real orchestrate invocation, before and after
  • tsc --noEmit / oxfmt --check clean
  • bun test ./src passes
  • bun run build succeeds with unchanged bundle size (confirms no new build-order dependency was introduced)

src/cli.ts (core) statically imported orchestratorPlugin straight from
plugins/orchestrator's private internals via a relative path
(../plugins/orchestrator/src/cli-plugin/index.ts) to make orchestrate
work without requiring --plugin @game-ci/orchestrator-plugin. That's
core depending directly on a plugin's source at compile time - every
other plugin (including this same orchestrator, when loaded
explicitly via --plugin) goes through PluginLoader's dynamic import
instead, resolved by public package name, never a relative path into
internals.

Orchestrator now goes through the same PluginLoader.load() mechanism,
by its public package name (@game-ci/orchestrator/cli-plugin) - it's
just always in the default load list, alongside unity/godot/unreal.
"Built-in" now means "always loaded", not "statically imported".

Two things had to be fixed to make this work correctly, not just
compile:

1. Root package.json never declared a dependency on @game-ci/orchestrator
   at all - which is *why* the code had to fall back to a raw relative
   path in the first place; there was no package-manager-created
   symlink to resolve the name through. Added it as a real workspace
   dependency.

2. @game-ci/orchestrator's package.json "exports" pointed only at its
   compiled dist/ output. Today's static import bundled straight from
   orchestrator's TypeScript source with zero separate build step -
   switching to the published dist/ path would have silently introduced
   a new build-order dependency (plugins/orchestrator's own `tsc` build
   would need to run before the root CLI's `bun build`) that isn't wired
   into any script or the release workflow today. Instead, added a
   "bun" export condition pointing at the TypeScript source directly -
   Bun's module resolver picks this over "default" automatically, so
   bundling behavior is unchanged (verified byte-identical: 1756
   modules, 10.52MB, both with and without this fix, with zero
   pre-built orchestrator dist/ present) while non-Bun consumers of the
   published package still resolve to compiled dist/ as before.

Verified functionally, not just structurally: `orchestrate <path>
--targetPlatform=... --providerStrategy=local` produces byte-identical
output on both the original static-import code and this fix (down to
the exact same downstream git error for a nonexistent project path) -
confirms the plugin's actual command/option wiring is unaffected, not
just that it type-checks.

Verification:
- tsc --noEmit: 736 errors vs 737 on baseline (one fewer - removed a
  .ts-extension-import error, not a new one).
- bun test ./src: 199 pass, 0 fail, including a new regression test
  confirming orchestrator is registered via PluginRegistry during
  default setup() and the existing "does not register built-in
  plugins more than once" test still passes for it.
- bun run build: succeeds, byte-identical module count/bundle size to
  baseline.
- oxfmt --check: clean.
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 41 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bc331ff9-c521-4f3a-94d0-415669e695a5

📥 Commits

Reviewing files that changed from the base of the PR and between 49c33e4 and 711c7b3.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • package.json
  • plugins/orchestrator/package.json
  • src/cli.test.ts
  • src/cli.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@frostebite
frostebite merged commit a5afb6f into main Aug 24, 2026
23 checks passed
@frostebite
frostebite deleted the fix/orchestrator-dynamic-plugin-load branch August 24, 2026 17:38
frostebite added a commit that referenced this pull request Aug 24, 2026
New plugins/steam-deploy package, thin-wrapper-migrated from a real,
production Steam deployment action (deploy-to-steam/action.yml,
~1,070 lines) rather than reimplemented from scratch. Only the
genuinely portable Steam-domain logic was ported - deliberately
excluded everything gameclient-private:

Ported (real Steam-domain logic, generically useful to any studio):
- VDF generation (app build manifest + depot definition), including
  the file-exclusion list reflecting real hard-won Unity-build
  knowledge (Burst debug info, backup folders that shouldn't ship).
- Local-vs-Docker steamcmd execution, with Steam config dir mounting
  for auth persistence in Docker mode.
- SteamCMD's output-parsing success/failure heuristic: exit code alone
  isn't reliable (a dropped connection or depot failure can still
  exit 0), so this reads "Successfully finished" / BuildID / known
  error signatures from the actual output text, exactly as the
  production script does.

Deliberately NOT ported (gameclient-private, does not belong in an
open-source plugin):
- Project-name auto-detection from path string matching and
  hardcoded per-project Steam AppIDs.
- ProfileLoader.ps1/frameworks.yml integration.
- A custom git-checkout-with-broker-token fallback (unrelated to
  Steam deployment anyway).
- A step posting build metadata to platform.frostebite.com, a private
  internal dashboard.
- Hardcoded drive-letter/folder-convention build-path discovery -
  replaced with an explicit --buildPath argument.

Command: `game-ci deploy steam <buildPath> --appId --depotId [--branch]
[--mode] [--steamCmdPath] [--steamConfigDir] [--extraExclusions]`.
STEAM_USERNAME/STEAM_PASSWORD read from environment only, never CLI
arguments (argv can leak through process listings).

Two small, generic (non-Steam-specific) extensions to the plugin
system were needed, since `deploy` is the first command with no
associated engine:
- PluginRegistry.createCommand now checks commandPlugins registered
  with engine: '*' after exact-engine matches, mirroring
  configureOptions' existing '*' handling for options plugins.
- CommandFactory special-cases `deploy` to skip engine detection
  entirely (same pattern already used for build-unity-image) - a
  deploy target's contents don't carry Unity/Godot/Unreal project
  markers for detectEngine() to find.
- cli.ts's registerCommand middleware folds yargs' named `target`
  positional (from `deploy <target> [buildPath]`) back into the
  command array passed to CommandFactory, since yargs only puts
  *undeclared* trailing tokens into `_` - a named positional never
  lands there. Caught via a real functional smoke test (not just
  types/unit tests): `deploy steam <path> --appId=... --depotId=...`
  initially failed with "Unknown arguments: appId, depotId" because
  configureOptions was silently never reached; fixed, then reverified
  with the same command end-to-end (VDF files written with correct
  paths, fails at the expected final step - no steamcmd installed on
  this dev machine).

steam-deploy itself is loaded exactly like orchestrator - via
PluginLoader.load('@game-ci/steam-deploy'), never a static import -
matching the app/plugin boundary fixed in #121, not repeating that
mistake for a second plugin.

Verification:
- tsc --noEmit: 737 errors, matching baseline exactly (confirmed via
  git stash -u comparison, correctly including the new untracked
  plugin directory in the baseline).
- plugins/steam-deploy's own tsc --noEmit: clean.
- bun test ./src: 202 pass, 0 fail (was 199 before this commit),
  including a new integration test confirming steam-deploy loads via
  PluginLoader and `deploy steam` resolves without engine detection.
- plugins/steam-deploy's own vitest: 9/9 pass (VDF generation,
  SteamCMD output-parsing heuristic - both the pure, most reusable,
  most valuable logic from the original script).
- bun run build: succeeds.
- Real functional smoke test end-to-end (see above).
- oxfmt --check: clean.
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