diff --git a/.gitea/workflows/gates.yml b/.gitea/workflows/gates.yml index 95731f0..3c14be2 100644 --- a/.gitea/workflows/gates.yml +++ b/.gitea/workflows/gates.yml @@ -16,7 +16,16 @@ # 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] +# 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: @@ -45,6 +54,22 @@ jobs: # 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 @@ -58,6 +83,8 @@ jobs: 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 @@ -75,7 +102,9 @@ jobs: body = json.dumps({ "from": "Felhom CI ", "to": ["admin@felhom.eu"], - "subject": "[felhom CI] gates FAILED in %s" % repo, + "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" diff --git a/CHANGELOG.md b/CHANGELOG.md index 87a789c..dfbfce8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## Unreleased (2026-10-17) — CI workflow: a manual proof that the failure mail can be sent (R-1129); no code change + +`.gitea/workflows/gates.yml` gains a manual `workflow_dispatch` trigger with one input, `force_alarm` (default false), and +one step that runs ONLY on a dispatch: it prints the length of `RESEND_API_KEY` (never the key) and, with +`force_alarm=true`, fails on purpose so the existing alarm step sends its real mail; the mail's subject then starts +„FORCED TEST, not a real failure". A push run is unchanged: the new step is skipped and the subject is the old one. Same +change in all four repos' workflows. Purpose: prove the failure mail now goes out after the operator set the key +(every failed run until today ended „ALARM FAILED: RESEND_API_KEY is empty"). + ## v0.326.0 — FileBrowser's default password is checked at every FileBrowser start; the decoy suite is a gate (2026-10-17) **MinAgent: 0.158.1** — the floor's, carried. Nothing here calls the agent. No golden (`09` §3 decision 250). Task: