How Push-to-Deploy Works in Openship: From GitHub Webhook to Live Deployment
Push-to-deploy in Openship automatically triggers deployments when you push code to GitHub by registering webhooks, processing push payloads at a unified inbound endpoint, and intelligently routing changes only to affected services.
Push-to-deploy (also called auto-deploy) is the core mechanism in the oblien/openship repository that transforms GitHub push events into live deployments. This feature bridges your Git repository and the Openship deployment engine, enabling continuous delivery without manual intervention. Understanding the internal flow—from webhook registration to service routing—helps you debug deployment failures and optimize your CI/CD pipeline.
Enabling Auto-Deploy for a Project
Before Openship can react to pushes, you must toggle the autoDeploy flag and establish a webhook connection. This process validates your project configuration and implements one of four webhook strategies based on your GitHub integration method.
The Configuration Endpoint
Enabling auto-deploy starts at the API layer in apps/api/src/modules/projects/project.routes.ts. The route POST /api/projects/:id/auto-deploy invokes the setAutoDeploy controller:
// project.routes.ts (line 212)
r.post("/:id/auto-deploy", …, ctrl.setAutoDeploy)
The controller validates the project and delegates to resolveWebhookStrategy to determine how Openship will receive GitHub events.
Webhook Strategy Resolution
Openship supports four distinct strategies for receiving push events, defined in apps/api/src/modules/projects/project.controller.ts:
- Strategy "app" – Relies on the Openship GitHub App, which already receives all repository events. The code simply updates the database flag:
await repos.project.update(id, { autoDeploy: enabled })(lines 82-86). - Strategy "domain" – Creates a shared webhook that posts to a verified custom domain (
domainWebhookUrl). TheensureSharedWebhookfunction registers the webhook and persists its ID (lines 88-101). - Strategy "repo" – Creates a repository-level webhook directly on GitHub using the same
ensureSharedWebhookhelper, but without a custom domain (lines 106-112). - Strategy "none" – Disables the feature entirely.
When disabling auto-deploy, the controller calls disableSharedWebhookIfUnused (lines 27-44) to clean up shared webhooks if no other projects in the same repository require them. Every change is recorded via audit.recordAsync(..., { action: "autoDeploy.set", … }) for compliance tracking.
Receiving GitHub Push Events
Once enabled, GitHub delivers push notifications to a unified inbound endpoint that validates signatures and dispatches events to the appropriate handler.
The Unified Inbound Endpoint
All GitHub webhooks route through /api/webhooks/github, implemented in the webhook service layer. After signature verification, the system constructs a WebhookHandlerContext and delegates push events to the handlePush function.
The handlePush Handler
Located in apps/api/src/modules/github/webhook-push.ts, the handlePush function extracts repository metadata and delegates to the deployment engine:
// webhook-push.ts (lines 44-75)
export async function handlePush(payload: GitHubPushPayload): Promise<WebhookHandlerResult> {
const owner = payload.repository?.owner?.login;
const repo = payload.repository?.name;
const ref = payload.ref;
const branch = ref.replace("refs/heads/", "");
return triggerBranchDeployments({ owner, repo, branch, … });
}
This handler filters out non-branch refs (like tags) and immediately hands off to triggerBranchDeployments, the core auto-deploy orchestrator.
Matching Projects and Smart Routing
The deployment decision logic lives in triggerBranchDeployments and its helper deployProjectFromPush, which determine exactly which services need rebuilding.
Finding Local Projects
First, the system queries for all projects referencing the pushed repository:
// webhook-push.ts (line 45)
const projects = await repos.project.findByGitRepo(input.owner, input.repo);
It then filters this list to include only projects where:
autoDeployis enabled, and- The project's configured branch matches the pushed branch (
projectWebhookBranchmatchesinput.branch)
If no local projects match, the system falls back to forwardPushToCloud (lines 57-70), which forwards the event to the Openship Cloud SaaS via cloudFetchAsOrgOwner for processing in the managed control plane.
Intelligent Service Routing
For matching projects, deployProjectFromPush executes a series of optimization steps:
- Load Services: Retrieves enabled services via
repos.service.listByProject(p.id)(lines 11-13). - Extract Changes: Calls
extractChangedFilesto parse the push diff via the GitHub Compare API (lines 27-30). - Force-All Detection: Checks if the changed-file set is truncated or if a "force-deploy-next" flag is set. If true,
forceAlltriggers a full project redeploy (lines 42-66). - Monorepo Routing: Uses
routeServicesByChanges(routableServices, extracted.files)(lines 67-79) to determine which specific services are affected by the commit. If no services match the changed files, the deployment is skipped to save resources. - Persist Metadata: Stores the changed file paths in the deployment row via
repos.deployment.setChangedPaths(...)(lines 22-29) for visibility in the dashboard UI.
If this process fails before a deployment row is created, notifyAutoDeployFailed emits a deployment.failed notification to prevent silent failures.
Triggering the Deployment
The final step invokes the core deployment engine. The triggerDeployment function (residing in apps/api/src/modules/deployments/build.service.ts) receives:
- The project ID
- Target branch
- Optional commit SHA
- List of specific service IDs (or "all")
- The list of changed files
This engine handles the actual build, containerization, and rollout processes. You can monitor the result via the dashboard, which polls /api/projects/:id to display fields like latestDeploymentId and latestDeploymentStatus.
End-to-End Example
Consider a typical workflow enabling auto-deploy via CLI and triggering a deployment:
Enable via CLI
openship project git auto-deploy proj_123 --enable
This command POSTs to /api/projects/proj_123/auto-deploy, executing the setAutoDeploy flow described above.
GitHub Delivers Payload
When you push to main, GitHub POSTs to https://your-openship-instance.com/_openship/hooks/github:
{
"ref": "refs/heads/main",
"repository": {
"owner": { "login": "myorg" },
"name": "my-app",
"default_branch": "main"
},
"head_commit": {
"id": "a1b2c3d4",
"message": "Add feature"
}
}
The server executes handlePush → triggerBranchDeployments → deployProjectFromPush → triggerDeployment.
Dashboard Result
The project configuration now reflects:
{
"auto_deploy": true,
"webhook_strategy": "repo",
"latestDeploymentId": "dep_987",
"latestDeploymentStatus": "success"
}
Summary
- Enablement flow: The
setAutoDeploycontroller inproject.controller.tstoggles the database flag and configures one of four webhook strategies (app, domain, repo, or none). - Inbound processing: GitHub push events hit
/api/webhooks/github, wherehandlePushinwebhook-push.tsvalidates payloads and extracts branch metadata. - Project matching:
triggerBranchDeploymentsqueries projects by repository, checks theautoDeployflag, and matches branches before proceeding. - Smart routing:
routeServicesByChangesanalyzes file diffs to deploy only affected services in monorepo setups, falling back to full redeploys when necessary. - Execution: The
triggerDeploymentfunction in the build service initiates the actual container build and rollout.
Frequently Asked Questions
What webhook strategies does Openship support for push-to-deploy?
Openship supports four strategies defined in project.controller.ts: "app" (uses the existing GitHub App installation), "domain" (creates a shared webhook posting to a custom verified domain), "repo" (creates a repository-specific webhook directly on GitHub), and "none" (disables auto-deploy). The strategy is automatically resolved based on your GitHub integration type when you call the auto-deploy endpoint.
How does Openship determine which services to deploy in a monorepo?
After matching the project and branch, Openship calls extractChangedFiles to retrieve the diff from GitHub's Compare API. It then passes these files to routeServicesByChanges, which maps file paths to specific services based on your configuration. Only services with matching file changes are deployed, unless the diff is truncated or a force-deploy flag is set, in which case all services are rebuilt.
What happens if auto-deploy is enabled but no local project matches the pushed repository?
If triggerBranchDeployments finds no local projects matching the pushed repository and branch, it executes forwardPushToCloud. This helper forwards the webhook payload to Openship Cloud SaaS using cloudFetchAsOrgOwner, allowing cloud-linked projects to process the deployment even when the self-hosted instance doesn't have a local database record for the project.
Where is the push-to-deploy logic implemented in the Openship codebase?
The core logic spans three main files: apps/api/src/modules/projects/project.controller.ts (enablement and webhook registration), apps/api/src/modules/github/webhook-push.ts (payload handling and routing decisions), and apps/api/src/modules/deployments/build.service.ts (the triggerDeployment function that executes the actual build and deployment).
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 →