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

7.8 KiB

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).

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

- 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:

- 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.