`report_job_log.py` hardcoded the word *failed* into the comment header. Every
caller guards the step with `if: failure()`, so it was true by construction --
until an unguarded probe in weblib-viewer#10 ran it on a job that passed, and
the successful run posted a comment reading as a failure report. Anyone
scrolling that PR would conclude the probe had failed.
The interesting uses of this script are exactly the ones that want
`if: always()`: a probe, or a job whose *output* is the point rather than its
exit code. Those all lied in the header.
`--status` now supplies the outcome. **The default is `failed`**, which is what
`if: failure()` means, so the four consuming repos are untouched -- a change in
required arguments would have broken all of them at once, since they take this
script from `@main`. `JOB_STATUS` in the environment does the same, matching how
every other argument here already reads its default from the Actions
environment.
`${{ job.status }}` yields `success`/`failure`/`cancelled`/`skipped` while a
human writing the flag reaches for `passed`/`failed`, so both spellings are
accepted and the expression can be passed straight through. An **unrecognised**
status goes into the header verbatim rather than being rejected: `argparse`'s
`choices=` would exit 2 on a value the table has not heard of, and the log --
the whole reason this script exists -- would never be posted. A reporter must
not become the thing that reports nothing.
The "no log file" note was status-dependent too; it claimed the step "failed
before the build started" regardless.
## Verified
`test_report_job_log.py`, new here: stdlib only and offline, posting to an
`http.server` on localhost that keeps what it is sent, so each test reads the
comment back. An exit status of 0 proves nothing -- the script deliberately
swallows HTTP errors so a failure to report cannot mask the failure being
reported. 21 tests, 0 skipped, 1.2s. Three of them drive the CLI as a
subprocess with only environment variables set, the way a workflow does.
Each check was shown to fire by injecting the fault and reverting it:
| injected fault | result |
|---|---|
| header hardcodes `failed` again (the original bug) | 10 failures |
| `DEFAULT_STATUS = "passed"` (would break the four callers) | 8 failures |
| unknown status raises, as `choices=` would | 2 errors |
| missing-log note keeps the failure wording | 1 failure |
| a stray `%` in the `--status` help text | 1 failure |
All five reverted; the file's checksum matches the pre-injection copy.
Also drops a tracked `__pycache__/report_job_log.cpython-313.pyc` and adds a
`.gitignore`. It was committed by accident in 061d8b2 and importing the module
from the tests rewrites it, so it would otherwise show up in every future diff
as stale bytecode of a file that had already changed.
Closes #8
Co-authored-by: bit <bit@das-labor.org>
110 lines
4.9 KiB
Markdown
110 lines
4.9 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*, and never touches an issue marked `Status/On Hold` or `Status/Abandoned`. |
|
|
|
|
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. It is standard library and
|
|
offline — the forge it posts to is an `http.server` on localhost that keeps
|
|
what it is sent, so a test reads the comment back rather than trusting an exit
|
|
status of 0, which this script returns even when the POST failed.
|
|
|
|
```bash
|
|
python3 test_report_job_log.py # 21 tests, ~1.2s, 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
|