ossbridge
v0.3.0
Published
CLI for bidirectional sync between a private monorepo and a public OSS mirror
Readme
ossbridge
A CLI for bidirectional sync between a private canonical monorepo and a public OSS mirror. Manages the full lifecycle of external contributions: validation, merge, sync, and attribution.
Why ossbridge?
Managing OSS contributions when your canonical repo is private is painful. You need to:
- Accept PRs on the public repo without exposing secrets
- Run full validation in the private repo
- Preserve contributor attribution across the sync
- Close OSS PRs automatically after merge
- Handle failures, race conditions, and edge cases
This typically means hundreds of lines of YAML spread across multiple workflows, with no tests, no type safety, and failures that manifest as PRs stuck in weird states.
ossbridge encapsulates this complexity in a single, testable CLI. Your workflows become thin declarations. The state machine is explicit. Failures are clear.
Quick Start
1. Create config files
Create ossbridge.json at the root of your OSS package:
{
"schema": "ossbridge/v1",
"privateRepo": "yourorg/private-monorepo",
"ossRepo": "yourorg/oss-repo",
"ossPath": "packages/mylib",
"maintainers": ["@yourorg/maintainers", "alice", "bob"],
"triggerComment": "/run-private-tests",
"requiredChecks": ["oss-quick-ci"],
"auth": {
"privateToken": "OSSBRIDGE_PRIVATE_TOKEN",
"ossToken": "OSSBRIDGE_OSS_TOKEN"
}
}At the private repo root, create a ref that points to the OSS package:
{
"schema": "ossbridge/v1",
"ref": "packages/mylib"
}The config lives in the OSS package because the OSS repo can only see files within its folder. The private repo uses a ref so the CLI knows where to find it.
2. Set up secrets
The auth block specifies which environment variable names the CLI reads at runtime. Add matching GitHub secrets to both repos:
| Token | Scopes |
| ------------------------- | -------------------------- |
| OSSBRIDGE_PRIVATE_TOKEN | repo |
| OSSBRIDGE_OSS_TOKEN | repo, write:discussion |
3. Add workflows
Wire up your GH action workflow yaml files to use the cli. This will be company and project specific in most cases, but see example-monorepo/ for a complete working setup with all workflow files, configs, and a step-by-step setup guide.
How It Works
Lifecycle
A contribution moves through eight steps across both repos:
Contributor opens a PR on the OSS repo. Standard CI runs (lint, tests) on OSS project without access to secrets.
Maintainer triggers validation by commenting the configured trigger phrase (e.g.
/run-private-tests). Thedispatch-prcommand (OSS repo) verifies the commenter is a maintainer, confirms required checks have passed, records the PR's HEAD SHA, labels the PRossbridge:accepted, and dispatches anossbridge-importevent to the private repo.Private repo imports the PR. The
import-prcommand (private repo) fetches the PR's commits, remaps file paths from OSS root into the monorepo subtree (/ → {ossPath}/), creates a branchossbridge/{pr-number}, and opens a PR titled[OSS #{n}] {title}against privatemain. The OSS PR is labeledossbridge:validating.Full CI runs in the private repo with secret access — integration tests, e2e, etc.
Results are reported back. The
report-resultcommand (private repo) reads the CI conclusion, updates the state comment on the OSS PR, and setsossbridge:validatedorossbridge:failed.Maintainer reviews and merges the private PR. Before merge completes,
verify-merge(private repo, wired as a required status check) confirms the OSS PR hasn't changed since validation (SHA match), is still open, and has theossbridge:validatedlabel.Changes sync to OSS. On push to private
main,sync-downstream(private repo) identifies commits touching the subtree, remaps paths back ({ossPath}/ → /), and pushes to OSSmain. Original author attribution is preserved —git blameshows the contributor on both repos.OSS PR closes automatically. On push to OSS
main,close-synced-prs(OSS repo) finds PRs whose changes have landed, posts a thank-you comment with commit links, removes allossbridge:*labels, and closes the PR.
State Machine
PRs move through states tracked via GitHub labels:
[opened] → [ossbridge:accepted] → [ossbridge:validating] → [ossbridge:validated] → [closed]
↘
[ossbridge:failed]Command Reference
All commands read ossbridge.json from the repository root by default. Use --config <path> to override.
dispatch-pr
Runs in: OSS repo | Trigger:
issue_comment
Preconditions (all must hold or exits 1):
- Comment body matches
triggerCommentfrom config - Commenter is in
maintainerslist (supports@org/teamslugs) - All
requiredCheckshave passed on the PR's HEAD - PR is open
- PR has no existing
ossbridge:*label (prevents double-dispatch)
Effects:
- Creates a comment on the OSS PR containing: contributor notification + embedded state JSON (records HEAD SHA, acceptor, timestamp)
- Adds
ossbridge:acceptedlabel - Dispatches
ossbridge-importevent to private repo with{ossPrNumber, ossPrSha}payload
Exit codes: 0 dispatched, 1 precondition failed
import-pr
Runs in: Private repo | Trigger:
repository_dispatchtypeossbridge-import
Preconditions:
- OSS PR is open
- OSS PR HEAD SHA matches the SHA from the dispatch payload (detects if PR was updated after trigger)
- No existing private PR for this OSS PR (unless
--force)
Effects:
- Creates branch
ossbridge/{oss-pr-number}in private repo - Opens PR titled
[OSS #{n}] {title}against privatemain, body contains ossbridge metadata and file list with remapped paths - Updates state comment on OSS PR with private PR link
- Sets
ossbridge:validatinglabel on OSS PR - Comments on OSS PR with link to private PR
Flags: --force re-import even if private PR exists
Exit codes: 0 private PR created/updated, 1 import failed
report-result
Runs in: Private repo | Trigger:
workflow_runonossbridge/*branches
Preconditions:
- Private PR body contains ossbridge metadata
- OSS repo in metadata matches config
Effects:
- Updates state comment on OSS PR with validation result and timestamp
- Sets
ossbridge:validatedorossbridge:failedlabel (replaces previous ossbridge label) - Posts pass/fail notification comment on OSS PR (with failure details if applicable)
Flags: --passed / --failed override the CI conclusion (for manual recovery)
Note: skipped CI conclusion is treated as passed.
Exit codes: 0 result reported, 1 could not determine result or post status
verify-merge
Runs in: Private repo | Trigger: required status check (PR check or merge queue)
Preconditions:
- Private PR body contains ossbridge metadata
Checks (any failure exits 1 and blocks merge):
- OSS PR is still open
- OSS PR HEAD SHA matches the SHA recorded at dispatch time
ossbridge:validatedlabel is present (notossbridge:failed, notossbridge:validating)
Effects on failure:
- SHA mismatch: posts "Validation Invalidated" comment on OSS PR showing expected vs current SHA
Exit codes: 0 safe to merge, 1 not safe
Wire this as a required status check to prevent merging stale validations.
sync-downstream
Runs in: Private repo | Trigger:
pushtomain
Effects:
- Compares commits between previous sync point and current HEAD
- Filters to commits that touch files under
ossPath - For each relevant commit: remaps paths
{ossPath}/* → /*, preserves author attribution - Pushes to OSS repo
main
Exit codes: 0 sync completed (or nothing to sync), 1 sync failed
close-synced-prs
Runs in: OSS repo | Trigger:
pushtomain
Effects:
- Finds open PRs with
ossbridge:validatedlabel - For each PR, checks if its changes have landed in
main(handles both regular and squash merges via commit comparison) - Posts thank-you comment with canonical and mirrored commit links
- Removes all
ossbridge:*labels - Closes the PR
Flags: --pr {n} close a specific PR
Exit codes: 0 PRs closed (or none to close), 1 failed to close
status
Runs in: Either repo | Trigger: manual
Inspect PR state for debugging. Reads state from OSS PR comments and labels, discovers private PR by branch name if no state exists.
bunx ossbridge status --pr 42OSS PR #42: Add custom validators
State: open
SHA: abc123def456
Phase: validating
Accepted by: alice at 2025-01-15T10:30:00Z
Private PR: #17 (https://github.com/yourorg/private-monorepo/pull/17)
Labels: ossbridge:validating
Validation started: 2025-01-15T10:30:12ZConfiguration
Full config
{
"schema": "ossbridge/v1",
"privateRepo": "yourorg/private-monorepo",
"ossRepo": "yourorg/oss-repo",
"ossPath": "packages/mylib",
"maintainers": ["@yourorg/maintainers", "alice", "bob"],
"triggerComment": "/run-private-tests",
"requiredChecks": ["oss-quick-ci"],
"mergeMethod": "squash",
"squashCommitIncludesHistory": true,
"labels": {
"accepted": "ossbridge:accepted",
"validating": "ossbridge:validating",
"validated": "ossbridge:validated",
"failed": "ossbridge:failed"
},
"auth": {
"privateToken": "OSSBRIDGE_PRIVATE_TOKEN",
"ossToken": "OSSBRIDGE_OSS_TOKEN"
},
"notifications": {
"validationStarted": "## Private Validation Started\n\n...",
"validationPassed": "## Validation Passed\n\n...",
"validationFailed": "## Validation Failed\n\n{failureDetails}",
"prClosed": "## Merged!\n\n..."
}
}Field reference
| Field | Type | Required | Description |
| ----------------------------- | -------- | -------- | ---------------------------------------------------------------------- |
| schema | string | yes | Must be "ossbridge/v1" |
| privateRepo | string | yes | owner/repo format |
| ossRepo | string | yes | owner/repo format |
| ossPath | string | yes | Path within private repo that maps to OSS root |
| maintainers | string[] | yes | GitHub usernames or @org/team slugs authorized to trigger validation |
| triggerComment | string | yes | Comment text that triggers validation |
| requiredChecks | string[] | yes | Status checks that must pass before dispatch |
| mergeMethod | string | no | "squash" (default), "merge", or "rebase" |
| squashCommitIncludesHistory | boolean | no | Include original commit messages in squash body (default: true) |
| labels | object | no | Custom label names (defaults: ossbridge:accepted, etc.) |
| auth.privateToken | string | yes | Env var name for private repo token |
| auth.ossToken | string | yes | Env var name for OSS repo token |
| notifications | object | no | Custom notification templates (see variables below) |
Notification template variables:
| Variable | Description |
| -------------------- | --------------------------------- |
| {ossPrNumber} | OSS PR number |
| {privatePrNumber} | Private PR number |
| {privatePrUrl} | URL to private validation PR |
| {failureDetails} | CI failure logs |
| {triggerComment} | The configured trigger comment |
| {privateCommitUrl} | URL to canonical commit |
| {ossCommitUrl} | URL to synced commit |
| {maintainer} | Username who triggered validation |
Config resolution
--config <path>flagossbridge.jsonin current working directoryossbridge.jsonin repository root (detected via git)
Ref configs are automatically followed to load the actual configuration.
CI Integration
Each repo gets an ossbridge.yml workflow. The OSS repo handles inbound PRs (dispatch-pr on comment, close-synced-prs on push to main). The private repo handles import, validation, and sync (import-pr on dispatch, report-result on workflow completion, sync-downstream on push to main). A separate ossbridge-verify.yml wires verify-merge as a required check.
Minimal OSS repo workflow:
name: ossbridge
on:
issue_comment:
types: [created]
push:
branches: [main]
jobs:
dispatch-pr:
if: github.event_name == 'issue_comment' && github.event.issue.pull_request
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bunx ossbridge dispatch-pr
env:
OSSBRIDGE_PRIVATE_TOKEN: ${{ secrets.OSSBRIDGE_PRIVATE_TOKEN }}
OSSBRIDGE_OSS_TOKEN: ${{ secrets.OSSBRIDGE_OSS_TOKEN }}See example-monorepo/ for the complete set of workflows for both repos, including the step-by-step setup guide.
Error Handling and Recovery
| Failure | Behavior |
| ------------------------------- | -------------------------------------------------------------------- |
| Maintainer not authorized | dispatch-pr exits 1, posts explanatory comment |
| Required checks not passed | dispatch-pr exits 1, posts which checks are missing |
| PR already in workflow | dispatch-pr exits 1 (duplicate-dispatch guard) |
| OSS PR closed during validation | verify-merge exits 1, blocks merge |
| OSS PR updated after validation | verify-merge exits 1, posts "Validation Invalidated" with SHA diff |
| Private CI fails | report-result sets ossbridge:failed label, posts failure details |
| Sync conflict | sync-downstream exits 1 |
| Missing auth token | All commands exit 1, message indicates which env var is missing |
Manual recovery
# Force re-import (clears existing private PR)
bunx ossbridge import-pr --force --pr 42
# Manually mark as passed
bunx ossbridge report-result --passed --pr 42
# Manually close a PR that landed
bunx ossbridge close-synced-prs --pr 42Limitations
- Single subtree only — one directory to one OSS repo
- GitHub only — no GitLab/Bitbucket support
- No merge conflict resolution — sync fails on conflicts
- Linear history assumed — complex merges may produce unexpected results
