This guide explains how to turn multiple GitHub repositories into a single monorepo with a clean developer experience, predictable releases, and environment-safe operations. It is written for an intermediate full-stack developer and is based on patterns used in this repository.
Target state:
- One repository with multiple services and shared packages.
- One consistent local environment via Dev Containers.
- One task orchestrator for local workflows using VS Code tasks.
- One dynamic environment switcher (dev/test/canary/stable) backed by 1Password.
- One release channel model with controlled promotion.
- Reusable CI/CD workflows that understand channels.
Anti-goals:
- Do not migrate everything in one giant PR.
- Do not keep per-service custom local setup scripts forever.
- Do not let release branches receive direct random commits.
Use top-level folders for products/services and shared runtime utilities.
Suggested shape:
.
├── .devcontainer/
├── .github/
│ └── workflows/
├── .vscode/
├── api/
├── web/
├── marketing/
├── runner/
├── image-worker/
├── sdk/
├── shared/
├── scripts/
├── envs/
└── tests/
Design notes:
- Keep each deployable app in its own folder with its own package manifest.
- Keep cross-service logic in
shared/(env resolution, constants, helpers). - Keep operational scripts in
scripts/. - Keep environment templates in
envs/.
- List all current repositories, services, and deploy targets.
- Mark ownership and runtime dependencies between services.
- Identify shared code duplicated in multiple repos.
- Create top-level folders for each service and shared code.
- Initialize root-level tooling (lint/test conventions, scripts, tasks).
- Add branch/channel policy docs immediately.
Two practical options:
- Use
git subtreefor each repo to preserve commit history under a subfolder. - Use
git filter-repoto rewrite each repo into a subdirectory, then merge.
Use one approach consistently.
- Make all services runnable from the monorepo root via tasks.
- Add smoke tests that validate core user paths.
- Cut over CI to the monorepo before deleting old pipelines.
- Set old repos to archived/read-only.
- Link all contributors to the monorepo contribution path.
- Keep rollback tags and migration notes for at least one release cycle.
Use Dev Containers as the source of truth for local setup.
Core files and responsibilities:
.devcontainer/devcontainer.json: editor/runtime config, ports, extensions..devcontainer/docker-compose.yml: app container plus dependencies (for example MongoDB, Redis)..devcontainer/post-create.sh: one-time bootstrap of CLI tools and dependencies..devcontainer/run-service.sh: wrapper for long-running services so env refresh/restart is controlled.
Why this matters:
- Every developer gets the same runtime and tools.
- Onboarding time drops sharply.
- Environment drift is minimized.
Practical rules:
- Keep stateful services (DB/queue/cache) in compose.
- Keep app processes started by tasks, not by compose entrypoints.
- Keep post-create script idempotent.
Treat .vscode/tasks.json as your local command plane.
Task taxonomy:
- Service tasks: API, web, worker, docs, runner.
- Infra tasks: tunnels, webhooks, remote helpers.
- Test tasks: service-specific and full-suite runners.
- Admin tasks: environment switching and secret sync.
- Composite tasks: startup/shutdown bundles.
Patterns to copy:
- Use
dependsOnanddependsOrderfor deterministic startup. - Build one top-level startup task for day-to-day development.
- Keep long-running tasks in background mode.
- Add explicit labels that map to team language.
Recommended startup model:
- Set environment first.
- Start core services in parallel.
- Start optional helpers (tunnels/webhooks) after core if needed.
Goal: switch environments safely without manual copy/paste of secrets.
Reference pattern:
.devcontainer/set-env.shselectsdev,test,canary, orstable.- Script reads secrets from 1Password Developer Environments.
- Script writes/updates
.envand preserves machine-specific values when needed. - Script restarts relevant processes so environment changes take effect.
Add plan-aware overrides:
- Store per-plan test fixtures in JSON.
- Select API keys or account context by plan (
free,pro,enterprise) at switch time.
Security/operations guidance:
- Prefer service account tokens in containers.
- Never commit resolved
.envwith secrets. - Separate credential values from infrastructure overrides.
Use channel branches that represent rollout stages:
test: integration/staging default branch.canary: beta production.stable: production.
Promotion flow:
- Merge feature work to
test. - Promote
testtocanaryvia controlled workflow. - Promote
canarytostablevia controlled workflow.
Key policy rules:
- No direct commits to
canaryandstable. - Promotions happen through automation only.
- Use branch sync status checks to reveal drift.
Versioning approach:
- Prerelease identifiers for non-stable channels (
-test.x,-canary.x). - Stable releases publish normal semver.
- Use one script to synchronize versions across all packages.
Design workflows as reusable building blocks.
Suggested model:
- Reusable release workflow using
workflow_call. - Reusable test workflow parameterized by environment/channel.
- Reusable deploy workflow parameterized by target channel.
- Promotion workflows that call reusable release and deploy workflows.
Typical workflow set:
release.yaml: versioning + package publishing + GitHub release metadata.promote-test-to-canary.yaml: branch promotion + release + deploy.promote-canary-to-stable.yaml: branch promotion + release + deploy.- test workflows for Linux/Windows or channel matrices.
Implementation tips:
- Keep environment-specific values in env files and workflow inputs, not hardcoded shell blocks.
- Use workflow outputs for version numbers and artifact handoff.
- Keep deployment workflows callable by other workflows and manual triggers.
For hosted services (for example Fly.io, trigger.dev), use sync scripts that translate channel to target resources.
Pattern:
- Read base secrets from 1Password.
- Apply channel-specific infrastructure overrides.
- Push to target platform using a deterministic script.
Benefits:
- One source of secret truth.
- Lower chance of manual drift.
- Easy repeatability in local and CI contexts.
Safeguard:
- Restart long-running processes from the switch script.
- Use service wrappers with PID tracking.
Safeguard:
- Protect
canaryandstable. - Allow updates only from promotion workflows.
Safeguard:
- Validate required auth variables before env switch.
- Fail fast with clear error messages.
Safeguard:
- Document precedence order clearly.
- Use a shared env loader utility across services.
Safeguard:
- Start with
dev/testchannels first. - Add
canary/stablepromotion after local and CI stability is proven.
Day 1:
- Create monorepo skeleton and import one core service.
- Add devcontainer and one startup task.
Day 2:
- Import remaining services and shared code.
- Add environment switch script with two channels.
Day 3:
- Add reusable test/release workflows.
- Add branch protection and promotion workflow draft.
Day 4:
- Add secrets sync scripts and channel overrides.
- Run full end-to-end release simulation on non-production channels.
Day 5:
- Freeze old repos to read-only.
- Enable canonical monorepo contribution path.
- Devcontainer with app + data dependencies.
- Shared env loader and channel metadata.
- Environment switch script with restart behavior.
- VS Code task graph with one composite startup task.
- Reusable release/test/deploy workflows.
- Protected promotion branches.
- Version sync script across packages.
- Secret sync scripts for external platforms.
- Migration and rollback notes.
If the project is small, start with this subset:
- Two channels only (
dev,stable). - One promotion workflow.
- No tunnels initially; direct local ports.
- One composite startup task.
- One reusable test workflow.
Then scale to test/canary/stable once release cadence increases.
Use these files as concrete examples while building your friend version:
.devcontainer/devcontainer.json.devcontainer/docker-compose.yml.devcontainer/post-create.sh.devcontainer/set-env.sh.devcontainer/run-service.sh.vscode/tasks.jsonscripts/sync-fly-secrets.shscripts/sync-trigger-secrets.shscripts/branch-status.shscripts/sync-versions.mjsshared/environments.jsonshared/load-env.js.github/workflows/release.yaml.github/workflows/promote-test-to-canary.yaml.github/workflows/promote-canary-to-stable.yaml
These are implementation references, not strict templates. Keep the architecture and rename details for your friend domain.