615940de35
gates / gates (push) Successful in 51s
The alarm step built its link from GITHUB_SERVER_URL, which on this runner is the in-cluster Gitea address, and from the run NUMBER, which Gitea 1.26 reads as a run id first (felhom.eu /actions/runs/1116 opened run id 1116 = run number 613). Now https://gitea.dooplex.hu + GITHUB_RUN_ID, and the link is printed into the job log as ALARM-LINK. Same change in all four repos.
142 lines
8.0 KiB
YAML
142 lines
8.0 KiB
YAML
# gates — re-run this repo's gate entry point on every push, on a machine that does not care who
|
|
# pushed or what they typed.
|
|
#
|
|
# *** THIS REPORTS. IT CANNOT REFUSE. ***
|
|
#
|
|
# felhom repos push straight to `main` with no pull request, so there is no merge for a status
|
|
# check to stand at. The refusing half is `.githooks/pre-push`, which is local to a clone and which
|
|
# `git push --no-verify` skips; this half is what notices when that happened. Neither half is the
|
|
# whole thing, and both are named in felhom.eu documentation/backlog/OPEN-ITEMS.md R-168.
|
|
#
|
|
# NO `uses:` STEP ANYWHERE, deliberately: JavaScript actions need a node runtime in the runner, and
|
|
# the runner is a host-mode container with python3 and git and nothing else (see
|
|
# homelab-manifests/gitea-system/act-runner.yaml for why it is not privileged). Probe P3 measured
|
|
# that a plain `git fetch` of the pushed SHA from the in-cluster Gitea service is enough.
|
|
#
|
|
# A failing run must reach a person — a detector nobody hears is the defect R-29 filed, rebuilt one
|
|
# layer up. That is the last step, and it runs ONLY on failure.
|
|
name: gates
|
|
# R-1129: `workflow_dispatch` is the manual PROOF that the failure mail can be sent. A push run is
|
|
# unchanged by it: the proof step below is skipped on a push, and `force_alarm` is then empty.
|
|
on:
|
|
push:
|
|
workflow_dispatch:
|
|
inputs:
|
|
force_alarm:
|
|
description: "Fail this run on purpose so the alarm step sends its real mail (a proof, not a gate result)"
|
|
type: boolean
|
|
default: false
|
|
|
|
jobs:
|
|
gates:
|
|
runs-on: felhom-gates
|
|
steps:
|
|
- name: Fetch the pushed commit and the sibling clone it needs
|
|
# This repo's entry point invokes a SHARED checker that lives in the felhom.eu clone next
|
|
# door and is deliberately never copied here — so CI has to reproduce the workspace's
|
|
# sibling layout or the gate fails closed with "gate is MISSING". The sibling is also
|
|
# needed for CONTENT: this repo's REUSE.md cites a path that lives in the hub.
|
|
run: |
|
|
# Shallow, and pinned to the exact SHA that was pushed — not to the branch tip,
|
|
# which can move under us if two pushes race.
|
|
mkdir -p ws/felhom-controller
|
|
cd ws/felhom-controller
|
|
git init -q .
|
|
git remote add origin http://gitea.gitea-system.svc.cluster.local:3000/admin/felhom-controller.git
|
|
git fetch -q --depth 1 origin "$GITHUB_SHA"
|
|
git checkout -q FETCH_HEAD
|
|
echo "checked out $(git rev-parse HEAD)"
|
|
cd .. && git clone -q --depth 1 http://gitea.gitea-system.svc.cluster.local:3000/admin/felhom.eu.git felhom.eu
|
|
echo "sibling felhom.eu present at $(cd felhom.eu && git rev-parse --short HEAD)"
|
|
|
|
- name: Run the gate entry point
|
|
# The ONLY thing CI runs. No go build, no go test, no linting, no deploy. The
|
|
# exit code IS the result: no `|| true`, no pipe that could swallow it.
|
|
run: cd ws/felhom-controller/controller && python3 scripts/controller_gates.py --fast
|
|
|
|
- name: Manual proof - alarm key present, optional forced failure (dispatch only, R-1129)
|
|
# Runs ONLY on a manual dispatch, never on a push. It prints the key's LENGTH, never the key,
|
|
# so "CI can see the secret" is an observable rather than an assumption. With
|
|
# force_alarm=true it then fails on purpose so the alarm step below sends its real mail;
|
|
# the commit itself stays green, because its push run skips this step.
|
|
if: github.event_name == 'workflow_dispatch'
|
|
env:
|
|
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
|
FORCE_ALARM: ${{ github.event.inputs.force_alarm }}
|
|
run: |
|
|
python3 -c 'import os;print("RESEND_API_KEY length: %d" % len(os.environ.get("RESEND_API_KEY","")))'
|
|
if [ "$FORCE_ALARM" = "true" ]; then
|
|
echo "force_alarm=true - failing on purpose so the alarm step runs (R-1129 proof)"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Alarm on failure
|
|
# THE POINT OF THE WHOLE THING. Probe P5 measured that a failed run produces NO mail, NO
|
|
# notification row and NO log line from Gitea itself — a red tick in a web UI nobody watches
|
|
# is exactly the shape R-29 filed against. So the run sends its own alarm, on the project's
|
|
# existing transactional path (Resend, the same one the hub uses), and prints the provider's
|
|
# accepted id so "a message left the machine" is an observable, not an assumption.
|
|
#
|
|
# Pure python3 and urllib, NOT curl: the runner image carries python3 and git and nothing
|
|
# else on purpose, and the first version of this step died on `curl: command not found`.
|
|
# Reaching for a bigger image to send one HTTP request would have been the wrong trade.
|
|
if: failure()
|
|
env:
|
|
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
|
|
# Empty on a push, so a push run's mail is exactly what it was before R-1129.
|
|
FORCE_ALARM: ${{ github.event.inputs.force_alarm }}
|
|
run: |
|
|
python3 - <<'PY'
|
|
import json, os, sys, urllib.request, urllib.error
|
|
|
|
key = os.environ.get("RESEND_API_KEY", "")
|
|
if not key:
|
|
sys.exit("ALARM FAILED: RESEND_API_KEY is empty — the alarm cannot be sent, and a "
|
|
"silent alarm is worse than none. Set the user-level Actions secret.")
|
|
|
|
repo = os.environ.get("GITHUB_REPOSITORY", "?")
|
|
sha = os.environ.get("GITHUB_SHA", "?")
|
|
# R-1135: the link is for a PERSON, so it is the public address and the run ID.
|
|
# NOT GITHUB_SERVER_URL: the runner registers at the in-cluster address, so that variable
|
|
# reads http://gitea.gitea-system.svc.cluster.local:3000, which nobody outside the cluster
|
|
# can open (forced-test mail, job 1997). NOT GITHUB_RUN_NUMBER: Gitea 1.26 reads
|
|
# /actions/runs/<n> as a run id first, so a run number that is also an older run's id
|
|
# opens THAT run (measured 2026-10-18: felhom.eu /actions/runs/1116 opened the run whose
|
|
# id is 1116 = run number 613). The link is printed too, so the job log carries it.
|
|
run = os.environ.get("GITHUB_RUN_ID", "?")
|
|
srv = "https://gitea.dooplex.hu"
|
|
print("ALARM-LINK %s/%s/actions/runs/%s" % (srv, repo, run))
|
|
|
|
body = json.dumps({
|
|
"from": "Felhom CI <monitoring@felhom.eu>",
|
|
"to": ["admin@felhom.eu"],
|
|
"subject": ("[felhom CI] FORCED TEST, not a real failure - gates FAILED in %s"
|
|
if os.environ.get("FORCE_ALARM") == "true"
|
|
else "[felhom CI] gates FAILED in %s") % repo,
|
|
"text": (
|
|
"The gate entry point exited non-zero.\n\n"
|
|
"Repository : %s\n"
|
|
"Commit : %s\n"
|
|
"Run : %s/%s/actions/runs/%s\n\n"
|
|
"The failing gate names itself in the run log.\n\n"
|
|
"If the local pre-push hook was GREEN for this commit, then CI and the hook\n"
|
|
"disagree - that is a finding about the gates themselves, not about CI, and it\n"
|
|
"outranks whatever the push was for.\n"
|
|
) % (repo, sha, srv, repo, run),
|
|
}).encode()
|
|
|
|
req = urllib.request.Request(
|
|
"https://api.resend.com/emails", data=body, method="POST",
|
|
headers={"Authorization": "Bearer %s" % key,
|
|
"Content-Type": "application/json",
|
|
# Cloudflare fronts api.resend.com and BLOCKS the default
|
|
# "Python-urllib/3.x" agent with its own 403 (error 1010) — which looks
|
|
# exactly like an auth failure and is not one. Measured 2026-08-02.
|
|
"User-Agent": "felhom-ci/1.0"})
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=30) as r:
|
|
print("RESEND-ACCEPTED id=%s" % json.load(r)["id"])
|
|
except urllib.error.HTTPError as e:
|
|
sys.exit("ALARM FAILED: Resend returned HTTP %s: %s" % (e.code, e.read().decode()[:300]))
|
|
PY
|