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

> Learn how Openship automates deployments using GitHub webhooks. Discover how push-to-deploy verifies code commits with HMAC signatures and triggers a build pipeline for seamless containerized releases.

- Repository: [oblien/openship](https://github.com/oblien/openship)
- Tags: how-to-guide
- Published: 2026-07-30

---

**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](https://github.com/oblien/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)](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:

```typescript
// 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:

```typescript
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:

```typescript
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`](https://github.com/oblien/openship/blob/main/packages/db/src/schema/webhook-delivery.ts#L20-L21) table:

```typescript
// 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`](https://github.com/oblien/openship/blob/main/packages/core/src/updates/deploy.ts)):

```typescript
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`](https://github.com/oblien/openship/blob/main/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.