upleb.uk

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

summaryrefslogtreecommitdiff
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
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
-rw-r--r--ci/ansible/20-ci-runner.yml9
-rw-r--r--ci/ansible/30-hardware-tests.yml69
-rw-r--r--ci/ansible/roles/ci_runner/tasks/main.yml72
-rwxr-xr-xci/scripts/ci_listener.py145
-rwxr-xr-xci/scripts/run_pipeline.sh80
-rw-r--r--ci/systemd/tollgate-ci-listener.service18
6 files changed, 393 insertions, 0 deletions
diff --git a/ci/ansible/20-ci-runner.yml b/ci/ansible/20-ci-runner.yml
new file mode 100644
index 0000000..62ff12a
--- /dev/null
+++ b/ci/ansible/20-ci-runner.yml
@@ -0,0 +1,9 @@
1---
2# Deploy CI runner for esp32-tollgate
3# Usage: ansible-playbook 20-ci-runner.yml
4
5- name: Deploy CI runner
6 hosts: localhost
7 connection: local
8 roles:
9 - role: ci_runner
diff --git a/ci/ansible/30-hardware-tests.yml b/ci/ansible/30-hardware-tests.yml
new file mode 100644
index 0000000..93a3a10
--- /dev/null
+++ b/ci/ansible/30-hardware-tests.yml
@@ -0,0 +1,69 @@
1---
2# Run hardware integration tests on localhost
3# Usage: ansible-playbook 30-hardware-tests.yml [-e "board=a"] [-e "skip_flash=false"]
4
5- name: Hardware integration tests
6 hosts: localhost
7 connection: local
8 vars:
9 board: "a"
10 skip_flash: false
11 esp32_tollgate_dir: "/home/c03rad0r/esp32-tollgate"
12 sudo_pw: "c03rad0r123"
13 board_config:
14 a:
15 port: "/dev/ttyACM0"
16 ip: "10.185.47.1"
17 ssid: "TollGate-B96D80"
18 b:
19 port: "/dev/ttyACM1"
20 ip: "10.192.45.1"
21 ssid: "TollGate-C0E9CA"
22
23 tasks:
24 - name: Set board facts
25 set_fact:
26 board_port: "{{ board_config[board].port }}"
27 board_ip: "{{ board_config[board].ip }}"
28 board_ssid: "{{ board_config[board].ssid }}"
29
30 - name: Verify board connected
31 command: "esptool.py --port {{ board_port }} chip_id"
32 changed_when: false
33
34 - name: Flash firmware
35 when: not skip_flash | bool
36 command: "make flash-{{ board }}"
37 args:
38 chdir: "{{ esp32_tollgate_dir }}"
39
40 - name: Wait for board boot
41 pause:
42 seconds: 15
43
44 - name: Run smoke tests
45 command: "make test-smoke"
46 args:
47 chdir: "{{ esp32_tollgate_dir }}"
48 environment:
49 TOLLGATE_IP: "{{ board_ip }}"
50 BOARD: "{{ board }}"
51 register: smoke_result
52 ignore_errors: true
53
54 - name: Run integration tests
55 command: "make test-integration"
56 args:
57 chdir: "{{ esp32_tollgate_dir }}"
58 environment:
59 TOLLGATE_IP: "{{ board_ip }}"
60 TOLLGATE_SSID: "{{ board_ssid }}"
61 register: integration_result
62 ignore_errors: true
63
64 - name: Report results
65 debug:
66 msg: |
67 Board: {{ board }} ({{ board_ip }})
68 Smoke: {{ 'PASS' if smoke_result is success else 'FAIL' }}
69 Integration: {{ 'PASS' if integration_result is success else 'FAIL' }}
diff --git a/ci/ansible/roles/ci_runner/tasks/main.yml b/ci/ansible/roles/ci_runner/tasks/main.yml
new file mode 100644
index 0000000..b29a237
--- /dev/null
+++ b/ci/ansible/roles/ci_runner/tasks/main.yml
@@ -0,0 +1,72 @@
1---
2# CI Runner role: Docker + ESP-IDF + nostr listener for CI/CD
3
4- name: Install Docker
5 become: true
6 block:
7 - name: Check if Docker is installed
8 command: docker --version
9 changed_when: false
10 ignore_errors: true
11 register: docker_check
12
13 - name: Install Docker
14 when: docker_check.failed
15 block:
16 - name: Install Docker packages
17 apt:
18 name:
19 - docker.io
20 - docker-compose-v2
21 state: present
22 update_cache: true
23
24 - name: Add user to docker group
25 user:
26 name: "{{ ansible_user_id }}"
27 groups: docker
28 append: true
29
30- name: Pull ESP-IDF Docker image
31 become: true
32 community.docker.docker_image:
33 name: espressif/idf
34 tag: latest
35 source: pull
36
37- name: Install Python dependencies for CI listener
38 pip:
39 name:
40 - websockets
41 state: present
42 extra_args: --user
43
44- name: Install CI listener systemd service
45 become: true
46 block:
47 - name: Copy service file
48 copy:
49 src: ../../systemd/tollgate-ci-listener.service
50 dest: /etc/systemd/system/tollgate-ci-listener.service
51 mode: "0644"
52
53 - name: Reload systemd
54 systemd:
55 daemon_reload: true
56
57 - name: Enable and start CI listener
58 systemd:
59 name: tollgate-ci-listener
60 enabled: true
61 state: started
62
63- name: Verify CI listener is running
64 block:
65 - name: Check service status
66 command: systemctl is-active tollgate-ci-listener
67 changed_when: false
68 register: listener_status
69
70 - name: Report status
71 debug:
72 msg: "CI listener status: {{ listener_status.stdout }}"
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
diff --git a/ci/systemd/tollgate-ci-listener.service b/ci/systemd/tollgate-ci-listener.service
new file mode 100644
index 0000000..287f474
--- /dev/null
+++ b/ci/systemd/tollgate-ci-listener.service
@@ -0,0 +1,18 @@
1[Unit]
2Description=ESP32-TollGate CI Nostr Listener
3After=network-online.target
4Wants=network-online.target
5
6[Service]
7Type=simple
8User=c03rad0r
9WorkingDirectory=/home/c03rad0r/esp32-tollgate/ci/scripts
10ExecStart=/usr/bin/python3 /home/c03rad0r/esp32-tollgate/ci/scripts/ci_listener.py
11Restart=always
12RestartSec=10
13Environment=CI_RELAY_URL=wss://ngit.orangesync.tech
14Environment=CI_BRANCH=feature/tollgate-core-v2
15Environment=CI_REPO_ID=npub12m5exm2uk3xa674cc5r0hlyvccs5xxn7qv83ezuteefv5972nquq4j4szl/esp32-tollgate
16
17[Install]
18WantedBy=multi-user.target