Merge pull request 'docs: Jenkins read-only build status tools design (#72)' (#88) from docs/issue-72-jenkins-readonly-design into master

This commit was merged in pull request #88.
This commit is contained in:
2026-07-02 14:01:36 -05:00
@@ -0,0 +1,151 @@
# Jenkins Read-Only Build Status Tools — Design Notes
- **Status:** Design (implementation-ready notes; **no implementation in this repo**)
- **Issue:** #72 (parent umbrella: #75; boundary decision: ADR-0001, #71)
- **Related:** #77 (repo/branch/PR → job mapping, designed separately)
- **Date:** 2026-07-02
## 1. Purpose and scope
Define the minimum **read-only** Jenkins MCP tool set that lets an LLM answer:
*"Did the latest build for this project/branch succeed or fail?"* — plus enough
detail (build URL, number, timing, result) to report or investigate.
Phase 1 is **strictly read-only**, per ADR-0001
([`adr-0001-mcp-control-plane-boundaries.md`](adr-0001-mcp-control-plane-boundaries.md)):
- **Excluded: build triggers.**
- **Excluded: deploy triggers.**
- **Excluded: parameterized job launches.**
- Excluded: job creation/deletion/config changes, queue manipulation, node
management — any Jenkins mutation whatsoever.
## 2. Boundary placement
These tools belong to the **`jenkins-mcp`** package/server of the MCP Control
Plane — **never** inside `gitea-mcp` (`mcp_server.py` in this repo).
Consequences (from `tool-boundaries.md`, `credential-isolation.md`, ADR-0001):
- `jenkins-mcp` runs as its own server process with its own `.env`.
- **Jenkins credentials never enter the Gitea MCP runtime**, and Gitea
credentials never enter `jenkins-mcp`.
- This document lands in this repo only because the repo currently hosts the
Control Plane's architecture docs; the code ships elsewhere (owner decision
#1 of ADR-0001).
## 3. Minimum read-only tool set
| Tool | Purpose |
|---|---|
| `jenkins_whoami` | Verify authenticated Jenkins identity + active profile (mirror of `gitea_whoami`; fail-closed identity proof before anything else) |
| `jenkins_list_jobs` | List visible jobs (supports folder paths), with pagination bounds |
| `jenkins_latest_build` | The primary question: latest build of a job (or job+branch for multibranch) → status summary |
| `jenkins_build_status` | Status of a specific build number (job, number) |
| `jenkins_get_build` | Full safe detail of a build (fields in §4) |
| `jenkins_console_tail` | Bounded, redacted tail of a build's console log (§6) — optional, approval-gated addition |
All tools are `GET`-only against the Jenkins JSON API (`/api/json`,
`.../lastBuild/api/json`, `.../consoleText`). No tool issues POST/PUT/DELETE.
## 4. Return payloads (safe fields)
`jenkins_latest_build` / `jenkins_build_status` / `jenkins_get_build` return:
| Field | Source | Notes |
|---|---|---|
| `job` | request | Fully-qualified job path (folders joined with `/`) |
| `build_number` | `number` | int |
| `result` | `result` | `SUCCESS` / `FAILURE` / `UNSTABLE` / `ABORTED` / `NOT_BUILT`; `null``IN_PROGRESS` when `building=true` |
| `building` | `building` | bool |
| `url` | `url` | Build URL |
| `branch` | multibranch job name / SCM action | Best-effort; omitted when unknown |
| `timestamp` | `timestamp` | ISO-8601 UTC (converted from epoch ms) |
| `duration_seconds` | `duration` | 0/omitted while building |
| `commit_sha` | SCM build action | Best-effort; omitted when unknown |
Rules: no raw Jenkins payload passthrough (allowlist projection only); no
`Authorization` header, token, or crumb material in any output or error
(reuse the shared redaction approach of `safety-model.md` §3 / `gitea_audit`).
## 5. Failure behavior (fail closed, clear, safe)
| Condition | Behavior |
|---|---|
| Unknown job | Explicit `{"found": false, "job": "<path>", "error": "job not found"}` — never guess or fuzzy-match a job name (hard rule; see also #77) |
| Jenkins unreachable (DNS/timeout/conn refused) | Clear `"network error contacting Jenkins: <redacted reason>"`; no retry storm — mirror `gitea_auth.api_request` timeout + failure conversion |
| 502/503/504 | Explicit "Jenkins upstream unavailable" |
| 401/403 | "Jenkins auth failed / insufficient permissions" — **without** echoing credentials or the request's auth material |
| Malformed JSON | "malformed JSON response from Jenkins" (no raw-body dump) |
| Missing profile/creds | Fail closed before any network call (§7) |
## 6. Console tail safety (`jenkins_console_tail`)
Console logs are the highest-risk surface (secrets, tokens, internal hosts
routinely leak into build logs). If included at all (owner may defer it):
- **Bounded:** hard server-side cap (default: last 200 lines AND ≤ 64 KiB,
whichever is smaller; caller may request less, never more).
- **Redacted:** pass through the shared secret redactor (token/`Basic`/`Bearer`/
password/key-value patterns) before returning; redaction failure ⇒ return an
error, never the raw text.
- **Default off:** summary fields (`result`, failing stage if cheaply available)
are preferred; the tail requires an explicit `allowed_operations` entry
(`jenkins.console.read`) distinct from plain `jenkins.build.read`.
## 7. Credentials and profile requirements
Follows the per-service profile model (`gitea-execution-profiles.md`, extended
by #76):
- Env/config: `JENKINS_URL`, `JENKINS_USER`, `JENKINS_TOKEN_SOURCE_NAME`
(name-of-secret only — value resolved at runtime, never logged/committed).
- Profile: e.g. `jenkins-readonly` with namespaced
`allowed_operations: ["jenkins.read", "jenkins.build.read"]`
(+ `jenkins.console.read` only if the tail tool is approved);
`forbidden_operations: ["jenkins.build.trigger", "jenkins.deploy", "jenkins.job.configure"]`
as belt-and-braces even though no mutating tool exists.
- Missing URL/user/token/profile ⇒ **fail closed** with a clear message.
- Since every tool is read-only, no confirmation gates are needed — but
identity (`jenkins_whoami`) must still work so workflows can prove which
Jenkins account they act as.
## 8. Job addressing and mapping
Tools accept an explicit fully-qualified job path (folder-aware:
`folder/subfolder/job`). How a *repo/branch/PR* resolves to that job path is
**out of scope here** and designed in **#77**, with these fixed constraints:
- No silent guessing of job names — unmapped input returns an explicit
"no mapping" result.
- Multibranch pipelines address a branch job as `<job>/<branch>` with proper
URL-encoding of branch names (e.g. `feature%2Fx`).
## 9. Testing strategy (for the implementing package)
Mocked-Jenkins unit tests only (no live Jenkins in unit CI), mirroring this
repo's conventions (`docs/developer-testing-guidelines.md`):
- Patch the HTTP client; assert method is always `GET` and URL shape is correct
(folders, multibranch encoding).
- Success projections: field allowlist exactly as §4; unknown fields dropped.
- `result=null + building=true``IN_PROGRESS`.
- Unknown job ⇒ found:false, no fuzzy match, no API retry.
- Timeout/DNS/5xx/malformed-JSON ⇒ safe errors, no secret/credential leakage
(explicit no-token-in-error assertions).
- Console tail: cap enforcement (lines and bytes), redaction applied, redaction
failure ⇒ error not raw text, gated behind `jenkins.console.read`.
- Profile gate: missing/insufficient profile ⇒ no network call
(`mock_api.assert_not_called()` pattern).
## 10. Implementation-readiness checklist
Ready to implement in `jenkins-mcp` once:
1. ADR-0001 owner decision #1 (where `jenkins-mcp` lives) is made.
2. #76 profile schema exists (or a minimal `jenkins-readonly` profile is
hand-rolled to the same rules).
3. #77 mapping design is accepted (or tools ship path-addressed only, mapping
deferred).
Explicitly **not** unlocked by this document: build triggers, deploys,
parameterized launches, any Jenkins code in `mcp_server.py`.