140 lines
5 KiB
Markdown
140 lines
5 KiB
Markdown
|
|
# 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:
|
||
|
|
|
||
|
|
```markdown
|
||
|
|
## 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)
|
||
|
|
```
|