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