---
title: "Jenkins Multibranch with Gitea: Why Pull Request Builds Never Run"
description: "Diagnose and fix the most common Jenkins org-folder + Gitea trap: PR commits never build because branch discovery excludes branches that have open PRs and pull-request discovery isn't enabled. Covers the navigator-to-child propagation gotcha, JCasC configmap ownership, the /reload trap, status-context matching, and the org-wide thundering herd."
url: https://agent-zone.ai/knowledge/cicd/jenkins-gitea-multibranch-pr-discovery/
section: knowledge
date: 2026-05-27
categories: ["cicd"]
tags: ["jenkins","gitea","multibranch-pipeline","ci","pull-requests","jcasc","troubleshooting"]
skills: ["jenkins-administration","ci-troubleshooting"]
tools: ["jenkins","gitea","helm","kubectl"]
levels: ["intermediate","advanced"]
word_count: 1073
formats:
  json: https://agent-zone.ai/knowledge/cicd/jenkins-gitea-multibranch-pr-discovery/index.json
  html: https://agent-zone.ai/knowledge/cicd/jenkins-gitea-multibranch-pr-discovery/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=Jenkins+Multibranch+with+Gitea%3A+Why+Pull+Request+Builds+Never+Run
---


# Jenkins Multibranch with Gitea: Why Pull Request Builds Never Run

A common, maddening symptom: your Jenkins organization folder (or multibranch pipeline) backed by Gitea builds the **default branch fine**, but **pull request commits never build** — the commit status stays `pending` forever (or never appears), so a branch-protection gate that requires a CI status can never be satisfied and the PR can never merge.

The pipeline is fine. The problem is **branch/PR discovery configuration**, and there are several layered traps. Here is how to diagnose and fix each.

## Symptom checklist

- The default branch (`main`) builds and posts a commit status.
- A feature branch builds **once** — right after you push it, *before* you open a PR.
- The moment a PR is opened on that branch, builds stop. New commits to the PR get no status.
- Branch protection requiring a status check (e.g. `org/repo/pipeline/head`) blocks the merge because the status is missing.

## Root cause: branch discovery excludes PR'd branches

The Gitea (and GitHub) branch source has a **Branch Discovery** trait with a strategy:

| strategyId | Meaning |
|---|---|
| 1 | Exclude branches that are also filed as PRs |
| 2 | Only branches that are also filed as PRs |
| 3 | All branches |

The default is **strategyId 1** — *exclude branches that are also filed as PRs*. So once you open a PR, the branch stops being built as a branch. If you have **no Pull Request Discovery trait**, the PR isn't built either. Result: PR commits are never built at all.

You might think setting branch discovery to **strategyId 3 (all branches)** fixes it. It often does **not** — many setups still route PR'd branches away from branch discovery, and an org scan will keep indexing only the default branch. **The reliable fix is to add Pull Request Discovery**, not to fiddle with the branch strategy.

## The fix: add Origin Pull Request discovery

In Job DSL (e.g. inside a JCasC `jobs:` block defining an `organizationFolder`):

```groovy
organizationFolder('my-org') {
  organizations {
    gitea {
      serverUrl('http://gitea:3000')
      credentialsId('gitea-creds')
      repoOwner('my-org')
      traits {
        giteaBranchDiscovery {
          strategyId(3)            // all branches (build main + non-PR branches)
        }
        giteaPullRequestDiscovery {
          strategyId(2)            // 2 = current PR HEAD revision
        }
      }
    }
  }
  projectFactories {
    workflowMultiBranchProjectFactory { scriptPath('Jenkinsfile') }
  }
}
```

Pull Request discovery strategy matters for the **commit status context**:

| strategyId | Builds | Status context suffix |
|---|---|---|
| 1 | PR merged with target | `pipeline/merge` |
| 2 | The current PR head | `pipeline/head` |
| 3 | Both | both |

If your branch-protection rule requires `org/repo/pipeline/head`, use **strategyId 2 (head)** so the posted context matches. A merge-strategy build posts `pipeline/merge` and your `pipeline/head` gate will never be satisfied. **Match the required status context to the discovery strategy.**

The Job DSL method `giteaPullRequestDiscovery` generates an `OriginPullRequestDiscoveryTrait` in the job config — confirm it landed:

```bash
grep -A1 "Discovery" $JENKINS_HOME/jobs/my-org/config.xml
# expect both BranchDiscoveryTrait and OriginPullRequestDiscoveryTrait
```

## Trap 1: navigator changes don't reach existing child projects

This is the subtle one that costs hours. An **organization folder** has a *navigator* (the SCM source traits). Each repo it discovers becomes a **child multibranch project** with **its own copy** of those traits, captured at creation time.

When you update the org navigator's traits, **existing child projects are not retroactively updated**. If you trigger the *per-repo multibranch scan*, it uses the child's stale source config — still missing PR discovery.

**You must trigger an organization-folder scan**, which re-evaluates children against the current navigator and pushes the new traits down:

```bash
# Org-folder scan (propagates navigator traits to child projects) — NOT the per-repo scan
curl -s -u "$USER:$TOKEN" -H "Jenkins-Crumb: $CRUMB" -X POST \
  "http://jenkins:8080/job/my-org/build?delay=0"
```

After the org scan, verify the child project picked up the trait:

```bash
grep "Discovery" $JENKINS_HOME/jobs/my-org/jobs/my-repo/config.xml
# now includes OriginPullRequestDiscoveryTrait
```

Then the multibranch finally creates `PR-1`, `PR-2`, … jobs and builds them.

## Trap 2: JCasC configmap is authoritative — live edits revert

If Jenkins is configured by Configuration-as-Code (JCasC), the org folder is (re)created from the JCasC `jobs:` Job DSL on every config reload. That means:

- **Editing `config.xml` on disk and reloading is fragile.** A *Reload Configuration from Disk* picks up your edit, but the next **JCasC reload re-runs the Job DSL from the ConfigMap** and overwrites it back to whatever the ConfigMap says.
- **The ConfigMap is the source of truth.** Make the change in your Helm values / JCasC source, render it into the ConfigMap, and reload JCasC.

When applying via Helm onto a cluster where the JCasC jobs ConfigMap was ever `kubectl`-patched, server-side apply throws a field-manager conflict:

```
conflict with "kubectl-patch" using v1: .data.jobs.yaml
```

Resolve by letting Helm reclaim ownership:

```bash
helm upgrade my-jenkins jenkins/jenkins -f values.yaml --force-conflicts
```

## Trap 3: the reload endpoint

- `POST /reload` = *Reload Configuration from Disk* (re-reads job `config.xml` files). This is what applies a disk edit.
- `POST /reloadConfiguration` = **404**, it isn't a real endpoint — easy to mistype and conclude "reload didn't work."
- A full `/reload` re-reads everything and can be slow; the UI is briefly unresponsive while it churns. Don't mistake "in-progress reload" for "Jenkins is down."

## Trap 4: the org-wide thundering herd

Enabling PR discovery on an **organization folder** turns it on for **every repository** in that org. The next org scan discovers **all open PRs across all repos at once** — easily dozens of queued builds — onto however many executors you have. If you run on the controller's built-in executors (e.g. 2), the whole org's CI gridlocks while the backlog drains.

Plan executor capacity *before* enabling org-wide PR discovery:

- Bump `controller.numExecutors`, or
- Run builds on dedicated agents (Kubernetes plugin pod templates) for real parallelism, or
- Accept a slow first drain and let it catch up.

## Debugging tip: large Jenkins API bodies over `kubectl exec`

When Jenkins runs in Kubernetes, `kubectl exec ... -- curl http://localhost:8080/api/json` often returns an **empty body for large responses** (job lists, queue, computer) while small endpoints (`/whoAmI/api/json`) come through — the large stream gets dropped through the exec pipe. Don't conclude the API is broken.

Instead, hit Jenkins from the host through a port-forward / ingress:

```bash
curl -s -u admin:*** -H "Host: jenkins.example.com" \
  "http://localhost/job/my-org/job/my-repo/api/json?tree=jobs%5Bname,color%5D"
```

URL-encode the `tree` brackets (`%5B` / `%5D`) and keep the field set small so the response is tiny and reliable.

## End-to-end verification

1. Org-folder config has both `BranchDiscoveryTrait` and `OriginPullRequestDiscoveryTrait` (ConfigMap → reload).
2. Org-folder **scan** triggered (not per-repo) — child project config now shows PR discovery.
3. `PR-N` jobs appear under the repo and build.
4. The build posts the commit status whose context matches your branch-protection rule (`pipeline/head` for head-strategy).
5. Executors aren't gridlocked by the org-wide discovery surge.

When all five hold, PR commits build, post their status, and gated PRs can merge.

