swactor-development-history/ci/CI_OUTPUT_IN_FORGEJO.md

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:

  1. Queries GET /repos/{owner}/{repo}/pulls?state=open to find the PR for the branch
  2. Builds a markdown comment with <details> 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:

## 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 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)