5 KiB
CI Output Visible in Forgejo — Development History
Added two mechanisms so CI results are visible directly in the Forgejo web UI without SSH-ing into the runner: enhanced commit status descriptions and PR comments with full job output.
Branch:
spot-instance
Problem
The CI pipeline worked end-to-end but job output was only visible in the
runner's stderr log on the Thinkpad. To see why clippy failed, you had to
ssh thinkpad 'tail ~/local-runner.log'. Forgejo's commit status descriptions
just said "Job 'clippy' completed" with no output.
Forgejo lacks GitHub's Checks API (no annotations, no log viewer), so we use two complementary approaches.
What Was Done
1. Enhanced Commit Status Descriptions
On job completion, the status description now includes:
- On success:
"Job 'check' passed" - On failure:
"Job 'clippy' failed: command exited with code 101\n[stderr] error: you should consider..."— last ~10 lines of output, capped at 250 characters.
This is visible directly on the PR page and commit page in Forgejo without clicking anything.
2. PR Comments with Full Output
When a pipeline reaches terminal state, the StatusReporter:
- Queries
GET /repos/{owner}/{repo}/pulls?state=opento find the PR for the branch - Builds a markdown comment with
<details>sections per job (up to 100 lines each) - Posts it via
POST /repos/{owner}/{repo}/issues/{pr_number}/comments - Re-posts the pipeline commit status with
target_urlpointing to the comment
Example comment format:
## Pipeline `ci` — failure
Commit: `5b7ae7b`
<details>
<summary>clippy — failed: command exited with code 101</summary>
_Showing last 100 of 523 lines_
\```
error[E0599]: ...
\```
</details>
<details>
<summary>check — passed</summary>
\```
$ cargo check --workspace
Compiling ...
\```
</details>
3. target_url on Commit Statuses
Added target_url: Option<String> to StatusUpdate. When a PR comment is
successfully posted, the pipeline's commit status badge links directly to that
comment. Clicking the status badge on the PR page jumps to the output.
Files Changed
| File | Change |
|---|---|
crates/ci/src/lib.rs |
Added target_url: Option<String> to StatusUpdate |
crates/ci/src/status_reporter.rs |
Added JobOutput, PostPipelineComment, find_pr_for_branch(), post_pr_comment(), build_pipeline_comment(), handle_pipeline_comment() |
crates/ci/src/local_coordinator.rs |
Enhanced handle_job_complete() descriptions; added emit_pipeline_comment(), called from try_schedule_next() |
crates/ci/src/coordinator.rs |
Mechanical target_url: None at 4 sites |
crates/simulation/src/ci/local_sim.rs |
Mechanical target_url: None at 3 sites |
crates/simulation/src/ci/sim.rs |
Mechanical target_url: None at 2 sites |
crates/ci/Cargo.toml |
Added features = ["json"] to ureq for into_json() |
Deployment & Verification
Built and deployed updated local-runner to the Thinkpad, pushed to the
spot-instance branch (which has PR #42 open), and observed:
Working:
- Commit statuses show descriptive output. The clippy failure status reads:
Job 'clippy' failed: command exited with code 101followed by the tail of the clippy output, truncated at 250 chars. - Passed jobs show
"Job 'check' passed"/"Job 'test' passed". - Pipeline-level status correctly reports
ci/ci → failure.
Blocked on token scope:
- PR comment posting returned HTTP 403. The Forgejo API token has
write:repositoryscope (sufficient for commit statuses) but needswrite:issuescope to post comments on PRs/issues. - The code degrades gracefully: logs the error, skips the comment, posts the
pipeline status without
target_url.
TODO
- Regenerate Forgejo API token with
write:issuescope to enable PR comments - After token update, re-deploy and verify the comment +
target_urlflow end-to-end
Edge Cases Handled
| Case | Behavior |
|---|---|
| No open PR for branch | Comment silently skipped, status posted without target_url |
| API failures (403, network) | Logged via eprintln!, degrades gracefully |
| Long output | Capped at last 100 lines per job in PR comment, with _Showing last N of M lines_ note |
| Long description | Capped at 250 chars for commit status description field |
| All HTTP code | Gated behind #[cfg(feature = "local")] — simulation builds unaffected |
Architecture Note
All new HTTP calls (PR listing, comment posting) happen in the StatusReporter actor, which is fire-and-forget. The LocalCoordinator never blocks on HTTP. The flow is:
LocalCoordinator StatusReporter
| |
|-- emit_status(StatusUpdate) ----->|-- POST /statuses/{sha}
| |
|-- PostPipelineComment ----------->|-- GET /pulls?state=open
| |-- POST /issues/{n}/comments
| |-- POST /statuses/{sha} (with target_url)