A FastAPI-based server that listens for GitHub workflow_run events and automatically
processes PDB symbol files into a Microsoft-compatible symbol store.
When a CI workflow completes successfully, GitHub POSTs a workflow_run event here.
The server finds the pdbs artifact for the run, downloads it using a configured PAT,
extracts the PDBs into the symbol store, and upserts a build record into the downloads DB.
An hourly APScheduler job reconciles any builds that were missed (e.g. while the server was down).
- Install uv, then install dependencies:
uv sync-
Configure the application by editing
config.yaml(see Configuration). -
Register a GitHub webhook on your repo:
- Payload URL: your server's
/webhookendpoint - Content type:
application/json - Events: Workflow runs
- Secret: the value you set in
github.webhook_secret
- Payload URL: your server's
/data/symbols/
└── sourcemod/ # repo.product_name (must be pre-created by an admin)
└── ... # Microsoft symstore layout
The product subdirectory must exist before the server starts processing builds. The server will not create it automatically.
The server is configured via config.yaml. Any value can be overridden with an
environment variable using the SECTION__KEY pattern (double underscore):
api:
host: "0.0.0.0" # env: API__HOST
port: 5000 # env: API__PORT
storage:
symbol_store_base_path: "/data/symbols" # env: STORAGE__SYMBOL_STORE_BASE_PATH
github:
webhook_secret: "..." # Required; env: GITHUB__WEBHOOK_SECRET
token: "ghp_..." # PAT for downloading Actions artifacts; env: GITHUB__TOKEN
retry_attempts: 3
retry_delay: 5
database:
host: "db-host"
port: 3306
user: "sm_commit_update"
password: "..." # env: DATABASE__PASSWORD
name: "sourcemod"
commit_log_table: "sm_commit_log" # env: DATABASE__COMMIT_LOG_TABLE; default: sm_commit_log
repo:
owner: "alliedmodders"
name: "sourcemod"
product_name: "sourcemod" # Subdirectory under symbol_store_base_path
log:
level: "INFO" # env: LOG__LEVELstorage is optional. Omitting it disables PDB symbol store processing and build-drop
downloads — useful for products that only need commit log tracking.
database and repo are optional — omitting them disables DB upsert and reconciliation.
The repo section supports additional options:
repo:
owner: "alliedmodders"
name: "sourcemod"
product_name: "sourcemod"
workflow_path: ".github/workflows/build-release.yml" # Workflow to monitor
version_branches: # Maps version prefix → branch name
"1.12": "1.12-dev"
"1.13": "master"
reconcile_max_age_days: 90 # How far back reconciliation looks (null = no limit)
asset_match_filter: null # If set, only release assets containing this string
# are matched for the windows/linux URL columns.
# Useful for multi-package releases, e.g. set to "base"
# to match only the base package archive.uv run python app.pyOr directly via uvicorn:
uv run uvicorn app:app --host 0.0.0.0 --port 5000Returns {"status": "ok"}. Used for monitoring and deployment verification.
Receives GitHub webhook events. All requests must include a valid
X-Hub-Signature-256 header (HMAC-SHA256 of the body using github.webhook_secret).
Handled events:
workflow_run(action: completed,conclusion: success): downloads thepdbsartifact for the run and processes it into the symbol store.ping: returns{"message": "pong", ...}.
All other events return 202 with an "unhandled event" message.
- Authentication: HMAC-SHA256 signature on every request (
X-Hub-Signature-256). The server returns500at startup ifwebhook_secretis not configured. - Path traversal: product names are validated (alphanumeric, hyphens, underscores only); all resolved paths are checked to remain within their base directories.
- Zip slip: ZIP entries are validated before extraction.
- Temp files: created with mode
0o700and deleted after use.
Use test_webhook.py to send a simulated workflow_run event:
# Test signature rejection (no secret):
python test_webhook.py --url http://localhost:5000
# Send a real run ID and commit SHA:
python test_webhook.py \
--url http://localhost:5000 \
--secret YOUR_WEBHOOK_SECRET \
--run-id 12345678 \
--head-sha abc123...
# Dry run (print request without sending):
python test_webhook.py --dry-run --secret mysecret --run-id 123The application logs to stdout. Log level is configured via config.yaml (log.level)
or the LOG__LEVEL environment variable.