Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit36ec667

feat(vapron-deploy): match the platform deploy-agent's webhook contract

feat(vapron-deploy): match the platform deploy-agent's webhook contract

Reconciling Gluecron's deploy-webhook SENDER with Vapron's actual
receiver so pushing ccantynz/Vapron to Gluecron can deploy Vapron itself.

Read Vapron's receiver code (services/deploy-agent/src/webhook.ts): the
platform self-deploy agent — reached at https://vapron.ai/__deploy_webhook
— expects the signature in the `X-Gluecron-Signature-256` header
(sha256=<hex> HMAC of the raw body with its DEPLOY_AGENT_HMAC secret),
a payload carrying `ref` + `after` (it ignores the rest), and only
deploys `refs/heads/Main`. It's deliberately GitHub-webhook-compatible.

Gluecron already sends a matching payload and signature FORMAT, but only
under the `X-Gluecron-Signature` header name (which the *tenant* receiver
at /api/hooks/gluecron/push reads). So `triggerVapronDeploy` now sends
BOTH header names with the same value — one sender works for both Vapron
receivers, no per-endpoint shim.

Also fixed the .env.example VAPRON_* docs: the old VAPRON_DEPLOY_URL
(/api/webhooks/gluecron-push) is a 404; corrected to /__deploy_webhook,
and VAPRON_REPO corrected to the case-sensitive ccantynz/Vapron.

This is the Gluecron-side code half. Remaining (pending SSH access to
Vapron's app box at 149.28.119.158): generate one shared secret and set
VAPRON_HMAC_SECRET (Gluecron) = DEPLOY_AGENT_HMAC (Vapron), set
VAPRON_REPO + VAPRON_DEPLOY_URL in the box .env (now reaches the
container via the env_file fix), point Vapron's deploy-agent poll remote
at Gluecron + neutralize the GitHub drift, then a live end-to-end test
push. typecheck 0 errors; vapron-deploy tests 24 pass; full suite 3110
pass, same 4 pre-existing unrelated fails.
ccantynz-alt committed on July 22, 2026Parent: 95d0b72
3 files changed+321236ec667ca17f68380a2f7134a8190e4dcfd728b3
3 changed files+32−12
Modified.env.example+15−10View fileUnifiedSplit
3131# ─────────────────────────────────────────────────────────────────────────
3232# Vapron (formerly Crontech) deploy integration. Legacy CRONTECH_* names
3333# are still honored as fallbacks for already-configured deployments.
34VAPRON_DEPLOY_URL=https://vapron.ai/api/webhooks/gluecron-push
35# BLK-016 — only fire the Vapron deploy webhook for pushes to this
36# `<owner>/<name>` (default `ccantynz-alt/vapron`). All other repo pushes
37# are ignored.
38VAPRON_REPO=ccantynz-alt/vapron
39# Shared HMAC secret used to sign the outbound Vapron deploy webhook.
40# Sent as `X-Gluecron-Signature: sha256=<hex>` of the JSON body. Must match
41# the secret configured on the Vapron deploy-agent. Unset → header omitted
42# and Vapron rejects with 401. (Legacy fallbacks: CRONTECH_HMAC_SECRET,
43# GLUECRON_WEBHOOK_SECRET.) Prefer setting this on /admin/integrations.
34#
35# To deploy VAPRON ITSELF from Gluecron (push to ccantynz/Vapron → Vapron
36# rebuilds), point this at the deploy-agent's Caddy-proxied webhook. The
37# OLD value below (/api/webhooks/gluecron-push) is a 404 — do NOT use it.
38VAPRON_DEPLOY_URL=https://vapron.ai/__deploy_webhook
39# Only fire the Vapron deploy webhook for pushes to this exact,
40# CASE-SENSITIVE `<owner>/<name>`. Self-hosted Vapron on Gluecron lives at
41# `ccantynz/Vapron` (capital V). Default `ccantynz-alt/vapron` will NOT match.
42VAPRON_REPO=ccantynz/Vapron
43# Shared HMAC secret used to sign the outbound Vapron deploy webhook. Sent
44# as BOTH `X-Gluecron-Signature` and `X-Gluecron-Signature-256` = sha256=<hex>
45# of the JSON body. Must equal the Vapron deploy-agent's `DEPLOY_AGENT_HMAC`.
46# Unset → header omitted and Vapron rejects with 401. (Legacy fallbacks:
47# CRONTECH_HMAC_SECRET, GLUECRON_WEBHOOK_SECRET.) Generate one strong value
48# and set it identically on both boxes; prefer /admin/integrations here.
4449VAPRON_HMAC_SECRET=
4550# Inbound bearer token Vapron MUST present on deploy.succeeded /
4651# deploy.failed callbacks to POST /api/events/deploy (Signal Bus P1 — E3/E4).
Modifiedsrc/__tests__/vapron-deploy.test.ts+6−1View fileUnifiedSplit
196196 .update(sentBody)
197197 .digest("hex");
198198 expect(headers["X-Gluecron-Signature"]).toBe(expected);
199 // Also sent as X-Gluecron-Signature-256 (same value) so the platform
200 // deploy-agent (/__deploy_webhook), which reads the -256 header, accepts
201 // the exact same request as the tenant receiver.
202 expect(headers["X-Gluecron-Signature-256"]).toBe(expected);
199203 expect(headers["Content-Type"]).toBe("application/json");
200204 });
201205
202 it("omits X-Gluecron-Signature when no secret is configured", async () => {
206 it("omits both signature headers when no secret is configured", async () => {
203207 const { calls, fn } = captureFetch();
204208
205209 await triggerVapronDeploy(makeArgs(), { fetchImpl: fn, sleep: noSleep });
206210
207211 const headers = calls[0]!.init.headers as Record<string, string>;
208212 expect(headers["X-Gluecron-Signature"]).toBeUndefined();
213 expect(headers["X-Gluecron-Signature-256"]).toBeUndefined();
209214 });
210215
211216 it("sends Authorization: Bearer <VAPRON_API_KEY> when the tenant key is set", async () => {
Modifiedsrc/hooks/post-receive.ts+11−1View fileUnifiedSplit
584584 "X-Gluecron-Event": "push",
585585 "X-Gluecron-Delivery": cryptoRandomId(),
586586 };
587 if (signature) headers["X-Gluecron-Signature"] = signature;
587 if (signature) {
588 // Send BOTH signature header names so one sender works for both Vapron
589 // receivers: the tenant/customer webhook (/api/hooks/gluecron/push) reads
590 // `X-Gluecron-Signature`, while the platform self-deploy agent
591 // (/__deploy_webhook, services/deploy-agent) reads
592 // `X-Gluecron-Signature-256`. Same `sha256=<hex>` value; the deploy-agent
593 // is intentionally GitHub-webhook-compatible (see its webhook.ts), so no
594 // per-endpoint shim is needed beyond matching the header name.
595 headers["X-Gluecron-Signature"] = signature;
596 headers["X-Gluecron-Signature-256"] = signature;
597 }
588598 // Vapron tenant auth: API key rides as a bearer alongside the HMAC
589599 // signature (key = who is calling, signature = payload integrity).
590600 if (config.vapronApiKey)
591601