Take the job's outcome as an argument, not as an assumption #10

Open
claude wants to merge 2 commits from fix/report-status-argument into main
Member

report_job_log.py hardcoded the word failed into the comment header. Every
caller guards the step with if: failure(), so that 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.

That matters because 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.

What changed

--status supplies the outcome (JOB_STATUS in the environment does the same,
matching how every other argument here already defaults).

The default is still failed. The four repos take this script from @main
and pass only the log path, so a change in required arguments would have broken
all of them at once. That backward compatibility has its own test, and the test
was shown to fail when the default is flipped.

Two decisions worth flagging, either of which could reasonably have gone the
other way:

  • No argparse choices=. ${{ job.status }} yields
    success/failure/cancelled/skipped, and both those and
    passed/failed are mapped — but an unrecognised status is put into the
    header verbatim rather than rejected. choices= would exit 2 on a value the
    table has not heard of and the log would never be posted; a reporter must not
    become the thing that reports nothing.
  • The "no log file" note was status-dependent too, claiming the step
    "failed before the build started" whatever the outcome. It now only says that
    for a failure.

Verified

test_report_job_log.py is new: stdlib only and offline, posting to an
http.server on localhost that keeps what it is sent, so every test reads the
comment back
. An exit status of 0 proves nothing here — the script
deliberately swallows HTTP errors so a failure to report cannot mask the failure
being reported. Three tests drive the CLI as a subprocess with only environment
variables set, the way a workflow does.

Ran 21 tests in 1.2s — OK (0 skipped)

Each check was proved to fire by injecting the fault, then 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, and the
suite is green again after each.

The rendered headers, from the real CLI against a throwaway server:

**`tests` failed** on `deadbeef`.          (no flag — unchanged)
**`tests` passed** on `deadbeef`.          (--status success)
**`tests` was cancelled** on `deadbeef`.   (--status cancelled)

Not verified, and two things found on the way

  • Nothing runs this suite in CI. weblib-ci has no .gitea/workflows at all,
    and no flake.lock for with-nixpkgs.sh to read, so adding one is a
    separate decision rather than a line in this PR. Filed as #9.
  • No caller was changed. Switching a workflow to if: always() is a
    per-repo judgement, not this repo's to make. The README shows the call.
  • __pycache__/report_job_log.cpython-313.pyc was tracked (committed by
    accident in 061d8b2). Importing the module from the tests rewrites it, so it
    showed up in this diff as stale bytecode of a file that had just changed. It
    is untracked here and a .gitignore added.

Closes #8

`report_job_log.py` hardcoded the word *failed* into the comment header. Every caller guards the step with `if: failure()`, so that 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. That matters because 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. ## What changed `--status` supplies the outcome (`JOB_STATUS` in the environment does the same, matching how every other argument here already defaults). **The default is still `failed`.** The four repos take this script from `@main` and pass only the log path, so a change in required arguments would have broken all of them at once. That backward compatibility has its own test, and the test was shown to fail when the default is flipped. Two decisions worth flagging, either of which could reasonably have gone the other way: - **No `argparse` `choices=`.** `${{ job.status }}` yields `success`/`failure`/`cancelled`/`skipped`, and both those and `passed`/`failed` are mapped — but an **unrecognised** status is put into the header verbatim rather than rejected. `choices=` would exit 2 on a value the table has not heard of and the log would never be posted; a reporter must not become the thing that reports nothing. - **The "no log file" note was status-dependent too**, claiming the step "failed before the build started" whatever the outcome. It now only says that for a failure. ## Verified `test_report_job_log.py` is new: stdlib only and offline, posting to an `http.server` on localhost that keeps what it is sent, so every test **reads the comment back**. An exit status of 0 proves nothing here — the script deliberately swallows HTTP errors so a failure to report cannot mask the failure being reported. Three tests drive the CLI as a subprocess with only environment variables set, the way a workflow does. ``` Ran 21 tests in 1.2s — OK (0 skipped) ``` Each check was proved to fire by injecting the fault, then 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, and the suite is green again after each. The rendered headers, from the real CLI against a throwaway server: ``` **`tests` failed** on `deadbeef`. (no flag — unchanged) **`tests` passed** on `deadbeef`. (--status success) **`tests` was cancelled** on `deadbeef`. (--status cancelled) ``` ## Not verified, and two things found on the way - **Nothing runs this suite in CI.** weblib-ci has no `.gitea/workflows` at all, and no `flake.lock` for `with-nixpkgs.sh` to read, so adding one is a separate decision rather than a line in this PR. Filed as #9. - **No caller was changed.** Switching a workflow to `if: always()` is a per-repo judgement, not this repo's to make. The README shows the call. - `__pycache__/report_job_log.cpython-313.pyc` was tracked (committed by accident in 061d8b2). Importing the module from the tests rewrites it, so it showed up in this diff as stale bytecode of a file that had just changed. It is untracked here and a `.gitignore` added. Closes #8
claude added 1 commit 2026-09-08 07:55:30 +00:00
`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>
claude force-pushed fix/report-status-argument from b4fcba7cca to 028c162874 2026-09-08 07:57:26 +00:00 Compare
claude added 1 commit 2026-09-08 08:01:01 +00:00
`status_phrase` renders a status it does not know verbatim, deliberately:
`argparse`'s `choices=` would exit 2 on an unexpected value and the log --
the entire reason this script exists -- would never be posted.

But the verbatim value lands in a code span inside a **bold** header, so a
backtick in it closes the span early and the rest renders as markdown:

    --status 'x` **loud** `y'
    -> **`tests` finished with status `x` **loud** `y`**

Nothing hostile is expected: the value comes from `${{ job.status }}` or a
hand-written flag, both written by whoever wrote the workflow. It is worth
closing anyway because this repo is public and four others consume the
script as a composite action, so a branch name or a matrix value could
reach this argument later without anyone revisiting this function.

Backticks are removed rather than escaped -- there is no escape for a
backtick inside a code span, only a wider fence, and the status is a short
word rather than something whose exact bytes matter.

Found in the cold re-read of #10, not by the suite, so the test that
covers it was proved to fail without the fix.

Co-authored-by: bit <bit@das-labor.org>
Author
Member

Cold re-read done. One change pushed, 13e396a.

Re-measured here rather than taken from the agent's report:

  • 22 tests, 0 skipped (grep -ciE "skipped|expected failure" → 0), 1.1 s.
  • Fault injection, independently: restoring the original hardcoded failed header fails 10 of them. Reverted; cksum report_job_log.py back to 3892834760 8913, git diff --stat empty.
  • Backward compatibility proved byte-for-byte, which is the thing that actually matters here — four repos consume this as a composite action. I ran main's copy and this branch's copy as subprocesses against the same local server with the existing call shape (log path only, no new flag) and compared the posted bodies: identical. Still identical after my commit.
  • Every new path driven through the entry point, not just the units: successpassed, cancelledwas cancelled, skippedwas skipped, unknown → verbatim, empty --statusfailed, JOB_STATUS honoured, and the flag beating the env var.
  • The missing-log note really is status-dependent: "it failed before the build started" only for a failure, "nothing was captured" otherwise.
  • Secret scan of the whole diff: every hit is a variable name or the fake literal t0ken. Nothing secret added, which this repo being public makes worth stating explicitly.

What I changed

The verbatim path for an unrecognised status broke out of its own code span:

--status 'x` **loud** `y'   ->   **`tests` finished with status `x` **loud** `y`**

Backticks are now removed from that value. Not a security finding${{ job.status }} and a hand-written flag are both written by whoever wrote the workflow, so there is no untrusted input here today. It is worth closing anyway because this repo is public and shared by four others, so a branch name or a matrix value could reach this argument later without anyone revisiting the function. Backticks are removed rather than escaped because there is no escape for a backtick inside a code span, only a wider fence.

The suite gained a test for it, and I proved it fails without the fix (1 failure) before reverting.

Agreeing with two judgement calls

  • No choices=. Right, and for the reason given: the script exists because a red job says nothing, so it must not become the thing that says nothing. Exiting 2 on an unfamiliar status would throw away the log.
  • The __pycache__ removal. Scope-adjacent but correct — a tracked .pyc of a file this PR changes is stale by construction, and it was committed by accident in 061d8b2.

Not merged, and why

weblib-ci PRs need your approval, so this is not armed. Requesting your review now that the re-read is done — deliberately not before, since a push after an approval would revoke it.

Also filed by the agent: #9, that nothing runs this repo's own tests. It has no .gitea/workflows at all and no flake.lock for with-nixpkgs.sh to read, so the 22 tests only run when someone runs them by hand.

## Cold re-read done. One change pushed, `13e396a`. Re-measured here rather than taken from the agent's report: - **22 tests, 0 skipped** (`grep -ciE "skipped|expected failure"` → 0), 1.1 s. - **Fault injection, independently**: restoring the original hardcoded `failed` header fails **10** of them. Reverted; `cksum report_job_log.py` back to `3892834760 8913`, `git diff --stat` empty. - **Backward compatibility proved byte-for-byte**, which is the thing that actually matters here — four repos consume this as a composite action. I ran `main`'s copy and this branch's copy as subprocesses against the same local server with the existing call shape (log path only, no new flag) and compared the posted bodies: **identical**. Still identical after my commit. - Every new path driven through the entry point, not just the units: `success` → *passed*, `cancelled` → *was cancelled*, `skipped` → *was skipped*, unknown → verbatim, empty `--status` → *failed*, `JOB_STATUS` honoured, and the flag beating the env var. - The missing-log note really is status-dependent: *"it failed before the build started"* only for a failure, *"nothing was captured"* otherwise. - **Secret scan of the whole diff**: every hit is a variable *name* or the fake literal `t0ken`. Nothing secret added, which this repo being public makes worth stating explicitly. ## What I changed The verbatim path for an unrecognised status broke out of its own code span: ``` --status 'x` **loud** `y' -> **`tests` finished with status `x` **loud** `y`** ``` Backticks are now removed from that value. **Not a security finding** — `${{ job.status }}` and a hand-written flag are both written by whoever wrote the workflow, so there is no untrusted input here today. It is worth closing anyway because this repo is public and shared by four others, so a branch name or a matrix value could reach this argument later without anyone revisiting the function. Backticks are *removed* rather than escaped because there is no escape for a backtick inside a code span, only a wider fence. The suite gained a test for it, and I proved it fails without the fix (1 failure) before reverting. ## Agreeing with two judgement calls - **No `choices=`.** Right, and for the reason given: the script exists because a red job says nothing, so it must not become the thing that says nothing. Exiting 2 on an unfamiliar status would throw away the log. - **The `__pycache__` removal.** Scope-adjacent but correct — a tracked `.pyc` of a file this PR changes is stale by construction, and it was committed by accident in `061d8b2`. ## Not merged, and why weblib-ci PRs need your approval, so this is not armed. Requesting your review now that the re-read is done — deliberately not before, since a push after an approval would revoke it. Also filed by the agent: #9, that nothing runs this repo's own tests. It has no `.gitea/workflows` at all and no `flake.lock` for `with-nixpkgs.sh` to read, so the 22 tests only run when someone runs them by hand.
claude requested review from bit 2026-09-08 08:01:27 +00:00
This pull request doesn't have enough required approvals yet. 0 of 1 approvals granted from users or teams on the allowlist.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/report-status-argument:fix/report-status-argument
git checkout fix/report-status-argument
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Reference: weblib/weblib-ci#10