docs: Jenkins repo/branch/PR to job mapping design (#77)
Add docs/architecture/jenkins-job-mapping-design.md: declarative versioned mapping config (exact-match repo/branch entries, no globs, fail-closed load on malformed/duplicate entries), resolution semantics for multibranch/ single/parameterized-view job types with URL-encoded branch and PR-<n> addressing, branch-pinned-over-repo-wide precedence, fork PRs resolving via base repo only, explicit machine-checkable no-match payload (never guess or probe job names), config location in the jenkins-mcp package (no secrets, env-overridable path), a read-only jenkins_resolve_job tool surface, and a mocked-config/mocked-Jenkins testing strategy. Design only; no implementation, no code behavior changed, no Jenkins write actions introduced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
|||||||
|
# Jenkins Repo/Branch/PR → Job Mapping — Design Notes
|
||||||
|
|
||||||
|
- **Status:** Design (implementation-ready notes; **no implementation in this repo**)
|
||||||
|
- **Issue:** #77 (parent: #72 read-only tools design; umbrella: #75; boundary: ADR-0001, #71)
|
||||||
|
- **Related docs:** [`jenkins-readonly-build-status-design.md`](jenkins-readonly-build-status-design.md)
|
||||||
|
- **Date:** 2026-07-02
|
||||||
|
|
||||||
|
## 1. Purpose
|
||||||
|
|
||||||
|
The #72 tool set addresses Jenkins jobs by **explicit fully-qualified job
|
||||||
|
path**. This document designs the layer above it: how a *(repository, branch,
|
||||||
|
PR)* tuple — the vocabulary of Gitea workflows — resolves deterministically to
|
||||||
|
a Jenkins job path, so an LLM can ask "did the build for `Gitea-Tools`
|
||||||
|
`master` pass?" without knowing Jenkins internals.
|
||||||
|
|
||||||
|
Hard constraints inherited from #72 / ADR-0001:
|
||||||
|
|
||||||
|
- **No silent guessing of job names.** Unmapped input returns an explicit
|
||||||
|
"no mapping" result — never a fuzzy match, never a constructed-and-probed
|
||||||
|
name.
|
||||||
|
- **Read-only.** Mapping introduces no Jenkins write actions.
|
||||||
|
- Lives in the **`jenkins-mcp`** boundary; no Gitea credentials involved.
|
||||||
|
|
||||||
|
## 2. Mapping format
|
||||||
|
|
||||||
|
Declarative, versioned config (TOML or JSON — match whatever config format
|
||||||
|
`jenkins-mcp` adopts; illustrated here as TOML):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
version = 1
|
||||||
|
|
||||||
|
[[mapping]]
|
||||||
|
# Source side (what the caller supplies)
|
||||||
|
repo = "Scaled-Tech-Consulting/Gitea-Tools" # org/repo, exact
|
||||||
|
# Target side (where it lives in Jenkins)
|
||||||
|
job = "scaled-tech/gitea-tools" # foldered job path
|
||||||
|
type = "multibranch" # "multibranch" | "single" | "parameterized-view"
|
||||||
|
|
||||||
|
[[mapping]]
|
||||||
|
repo = "Scaled-Tech-Consulting/Timesheet"
|
||||||
|
branch = "master" # optional: branch-specific override
|
||||||
|
job = "scaled-tech/timesheet-master"
|
||||||
|
type = "single"
|
||||||
|
```
|
||||||
|
|
||||||
|
Field semantics:
|
||||||
|
|
||||||
|
| Field | Required | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `repo` | yes | Exact `org/repo` (case-insensitive compare, stored canonical) |
|
||||||
|
| `branch` | no | Exact branch name this entry pins; absent = all branches |
|
||||||
|
| `job` | yes | Fully-qualified Jenkins job path, folders `/`-joined |
|
||||||
|
| `type` | yes | How branch/PR resolves under the job (§3) |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- **Exact matching only** on `repo` and `branch`. No globs in v1 (globs invite
|
||||||
|
accidental over-matching; add later behind an explicit `pattern = true` flag
|
||||||
|
if ever needed).
|
||||||
|
- Unknown `type` or malformed entry ⇒ config load fails closed with a clear
|
||||||
|
error naming the entry — a broken mapping file must not half-load.
|
||||||
|
- Duplicate `(repo, branch)` keys ⇒ load error (ambiguity is refused, not
|
||||||
|
resolved).
|
||||||
|
|
||||||
|
## 3. Resolution semantics by job type
|
||||||
|
|
||||||
|
Given caller input `(repo, branch?, pr?)`:
|
||||||
|
|
||||||
|
- **`multibranch`** — branch job addressed as `<job>/<url-encoded-branch>`
|
||||||
|
(e.g. `feature/x` → `feature%2Fx`). PRs addressed as `<job>/PR-<number>`
|
||||||
|
(Jenkins multibranch PR-discovery naming). Both per #72 §8.
|
||||||
|
- **`single`** — the job path is used as-is; `branch`/`pr` input beyond the
|
||||||
|
entry's pinned branch is a **no-mapping** result (a single job cannot answer
|
||||||
|
for arbitrary branches).
|
||||||
|
- **`parameterized-view`** — read-only variant for jobs that encode branch as
|
||||||
|
a build parameter: resolution returns the base job path plus a
|
||||||
|
`branch_param` filter hint the status tools may apply client-side when
|
||||||
|
scanning recent builds. It never triggers anything (read-only rule).
|
||||||
|
|
||||||
|
## 4. Precedence
|
||||||
|
|
||||||
|
Most-specific entry wins, evaluated in this order:
|
||||||
|
|
||||||
|
1. `(repo, branch)` exact entry — branch-pinned override.
|
||||||
|
2. `(repo)` entry — repo-wide (multibranch typical).
|
||||||
|
3. Nothing → **no mapping** (§5).
|
||||||
|
|
||||||
|
PR input resolves through the same chain: a PR belongs to its **base repo**'s
|
||||||
|
mapping; forks never introduce their own mapping (a fork's head repo is not
|
||||||
|
consulted — CI runs live under the base repo's job). If the base repo is
|
||||||
|
unmapped, the PR is unmapped.
|
||||||
|
|
||||||
|
Ties are impossible by construction (duplicate keys refused at load).
|
||||||
|
|
||||||
|
## 5. No-match behavior
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mapped": false,
|
||||||
|
"repo": "org/unknown-repo",
|
||||||
|
"branch": "master",
|
||||||
|
"error": "no Jenkins job mapping for this repo/branch",
|
||||||
|
"hint": "add an entry to the jenkins-mcp mapping config"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Deterministic, explicit, machine-checkable (`mapped: false`).
|
||||||
|
- **Never** falls back to name construction ("repo name probably equals job
|
||||||
|
name"), never probes Jenkins for candidates, never string-similarity ranks.
|
||||||
|
- The hint names the config, not a guessed job.
|
||||||
|
|
||||||
|
## 6. Where the mapping config lives
|
||||||
|
|
||||||
|
- **In the `jenkins-mcp` package/deployment** (e.g. `jenkins-mcp/mapping.toml`),
|
||||||
|
version-controlled next to the server that consumes it — *not* in Gitea-Tools
|
||||||
|
and *not* in per-user env vars (mappings are shared team facts, not
|
||||||
|
credentials).
|
||||||
|
- Path overridable via env (`JENKINS_MCP_MAPPING_FILE`) for tests/containers.
|
||||||
|
- Contains **no secrets** — job paths and repo names only — so it is safe to
|
||||||
|
commit and review like any other config.
|
||||||
|
- Reloaded at server start; a hot-reload tool is out of scope (restart is the
|
||||||
|
documented path).
|
||||||
|
|
||||||
|
## 7. Exposed tool surface (read-only)
|
||||||
|
|
||||||
|
One addition to the #72 tool set:
|
||||||
|
|
||||||
|
| Tool | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `jenkins_resolve_job` | `(repo, branch?, pr?)` → `{mapped, job, addressed_path, type}` or the §5 no-match result. Pure config lookup — **no Jenkins API call at all.** |
|
||||||
|
|
||||||
|
Status tools (`jenkins_latest_build` etc.) accept either an explicit job path
|
||||||
|
(as designed in #72) **or** `(repo, branch)` which they resolve via the same
|
||||||
|
mapping layer first. Resolution failure surfaces the §5 payload rather than
|
||||||
|
querying Jenkins.
|
||||||
|
|
||||||
|
## 8. Testing strategy (mocked; for the implementing package)
|
||||||
|
|
||||||
|
Config-layer tests (no network at all):
|
||||||
|
|
||||||
|
- Exact-match hit: repo-wide and branch-pinned entries.
|
||||||
|
- Precedence: branch-pinned beats repo-wide.
|
||||||
|
- Multibranch encoding: `feature/x` → `<job>/feature%2Fx`; PR → `<job>/PR-7`.
|
||||||
|
- `single` type with non-pinned branch ⇒ no-mapping.
|
||||||
|
- Fork PR resolves through base repo; unmapped base ⇒ no-mapping.
|
||||||
|
- Unknown repo/branch ⇒ §5 payload, and **no Jenkins client call**
|
||||||
|
(`mock_api.assert_not_called()`).
|
||||||
|
- Malformed config / duplicate keys / unknown type ⇒ load fails closed with
|
||||||
|
entry-naming error.
|
||||||
|
- No-secret check: mapping load/error paths never touch or print credentials.
|
||||||
|
|
||||||
|
Integration with mocked Jenkins API (per #72 §9): resolved path is used
|
||||||
|
verbatim in the GET URL; no write verbs anywhere.
|
||||||
|
|
||||||
|
## 9. Standalone-worthiness and readiness
|
||||||
|
|
||||||
|
#77 was split from #72 on the condition it stays "standalone only if mapping
|
||||||
|
is nontrivial." The precedence rules, fork/PR semantics, three job types, and
|
||||||
|
fail-closed config loading above are the nontrivial part; this document is the
|
||||||
|
justification.
|
||||||
|
|
||||||
|
Ready to implement in `jenkins-mcp` when #72's readiness checklist clears
|
||||||
|
(ADR-0001 owner decision #1; profile schema per #76 or hand-rolled
|
||||||
|
`jenkins-readonly`). Nothing here unlocks build triggers, deploys, or
|
||||||
|
parameterized launches.
|
||||||
Reference in New Issue
Block a user