6f25e02828
gates / gates (push) Successful in 19s
CI checks out ONE repo, shallow. The R-242 gate reads the newest RELEASED controller from the sibling clone's CHANGELOG.md - the same sibling assumption reuse_refs_check.py and instructions_gate.py already make - so in CI it was exiting 2 (INCONCLUSIVE) and CI went red on every push. Caught by checking the CI result rather than assuming it: runs 241, 242 and 243 all failed while the gates were green locally. A PERMANENTLY-RED CI IS THE DETECTOR-NOBODY-HEARS FAILURE THIS WORKFLOW EXISTS TO PREVENT - people stop reading it, and then it catches nothing. So the fix is to give the gate what it needs, not to let it skip when the sibling is absent: a skip would be the fail-open shape this project keeps removing, and the gate would then run in NEITHER of its two automated homes (the pre-push hook and CI). Depth 1, pinned to main, plain git - no JavaScript-action step, per the workflow's own rule about the runner having python3 and git and nothing else. If the fetch fails the gate still reports INCONCLUSIVE rather than passing.
119 lines
6.5 KiB
YAML
119 lines
6.5 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 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
|
|
on: [push]
|
|
|
|
jobs:
|
|
gates:
|
|
runs-on: felhom-gates
|
|
steps:
|
|
- name: Fetch the pushed commit
|
|
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. Probe P3 proved the two are equal when done this way.
|
|
git init -q .
|
|
git remote add origin http://gitea.gitea-system.svc.cluster.local:3000/admin/felhom.eu.git
|
|
git fetch -q --depth 1 origin "$GITHUB_SHA"
|
|
git checkout -q FETCH_HEAD
|
|
echo "checked out $(git rev-parse HEAD)"
|
|
|
|
- name: Fetch the controller CHANGELOG (golden-currency gate needs the sibling repo)
|
|
# R-242's gate compares the newest RELEASED controller against the newest golden baked here,
|
|
# and it reads the released version from the sibling clone's CHANGELOG.md — the same sibling
|
|
# assumption reuse_refs_check.py and instructions_gate.py already make on a workstation.
|
|
#
|
|
# CI checks out ONE repo, shallow, so without this the gate exits 2 (INCONCLUSIVE) and CI is
|
|
# red for ever. **A permanently-red CI is the detector-nobody-hears failure this whole
|
|
# workflow exists to prevent**, so the fix is to give the gate what it needs rather than to
|
|
# let it skip: a silent skip would be the fail-open shape, and the gate would then run in
|
|
# NEITHER of its two automated homes.
|
|
#
|
|
# Depth 1, pinned to main, and only this repo's CHANGELOG is used. If the fetch fails the
|
|
# gate still reports INCONCLUSIVE rather than passing — not knowing is never a pass.
|
|
run: |
|
|
git init -q ../felhom-controller
|
|
cd ../felhom-controller
|
|
git remote add origin http://gitea.gitea-system.svc.cluster.local:3000/admin/felhom-controller.git
|
|
git fetch -q --depth 1 origin main
|
|
git checkout -q FETCH_HEAD
|
|
echo "controller CHANGELOG at $(git rev-parse --short=12 HEAD): $(head -1 CHANGELOG.md)"
|
|
|
|
- name: Run the gate entry point
|
|
# The ONLY thing CI runs. No go build, no go test, no linting, no deploy — those are either
|
|
# already reliably run by a person or none of CI's business. The exit code IS the result:
|
|
# no `|| true`, no pipe that could swallow it.
|
|
run: python3 scripts/repo_gates.py --fast
|
|
|
|
- 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 }}
|
|
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", "?")
|
|
run = os.environ.get("GITHUB_RUN_NUMBER", "?")
|
|
srv = os.environ.get("GITHUB_SERVER_URL", "https://gitea.dooplex.hu")
|
|
|
|
body = json.dumps({
|
|
"from": "Felhom CI <monitoring@felhom.eu>",
|
|
"to": ["admin@felhom.eu"],
|
|
"subject": "[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
|