upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/ci/scripts
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-23 23:31:31 +0530
committerYour Name <you@example.com>2026-05-23 23:31:31 +0530
commitcfa17135a034207f4005f754a94093ebc3d547d2 (patch)
tree1c4a3d93b27d273b58be636cdafabf202b50bfa7 /ci/scripts
parent85ef20a2004c600dd04428520a79a0e47a54ed2d (diff)
ci: add Ansible CI/CD workflow with nostr relay listener
- ci_listener.py: WebSocket client subscribed to ngit.orangesync.tech for NIP-34 git state events (kind 30617), triggers pipeline on push - run_pipeline.sh: clone + make test-unit + Docker ESP-IDF build - systemd service: tollgate-ci-listener with auto-restart - Ansible role ci_runner: Docker, ESP-IDF image, listener deployment - 20-ci-runner.yml: playbook for CI runner setup - 30-hardware-tests.yml: playbook for localhost board testing
Diffstat (limited to 'ci/scripts')
-rwxr-xr-xci/scripts/ci_listener.py145
-rwxr-xr-xci/scripts/run_pipeline.sh80
2 files changed, 225 insertions, 0 deletions
diff --git a/ci/scripts/ci_listener.py b/ci/scripts/ci_listener.py
new file mode 100755
index 0000000..50549a9
--- /dev/null
+++ b/ci/scripts/ci_listener.py
@@ -0,0 +1,145 @@
1#!/usr/bin/env python3
2"""Nostr relay listener for CI/CD triggers.
3
4Subscribes to wss://ngit.orangesync.tech for NIP-34 git state events (kind 30617).
5When a push event is detected for esp32-tollgate, triggers the CI pipeline.
6"""
7
8import json
9import asyncio
10import hashlib
11import subprocess
12import logging
13import os
14import sys
15import time
16from datetime import datetime, timezone
17
18try:
19 import websockets
20except ImportError:
21 print("websockets not installed. Run: pip install websockets")
22 sys.exit(1)
23
24logging.basicConfig(
25 level=logging.INFO,
26 format="%(asctime)s [%(levelname)s] %(message)s",
27)
28log = logging.getLogger("ci-listener")
29
30RELAY_URL = os.environ.get("CI_RELAY_URL", "wss://ngit.orangesync.tech")
31REPO_ID = os.environ.get("CI_REPO_ID", "npub12m5exm2uk3xa674cc5r0hlyvccs5xxn7qv83ezuteefv5972nquq4j4szl/esp32-tollgate")
32BRANCH = os.environ.get("CI_BRANCH", "feature/tollgate-core-v2")
33PIPELINE_SCRIPT = os.environ.get("CI_PIPELINE_SCRIPT", os.path.join(os.path.dirname(__file__), "run_pipeline.sh"))
34RECONNECT_DELAY = 5
35POLL_TIMEOUT = 300
36
37
38def generate_sub_id():
39 h = hashlib.sha256(f"ci-listener-{time.time()}".encode()).hexdigest()
40 return f"ci-{h[:16]}"
41
42
43async def run_pipeline(event_data):
44 log.info("Triggering CI pipeline for branch=%s", BRANCH)
45 env = os.environ.copy()
46 env["CI_EVENT_JSON"] = json.dumps(event_data)
47 env["CI_BRANCH"] = BRANCH
48 try:
49 proc = subprocess.run(
50 ["bash", PIPELINE_SCRIPT],
51 env=env,
52 capture_output=True,
53 text=True,
54 timeout=600,
55 )
56 log.info("Pipeline exit=%d", proc.returncode)
57 if proc.stdout:
58 for line in proc.stdout.strip().split("\n"):
59 log.info(" pipeline: %s", line)
60 if proc.returncode != 0 and proc.stderr:
61 for line in proc.stderr.strip().split("\n"):
62 log.error(" pipeline: %s", line)
63 return proc.returncode
64 except subprocess.TimeoutExpired:
65 log.error("Pipeline timed out after 600s")
66 return 1
67 except Exception as e:
68 log.error("Pipeline failed: %s", e)
69 return 1
70
71
72async def listen():
73 sub_id = generate_sub_id()
74 filters = {
75 "kinds": [30617],
76 "#d": [REPO_ID],
77 "limit": [1],
78 }
79
80 while True:
81 try:
82 log.info("Connecting to %s", RELAY_URL)
83 async with websockets.connect(RELAY_URL, ping_interval=30) as ws:
84 req = json.dumps(["REQ", sub_id, filters])
85 await ws.send(req)
86 log.info("Subscribed to kind 30617 for %s", REPO_ID)
87
88 last_event_id = None
89
90 while True:
91 try:
92 msg = await asyncio.wait_for(ws.recv(), timeout=POLL_TIMEOUT)
93 except asyncio.TimeoutError:
94 await ws.send(json.dumps(["PING", sub_id]))
95 continue
96
97 data = json.loads(msg)
98
99 if data[0] == "EVENT":
100 event = data[2]
101 eid = event.get("id", "")
102 if eid == last_event_id:
103 continue
104 last_event_id = eid
105
106 tags = {k: [] for k in ["d", "branch", "commit", "clone"]}
107 for t in event.get("tags", []):
108 if len(t) >= 2 and t[0] in tags:
109 tags[t[0]].append(t[1])
110
111 event_branch = tags.get("branch", [None])[0]
112 if event_branch and event_branch != BRANCH:
113 log.info("Ignoring branch=%s (want %s)", event_branch, BRANCH)
114 continue
115
116 log.info("New push: commit=%s branch=%s",
117 tags.get("commit", ["?"])[0],
118 event_branch or BRANCH)
119 rc = await run_pipeline(event)
120 log.info("Pipeline result: %s", "PASS" if rc == 0 else "FAIL")
121
122 elif data[0] == "EOSE":
123 log.info("End of stored events, listening for new ones...")
124
125 elif data[0] == "OK":
126 pass
127
128 elif data[0] == "NOTICE":
129 log.warning("Relay notice: %s", data[1] if len(data) > 1 else data)
130
131 except websockets.ConnectionClosed as e:
132 log.warning("Connection closed: %s, reconnecting in %ds", e, RECONNECT_DELAY)
133 except Exception as e:
134 log.error("Error: %s, reconnecting in %ds", e, RECONNECT_DELAY)
135
136 await asyncio.sleep(RECONNECT_DELAY)
137
138
139def main():
140 log.info("CI Listener starting: relay=%s repo=%s branch=%s", RELAY_URL, REPO_ID, BRANCH)
141 asyncio.run(listen())
142
143
144if __name__ == "__main__":
145 main()
diff --git a/ci/scripts/run_pipeline.sh b/ci/scripts/run_pipeline.sh
new file mode 100755
index 0000000..2bef754
--- /dev/null
+++ b/ci/scripts/run_pipeline.sh
@@ -0,0 +1,80 @@
1#!/bin/bash
2# CI pipeline for esp32-tollgate
3# Triggered by ci_listener.py on NIP-34 push events
4set -euo pipefail
5
6BRANCH="${CI_BRANCH:-feature/tollgate-core-v2}"
7REPO_DIR="/tmp/ci-esp32-tollgate"
8LOG_FILE="/tmp/ci-esp32-tollgate-pipeline.log"
9RESULT_FILE="/tmp/ci-esp32-tollgate-result.txt"
10
11echo "=== CI Pipeline Start ===" | tee "$LOG_FILE"
12echo "Branch: $BRANCH" | tee -a "$LOG_FILE"
13echo "Time: $(date -Iseconds)" | tee -a "$LOG_FILE"
14
15# Clone or update
16if [ -d "$REPO_DIR" ]; then
17 echo "Updating existing clone..." | tee -a "$LOG_FILE"
18 git -C "$REPO_DIR" fetch --all 2>&1 | tee -a "$LOG_FILE" || true
19 git -C "$REPO_DIR" checkout "$BRANCH" 2>&1 | tee -a "$LOG_FILE" || true
20 git -C "$REPO_DIR" reset --hard "origin/$BRANCH" 2>&1 | tee -a "$LOG_FILE" || true
21else
22 echo "Cloning repository..." | tee -a "$LOG_FILE"
23 mkdir -p "$(dirname "$REPO_DIR")"
24 git clone --branch "$BRANCH" \
25 "nostr://npub12m5exm2uk3xa674cc5r0hlyvccs5xxn7qv83ezuteefv5972nquq4j4szl/ngit.orangesync.tech/esp32-tollgate" \
26 "$REPO_DIR" 2>&1 | tee -a "$LOG_FILE"
27fi
28
29cd "$REPO_DIR"
30
31# Init submodules
32echo "=== Initializing submodules ===" | tee -a "$LOG_FILE"
33git submodule update --init --recursive 2>&1 | tee -a "$LOG_FILE" || true
34
35# Run host unit tests
36echo "=== Running unit tests ===" | tee -a "$LOG_FILE"
37if make test-unit 2>&1 | tee -a "$LOG_FILE"; then
38 echo "UNIT_TESTS=PASS" | tee -a "$LOG_FILE"
39 UNIT_RESULT="PASS"
40else
41 echo "UNIT_TESTS=FAIL" | tee -a "$LOG_FILE"
42 UNIT_RESULT="FAIL"
43fi
44
45# ESP-IDF build (in Docker)
46echo "=== ESP-IDF Build ===" | tee -a "$LOG_FILE"
47if command -v docker &>/dev/null; then
48 docker run --rm \
49 -v "$REPO_DIR:/project" \
50 -w /project \
51 espressif/idf:latest \
52 bash -c "idf.py build 2>&1" | tee -a "$LOG_FILE"
53 BUILD_RC=${PIPESTATUS[0]:-1}
54else
55 echo "Docker not available, skipping ESP-IDF build" | tee -a "$LOG_FILE"
56 BUILD_RC=0
57fi
58
59if [ "$BUILD_RC" -eq 0 ]; then
60 echo "IDF_BUILD=PASS" | tee -a "$LOG_FILE"
61 BUILD_RESULT="PASS"
62else
63 echo "IDF_BUILD=FAIL" | tee -a "$LOG_FILE"
64 BUILD_RESULT="FAIL"
65fi
66
67# Summary
68echo "=== Summary ===" | tee -a "$LOG_FILE"
69echo "Unit tests: $UNIT_RESULT" | tee -a "$LOG_FILE"
70echo "IDF build: $BUILD_RESULT" | tee -a "$LOG_FILE"
71
72if [ "$UNIT_RESULT" = "PASS" ] && [ "$BUILD_RESULT" = "PASS" ]; then
73 echo "OVERALL=PASS" | tee -a "$LOG_FILE"
74 echo "PASS" > "$RESULT_FILE"
75 exit 0
76else
77 echo "OVERALL=FAIL" | tee -a "$LOG_FILE"
78 echo "FAIL" > "$RESULT_FILE"
79 exit 1
80fi