report Action Reference¶
actions/report publishes an already-produced abicheck JSON report to a
pull request, as a sticky comment and/or a job summary. It is the
publication half of the two-workflow split described in
Reporting on fork pull requests, and its
design record is
ADR-073.
It analyses nothing. No comparison, no dump, no build query, no compiler, no project-dependency install. It installs abicheck and renders. That is what makes it safe to run in a trusted
workflow_runjob holding a token that can write to the pull request, and it is checked mechanically —tests/test_action_report_contract.pyfails if either of this Action's scripts reaches for a toolchain, and runs the Action end to end against aPATHwhose compilers andghall record their own invocation and fail.
What it does¶
- Reads the report at
report. Four shapes are recognised: acomparereport, a directory/package release report, acompare --no-baselineaudit report, and anaggregatefan-in document. - Renders the sticky comment body — the same projection the root Action posts (ADR-072), unchanged.
- Bounds the body and the job summary independently against GitHub's real limits, disclosing any truncation in the body itself.
- Decides what to do: create, update in place, clear a now-resolved result, or skip.
- Performs that one API call.
Inputs¶
| Input | Default | Description |
|---|---|---|
report |
(required) | Path to a canonical abicheck JSON report. An aggregate document's per-target member reports are read from that document's own directory. |
detail |
standard |
summary, standard or full. |
on |
changes |
always, changes or never. Must be quoted in the caller's YAML ("on": changes) — an unquoted on key is a YAML 1.1 boolean. Use post-on to avoid the quoting. |
post-on |
(empty) | Alias for on that needs no quoting. Wins when both are set. |
sha |
(empty) | The analysed head/build SHA to display. For a pull_request run this is the ephemeral merge commit, not the PR head — pass what was actually analysed. |
run-label |
(empty) | Footer label, e.g. run #128. |
report-url |
(empty) | Linked as "View workflow run"; also used when the body is shortened. |
report-artifact-url |
(empty) | Linked as "Download full report". Pass it only for an upload that succeeded. |
path-prefix |
(empty) | Checkout root to strip from rendered source locations. |
gate-api-break |
false |
Render API/source breaks under "Breaking", mirroring a producer that gated on them. Presentation only. |
gate-breaking |
true |
Whether the producer gated on ABI breaks. Affects only the analysis-incomplete section's wording. |
profile |
default |
Profile this report covers; the default comment identity is derived from it. |
comment-identity |
(empty) | Explicit sticky identity. Defaults to abicheck:<profile>. |
repository |
current repo | owner/repo to publish into. |
pr-number |
(empty) | The pull request. Required unless dry-run. Resolve it through the API — see verify-source-run — never from a contributor-produced artifact. |
github-token |
(empty) | Token with pull-requests: write. Required unless dry-run. |
source-run-id |
this job's run | The run that produced the report — ${{ github.event.workflow_run.id }} in a workflow_run publisher. This is what the ordering guard compares; see below. |
source-run-attempt |
this job's attempt | The producing run's attempt, paired with source-run-id. |
job-summary |
true |
Also write the body to this job's summary. |
max-comment-bytes |
60000 |
Byte budget for the comment body. |
max-summary-bytes |
900000 |
Byte budget for the job summary. |
dry-run |
false |
Render and write body-path without contacting the API. |
python-version |
3.13 |
For actions/setup-python. |
abicheck-version |
(empty) | PyPI version to install instead of this Action's own checkout. |
Outputs¶
| Output | Description |
|---|---|
posted |
true when a comment was created, updated or cleared. |
comment-url |
URL of the comment, when one was written. |
body-path |
Path to the rendered body on disk. Written even under dry-run. |
body-bytes |
Size of the rendered body in UTF-8 bytes. |
skipped-reason |
never, no-changes, stale, dry-run, or empty when something was published. |
Sticky comments and the ordering guard¶
The body carries a hidden marker:
<!-- abicheck-report-identity: {"head_sha": "…", "identity": "abicheck:linux-gcc", "run_attempt": 1, "run_id": "1234"} -->
identity is the sticky key: one comment per identity per pull request.
Because it defaults to abicheck:<profile>, a matrix publishing several
profiles to one PR gets one comment each rather than several fighting over
one.
run_id and run_attempt are the ordering guard, and they must be the
producing run's — pass source-run-id/source-run-attempt. They default
to this job's own, which is correct only when one run both analyses and
publishes. In a workflow_run publisher they are different runs, and a
publisher's id orders by when it was triggered: a re-run of an older commit
is triggered later, so its publisher carries the larger id and would be read
as the newer result — inverting the very guarantee this guard exists to give.
Producer runs finish out of order — a re-run of an older commit, a slow
matrix leg, a retried publisher — and without an ordering record the last
writer wins, which means the oldest result can be the one a reviewer is
left looking at. When the existing comment records a strictly newer run,
this Action publishes nothing and sets skipped-reason=stale, even if it
has findings and the newer comment is clean.
The guard orders this Action's own publications. It is not an
authentication boundary: the marker is a plain HTML comment in a public
body, so anyone who can comment on the pull request can write one. A
same-identity comment carrying an implausible run id will make this Action
stand down — visibly, as skipped-reason=stale, never by publishing
something wrong.
An existing marker that cannot be ordered (a malformed payload, no run id) is not treated as newer: the Action publishes. A publisher frozen forever by one broken marker is worse than one redundant update.
Resolution and clearing¶
When the report now shows nothing and a prior comment for this identity
exists, that comment is updated to state the previously reported findings
are resolved — under on: changes too, because "the report no longer shows
this" is itself the news. Not under on: never, which suppresses every
write: never short-circuits before the report is read at all, so a
"resolved" notice there would assert an all-clear this Action never
established. It is rewritten rather than deleted so it keeps its
ordering marker, which a delete would discard along with the ability of a
slower older run to know it has been superseded.
When there is no prior comment and nothing to say, the Action stays quiet.
Size limits¶
| Destination | GitHub's limit | This Action's default budget |
|---|---|---|
| Pull-request comment | 65,536 characters (262,144 bytes in storage) | 60,000 bytes |
| Job summary | 1 MiB per step, over which the summary is dropped entirely | 900,000 bytes |
Both are measured in UTF-8 bytes, which also bounds characters (a UTF-8 string never has more characters than bytes), and the two are bounded independently from the same full render — each gets the whole body and its own budget. Bounding the summary from the already-cut comment would hand it the comment's budget and the comment's truncation notice, making its ~15x larger limit buy nothing.
A body over budget is cut on a line boundary (never mid-row, never
mid-codepoint), any open <details> is closed, and the cut is stated in the
body. Truncation is never presented as an empty or smaller finding set;
the exact counts stay in the header and the complete machine-readable report
stays intact and linked.
Failure semantics¶
Two rules, and they are the reason this Action exists rather than a
continue-on-error step:
- A publication failure fails the step, with a message naming what
failed, and sets
posted=false. It is never downgraded to a warning and never reported as a clean compatibility result. - A non-clean compatibility verdict never fails this Action. It is a reporter, not a gate. The gate already ran in the producer job, and its exit code is that job's to publish.
Untrusted input¶
Report contents, symbol names and PR text are treated as untrusted throughout: nothing from the report becomes a shell word, an argument or part of a URL. The body is written to a file by Python, the API request document is JSON that Python serialized, and the shell passes file paths. Rendered Markdown is escaped by the renderer's own cell escaping.
Validated before use: repository must match owner/repo, and pr-number
must be a positive integer — both are checked before either reaches an API
path.
Example¶
- uses: abicheck/abicheck/actions/report@v1
with:
report: abicheck-source-artifact/aggregate.json
repository: ${{ github.repository }}
pr-number: ${{ steps.verify.outputs.pr-number }}
sha: ${{ steps.verify.outputs.tested-sha }}
profile: linux-gcc
detail: standard
post-on: changes
report-url: ${{ github.event.workflow_run.html_url }}
github-token: ${{ secrets.GITHUB_TOKEN }}
See Reporting on fork pull requests for the complete two-workflow example.