Closes weblib-ci#11 in part: the reconciler could add Status/Blocked but never remove it when every blocker lived in another repo, and, found live on weblib-archive#30 (2026-09-09), it went further and *reasserted* the label over a human's hand-removal every 15 minutes. Established which of the two candidate mechanisms in #11 was real by instrumenting the script and running it under a real Gitea Actions token scoped to weblib-archive, against weblib-archive#68 (a throwaway issue depending on the already-closed weblib/cfbypass#1) and the live weblib-archive#30: `deps` is never empty for a cross-repo edge (rules out "Gitea omits the entry"), but every cross-repo entry comes back as Go's zero-value Issue struct -- id 0, state "", repository null -- rather than being omitted or erroring. The old `d.get("state") != "closed"` read that blank state as "still open", which is why the label could never come off, and why it kept getting silently reapplied. `is_resolved()` keys on `id == 0` rather than on the empty `state`, since `id` cannot legitimately be zero for a real issue and a future Gitea could start populating `state` while still stubbing the rest of the entry. `reconcile()` now keeps the existing add/remove behaviour whenever every blocker resolves (same-repo dependencies always do; pinned by TestSameRepoUnchanged so this cannot regress), but when a blocker cannot be resolved and none of the resolvable ones are positively open, it changes nothing and logs the issue number instead of guessing -- keeping the asymmetry from cfbypass#19 (a false "unblocked" sends someone at work that cannot be done; a false "blocked" only hides work) rather than inverting it. Also probed whether the repo-scoped token could resolve a cross-repo blocker by querying the other repo directly instead of via `/dependencies`: `GET repos/weblib/cfbypass/issues/1` and `GET repos/weblib/cfbypass` both 404 under weblib-archive's own Actions token, while the same call against its own repo succeeds. So this token cannot see cfbypass at all; making cross-repo state resolvable would need a broader-scoped credential, which is a real blast-radius decision left for bit rather than assumed here. The "unknown -> leave alone, log it" behaviour in this PR fixes the stuck/reasserted label either way, with or without that credential ever existing. 12 new tests in test_sync_blocked_label.py, offline against a local http.server fake. Proved by fault injection: reverting sync_blocked_label.py to its pre-fix form (keeping the new tests) fails exactly the 5 tests in TestUnresolvableCrossRepoBlocker -- one of them reproducing the live reassert bug verbatim (`[(30, 'add', 'blocked by #0, #0')]`) -- while every other test still passes; restoring the fix (diffed byte-identical against the pre-revert copy) turns the suite green again. python3 test_sync_blocked_label.py # 12 tests, OK python3 test_report_job_log.py # 22 tests, OK (unaffected) Co-authored-by: bit <bit@das-labor.org>
112 lines
5.1 KiB
Markdown
112 lines
5.1 KiB
Markdown
# weblib-ci
|
|
|
|
The CI scripts shared by [cfbypass], [weblib-archive], [weblib-fs] and
|
|
[weblib-viewer]. Split out per weblib-archive#44, where they had been
|
|
hand-copied into each repo and had already drifted once.
|
|
|
|
**Public deliberately.** Nothing here is a secret or specific to the archive's
|
|
contents: a nixpkgs-pinning wrapper, a log poster and a label reconciler.
|
|
Public means a consumer needs no deploy key, no ssh setup and no secret to
|
|
fetch it — which was measured to be the difference between one step and three.
|
|
|
|
## What is here
|
|
|
|
| file | what it does |
|
|
|---|---|
|
|
| `with-nixpkgs.sh` | Runs a command with one nixpkgs package on PATH, pinned to the *consuming* repo's `flake.lock`. Avoids `nix shell nixpkgs#x`, which re-resolves the registry and refetches a channel tarball whenever the branch moves. |
|
|
| `report_job_log.py` | Posts the tail of a build log as a PR comment. Exists because `actions/jobs/{id}/logs` returns 500 for every id on Gitea 1.25.2, so a red job otherwise says only that it failed. Takes `--status`; see below. |
|
|
| `sync_blocked_label.py` | Keeps `Status/Blocked` in step with Gitea's dependency graph. Resolves the label from the repo *or the organisation*, never touches an issue marked `Status/On Hold` or `Status/Abandoned`, and leaves the label alone (rather than guessing) on a blocker whose state a repo-scoped token cannot resolve — see weblib-ci#11. |
|
|
|
|
All three are standard library / plain bash only. They are *run*, not built, so
|
|
this repo has no flake.
|
|
|
|
`test_report_job_log.py` covers the reporter and `test_sync_blocked_label.py`
|
|
covers the reconciler. Both are standard library and offline — the forge each
|
|
talks to is an `http.server` on localhost that keeps what it is sent, so a
|
|
test reads state back rather than trusting an exit status of 0 (which the
|
|
reporter in particular returns even when its POST failed).
|
|
|
|
```bash
|
|
python3 test_report_job_log.py # 22 tests, ~1.2s, no network
|
|
python3 test_sync_blocked_label.py # 12 tests, no network
|
|
```
|
|
|
|
There is no workflow running it: this repo has no `.gitea/workflows` at all,
|
|
and no `flake.lock` for `with-nixpkgs.sh` to read. Run it by hand before
|
|
pushing.
|
|
|
|
## Using it
|
|
|
|
```yaml
|
|
- uses: actions/checkout@v4
|
|
- id: ci
|
|
uses: https://git.chaosbit.de/weblib/weblib-ci@main
|
|
- run: bash ${{ steps.ci.outputs.path }}/with-nixpkgs.sh python3 \
|
|
python3 ${{ steps.ci.outputs.path }}/report_job_log.py /tmp/build.log
|
|
```
|
|
|
|
No credentials anywhere: the repo is public, which is the point of it being so.
|
|
|
|
`with-nixpkgs.sh` reads the **consuming** repo's `flake.lock` relative to the
|
|
working directory, so it keeps working when invoked by absolute path from
|
|
outside the checkout.
|
|
|
|
### Use the full URL, not `weblib/weblib-ci@main`
|
|
|
|
Measured on weblib-archive#44 (2026-09-07), one job per form because Gitea
|
|
posts one commit status per job and job logs return 500:
|
|
|
|
| form | result |
|
|
|---|---|
|
|
| `uses: https://git.chaosbit.de/weblib/weblib-ci@main` | **works** |
|
|
| `uses: weblib/weblib-ci@main` | **fails** |
|
|
| `git clone https://…/weblib-ci.git` with no credentials | works |
|
|
| `steps.<id>.outputs.path`, then running a tool through it | works |
|
|
|
|
The bare `owner/repo` form resolves against the instance's default actions URL
|
|
rather than this host, so it has to be the full URL. Both forms failed while
|
|
this repo was **private**, which is the other half of why it is public — the
|
|
alternative was a deploy key and an ssh setup step in four repos.
|
|
|
|
The `outputs.path` row is listed separately on purpose: the action *running* and
|
|
its output *reaching the caller* are different claims, and a composite action
|
|
returning an empty string is exactly the sort of thing that looks green.
|
|
|
|
### `report_job_log.py --status`
|
|
|
|
The header used to be hardcoded to *failed*. That is true of every caller here,
|
|
because each guards the step with `if: failure()` — but a probe run under
|
|
`if: always()` posted a failure report for a job that had passed
|
|
(weblib-viewer#10, filed as #8).
|
|
|
|
**The default is still `failed`**, so a caller passing only the log path is
|
|
unchanged. A step that can run on success has to say so:
|
|
|
|
```yaml
|
|
- name: report the log
|
|
if: always()
|
|
env:
|
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
|
run: |
|
|
bash "${{ steps.ci.outputs.path }}/with-nixpkgs.sh" python3 \
|
|
python3 "${{ steps.ci.outputs.path }}/report_job_log.py" /tmp/build.log \
|
|
--status "${{ job.status }}"
|
|
```
|
|
|
|
`${{ job.status }}` yields `success`/`failure`/`cancelled`/`skipped`, so those
|
|
spellings are accepted alongside `passed`/`failed`. `JOB_STATUS` in the
|
|
environment does the same thing if a flag is awkward. An **unrecognised**
|
|
status is put in the header verbatim rather than rejected: `argparse`'s
|
|
`choices=` would exit 2 on a value this list has not heard of, and the log —
|
|
the entire reason the script exists — would never be posted.
|
|
|
|
### Why not a flake input
|
|
|
|
These are scripts a workflow runs, not derivations. A flake input would cost a
|
|
`flake.lock` bump in four repos every time one changes, and buys nothing.
|
|
|
|
[cfbypass]: https://git.chaosbit.de/weblib/cfbypass
|
|
[weblib-archive]: https://git.chaosbit.de/weblib/weblib-archive
|
|
[weblib-fs]: https://git.chaosbit.de/weblib/weblib-fs
|
|
[weblib-viewer]: https://git.chaosbit.de/weblib/weblib-viewer
|