Every team has its small, invisible friction points. The ones that don’t show up in a postmortem but quietly slow everyone down, every single day.
For us, it was this: a developer opens a Pull Request in Bitbucket.
They want to run a Jenkins build against their branch before merging.
So they stop what they’re doing, open a new tab, navigate to Jenkins, find the right Multibranch Pipeline project, locate the correct branch or PR job, click Build, configure parameters — and then go back to the PR.
That’s assuming they found the right job. That’s assuming they remembered the parameters.
It’s not catastrophic. But it breaks the flow every time. And for a team running multiple repositories and Multibranch pipelines, it adds up.
I fixed it with a platform I hadn’t used before: Atlassian Forge.
What is Atlassian Forge?
If you’ve never heard of it, Forge is Atlassian’s hosted development platform for building apps that extend Jira, Confluence, Bitbucket, and other Atlassian products. It was designed to replace Atlassian Connect — the older extension model that required you to host your own server, expose public URLs, and manage the authentication handshake yourself.
Forge is different. When you build a Forge app, Atlassian hosts the compute. Your backend runs inside their cloud infrastructure. Your frontend renders natively inside the product UI. You don’t manage servers, TLS certificates, or public endpoints. You deploy with a single command, and the app is live.
From a DevOps engineer’s perspective, Forge is essentially a serverless FaaS platform scoped to the Atlassian ecosystem. Your backend code runs in Forge’s native Node.js runtime on Atlassian-managed secure VMs, with tightly controlled outbound access. If your app needs to call an external service, you explicitly allowlist the destination in manifest.yml, if it isn’t declared there, Forge blocks the request. Atlassian enforces that egress policy at the platform level, so it’s not a firewall rule you maintain yourself.
How Forge Bridged the Gaps Webhooks Couldn’t
To be clear, we do use webhooks. They already power our automated builds behind the scenes, and they’re a good fit for event-driven automation. But our developers also wanted something different: the ability to trigger a build manually, on demand, directly from within Bitbucket.
That requirement went beyond simply “calling Jenkins.” A standard webhook runs in the background with no built-in user interface. It can notify an external service that something happened, but it does not create a native interaction point inside a pull request. A Bitbucket Pipe lives in the CI pipeline itself, which is useful for pipeline execution, but it still does not add an interactive control to the PR experience. Bitbucket’s webhook model is event-to-URL delivery, and Bitbucket Pipelines supports manual and custom pipeline triggers through the Pipelines interface, but neither gives you a custom PR action with its own UI inside the pull request. Forge does: it lets you register a bitbucket:repoPullRequestAction, which adds a menu item to the PR’s Actions menu and opens a modal dialog inside Bitbucket.
That gave us the integration point we actually needed. From the pull request itself, a developer could open the action, provide runtime parameters, and trigger the appropriate build without leaving their workflow. The key benefit was not that the developer could “see” the PR context—they were already on the PR page—but that the app could use that context directly when the action was invoked. Forge documents that bitbucket:repoPullRequestAction provides pull request and repository extension context, including the pull request ID and repository details, which made it possible to prefill values, apply defaults, route the request correctly, and return feedback in the same user flow.
That was the integration point we needed. Forge turned the build trigger from a background integration into a first-class, contextual action inside the pull request.
How the App Works
The architecture is simple, and that simplicity is intentional.
- The manifest (
manifest.yml) declares the app to Atlassian: which product it targets, which UI module it registers, which permissions it needs, and which external URLs it’s allowed to call. - The frontend (
src/frontend/index.jsx) is a React component. It runs inside Bitbucket’s PR panel. When it loads, it calls the backend to get context (repo, branch, PR ID) and fetch the Jenkins job’s parameter definitions. It then renders a dynamic form — text fields, dropdowns, checkboxes — based on whatever parameters that particular Jenkins job declares. - The backend (
src/resolvers/index.js) contains the Jenkins integration. It handles two operations:getContext— reads the PR context from Bitbucket’s API, maps the repo to the right Jenkins Multibranch job, and fetches the job’s parameter definitions from Jenkins.triggerBuild— authenticates to Jenkins with Basic Auth + a CSRF crumb, determines whether to target aPR-XXchild job or a branch job, builds the parameter payload, and POSTs to Jenkins’buildWithParametersendpoint.
The Part That Actually Took Time: Dynamic Parameters
I expected Jenkins authentication to be the tricky part — Basic Auth, CSRF crumbs, and the double-encoding required for branch names with slashes in multibranch job URLs. That was annoying, but tractable.
What took real time was handling Jenkins parameters dynamically.
That sounds simple until you look at how varied Jenkins jobs can be. Some expose plain string parameters. Others use booleans, single-select dropdowns, extended multi-select options, or Active Choice parameters whose values are generated at runtime and may depend on other inputs. Because different jobs across our workspace use different parameter configurations, hardcoding a fixed form was never going to work.
The real challenge was building a UI flexible enough to adapt to whatever a given Jenkins job exposed, while still keeping the experience simple inside Bitbucket. We had to inspect the Jenkins job metadata, normalize inconsistent parameter definitions into something the frontend could work with, and then map those definitions into the right input components. Active Choice parameters were especially awkward, because their values are computed dynamically by Jenkins rather than being statically available in a clean format.
This was also the part of the project where the forge tunnel became essential. Dynamic behavior like this is difficult to get right in the abstract — you have to test it against real pull requests and real Jenkins jobs. Running the app locally with a tunnel lets me iterate on the parsing and rendering logic quickly, view live logs, and test the UI against actual repository context without redeploying after every change.
That was the moment Forge stopped feeling like just a hosting model and started feeling like a practical developer platform.
What Forge Actually Gives You
A few things stand out after building this that I didn’t fully appreciate going in.
- No application infrastructure to operate. There is no server, container, or public endpoint for us to host and maintain. I run
forge deploy, and Atlassian handles the hosting and runtime environment. For an internal integration like this, that is the right trade: we get the behavior we need without adding another service to our platform estate. - Built-in secure platform capabilities. Forge does not remove the need for credentials, but it gives you a secure, platform-native way to store and use them. The Jenkins API token lives in Forge’s encrypted variables rather than in source code or in ad hoc infrastructure I would otherwise have to secure myself. Combined with Forge’s permission model and explicit egress allowlist, that means access is constrained by design and enforced by the platform. That is a meaningful advantage for internal tooling: you still manage what secrets exist, but you do not have to build the secret-handling foundation yourself.
- Native Bitbucket context. The app can call Bitbucket APIs using the current product context, without building a separate OAuth flow, managing user tokens, or exposing callback endpoints. Forge handles the identity handoff and gives the app a native place inside the Bitbucket experience. That is the part that would have been most painful to reproduce with a traditional external integration.
The tradeoffs are real. Forge is a managed platform, and it behaves like one. There are outbound egress restrictions, runtime constraints, cold starts, and limits in the available UI components. The @forge/react library is not the full Atlassian Design System, so some components you might expect are not available. And the debugging experience is very much “work with the platform” rather than “control every layer yourself.”
What I Would Tell Myself Before Starting
- Start with the forge tunnel from day one. Running the app locally against real Bitbucket events is orders of magnitude faster than the deploy-test-redeploy loop. You get live logs in your terminal and can iterate in seconds. The catch: the app needs to be installed in the workspace first before the tunnel will work, which tripped me up initially.
- Read the manifest documentation carefully. The
manifest.ymlis the contract between your app and Atlassian’s platform. Getting the module type, scopes, and external fetch permissions right before you write any code saves a lot of mysterious failures later. - Forge’s security model is a feature, not a bug. At first, the egress restrictions and platform constraints can feel like friction. In practice, they force you to be explicit about what your app can access, which makes the integration easier to reason about, audit, and trust.
The Result
Developers now trigger Jenkins builds from inside the PR. They see the source branch, the target branch, and the mapped Jenkins job. They fill in parameters (pre-populated with defaults) and click a button. The build starts. They get a link directly to the Jenkins job.
No tab switching. No hunting for the right pipeline. No wrong-branch builds.
The app took a few days to build, is already being used across multiple repositories, and costs nothing to run beyond the Atlassian Developer account. That’s the pitch for Forge: if you’re already running on the Atlassian stack and you have an integration problem inside that ecosystem, it’s the fastest path from idea to working app — without adding a single server to your infrastructure.