How Openship Detects Monorepos and Performs Selective Rebuilds on Push
Openship automatically detects monorepo projects by scanning workspace configuration files and only rebuilds the specific services that changed when a Git push arrives.
Managing large monorepos requires intelligent build systems that avoid redundant work. In the oblien/openship repository, the platform implements a three-stage pipeline that detects monorepo structures, calculates precise change sets from Git webhooks, and executes selective rebuilds. This architecture ensures that only modified sub-applications trigger new deployments, dramatically reducing CI time and resource consumption.
How Monorepo Detection Works
Openship identifies monorepo projects during the initial configuration parsing phase by looking for specific workspace markers across multiple language ecosystems.
Workspace Detector Registry
The detection logic starts in packages/core/src/workspaces/types.ts, which maintains a registry of workspace detectors. Each detector maps a language or toolchain to a function that returns a boolean indicating whether the repository uses that workspace system.
The scanner checks for the following configuration files to determine the monorepo type:
- JavaScript/TypeScript:
pnpm-workspace.yaml,lerna.json, or Yarn workspace settings inpackage.json - Go:
go.workfiles - Rust:
cargoworkspace definitions
When a push lands, the system initializes the appropriate detector based on the repository contents.
Parsing the Monorepo Configuration
The parseMonorepo() function in packages/core/src/openship-config/parse.ts (line 402) constructs an OpenshipMonorepo object containing:
- The list of sub-applications (
monorepoApps) - The root directory path
- Shared-path globs (
monorepoSharedPaths) that apply to multiple services
This object becomes the source of truth for all subsequent build decisions.
Calculating the Change Set on Push
When GitHub delivers a push webhook, Openship calculates exactly which services need rebuilding by comparing changed files against the monorepo structure.
Webhook Processing and File Changes
Every push triggers a webhook stored in the webhook_delivery table defined in packages/db/src/schema/webhook-delivery.ts. The webhook handler in packages/dashboard/src/lib/api/deploy.ts extracts the list of changed files from the payload.
The system then applies two rules to determine impact:
- Direct path matching: Files within a sub-application's directory trigger that specific service
- Shared path matching: Files matching
monorepoSharedPathsglobs trigger all dependent services
Shared Path Matching Logic
The serviceKind helper in packages/dashboard/src/lib/api/services.ts (line 15) and the deployment planner use the monorepoSharedPaths configuration defined in packages/db/src/schema/project.ts (line 260) to classify changes.
If a changed file matches a sub-app's defined path or matches a shared glob pattern, that sub-app is added to the changed-apps set. Files outside these boundaries are ignored, preventing unnecessary builds.
The Selective Rebuild Pipeline
Once the change set is determined, the build pipeline optimizes resource usage by rebuilding only the flagged services while reusing shared preparation steps.
Build Pipeline Orchestration
The packages/adapters/src/runtime/build-pipeline.ts (line 304) receives the filtered list of changed applications. For each entry, it constructs a Docker image or bare-mode binary. Unchanged applications retain their existing images, eliminating redundant compilation.
Docker Build Context Reuse
The docker-build-context.ts module (line 288) implements a critical optimization: it executes the prepare-source-tree step exactly once for the entire monorepo. After preparation, it runs each changed app's build in its own isolated context. This approach avoids the overhead of re-cloning or re-installing dependencies for every sub-service.
Service Routing Updates
After successful builds, service-routing.ts updates only the reverse-proxy entries for rebuilt services. Unchanged routes remain untouched, ensuring zero-downtime deployments for stable services while new traffic routes to updated containers.
End-to-End Implementation Example
The following TypeScript demonstrates the core detection and filtering logic:
// Detecting a monorepo workspace
import { detectWorkspace } from '@openship/core/workspaces';
const isMonorepo = detectWorkspace(repoRoot, 'pnpm');
// Returns true if pnpm-workspace.yaml is present
// Computing changed applications in a webhook handler
import { getChangedFiles } from '@openship/api/webhooks';
import { matchSharedPaths } from '@openship/db/project';
const changed = getChangedFiles(payload);
const appsToBuild = monorepoApps.filter(app =>
changed.some(file =>
file.startsWith(app.path) ||
matchSharedPaths(file, project.monorepoSharedPaths)
)
);
You can also trigger selective rebuilds via CLI:
openship deploy \
--project my-monorepo \
--only $(git diff --name-only HEAD~1 HEAD | grep '^packages/web/')
Summary
- Monorepo detection relies on a registry in
packages/core/src/workspaces/types.tsthat scans for workspace configuration files across ecosystems like pnpm, Lerna, Go workspaces, and Cargo. - Change calculation uses
monorepoSharedPathsglobs frompackages/db/src/schema/project.tsto determine whether a file change affects a specific sub-app or the shared infrastructure. - Selective rebuilding is orchestrated by
packages/adapters/src/runtime/build-pipeline.ts, which rebuilds only changed services whiledocker-build-context.tsreuses the prepare step across all builds. - Routing isolation ensures that only updated services receive new proxy entries, leaving stable services untouched during deployment.
Frequently Asked Questions
How does Openship determine if a repository is a monorepo?
Openship checks for workspace configuration files such as pnpm-workspace.yaml, lerna.json, go.work, or cargo workspace definitions. The detectWorkspace function in packages/core/src/workspaces/types.ts returns a boolean based on the presence of these files, and parseMonorepo() in packages/core/src/openship-config/parse.ts constructs the OpenshipMonorepo object containing all sub-applications.
What happens when a file in a shared directory changes?
When a changed file matches the monorepoSharedPaths glob patterns defined in the project schema, Openship flags all dependent sub-applications for rebuilding. This ensures that updates to shared libraries or configuration files trigger builds for every service that relies on them, maintaining consistency across the deployment.
Does Openship support all package managers for monorepo detection?
Openship supports the major JavaScript package managers including pnpm, Yarn, and npm (via Lerna), as well as Go modules and Rust Cargo workspaces. The detector registry in packages/core/src/workspaces/types.ts maps each tool to its specific detection logic, making it extensible for additional ecosystems.
How does selective rebuilding reduce resource consumption?
By parsing the Git webhook payload to identify exact file changes and matching them against sub-application paths, Openship builds only the Docker images or binaries for services that actually changed. The docker-build-context.ts module further optimizes this by running the expensive prepare-source-tree step once per monorepo rather than once per service, significantly reducing CPU time and storage usage.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →