# GitHub Webhook and Deployment Guide

Slackzilla exposes a deployment webhook at `POST /webhook`, handled by `server/server.js`. The webhook service shares port `9000` with the Slackzilla dashboard and API.

The webhook is used by the GitHub Actions deployment workflow to trigger a deployment when changes are pushed to the configured deployment branch.

## Request Contract

The GitHub Actions workflow sends a `POST` request to `https://rylvion.hackclub.app/webhook` with a JSON payload containing the branch and commit information.

The request uses these headers:
```bash
Content-Type: application/json
X-GitHub-Event: push
X-GitHub-Delivery: <github-run-id>-<github-run-attempt>
X-Hub-Signature-256: sha256=<hmac-sha256>
```

### Request payload

Slackzilla does not receive GitHub's full native `push` event payload. The deployment workflow constructs a smaller payload containing only the information required by the webhook:

```json
{
    "ref": "refs/heads/main",
    "after": "<commit-sha>",
    "repository": {
        "full_name": "<owner>/<repository>",
        "default_branch": "main"
    },
    "sender": {
        "login": "github-actions"
    }
}
```

The values are generated by the GitHub Actions workflow:

- `ref` comes from `GITHUB_REF`.
- `after` comes from `GITHUB_SHA`.
- `repository.full_name` comes from `GITHUB_REPOSITORY`.
- `repository.default_branch` is currently hardcoded to `main`.
- `sender.login` is currently hardcoded to `github-actions`.

The webhook currently uses `ref` and `after`. The other payload fields are retained as metadata but are not required for deployment.

## HMAC Verification

The workflow signs the exact JSON payload before sending it:

```bash
sha256=HMAC-SHA256(WEBHOOK_SECRET, raw request body)
```

Slackzilla reads the raw request body before parsing JSON so that the signature is calculated against exactly the same bytes that were signed. The server calculates its own signature:

```js
const expected = `sha256=${crypto.createHmac("sha256", webhookSecret).update(body).digest("hex")}`;
```

The supplied and calculated signatures are compared using `crypto.timingSafeEqual()`. If the signature is missing, malformed, or does not match, the request receives:

```bash
401 Invalid signature
```

The webhook secret used by GitHub Actions must match the `WEBHOOK_SECRET` configured for the Slackzilla webhook service. When rotating the secret, update both locations and restart the webhook service.

## Event and Branch Filtering

The webhook validates the event after verifying the signature. Only requests with: `X-GitHub-Event: push` are processed. Other event types receive `202 Ignored non-push event` and do not trigger a deployment. The deployment branch is controlled by the server's `BRANCH` configuration and defaults to `main`.

The webhook expects: `payload.ref = refs/heads/main` (or the configured branch). If the push is to another branch that doesnt match the `.env` configuration, the request receives `202 Ignored push for refs/heads/<branch>` and does not trigger a deployment. For example, when `BRANCH=main`, `refs/heads/main` triggers deployment. A push to another branch is ignored with `202 Ignored push for refs/heads/<branch>` and the deployment script is not executed.

## Deployment Lifecycle

Once a valid push for the configured branch is received, the webhook:

1. Records the deployment as `queued`.
2. Stores the commit from `payload.after`.
3. Sends an early `202` response to the caller.
4. Broadcasts the queued deployment state to connected admin clients.
5. Runs the configured deployment script.
6. Records the final deployment state.
7. Broadcasts the result to connected admin clients.
8. Refreshes and broadcasts the current Slackzilla status.

The deployment runs after the HTTP response has been sent, so the webhook does not keep the GitHub Actions request waiting for the entire deployment.

### Successful deployment

A successful deployment records the deployment status, timestamp, commit, output, and a result of `success`. The deployment output is stored and broadcast to connected admin clients. The webhook also refreshes the runtime status and broadcasts the updated state.

### Failed deployment

A failed deployment records the deployment status, timestamp, commit, error, and a result of `failed`. The deployment error is also appended to the deployment log.

## GitHub Actions Configuration

The deployment workflow requires these secrets:

```bash
WEBHOOK_URL
WEBHOOK_SECRET
```

`WEBHOOK_URL` should point to the Slackzilla webhook endpoint: `https//rylvion.hackclub.app/webhook`.

`WEBHOOK_SECRET` must contain the same secret configured for the Slackzilla webhook service.

The workflow constructs the payload, calculates its HMAC signature, and sends the request with `curl`.

The workflow process is:


1. GitHub push
2. GitHub Actions workflow
3. Construct deployment payload
4. Calculate HMAC-SHA256
5. POST /webhook
6. Slackzilla verifies signature
7. Check event
8. Check branch
9. Run deployment


## Reverse Proxy Requirements

If the webhook is exposed through a reverse proxy , the proxy must forward the request body and webhook headers unchanged. In particular, the following header must reach Slackzilla:

```bash
X-Hub-Signature-256
```

The request body must also remain byte-for-byte equivalent to the body that was signed.

Do not parse, re-serialise, or otherwise modify the JSON body before it reaches Slackzilla, as this can cause HMAC verification to fail.

## Local Testing

An unsigned request should be rejected:

```bash
curl -i -X POST http://localhost:9000/webhook -H "X-GitHub-Event: push" -H "Content-Type: application/json" -d "{\"ref\":\"refs/heads/main\"}"
```

Expected response:

```bash
HTTP/1.1 401 Unauthorized
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Type: text/plain; charset=utf-8
Date: Tue, 06 Oct 2026 18:12:51 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 18

Invalid signature
```

To test a valid request, calculate an `HMAC-SHA256` signature over the exact request body using the configured `WEBHOOK_SECRET`.

For example:

```bash
payload='{"ref":"refs/heads/test","after":"0000000000000000000000000000000000000000","repository":{"full_name":"test/test","default_branch":"main"},"sender":{"login":"github-actions"}}'

signature="sha256=$(printf '%s' "$payload" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | awk '{print $2}')"

curl -i -X POST http://localhost:9000/webhook \
    -H "Content-Type: application/json" \
    -H "X-GitHub-Event: push" \
    -H "X-GitHub-Delivery: local-test" \
    -H "X-Hub-Signature-256: $signature" \
    --data "$payload"
```

Using a non-deployment branch is recommended when testing the webhook so that the deployment script is not invoked.

## Response Codes

| Status | Response                                  | Meaning                                                                  |
| ------ | ----------------------------------------- | ------------------------------------------------------------------------ |
| `202`  | `Accepted deploy for refs/heads/<branch>` | Valid deployment request accepted and deployment started asynchronously. |
| `202`  | `Ignored non-push event`                  | The request was authenticated but was not a `push` event.                |
| `202`  | `Ignored push for <ref>`                  | The request was authenticated but targeted another branch.               |
| `400`  | `Invalid JSON payload`                    | The request body could not be parsed as JSON.                            |
| `401`  | `Invalid signature`                       | The HMAC signature was missing or invalid.                               |
| `405`  | `Method not allowed`                      | The request was not a `POST` request.                                    |

## Failure Diagnosis

| Symptom                                          | Likely cause                                                                                                       |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `401 Invalid signature`                          | Secret mismatch, modified request body, missing signature header, or proxy configuration issue.                    |
| `400 Invalid JSON payload`                       | The request body is not valid JSON.                                                                                |
| `202 Ignored non-push event`                     | `X-GitHub-Event` is not `push`.                                                                                    |
| `202 Ignored push for ...`                       | `payload.ref` does not match the configured deployment branch.                                                     |
| Deployment is marked failed                      | Inspect the deployment output and logs, then check Git, permissions, paths, dependencies, and deployment commands. |
| GitHub receives `202` but deployment later fails | Expected behaviour. The webhook acknowledges the request before the deployment script finishes.                    |

The webhook service user must have access to the Slackzilla working tree, Git repository, deployment script, required commands, and any files or services used by the deployment process.

# Deployment Script

The deployment script automates updating Slackzilla from its remote Git repository.

It can be invoked by the webhook after a valid GitHub push or directly through the admin dashboard's `redeploy` action.

The exact deployment behaviour is defined by `server/deploy.sh`.

## Deployment Process

The script performs the following operations.

### 1. Initialise the deployment environment

The script:

- Enables `set -e` so command failures stop the deployment.
- Loads configuration from the `.env` file alongside the script.
- Determines the project directory, target branch, and service names.
- Uses configured values where available.
- Defaults the deployment branch to `main`.
- Defaults the main service to `slackzilla`.
- Defaults the webhook service to `slackzilla-webhook`.

### 2. Verify the repository

The script:

- Changes to the configured project directory.
- Updates the Git remote using `REPO_URL`.
- Fetches the latest commits from the configured branch.
- Compares the local `HEAD` commit with the fetched remote commit.

### 3. Check for changes

If the local and remote commits are identical, deployment finishes successfully without rebuilding or restarting the application.

If the commits differ, deployment continues.

### 4. Synchronise the project

When changes are detected, the script:

- Resets the working tree to the fetched remote branch.
- Installs dependencies with `npm ci`.
- Builds the React client with `npm run build`.

### 5. Restart services

The script:

- Restarts the configured Slackzilla systemd service.
- Restarts Nginx when required by the deployment configuration.
- Schedules a delayed restart of the webhook service using `systemd-run`.

The webhook restart is scheduled separately so the active deployment process can finish before the webhook service itself is restarted.

### 6. Report the result

A successful deployment prints the deployed commit and exits with status `0`.

If a command fails, the script exits with a non-zero status. The calling process then records the deployment as failed.

## Deployment Behaviour

### No changes

If the local repository already matches the remote branch:

- No dependencies are reinstalled.
- The client is not rebuilt.
- Services are not restarted.
- The deployment still finishes successfully.

### Successful deployment

After the script completes successfully, the webhook records the deployment as successful, stores the deployment output, refreshes runtime status, and broadcasts the updated state to connected admin clients.

### Failed deployment

If the script fails:

- The failing command stops the deployment.
- The webhook records the deployment as failed.
- The deployment error is stored.
- The error is appended to the deployment log.
- The updated failure state is broadcast to connected admin clients.

### Webhook service restart

The webhook service restart is scheduled separately rather than performed directly inside the active deployment process.

See [Webhook Service](../webhook.md) for the webhook service's request handling and deployment lifecycle.

## Summary

Slackzilla's deployment webhook provides an authenticated bridge between the GitHub Actions workflow and the Slackzilla deployment system.

A successful workflow run follows this process:

1. GitHub Actions
2. POST /webhook
3. Verify HMAC signature
4. Verify push event
5. Verify deployment branch
6. Queue deployment
7. Run deploy.sh
8. Update deployment state
9. Broadcast result

This allows Slackzilla to automatically deploy changes pushed to the configured branch without requiring a manual deployment after every commit.
