relay: take the Hermes route from the request path
Renames the subscription to gitea-pr-comments (it handles one event; the old
gitea-events name promised more than it delivered) and drops --deliver.
Rather than move the hardcoded route from one constant to another, the relay
now reads it from the request path: POST /gitea/<route> forwards to
<base>/webhooks/<route>. The route name was the last thing tying this service
to a specific subscription, so a second Hermes route is now a `hermes webhook
subscribe <name>` plus a Gitea hook at /gitea/<name>, with no relay change --
previously it would also have needed a second relay URL baked in here.
The path segment is interpolated into an outbound URL, so it is validated
against ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ and refused rather than sanitised
when it does not match. The path is matched raw and never URL-decoded, so
percent-encoded separators fail the charset check instead of surviving it;
requiring an alphanumeric first character also rejects "." and "..". Without
this, POST /gitea/..%2fadmin would let anything that can reach the relay
steer it at other Hermes endpoints. Tests cover traversal, encoded traversal,
embedded slashes, leading dot/dash, and the length bound, and assert nothing
reaches the stub Hermes in any of those cases.
Dropping --deliver leaves it at its default of `log`. The prompt tells her to
answer in the pull request, so the PR comment is the delivery and a Telegram
copy would only duplicate it; this also removes the hardcoded chat id that
was a third copy of TELEGRAM_HOME_CHANNEL.
Provisioning retires the pre-rename hook by its EXACT old URL rather than by
"points at the relay". Now that sibling hooks for other routes are the
intended pattern, a prefix match would delete them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S94o42aQ8VkBmEWvDem5xa
This commit is contained in:
@@ -44,7 +44,14 @@ authenticated request body and Gitea's own signature to Hermes over localhost
|
|||||||
completely unchanged, and copies `X-Gitea-Event` into `X-GitHub-Event`. It has
|
completely unchanged, and copies `X-Gitea-Event` into `X-GitHub-Event`. It has
|
||||||
no event, repository, action, payload, or prompt policy; Hermes owns
|
no event, repository, action, payload, or prompt policy; Hermes owns
|
||||||
interpretation and response behavior. Jupiter's Gitea provisioning service
|
interpretation and response behavior. Jupiter's Gitea provisioning service
|
||||||
registers the webhook idempotently at `http://mars.orbit.sol:8645/gitea`.
|
registers the webhook idempotently at
|
||||||
|
`http://mars.orbit.sol:8645/gitea/gitea-pr-comments`.
|
||||||
|
|
||||||
|
The path after `/gitea/` names the Hermes route to forward into, so the relay
|
||||||
|
is not tied to any one subscription: another Hermes route needs a
|
||||||
|
`hermes webhook subscribe <name>` and a Gitea hook pointing at
|
||||||
|
`/gitea/<name>`, and no relay change. Route names are validated against a
|
||||||
|
strict charset before being used in the outbound URL.
|
||||||
|
|
||||||
That one header copy is the entire reason the relay exists. Gitea signs every
|
That one header copy is the entire reason the relay exists. Gitea signs every
|
||||||
webhook with `X-Hub-Signature-256` in GitHub's exact format, which Hermes
|
webhook with `X-Hub-Signature-256` in GitHub's exact format, which Hermes
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ open(os.path.join(creds, "webhook_secret"), "wb").write(SECRET + b"\n")
|
|||||||
|
|
||||||
env = {**os.environ, "CREDENTIALS_DIRECTORY": creds, "LISTEN_HOST": "127.0.0.1",
|
env = {**os.environ, "CREDENTIALS_DIRECTORY": creds, "LISTEN_HOST": "127.0.0.1",
|
||||||
"LISTEN_PORT": str(RELAY_PORT),
|
"LISTEN_PORT": str(RELAY_PORT),
|
||||||
"HERMES_WEBHOOK_URL": f"http://127.0.0.1:{HERMES_PORT}/webhooks/gitea-events"}
|
"HERMES_WEBHOOK_BASE": f"http://127.0.0.1:{HERMES_PORT}/webhooks",
|
||||||
|
"DEFAULT_ROUTE": "gitea-pr-comments"}
|
||||||
RELAY = str(pathlib.Path(__file__).with_name('gitea-hermes-webhook-relay.py'))
|
RELAY = str(pathlib.Path(__file__).with_name('gitea-hermes-webhook-relay.py'))
|
||||||
relay = subprocess.Popen([sys.executable, RELAY],
|
relay = subprocess.Popen([sys.executable, RELAY],
|
||||||
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
@@ -49,8 +50,8 @@ for _ in range(50):
|
|||||||
urllib.request.urlopen(f"http://127.0.0.1:{RELAY_PORT}/health", timeout=1); break
|
urllib.request.urlopen(f"http://127.0.0.1:{RELAY_PORT}/health", timeout=1); break
|
||||||
except Exception: time.sleep(0.1)
|
except Exception: time.sleep(0.1)
|
||||||
|
|
||||||
def post(body, headers):
|
def post(body, headers, path="/gitea"):
|
||||||
req = urllib.request.Request(f"http://127.0.0.1:{RELAY_PORT}/gitea", data=body,
|
req = urllib.request.Request(f"http://127.0.0.1:{RELAY_PORT}{path}", data=body,
|
||||||
headers=headers, method="POST")
|
headers=headers, method="POST")
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=5) as r: return r.status, json.load(r)
|
with urllib.request.urlopen(req, timeout=5) as r: return r.status, json.load(r)
|
||||||
@@ -90,7 +91,8 @@ check("signature still valid over forwarded body",
|
|||||||
fwd["headers"]["x-hub-signature-256"].removeprefix("sha256="),
|
fwd["headers"]["x-hub-signature-256"].removeprefix("sha256="),
|
||||||
hmac.new(SECRET, fwd["body"], hashlib.sha256).hexdigest()))
|
hmac.new(SECRET, fwd["body"], hashlib.sha256).hexdigest()))
|
||||||
check("delivery id propagated", fwd["headers"].get("x-request-id") == "abc-123")
|
check("delivery id propagated", fwd["headers"].get("x-request-id") == "abc-123")
|
||||||
check("path preserved", fwd["path"] == "/webhooks/gitea-events")
|
check("bare /gitea uses DEFAULT_ROUTE", fwd["path"] == "/webhooks/gitea-pr-comments",
|
||||||
|
f"got {fwd['path']}")
|
||||||
|
|
||||||
# 3. gitea-only signature header (no X-Hub-Signature-256)
|
# 3. gitea-only signature header (no X-Hub-Signature-256)
|
||||||
received.clear()
|
received.clear()
|
||||||
@@ -120,6 +122,38 @@ check("normal-size body still ok", st == 200)
|
|||||||
st, _ = post(payload, {"X-Hub-Signature-256": "sha256=" + sig})
|
st, _ = post(payload, {"X-Hub-Signature-256": "sha256=" + sig})
|
||||||
check("POST /gitea ok baseline", st == 200)
|
check("POST /gitea ok baseline", st == 200)
|
||||||
|
|
||||||
|
# 7. route travels in the path: /gitea/<route> -> /webhooks/<route>
|
||||||
|
hdrs = {"Content-Type": "application/json", "X-Gitea-Event": "push",
|
||||||
|
"X-Hub-Signature-256": "sha256=" + sig}
|
||||||
|
for route in ("gitea-pr-comments", "some-other_route.v2", "a"):
|
||||||
|
received.clear()
|
||||||
|
st, resp = post(payload, hdrs, path=f"/gitea/{route}")
|
||||||
|
check(f"route {route!r} forwarded to /webhooks/{route}",
|
||||||
|
st == 200 and received and received[0]["path"] == f"/webhooks/{route}",
|
||||||
|
f"status={st} path={received[0]['path'] if received else None}")
|
||||||
|
check(f"route {route!r} echoed in response", resp.get("route") == route, f"got {resp}")
|
||||||
|
|
||||||
|
# 8. route validation — these must never reach Hermes at all
|
||||||
|
for bad, label in [
|
||||||
|
("../admin", "parent-dir traversal"),
|
||||||
|
("..%2fadmin", "encoded traversal"),
|
||||||
|
("..", "bare .."),
|
||||||
|
(".", "bare ."),
|
||||||
|
(".hidden", "leading dot"),
|
||||||
|
("-dash", "leading dash"),
|
||||||
|
("route%20name", "percent-encoded space"),
|
||||||
|
("route/extra", "embedded slash"),
|
||||||
|
("x" * 65, "over length limit"),
|
||||||
|
]:
|
||||||
|
received.clear()
|
||||||
|
st, _ = post(payload, hdrs, path=f"/gitea/{bad}")
|
||||||
|
check(f"rejects {label}", st == 404 and not received,
|
||||||
|
f"status={st} forwarded={len(received)}")
|
||||||
|
|
||||||
|
received.clear()
|
||||||
|
st, _ = post(payload, hdrs, path="/webhooks/gitea-pr-comments")
|
||||||
|
check("rejects non-/gitea prefix", st == 404 and not received, f"status={st}")
|
||||||
|
|
||||||
relay.terminate(); relay.wait(timeout=5); hermes.shutdown()
|
relay.terminate(); relay.wait(timeout=5); hermes.shutdown()
|
||||||
print()
|
print()
|
||||||
print(f"{'ALL PASSED' if not fails else 'FAILURES: ' + ', '.join(fails)}")
|
print(f"{'ALL PASSED' if not fails else 'FAILURES: ' + ', '.join(fails)}")
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ in
|
|||||||
environment = {
|
environment = {
|
||||||
LISTEN_HOST = "0.0.0.0";
|
LISTEN_HOST = "0.0.0.0";
|
||||||
LISTEN_PORT = "8645";
|
LISTEN_PORT = "8645";
|
||||||
HERMES_WEBHOOK_URL = "http://127.0.0.1:8644/webhooks/gitea-events";
|
# Base only. The Hermes route rides in the request path
|
||||||
|
# (/gitea/<route>), so this relay is not tied to any one subscription;
|
||||||
|
# DEFAULT_ROUTE only serves the legacy bare /gitea path.
|
||||||
|
HERMES_WEBHOOK_BASE = "http://127.0.0.1:8644/webhooks";
|
||||||
|
DEFAULT_ROUTE = "gitea-pr-comments";
|
||||||
MAX_BODY_BYTES = "1048576";
|
MAX_BODY_BYTES = "1048576";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -76,12 +80,19 @@ in
|
|||||||
# pull_request_comment as a value distinct from issue_comment, so plain issue
|
# pull_request_comment as a value distinct from issue_comment, so plain issue
|
||||||
# comments do not reach the agent.
|
# comments do not reach the agent.
|
||||||
#
|
#
|
||||||
# A route carries exactly one prompt, so widening this list means either
|
# A route carries exactly one prompt, so widening this list means branching
|
||||||
# branching inside the prompt on {action}/{issue.number}, or adding a second
|
# inside the prompt on {action}, or adding a second subscription. The second
|
||||||
# subscription (and a second relay URL) for the other events. The Gitea-side
|
# subscription is cheap now: the relay takes its target route from the
|
||||||
# hook still sends the full event set at the relay; Hermes drops the
|
# request path, so it is a new `hermes webhook subscribe <name>` plus a
|
||||||
|
# Gitea hook pointing at /gitea/<name>, with no relay change at all. The
|
||||||
|
# Gitea-side hook still sends the full event set; Hermes drops the
|
||||||
# non-matching ones cheaply, before any LLM call.
|
# non-matching ones cheaply, before any LLM call.
|
||||||
#
|
#
|
||||||
|
# No --deliver: it defaults to `log`. The prompt tells her to answer in the
|
||||||
|
# pull request, so the PR comment IS the delivery, and a Telegram copy would
|
||||||
|
# just duplicate it. This also drops the hardcoded chat id that used to be a
|
||||||
|
# third copy of TELEGRAM_HOME_CHANNEL.
|
||||||
|
#
|
||||||
# --script does the selection that MUST NOT be retunable at runtime.
|
# --script does the selection that MUST NOT be retunable at runtime.
|
||||||
# hosts/mars/gitea-pr-comment-filter.py drops luna's own comments before
|
# hosts/mars/gitea-pr-comment-filter.py drops luna's own comments before
|
||||||
# any LLM call, which is what stops the reply loop: the prompt tells her to
|
# any LLM call, which is what stops the reply loop: the prompt tells her to
|
||||||
@@ -127,8 +138,12 @@ in
|
|||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
podman exec hermes-agent hermes webhook remove gitea-pr-comments >/dev/null 2>&1 || true
|
# gitea-events is the old name of this route (renamed to say what it
|
||||||
|
# actually handles); removing it keeps a redeployed host from serving
|
||||||
|
# both. The second remove is the idempotency step for the subscribe
|
||||||
|
# below, not cleanup.
|
||||||
podman exec hermes-agent hermes webhook remove gitea-events >/dev/null 2>&1 || true
|
podman exec hermes-agent hermes webhook remove gitea-events >/dev/null 2>&1 || true
|
||||||
|
podman exec hermes-agent hermes webhook remove gitea-pr-comments >/dev/null 2>&1 || true
|
||||||
# `set -eu` inside the container shell is load-bearing: without it a
|
# `set -eu` inside the container shell is load-bearing: without it a
|
||||||
# missing prompt file makes `cat` fail, the command substitution yields
|
# missing prompt file makes `cat` fail, the command substitution yields
|
||||||
# an empty string, and the subscription is created with an EMPTY prompt
|
# an empty string, and the subscription is created with an EMPTY prompt
|
||||||
@@ -138,13 +153,12 @@ in
|
|||||||
set -eu
|
set -eu
|
||||||
prompt="$(cat /opt/data/prompts/gitea-pr-comment.md)"
|
prompt="$(cat /opt/data/prompts/gitea-pr-comment.md)"
|
||||||
[ -n "$prompt" ] || { echo "gitea-pr-comment prompt is empty" >&2; exit 1; }
|
[ -n "$prompt" ] || { echo "gitea-pr-comment prompt is empty" >&2; exit 1; }
|
||||||
hermes webhook subscribe gitea-events \
|
hermes webhook subscribe gitea-pr-comments \
|
||||||
--secret "$GITEA_HERMES_WEBHOOK_SECRET" \
|
--secret "$GITEA_HERMES_WEBHOOK_SECRET" \
|
||||||
--description "Gitea PR comments -> L.U.N.A." \
|
--description "Gitea PR comments -> L.U.N.A." \
|
||||||
--events pull_request_comment \
|
--events pull_request_comment \
|
||||||
--script gitea-pr-comment-filter.py \
|
--script gitea-pr-comment-filter.py \
|
||||||
--prompt "$prompt" \
|
--prompt "$prompt"
|
||||||
--deliver telegram --deliver-chat-id "15151223"
|
|
||||||
'
|
'
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ process re-signs nothing and rewrites no payload. It copies one header.
|
|||||||
|
|
||||||
It still verifies the signature itself rather than forwarding blindly, so an
|
It still verifies the signature itself rather than forwarding blindly, so an
|
||||||
unauthenticated caller that reaches this port never reaches the agent.
|
unauthenticated caller that reaches this port never reaches the agent.
|
||||||
|
|
||||||
|
The target Hermes route travels in the request path (POST /gitea/<route> ->
|
||||||
|
POST <base>/webhooks/<route>) rather than being configured here, so one relay
|
||||||
|
serves every subscription and adding a Hermes route means adding a Gitea hook
|
||||||
|
URL, nothing more.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -27,6 +32,7 @@ import hmac
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.error import HTTPError, URLError
|
from urllib.error import HTTPError, URLError
|
||||||
@@ -36,10 +42,23 @@ LOG = logging.getLogger("gitea-hermes-webhook-relay")
|
|||||||
|
|
||||||
LISTEN_HOST = os.environ.get("LISTEN_HOST", "0.0.0.0")
|
LISTEN_HOST = os.environ.get("LISTEN_HOST", "0.0.0.0")
|
||||||
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8645"))
|
LISTEN_PORT = int(os.environ.get("LISTEN_PORT", "8645"))
|
||||||
HERMES_URL = os.environ.get(
|
# The Hermes route is taken from the request path (POST /gitea/<route>), not
|
||||||
"HERMES_WEBHOOK_URL",
|
# baked in here, so one relay serves every subscription: a new Hermes route
|
||||||
"http://127.0.0.1:8644/webhooks/gitea-events",
|
# needs a new Gitea hook URL and nothing else. HERMES_WEBHOOK_BASE is the
|
||||||
)
|
# prefix the route name is appended to; DEFAULT_ROUTE serves the legacy bare
|
||||||
|
# /gitea and / paths.
|
||||||
|
HERMES_WEBHOOK_BASE = os.environ.get(
|
||||||
|
"HERMES_WEBHOOK_BASE",
|
||||||
|
"http://127.0.0.1:8644/webhooks",
|
||||||
|
).rstrip("/")
|
||||||
|
DEFAULT_ROUTE = os.environ.get("DEFAULT_ROUTE", "gitea-pr-comments")
|
||||||
|
|
||||||
|
# The route name is interpolated into an outbound URL, so it is validated
|
||||||
|
# strictly rather than sanitised: anything outside this charset is refused
|
||||||
|
# instead of being cleaned up. This is what stops POST /gitea/..%2fadmin (or
|
||||||
|
# any other traversal) from steering the relay at a different Hermes endpoint.
|
||||||
|
# Leading character must be alphanumeric, which also rejects "." and "..".
|
||||||
|
ROUTE_RE = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}")
|
||||||
MAX_BODY_BYTES = int(os.environ.get("MAX_BODY_BYTES", str(1024 * 1024)))
|
MAX_BODY_BYTES = int(os.environ.get("MAX_BODY_BYTES", str(1024 * 1024)))
|
||||||
CREDENTIAL_NAME = os.environ.get("WEBHOOK_CREDENTIAL_NAME", "webhook_secret")
|
CREDENTIAL_NAME = os.environ.get("WEBHOOK_CREDENTIAL_NAME", "webhook_secret")
|
||||||
|
|
||||||
@@ -86,6 +105,25 @@ def signature_matches(secret: bytes, body: bytes, headers) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def route_from_path(path: str) -> str | None:
|
||||||
|
"""Map a request path to a Hermes route name, or None if it is not ours.
|
||||||
|
|
||||||
|
/gitea/<route> -> <route>; /gitea and / -> DEFAULT_ROUTE.
|
||||||
|
The path is matched raw, never URL-decoded, so percent-encoded separators
|
||||||
|
fail the charset check rather than surviving it.
|
||||||
|
"""
|
||||||
|
path = path.split("?", 1)[0].split("#", 1)[0]
|
||||||
|
if path in ("/", "/gitea"):
|
||||||
|
return DEFAULT_ROUTE
|
||||||
|
prefix = "/gitea/"
|
||||||
|
if not path.startswith(prefix):
|
||||||
|
return None
|
||||||
|
route = path[len(prefix):].rstrip("/")
|
||||||
|
if not ROUTE_RE.fullmatch(route):
|
||||||
|
return None
|
||||||
|
return route
|
||||||
|
|
||||||
|
|
||||||
class Handler(BaseHTTPRequestHandler):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
server_version = "gitea-hermes-relay/1.0"
|
server_version = "gitea-hermes-relay/1.0"
|
||||||
|
|
||||||
@@ -107,9 +145,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_json(404, {"status": "not_found"})
|
self.send_json(404, {"status": "not_found"})
|
||||||
|
|
||||||
def do_POST(self) -> None:
|
def do_POST(self) -> None:
|
||||||
if self.path not in {"/gitea", "/"}:
|
route = route_from_path(self.path)
|
||||||
|
if route is None:
|
||||||
|
LOG.warning("rejected POST to unroutable path %r", self.path)
|
||||||
self.send_json(404, {"status": "not_found"})
|
self.send_json(404, {"status": "not_found"})
|
||||||
return
|
return
|
||||||
|
hermes_url = f"{HERMES_WEBHOOK_BASE}/{route}"
|
||||||
|
|
||||||
raw_length = self.headers.get("Content-Length")
|
raw_length = self.headers.get("Content-Length")
|
||||||
if raw_length is None:
|
if raw_length is None:
|
||||||
@@ -171,7 +212,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
forwarded_headers["X-Gitea-Delivery"] = delivery_id
|
forwarded_headers["X-Gitea-Delivery"] = delivery_id
|
||||||
|
|
||||||
request = Request(
|
request = Request(
|
||||||
HERMES_URL,
|
hermes_url,
|
||||||
data=body,
|
data=body,
|
||||||
headers=forwarded_headers,
|
headers=forwarded_headers,
|
||||||
method="POST",
|
method="POST",
|
||||||
@@ -189,11 +230,12 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"forwarded Gitea event=%s delivery=%s",
|
"forwarded Gitea event=%s delivery=%s to route=%s",
|
||||||
gitea_event or gitea_event_type or "unknown",
|
gitea_event or gitea_event_type or "unknown",
|
||||||
delivery_id or "none",
|
delivery_id or "none",
|
||||||
|
route,
|
||||||
)
|
)
|
||||||
self.send_json(200, {"status": "forwarded"})
|
self.send_json(200, {"status": "forwarded", "route": route})
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -202,7 +244,10 @@ def main() -> None:
|
|||||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||||
)
|
)
|
||||||
server = ThreadingHTTPServer((LISTEN_HOST, LISTEN_PORT), Handler)
|
server = ThreadingHTTPServer((LISTEN_HOST, LISTEN_PORT), Handler)
|
||||||
LOG.info("listening on %s:%s; forwarding to %s", LISTEN_HOST, LISTEN_PORT, HERMES_URL)
|
LOG.info(
|
||||||
|
"listening on %s:%s; forwarding to %s/<route> (default route %s)",
|
||||||
|
LISTEN_HOST, LISTEN_PORT, HERMES_WEBHOOK_BASE, DEFAULT_ROUTE,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
|||||||
+19
-2
@@ -332,7 +332,17 @@ in
|
|||||||
admin_token="$(cat "$TOKEN_FILE")"
|
admin_token="$(cat "$TOKEN_FILE")"
|
||||||
secret="$(cat "$SECRET_FILE")"
|
secret="$(cat "$SECRET_FILE")"
|
||||||
auth=(-H "Authorization: token $admin_token")
|
auth=(-H "Authorization: token $admin_token")
|
||||||
target="http://mars.orbit.sol:8645/gitea"
|
# The path carries the Hermes route the relay should forward into, so
|
||||||
|
# each Hermes subscription gets its own hook here and the relay itself
|
||||||
|
# stays generic. Adding one is a new subscribe + a new hook URL.
|
||||||
|
relay="http://mars.orbit.sol:8645"
|
||||||
|
target="$relay/gitea/gitea-pr-comments"
|
||||||
|
|
||||||
|
# Retire the pre-rename hook, which posted to the relay's bare path and
|
||||||
|
# would now double-deliver alongside $target. Matched by its EXACT old
|
||||||
|
# URL, deliberately: anything else pointing at $relay is a hook for a
|
||||||
|
# different Hermes route and must survive.
|
||||||
|
legacy_target="$relay/gitea"
|
||||||
|
|
||||||
# Same readiness gate as gitea-ci-bot-provision / gitea-luna-provision
|
# Same readiness gate as gitea-ci-bot-provision / gitea-luna-provision
|
||||||
# above: After=gitea.service only means the process started, not that it
|
# above: After=gitea.service only means the process started, not that it
|
||||||
@@ -351,7 +361,14 @@ in
|
|||||||
--argjson events '${builtins.toJSON giteaWebhookEvents}' \
|
--argjson events '${builtins.toJSON giteaWebhookEvents}' \
|
||||||
'{type: "gitea", config: {content_type: "json", url: $url, secret: $secret}, events: $events, active: true}')"
|
'{type: "gitea", config: {content_type: "json", url: $url, secret: $secret}, events: $events, active: true}')"
|
||||||
|
|
||||||
hook_id="$(curl -fsS "''${auth[@]}" "$api/repos/darman/homelab/hooks" \
|
hooks="$(curl -fsS "''${auth[@]}" "$api/repos/darman/homelab/hooks")"
|
||||||
|
|
||||||
|
for stale in $(printf '%s' "$hooks" \
|
||||||
|
| jq -r --arg url "$legacy_target" '.[] | select(.type == "gitea" and .config.url == $url) | .id'); do
|
||||||
|
curl -fsS "''${auth[@]}" -X DELETE "$api/repos/darman/homelab/hooks/$stale" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
hook_id="$(printf '%s' "$hooks" \
|
||||||
| jq -r --arg url "$target" 'first(.[] | select(.type == "gitea" and .config.url == $url)) | .id // empty')"
|
| jq -r --arg url "$target" 'first(.[] | select(.type == "gitea" and .config.url == $url)) | .id // empty')"
|
||||||
if [ -n "$hook_id" ]; then
|
if [ -n "$hook_id" ]; then
|
||||||
printf '%s' "$body" | curl -fsS "''${auth[@]}" -H 'Content-Type: application/json' \
|
printf '%s' "$body" | curl -fsS "''${auth[@]}" -H 'Content-Type: application/json' \
|
||||||
|
|||||||
Reference in New Issue
Block a user