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>
6.7 KiB
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 - 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-mcpboundary; no Gitea credentials involved.
2. Mapping format
Declarative, versioned config (TOML or JSON — match whatever config format
jenkins-mcp adopts; illustrated here as 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
repoandbranch. No globs in v1 (globs invite accidental over-matching; add later behind an explicitpattern = trueflag if ever needed). - Unknown
typeor 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/prinput 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 abranch_paramfilter 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:
(repo, branch)exact entry — branch-pinned override.(repo)entry — repo-wide (multibranch typical).- 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
{
"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-mcppackage/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. singletype 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.