Skip to main content
Version: v4 (current)

Monorepo Support

Orchestrator provides first-class support for Unity monorepos — single repositories that contain multiple products sharing engine code, submodules, and build configuration.

Overview

A Unity monorepo typically contains:

  • A shared engine layer (Assets/_Engine/) used by all products
  • Game-specific code separated into submodules (Assets/_Game/Submodules/)
  • Per-product build configuration (build methods, target platforms, Steam IDs)
  • Shared tooling, CI workflows, and automation scripts

The challenge is that different products need different subsets of the repository. A build of Product A should not initialize submodules that only Product B needs. A server build should not pull in client-only assets. A CI run for a hotfix should not reimport assets belonging to an unrelated product.

Orchestrator addresses this through a profile system that controls submodule initialization per product and build type, combined with per-product framework configuration.

Submodule Profile System

Profiles are YAML files that declare which submodules to initialize for a given product and context. Each submodule entry specifies either branch: main (initialize) or branch: empty (skip).

Profile Format

primary_submodule: MyGameFramework
submodules:
- name: CoreFramework
branch: main
- name: OptionalModule
branch: empty
- name: Plugins*
branch: main

primary_submodule identifies the main game submodule — the orchestrator uses this as the root of the build context.

Each entry under submodules names a submodule (or a glob pattern matching multiple submodules) and declares whether it should be initialized. Submodules marked branch: empty are never touched during initialization, regardless of what other profiles or defaults specify.

Glob Pattern Support

Glob patterns match multiple submodules with a single entry. This is useful for plugin groups that always travel together:

submodules:
- name: Plugins*
branch: main # initializes PluginsCore, PluginsAudio, PluginsLocalization, etc.
- name: ThirdParty*
branch: empty # skips all ThirdParty submodules

Patterns are matched in order. The first matching entry wins, so more specific entries should appear before broader patterns.

Variant Overlays

Variants override specific submodule entries from a base profile. A common use case is a server.yml variant that skips client-only assets and adds server-specific submodules:

Base profile (config/submodule-profiles/my-game/ci/profile.yml):

primary_submodule: MyGameFramework
submodules:
- name: CoreFramework
branch: main
- name: ClientUI
branch: main
- name: ServerRuntime
branch: empty

Server variant (config/submodule-profiles/my-game/ci/server.yml):

overrides:
- name: ClientUI
branch: empty # not needed for server builds
- name: ServerRuntime
branch: main # required for server builds

The variant is merged on top of the base profile at initialization time. Only the listed entries are overridden; all others inherit from the base.

Configuration

Specify profiles and variants as action inputs:

- uses: game-ci/unity-builder@v4
with:
submoduleProfilePath: config/submodule-profiles/my-game/ci/profile.yml
submoduleVariantPath: config/submodule-profiles/my-game/ci/server.yml

When submoduleProfilePath is set, orchestrator reads the profile before any git operations and initializes only the listed submodules. Skipped submodules are never cloned, fetched, or touched.

When submoduleVariantPath is also set, it is merged on top of the base profile before initialization begins.

If neither input is set, orchestrator falls back to standard git submodule initialization (all submodules, no filtering).

Multi-Product CI Matrix

A monorepo with multiple products can build all products in parallel using a GitHub Actions matrix. Each matrix entry specifies a different profile, producing independent builds from the same repository checkout.

jobs:
build:
strategy:
matrix:
include:
- product: my-game
profile: config/submodule-profiles/my-game/ci/profile.yml
buildMethod: MyGame.BuildScripts.BuildGame
targetPlatform: StandaloneLinux64
- product: my-game-server
profile: config/submodule-profiles/my-game/ci/profile.yml
variant: config/submodule-profiles/my-game/ci/server.yml
buildMethod: MyGame.BuildScripts.BuildServer
targetPlatform: StandaloneLinux64
- product: my-other-game
profile: config/submodule-profiles/my-other-game/ci/profile.yml
buildMethod: MyOtherGame.BuildScripts.BuildGame
targetPlatform: StandaloneWindows64
steps:
- uses: game-ci/unity-builder@v4
with:
submoduleProfilePath: ${{ matrix.profile }}
submoduleVariantPath: ${{ matrix.variant }}
buildMethod: ${{ matrix.buildMethod }}
targetPlatform: ${{ matrix.targetPlatform }}

Each matrix job initializes only the submodules its profile declares. Products do not interfere with each other's initialization or build output.

Shared Code and Assembly Definitions

Unity Assembly Definitions (.asmdef files) enforce code boundaries within the monorepo. The recommended layout separates shared engine code from game-specific code:

Assets/_Engine/               ← shared engine systems
Physics/
Engine.Physics.asmdef
Rendering/
Engine.Rendering.asmdef

Assets/_Game/
Shared/Code/ ← shared game framework
Services/
Game.Services.asmdef
UI/
Game.UI.asmdef
Submodules/
MyGameFramework/ ← product-specific, in its own submodule
Code/
MyGame.asmdef

Assembly definitions in Assets/_Engine/ reference only engine-level namespaces. Assembly definitions in Assets/_Game/Shared/Code/ may reference engine assemblies. Product-specific assemblies reference both, but never each other across product boundaries.

This structure means Unity can compile and test each product's assembly graph independently, and compile errors in one product do not block builds of another.

Framework Configuration

Per-product build configuration belongs in a central frameworks file. This is the single source of truth for Steam App IDs, build methods, and supported platforms per product:

# config/frameworks.yml
frameworks:
my-game:
steamAppId: 123456
buildMethod: MyGame.BuildScripts.BuildGame
platforms:
- StandaloneLinux64
- StandaloneWindows64
- StandaloneOSX
my-game-server:
steamAppId: 123457
buildMethod: MyGame.BuildScripts.BuildServer
platforms:
- StandaloneLinux64
my-other-game:
steamAppId: 789012
buildMethod: MyOtherGame.BuildScripts.BuildGame
platforms:
- StandaloneWindows64
- WebGL

CI workflows read from this file to populate matrix entries and avoid duplicating product-specific values across workflow files. A single change to frameworks.yml propagates to all workflows that reference it.

Best Practices

Keep profiles small. A profile should list only what a build actually requires. Err toward branch: empty for anything that is not clearly needed. Unnecessary submodule initialization adds clone time and increases the chance of conflicts.

Use glob patterns for plugin groups. Plugins that always travel together (a vendor SDK split across three submodules, for example) should share a naming prefix so a single glob entry controls them all. This reduces profile maintenance as new plugins are added.

Use variant overlays for build-type differences. Do not create separate base profiles for client and server builds. Start with a shared base profile and apply a server variant on top. This keeps the diff between build types explicit and minimizes duplication.

Validate profiles in CI. Add a profile validation step that checks every submodule named in a profile actually exists in .gitmodules. This catches typos and stale entries before they cause initialization failures on build agents.

- name: Validate submodule profiles
run: |
.\automation\ValidateSubmoduleProfiles.ps1 \
-ProfileDir config/submodule-profiles \
-GitmodulesPath .gitmodules
shell: powershell

Document the profile per product. Each product's profile directory should contain a brief README explaining which submodules are in scope, which are intentionally excluded, and which variant files exist. New contributors should not need to reverse-engineer profile intent from the YAML alone.

Inputs Reference

InputDescription
submoduleProfilePathPath to the profile YAML file controlling submodule initialization
submoduleVariantPathPath to a variant YAML file overlaid on top of the base profile