Webhook workflow

Path to file (view file on GitHub)

The original file is located at the following path:

.github / workflows / CD_webhook.yml 

This GitHub Actions workflow deploys uvlhub to the server. It does not run on push. It waits for the Pytest workflow to finish, and only fires when that run succeeded on main. The deployment itself is a single authenticated POST to a webhook endpoint served by the running application.

Table of contents

  1. Workflow name
  2. Trigger
  3. The gate
  4. Step
  5. Required secrets
  6. The receiving end
  7. Testing the endpoint by hand

Workflow name

  • name: Deploy on Webhook

Trigger

on:
  workflow_run:
    workflows: 
      - "Pytest"
    types:
      - completed

workflow_run chains one workflow onto another. This workflow is scheduled when a run of the workflow named Pytest completes, whatever its conclusion.

The link between the two workflows is the name, not the filename

workflows: ["Pytest"] matches the name: field declared inside .github/workflows/CI_pytest.yml. Renaming that workflow, or renaming this list entry, breaks the chain silently: no error, no failed run, deployments simply stop happening. See the Testing workflow.

The gate

completed includes failed runs, so the real gate is the job condition:

jobs:
  deploy:
    if: github.event.workflow_run.conclusion == 'success' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-24.04

Two conditions must both hold:

  • github.event.workflow_run.conclusion == 'success': the test run that triggered this workflow passed. A red test suite never reaches the server.
  • github.ref == 'refs/heads/main': only main deploys.

If either condition fails, the job is skipped. A skipped job is not a failed job, so a pull request whose tests passed will show this workflow as skipped rather than red.

Step

- name: Trigger Deployment Webhook
  env:
    WEBHOOK_DOMAIN: ${{ secrets.WEBHOOK_DOMAIN }}
    WEBHOOK_TOKEN: ${{ secrets.WEBHOOK_TOKEN }}
  run: |
    curl -X POST \
      https://${{ secrets.WEBHOOK_DOMAIN }}/webhook/deploy \
      -H "Authorization: Bearer ${{ secrets.WEBHOOK_TOKEN }}"

GitHub does not have access to the server. It only sends a signed request and the server does the work.

Required secrets

Secret Purpose
WEBHOOK_DOMAIN The host that serves the deployment endpoint, without scheme, for example uvlhub.io
WEBHOOK_TOKEN The bearer token the endpoint checks. It must match the WEBHOOK_TOKEN environment variable on the server

Register them the same way as the Docker Hub secrets: Settings then Secrets and variables then Actions then New repository secret.

The receiving end

The endpoint is provided by the webhook feature, at app/features/webhook/routes.py:

@webhook_bp.route("/webhook/deploy", methods=["POST"])
def deploy():
    if request.headers.get("Authorization") != f"Bearer {WEBHOOK_TOKEN}":
        abort(403, description="Unauthorized")

    webhook_service.deploy()
    return "Deployment successful", 200

The token comparison is the only authentication, so treat WEBHOOK_TOKEN as a production credential.

WebhookService.deploy() then performs the deployment inside the running web container, in this order:

  1. Run scripts/git_update.sh to pull the new code.
  2. Run pip install -r requirements.txt to refresh dependencies.
  3. Run flask db upgrade to apply pending migrations.
  4. Append a timestamped entry to /workspace/deployments.log.
  5. Restart the container through scripts/restart_container.sh.

Note that the service needs the Docker CLI and the Docker socket, which is why the webhook feature belongs to a Docker based deployment and is declared under features_dev in the root pyproject.toml:

[tool.splent]
features_dev = [
    "webhook",
]

Testing the endpoint by hand

You can trigger a deployment without going through GitHub, which is the fastest way to tell whether a failed deployment is a workflow problem or a server problem:

curl -i -X POST \
  https://<your-domain>/webhook/deploy \
  -H "Authorization: Bearer <your-token>"

Expected responses:

  • 200 Deployment successful: the deployment ran.
  • 403: the token does not match the server’s WEBHOOK_TOKEN.
  • 404: the container named web_app_container was not found on the host.
  • 405: you sent a GET. The route only accepts POST.