Repository navigation
152 lines (147 loc) · 6.08 KB
/
Copy pathci.yml
File metadata and controls
152 lines (147 loc) · 6.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
name: ci
on:
workflow_dispatch:
# The brain lives in hookjudge/ with its own workflow (ci-hookjudge.yml).
# Filtering here means a change to one service does not queue the other's
# jobs, and a red brain never stands between the pipe and a hotfix.
pull_request:
paths:
- "hookrelay/**"
- "!hookrelay/docs/**"
- "!hookrelay/**/*.md"
- ".github/workflows/ci.yml"
push:
branches:
- main
paths:
- "hookrelay/**"
- "!hookrelay/docs/**"
- "!hookrelay/**/*.md"
- ".github/workflows/ci.yml"
permissions:
contents: read
# One run per branch or PR at a time; a push supersedes the run it interrupted.
# Verification only — no registry push, no deploy, nothing outside the runner —
# so cancelling costs an answer about a commit that is no longer the tip, which
# is an answer nobody was going to read.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# One job on purpose: the suite is fast (sub-second) and hermetic — no
# database, no network, no services. Splitting it would cost more in runner
# startup than it saves. scripts/gate.sh runs this exact list locally.
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: hookrelay
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
# The version this service SHIPS (Dockerfile: python:3.15-slim). It
# tested on 3.12 while production ran 3.14, so the suite never once
# ran on the interpreter that serves traffic.
python-version: "3.15"
cache: "pip"
- run: python -m pip install --upgrade pip
# No ruff pin here: requirements.txt carries it. Naming the version in two
# places meant dependabot bumped one of them, and pip refused the pair —
# "Cannot install ruff==0.16.1 and ruff==0.16.3". One source of truth.
- run: pip install -r requirements.txt
- run: python -m compileall -q hookrelay tests
- run: ruff check hookrelay tests
- run: ruff format --check hookrelay tests
- run: python -m mypy hookrelay
- run: python -m bandit -q -r hookrelay
# Node ships pre-installed on GitHub-hosted ubuntu runners.
- name: inline page JS parses
run: |
node -e '
const fs = require("fs");
const html = fs.readFileSync("hookrelay/status.html", "utf8");
const m = html.match(/<script>([\s\S]*)<\/script>/);
if (!m) { console.error("no inline script found"); process.exit(1); }
new Function(m[1]);
new Function(fs.readFileSync("hookrelay/static/sw.js", "utf8"));
JSON.parse(fs.readFileSync("hookrelay/static/manifest.webmanifest", "utf8"));
console.log("status.html inline JS, sw.js and the manifest: OK");
'
- name: example plugins import cleanly
run: |
python - <<'PYEOF'
from pathlib import Path
from hookrelay import registry
loaded = registry.load_plugins(Path("examples/plugins"))
assert loaded, "examples/plugins loaded nothing — an untested example is a lie"
print("plugins:", ", ".join(loaded))
PYEOF
- name: config.example.yaml is valid
env:
FEISHU_WEBHOOK_URL: https://example.invalid/hook
DINGTALK_WEBHOOK_URL: https://example.invalid/hook
WECOM_WEBHOOK_URL: https://example.invalid/hook
ARCHIVE_WEBHOOK_URL: https://example.invalid/hook
run: |
python - <<'PYEOF'
from hookrelay.config import Config
cfg = Config.from_file("config.example.yaml")
print(f"{len(cfg.sources)} sources, {len(cfg.channels)} channels, {len(cfg.routes)} routes")
PYEOF
# Coverage floor, matching gate.sh: a regression catch, not a target.
- run: pytest -q --cov=hookrelay --cov-fail-under=90
- run: python -m pip_audit --progress-spinner off
docker:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
# The image is how this actually ships; a build break must not wait for
# a deploy to surface.
- uses: docker/build-push-action@v7
with:
context: ./hookrelay
load: true
tags: hookrelay-ci:latest
cache-from: type=gha
cache-to: type=gha,mode=max
- name: the built image boots and serves
run: |
mkdir -p /tmp/relaydata
cat > /tmp/relaydata/config.yaml <<'YEOF'
sources:
- name: ci
secret: ""
title: "{title}"
channels:
- name: sink
type: generic
url: http://127.0.0.1:9/discard
routes:
- {name: all, source: "*", send_to: [sink]}
YEOF
docker run -d --name relay -p 8100:8100 \
-v /tmp/relaydata:/data -e HOOKRELAY_PORT=8100 hookrelay-ci:latest
for i in $(seq 1 30); do
if curl -sf http://127.0.0.1:8100/healthz > /dev/null; then break; fi
sleep 1
done
# The log BEFORE the failure, not after it. This assertion used to be a
# bare curl, so an image that never came up failed the step with
# `exit code 7` and printed the container log at the very end of the
# script — which never ran. One dependency bump later that cost a long
# look at a number, when the container had said
# `ModuleNotFoundError: No module named 'httpx'` on line one.
curl -sf http://127.0.0.1:8100/healthz || {
echo '--- it never became healthy; the container said: ---'
docker logs relay 2>&1 | tail -40
exit 1
}
# A real event walks the real pipeline in the real image.
curl -sf -X POST http://127.0.0.1:8100/hook/ci \
-H 'content-type: application/json' -d '{"title":"ci smoke"}' | tee /tmp/out.json
grep -q '"outcome": *"routed"' /tmp/out.json
docker logs relay | tail -20