# 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: 1. Queries `GET /repos/{owner}/{repo}/pulls?state=open` to find the PR for the branch 2. Builds a markdown comment with `
` sections per job (up to 100 lines each) 3. Posts it via `POST /repos/{owner}/{repo}/issues/{pr_number}/comments` 4. Re-posts the pipeline commit status with `target_url` pointing to the comment Example comment format: ```markdown ## Pipeline `ci` — failure Commit: `5b7ae7b`
clippy — failed: command exited with code 101 _Showing last 100 of 523 lines_ \``` error[E0599]: ... \```
check — passed \``` $ cargo check --workspace Compiling ... \```
``` ### 3. `target_url` on Commit Statuses Added `target_url: Option` 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` 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 101` followed 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:repository` scope (sufficient for commit statuses) but needs `write:issue` scope 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:issue` scope to enable PR comments - [ ] After token update, re-deploy and verify the comment + `target_url` flow 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) ```