diff --git a/.gitea/workflows/checks.yml b/.gitea/workflows/checks.yml index 0d4f451..7366a2f 100644 --- a/.gitea/workflows/checks.yml +++ b/.gitea/workflows/checks.yml @@ -1,24 +1,15 @@ -# CI-01: this repository's FIRST automated check. Before this file, admin/goodwe-addon -# had no `.gitea/workflows/` directory at all - eight runnable test files, ~100 s -# of real coverage including a wall-clock cadence test, and not one machine ever -# ran them. Every merge here was a human reading a diff. +# CI-02 PROBE WORKFLOW - THROWAWAY. This branch is deleted after the run. # -# ⚠️ NO `paths:` FILTER, AND ITS ABSENCE IS LOAD-BEARING. Branch protection on -# release/1.0 requires the context `checks / unit (pull_request)`. Gitea leaves a -# required context it never receives PENDING forever, so the moment this workflow -# learns to sit a PR out, that PR becomes unmergeable. Condition a STEP if you -# must; never the workflow. +# It replaces the real gate with a set of one-bit steps, because this Gitea +# 404s its Actions log endpoints and the ONLY machine-readable channel out of a +# job is /api/v1/repos/.../actions/runs//jobs, which carries a per-STEP +# conclusion. So each question this ticket has to answer is asked as its own +# step, and the answer is read as that step's success or failure. # -# ⚠️ THE RUNNER IS SHARED WITH THE OTHER REPO'S RIG. `goodwe-runner` on -# 192.168.2.147 carries the labels `self-hosted` and `goodwe-rig` and is -# registered `:host`, and it is the only runner there is - so this job wears the -# rig label without wanting a rig. Nothing below starts, stops, or reads the -# `goodwe-sim` compose project or takes `sim/riglock.py`; it runs one short -# container and exits. It does occupy the runner's single slot (`capacity: 1`), -# so it QUEUES behind a rig campaign - a delayed verdict, never a destroyed one. -# -# NO JS ACTIONS: the runner image has no node, so `actions/checkout` cannot run. -# Checkout is seven lines of git below. +# There is deliberately NO `concurrency:` block: the point of pushes B and C is +# to find out whether a still-WAITING run is cancelled by the next push even +# when the workflow asks for no concurrency at all. If it is, the cancellation +# is the server's and no edit to this file can prevent it. name: checks on: @@ -26,111 +17,91 @@ on: branches: [release/1.0, master] workflow_dispatch: -concurrency: - # ⚠️ PER PULL REQUEST, NOT A BARE NAME. A shared group holds one running and - # one pending run, so a bare name lets a second PR supersede a first PR's run - # before any runner has picked it up - which is exactly what happened to the - # other repo's run 521 (four jobs, `started_at` at the Unix epoch, - # `runner_id: 0`, concluded `cancelled`, and the PR merged anyway). Do not - # replace this with a constant. - group: addon-checks-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - jobs: unit: runs-on: [self-hosted, goodwe-rig] - timeout-minutes: 70 # ~4 to run; the rest is queueing for the single slot + timeout-minutes: 20 env: - # ⚠️ UNDER /home/homelab, NOT $GITHUB_WORKSPACE (ENV-01, and re-measured - # during CI-01). act's workspace is `/root/.cache/act//hostexecutor`, a - # path that exists only INSIDE the runner container. Hand it to the host's - # snap-confined dockerd and the build fails with "failed to read - # dockerfile", or - worse, and this is the documented case - a bind mount - # resolves to an EMPTY DIRECTORY with no error at all. /home/homelab is - # mirrored 1:1 into the runner and is under $HOME, where snap confinement - # accepts it. The same probe under /tmp failed and under $HOME succeeded. - WS: /home/homelab/ci/addon-checks-${{ github.run_number }} + WS: /home/homelab/ci/ci02probe-${{ github.run_number }} + PRNUM: ${{ github.event.pull_request.number }} + PRHEAD: ${{ github.event.pull_request.head.sha }} + BASEREF: ${{ github.base_ref }} + TOKEN: ${{ secrets.GITHUB_TOKEN }} + URL: ${{ github.server_url }}/${{ github.repository }}.git steps: - # ⚠️ THE CHECKOUT IS TWO LEVELS BELOW THE MOUNT ROOT, DELIBERATELY. - # test_freeze.py's find_hwsim() reads `HERE.parents[1] / "goodwe" / "sim"`, - # looking for the infra repo as a SIBLING of this one. Check out flat - - # goodwe_controller directly at the mount root - and `parents[1]` does not - # exist, so the test dies on `IndexError: 1` before its first assertion. - # Measured during CI-01; the flat layout was tried first and did exactly - # that. $WS/goodwe-addon/goodwe_controller gives parents[1] == $WS. - - name: checkout into a path the host shares (git; no node for actions/checkout) - env: - URL: ${{ github.server_url }}/${{ github.repository }}.git - TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: P00 an empty clone to ask the server questions from run: | set -eu - rm -rf "$WS" && mkdir -p "$WS/goodwe-addon" && cd "$WS/goodwe-addon" + rm -rf "$WS" && mkdir -p "$WS" && cd "$WS" git init -q . git remote add origin "$(echo "$URL" | sed "s#://#://x-access-token:${TOKEN}@#")" - git fetch -q --depth 1 origin "$GITHUB_REF" || git fetch -q --depth 1 origin "$GITHUB_SHA" - git checkout -qf FETCH_HEAD - echo "checked out $(git log --oneline -1)" + echo "GITHUB_REF=$GITHUB_REF" + echo "GITHUB_SHA=$GITHUB_SHA" + echo "PRNUM=$PRNUM PRHEAD=$PRHEAD BASEREF=$BASEREF" - - name: build the test image (shipped deps, from apk - see .gitea/ci/Dockerfile) - run: | - set -eu - docker build -q -t goodwe-addon-checks:1 "$WS/goodwe-addon/.gitea/ci" >/dev/null - docker run --rm goodwe-addon-checks:1 python3 -c \ - 'import aiohttp, paho.mqtt.client as m, sys; print("python", sys.version.split()[0], "aiohttp", aiohttp.__version__)' - - # ⚠️ THE FILE LIST IS DERIVED FROM THE TREE, NOT TYPED HERE. A hand-written - # list is how a new test file joins the repo and is never run - the exact - # shape of failure this ticket exists to close. The step globs `test_*.py`, - # prints the count, and REFUSES A COUNT OF ZERO: a loop over an empty glob - # exits 0 and would report a green gate that examined nothing. - # - # ⚠️ test_loop.py TAKES ~93 SECONDS OF WALL CLOCK ON PURPOSE (its own - # docstring says so, and says why: it drives the real `run_control()` - # against a real wall-clock floor, and mocking the clock would leave the - # assertion testing the mock). It is not hung. Do not "fix" it with a - # shorter timeout - a 60 s limit was tried during CI-01 and reported - # exactly the false hang the docstring predicts. - # - # ⚠️ WHAT THIS RUN DOES NOT COVER, stated because a pass here would - # otherwise over-claim. test_freeze.py's third section wants the infra - # repo's `sim/hwsim.py` to drive a real HTTP meter through the real - # transport. admin/goodwe is PRIVATE (checked via the API) and this - # workflow's GITHUB_TOKEN is minted for THIS repository only, so hwsim is - # absent here and `find_hwsim()` returns None. The rest of the file runs - # and passes. It does so SILENTLY - the file prints nothing to say the - # hwsim section was skipped - so "test_freeze.py: PASS" below is a weaker - # claim than it looks. That belongs to whoever owns test_freeze.py next; - # naming it here is the least this gate can do about it. - - name: the add-on's own tests - run: | - set -eu - docker run --rm -v "$WS:/w:ro" goodwe-addon-checks:1 sh -eu -c ' - cd /w/goodwe-addon/goodwe_controller - set -- test_*.py - if [ "$1" = "test_*.py" ]; then - echo "NO test_*.py FOUND - the mount is empty or the layout moved. NOTHING has been checked." - exit 1 - fi - echo "$# test file(s) to run" - rc=0 - for f in "$@"; do - start=$(date +%s) - if python3 "$f" > /tmp/out 2>&1; then - echo "PASS $f ($(( $(date +%s) - start ))s)" - else - echo "FAIL $f ($(( $(date +%s) - start ))s)" - tail -40 /tmp/out - rc=1 - fi - done - exit $rc - ' - - - name: clean up + - name: P01 GITHUB_REF is refs-pull-N-head (the bare head) if: always() run: | - rm -rf "$WS" || true - # Root-owned by this runner inside a directory the `homelab` user owns, - # and sudo on that box is interactive-only, so the user cannot clean - # this up and the job that made it has to. - ls -1dt /home/homelab/ci/addon-checks-*/ 2>/dev/null | tail -n +6 | xargs -r rm -rf || true + set -eu + test "$GITHUB_REF" = "refs/pull/$PRNUM/head" + + - name: P02 GITHUB_REF is refs-pull-N-merge (merged content) + if: always() + run: | + set -eu + test "$GITHUB_REF" = "refs/pull/$PRNUM/merge" + + - name: P03 GITHUB_SHA equals the PR head sha + if: always() + run: | + set -eu + test "$GITHUB_SHA" = "$PRHEAD" + + - name: P04 the first half of the shipped fallback fetches at all + if: always() + run: | + set -eu + cd "$WS" + git fetch -q --depth 1 origin "$GITHUB_REF" + + - name: P05 ...and what it lands on IS the bare PR head + if: always() + run: | + set -eu + cd "$WS" + git fetch -q --depth 1 origin "$GITHUB_REF" + test "$(git rev-parse FETCH_HEAD)" = "$PRHEAD" + + - name: P06 refs-pull-N-merge exists on the server for an OPEN pr + if: always() + run: | + set -eu + cd "$WS" + git fetch -q --depth 1 origin "refs/pull/$PRNUM/merge" + + - name: P07 a real merge of this head into the base tip applies cleanly + if: always() + run: | + set -eu + cd "$WS" + git fetch -q origin "+refs/heads/$BASEREF:refs/remotes/origin/base" + git fetch -q origin "+refs/pull/$PRNUM/head:refs/remotes/origin/pr" + git checkout -qf refs/remotes/origin/base + git -c user.email=ci@local -c user.name=ci merge --no-edit --no-ff refs/remotes/origin/pr + + - name: P08 the base tip is already an ancestor of the head + if: always() + run: | + set -eu + cd "$WS" + git fetch -q origin "+refs/heads/$BASEREF:refs/remotes/origin/base" + git fetch -q origin "+refs/pull/$PRNUM/head:refs/remotes/origin/pr" + git merge-base --is-ancestor refs/remotes/origin/base refs/remotes/origin/pr + + - name: P09 hold the single runner slot so the next push has to wait + if: always() + run: sleep 150 + + - name: P99 clean up + if: always() + run: rm -rf "$WS" || true diff --git a/goodwe_controller/CHANGELOG.md b/goodwe_controller/CHANGELOG.md index ef47f8b..9e92b23 100644 --- a/goodwe_controller/CHANGELOG.md +++ b/goodwe_controller/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +**CI-02 PROBE - THROWAWAY, DO NOT MERGE.** This paragraph exists only to +collide with the entries release/1.0 has gained since this branch point, so +that the pull request is genuinely unmergeable and the gate can be observed +reporting on it. The branch and its pull request are deleted afterwards. + **Thirty days of the real meter, and the acceptance criterion three cards deferred is now a number (DATA-01).** TARIFF-01 and SAFETY-07 both merged with AC 3 - *"monthly peak held under the soft limit in >= 95 % of intervals over a