How Push-to-Deploy Works with GitHub Webhooks in Openship

Openship automates deployments by registering a per-project GitHub webhook, verifying incoming push events via HMAC signatures, and triggering a containerized build pipeline every time you commit code to your repository.

Push-to-deploy in Openship eliminates manual deployment steps by connecting your GitHub repository to a managed infrastructure pipeline. When you link a project to a repo, Openship provisions a unique webhook endpoint, persists the credentials in its database schema, and listens for push events to automatically rebuild and redeploy your application.

Webhook Registration and Configuration

When you initialize a project or connect a repository through the UI, Openship generates a cryptographically secure secret and registers a webhook directly with the GitHub API. This process binds the repository to your project's specific deployment endpoint.

Storing Webhook Credentials

Per-project webhook configuration is persisted in the database schema defined in [packages/db/src/schema/project.ts](https://github.com/oblien/openship/blob/main/packages/db/src/schema/project.ts#L319-L326). The table stores the GitHub webhook ID, the public domain that will receive events, and the HMAC secret used for signature verification:

// packages/db/src/schema/project.ts (lines 319-326)
webhookId: integer("webhook_id"),
webhookDomain: text("webhook_domain"),
webhookSecret: text("webhook_secret"),

These columns enable Openship to map incoming requests to the correct project and verify their authenticity.

Creating the GitHub Webhook

Openship programmatically creates the webhook via the GitHub REST API, configuring it to fire on push events and deliver payloads to your project's unique endpoint. Below is a simplified version of the registration logic:

import fetch from 'node-fetch';

async function registerGitHubWebhook(projectId: string, owner: string, repo: string) {
  // Load project configuration including the generated secret
  const { webhookDomain, webhookSecret } = await getProjectConfig(projectId);
  const webhookUrl = `https://${webhookDomain}/api/webhooks/github`;
  
  const response = await fetch(
    `https://api.github.com/repos/${owner}/${repo}/hooks`,
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        name: 'web',
        active: true,
        events: ['push'],
        config: {
          url: webhookUrl,
          secret: webhookSecret,
          content_type: 'json',
        },
      }),
    }
  );
  
  const data = await response.json();
  // Store the returned webhook ID for later reference
  await saveWebhookId(projectId, data.id);
  return data;
}

Receiving and Verifying Webhook Events

Once registered, GitHub delivers JSON payloads to the public endpoint whenever commits are pushed. Openship validates these payloads before invoking any deployment logic.

The Inbound Endpoint

The HTTP handler resides at POST /api/webhooks/github (implemented in the API package, typically within the webhook routes handler). This endpoint extracts headers and the raw body for processing.

HMAC Signature Verification

Security is enforced by validating the X-Hub-Signature-256 header against the stored webhookSecret. The handler rejects requests with invalid signatures before recording the delivery:

import crypto from 'crypto';

function verifyGitHubSignature(
  signature: string,
  payload: string,
  secret: string
): boolean {
  const hmac = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  const expected = `sha256=${hmac}`;
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

// Usage in the webhook handler
export async function githubWebhookHandler(req, res) {
  const signature = req.headers['x-hub-signature-256'];
  const payload = await req.text();
  
  if (!verifyGitHubSignature(signature, payload, project.webhookSecret)) {
    return res.status(401).send('Invalid signature');
  }
  
  // Proceed to processing...
}

Idempotency and Delivery Tracking

To prevent duplicate deployments from GitHub's retry logic, Openship records every delivery in the webhook_delivery table:

// packages/db/src/schema/webhook-delivery.ts (lines 20-21)
hookId: integer("hook_id"),
githubDeliveryId: text("github_delivery_id"),
uniqueIndex("uq_webhook_delivery_github_delivery").on(hookId, githubDeliveryId),

The composite unique index ensures that a specific GitHub delivery ID for a given webhook can only be processed once, making the pipeline inherently idempotent.

Triggering the Deployment Pipeline

After verification and persistence, Openship enqueues a deployment job. The pipeline re-detects the repository state, rebuilds the application container, and updates the edge router to point to the new deployment. This process is typically orchestrated by the core deployment engine (referenced in packages/core/src/updates/deploy.ts):

export async function triggerDeploy(projectId: string, metadata: object) {
  // Record deployment source for audit trails
  await recordDeploymentStart(projectId, { source: 'webhook', ...metadata });
  
  // Execute build-detect-run sequence
  const build = await createBuild(projectId);
  await runPipeline(build);
  
  // Update routing and SSL certificates if domain configuration changed
  await updateEdgeRouter(projectId);
}

Summary

  • Per-project credentials: Openship stores webhookId, webhookDomain, and webhookSecret in the project schema to isolate repositories and secure communications.
  • Programmatic registration: Webhooks are created via the GitHub API during project initialization, automatically configuring the push event listener.
  • Cryptographic verification: Every inbound request is validated using HMAC-SHA256 signatures before processing.
  • Idempotent processing: Deliveries are tracked in the webhook_delivery table using a unique composite key to prevent duplicate builds from retry storms.
  • Automated pipeline: Validated webhooks trigger the full build-detect-run pipeline, updating your application without manual intervention.

Frequently Asked Questions

What URL does Openship use for GitHub webhooks?

Openship constructs a unique HTTPS endpoint for each project using the format https://<webhookDomain>/api/webhooks/github, where webhookDomain is stored in the project configuration table. This domain is typically auto-generated or custom-configured during project setup.

How does Openship prevent duplicate deployments from webhook retries?

The system records every GitHub delivery ID in the webhook_delivery table alongside the GitHub hook ID. A database-level unique constraint (uq_webhook_delivery_github_delivery) prevents the same delivery from being processed twice, ensuring only one deployment occurs per push even if GitHub retries the request.

Where are the webhook secrets stored in Openship?

Webhook secrets are stored in the webhookSecret column of the project table defined in packages/db/src/schema/project.ts. These secrets are generated per-project and used exclusively for HMAC signature verification of inbound GitHub payloads.

Can I manually trigger a deployment without a Git push?

Yes. While push-to-deploy automates the process based on GitHub webhooks, Openship typically exposes CLI commands (such as openship deploy) or dashboard triggers that invoke the same triggerDeploy function directly, bypassing the webhook verification step to force a rebuild and redeployment.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →