Files
weblib-ci/README.md
T
claudeandbit c2947db08a
report_job_log live-smoke-test passed: live smoke test log line REGRESSION-CHECK-LIVE-SMOKE
Give a push-triggered job somewhere to report a failure
report_job_log.py only ever commented on an open PR, so a push to
development/main -- the shape of a publish pipeline, which has no
pull_request trigger -- had nowhere to post: the branch pushed to is the
merge's target, not the already-closed PR that produced it. Combined with
job logs 500ing on this Gitea, that made a push-triggered failure completely
invisible (weblib-viewer run 1769, diagnosed only by hardcoding --pr on a
throwaway commit).

A GitHub-style commit-comment endpoint, the first thing tried, does not
exist on this Gitea: 404, and absent from /swagger.v1.json entirely. What
does exist and was verified live: GET commits/{sha}/pull resolves the PR
that produced a given commit, open or closed, across both merge styles this
org uses -- so the closed PR the push actually came from can still be found
and commented on as before. When even that finds nothing (a commit that was
never part of any PR), fall back to a commit status, since that endpoint is
already proven to work here.

No CLI changes; existing callers are unaffected.

Closes #14

Co-authored-by: bit <bit@das-labor.org>
2026-09-10 10:46:23 +00:00

156 lines
7.8 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, falling back to a commit status when a `push`-triggered job has no PR to comment on. 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 # 28 tests, ~3s, 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.
### `push`-triggered jobs: no open PR does not mean no channel
Found the hard way on weblib-ci#14: a `push` to `development`/`main` — the
common shape of a workflow like a publish pipeline, which has no
`pull_request` trigger at all — has no *open* PR for `report_job_log.py` to
comment on. The branch that was pushed to is the merge's **target**, not the
PR that produced it, and that PR is already closed. The old behaviour was to
print "no pull request for this run" and exit 0, so a failure there was
completely invisible: not the log (500 on this Gitea), not a comment
(nowhere to put it), nothing. That is how weblib-viewer run 1769's failure
(`publish.sh` refusing to overwrite an existing APK) went unnoticed until it
was reproduced by hand.
Two things changed, tried in order, neither needing a new flag:
1. **Ask Gitea which PR produced this exact commit.**
`GET repos/{repo}/commits/{sha}/pull` resolves the PR a commit came from —
open or closed — keyed on the commit itself rather than a branch name.
Verified against both merge styles used in this org: an explicit merge
commit, and a fast-forward promotion where the "merge commit" is just the
PR branch's own tip. This needs the **full** 40-character `GITHUB_SHA`; a
short sha gets the same 404 shape as a commit with no PR at all, so it
would silently look like "not found". This covers the ordinary case —
almost every push to a protected branch here is a merge landing.
2. **If that also finds nothing** — a commit that was never part of any
PR — post a **commit status** instead
(`POST repos/{repo}/statuses/{sha}`, context `report_job_log`). It is not
a substitute for the full log: this Gitea did not truncate a 3000-character
description in testing, but a status is UI chrome, not a page, so what
goes there is deliberately a short snippet (`STATUS_TAIL`, 500 characters),
not the whole tail. `state` follows the same outcome as the header phrase
(`failed → failure`, `passed → success`, `cancelled → warning`,
`skipped → skipped`; an unrecognised status maps to `error` rather than
guessing pass or fail), and `target_url` points at the commit.
**A commit *comment* API (GitHub-style, `commits/{sha}/comments`) does not
exist on this Gitea** — 404, and absent from `/swagger.v1.json` entirely, not
merely unauthorised. That ruled out the most direct-looking option; the
commit-status endpoint is the one that is actually there.
If a PR *is* found (by either the original lookups or the new sha lookup),
behaviour is identical to before: a comment, same format. The status path is
only reached when no PR exists anywhere for the commit.
### 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