Release Engineering — The Pipeline as the Single Source of Truth
DRAFT — pending Harsh's verification and employer sign-off.
This page documents the release-engineering platform Harsh built and operated at Auction.com across roughly seven years (2019→2026): the Jenkins pipeline as the single driver of release state, the two-layer library-and-DSL design that made it reproducible, and the trunk-based daily-release cadence the platform eventually enabled. It is drawn from two bodies of work — the release orchestration that tied pipeline execution to an issue tracker's release lifecycle, and the longer arc of the shared library and job-DSL platform that carried it — with the hard-won friction kept as seasoning.
1. The pipeline as the single source of release truth
There were two sources of truth for "what is being released": the CI/CD
pipeline (Jenkins) and the release object in an external issue tracker, whose
stages ran Planned → Deploying → Deployed-to-CI → Deployed-to-QA → Released.
Humans kept them in sync by hand — error-prone, and release status was never
trustworthy at a glance.
The redesign made one authority the writer and the other the mirror: pipeline execution drives release state. A set of stage-driven Jenkinsfiles — one per environment and flow — covers CI, QA, PROD, PROD-DR, on-demand release, mobile release, cleanup, and release-request flows: roughly nine distinct pipelines. A release-steps module in the shared library talks to the tracker's release API (GraphQL) to resolve a pipeline, list releases and their stages, fetch a build, create or advance a release, mark it released, attach issues, and publish release notes.
Pipeline stages map 1:1 to release stages. As a deploy pipeline clears a stage, it advances the corresponding release stage — a successful CI deploy moves the release to "Deployed to CI," and so on up to "Released." Two release modes coexist: a scheduled Standard release and an On-Demand release kicked off by a submit-release-request job and dispatcher.
flowchart LR
subgraph pipeline["Jenkins pipeline stage · writer"]
checkout["checkout / build"]
deployCI["deploy -> CI"]
deployQA["deploy -> QA"]
deployPROD["deploy -> PROD"]
end
subgraph tracker["tracker release stage · mirror"]
planned[Planned]
deployedCI[Deployed-to-CI]
deployedQA[Deployed-to-QA]
released[Released]
end
checkout --> planned
deployCI --> deployedCI
deployQA --> deployedQA
deployPROD --> released
The rejected alternative — tracker drives pipeline — was considered and turned down: the build is the ground truth of what actually deployed, so it should be the writer. The tracker only reflects it.
2. Lifecycle sync: best-effort, never fails a deploy
Release bookkeeping is important, but it must never block shipping. Every tracker call is wrapped in try/catch, logs a warning, and continues: deploy reliability outranks sync fidelity. A sync failure is a paper cut, not a blocked release. Hard-failing the deploy on a sync error was explicitly rejected as a design.
Three real bugs shaped the integration, and each one left a constraint behind it:
- A lock around release creation. When jobs overlapped, stray "New release"
objects appeared after a release was already done — an idempotency gap. The
fix put a lock around release creation and routed the final "Released"
state through an explicit
markReleasedtransition instead of creating a placeholder. - Defensive reads against an API shape. The tracker's
stagesfield is a GraphQL connection (read via.nodes), not a flat list — a subtle bug that stalled stage advancement until the query read the connection correctly. Attaching issues to a release hit the same class of bug (a paginated connection read as a flat list). - Overlapping releases. Multiple releases can be in flight at once (with
-01 / -02 / -03suffixes), so the system has to reason about which pipeline run owns which release object. A verified behavior matrix was maintained for the overlap cases and the specific sync bugs already diagnosed.
Code-level toggles act as operational escape hatches: skip publishing release notes, or skip creating new placeholder releases, for edge cases.
Reusable pattern: syncing a pipeline to an external stateful system needs idempotency + locking + defensive reads — and every external call wrapped so a sync failure never fails the deploy. The external system is a mirror, not a gate.
3. The two-layer platform: how (library) + what (DSL)
Underneath the release orchestration sits a two-layer design that carried the whole org's CI for seven years:
- A shared library of reusable Groovy pipeline "vars" — the steps every
service's
Jenkinsfilecalls. Pipelines for Java, Node, mobile (App Store / Google Play / Firebase), Heroku, MuleSoft, and library-only builds, in single-branch and multi-branch variants. This is where how we build, test, publish, and deploy lives, once, for the whole org. - A job-DSL repo that declares the Jenkins jobs themselves as code — a seed job generates them — so job configuration is version-controlled and reproducible rather than clicked into the UI.
Reusable pattern: put the "how" (steps) in a shared library and the "what jobs exist" (topology) in DSL. Two axes, two repos, both as code.
4. Seven years visible in the history
The platform's shape changed repeatedly as the org, the cloud, and the tooling changed, and each change is legible in the commit history:
- 2019–2021 — foundation. The shared library was established; early secrets handling moved off a legacy tool onto AWS Secrets Manager.
- 2022–2023 — the Bitbucket-Cloud migration era. Heavy DSL churn tagged "BB Cloud Migration"; pipelines and job definitions adjusted for the new SCM host, plus a large batch of one-off data and migration jobs generated via DSL.
- 2023–2024 — dynamic configuration. "Define branches dynamically for each job type," "configure seed-job as pipeline" — a move from hardcoded job specs toward parameterized, generated ones.
- 2024 — the GitHub migration. DSL and library updated for the move off Bitbucket to GitHub — the second full SCM migration the platform survived (Bitbucket Server → Bitbucket Cloud → GitHub) without being abandoned.
- 2025 — arm64 / Graviton migration + a Java pipeline refactor. The default build agent flipped to arm64, a Java pipeline was refactored, and a hotfix workflow was added that deploys hotfixes straight to QA.
- 2026 — ECR consolidation + release-lifecycle integration. Docker build/publish moved off an external artifact registry onto the cloud-native container registry (ECR), and the release-lifecycle sync described above landed as a series of roughly fifteen commits.
5. Decisions and their tradeoffs
Each major choice was a tradeoff made under real constraints:
- Jobs-as-code via a seed job over UI-configured jobs — reproducibility and review, at the cost of a DSL learning curve and a regeneration step.
- One shared library over per-team copy-paste — consistency and a single place to fix a CVE or add a gate, at the cost of blast radius: a bad library change touches every build. That blast radius is exactly why the defensive patterns below exist.
- Dynamic branch and job definition over static enumeration — scales to many services without hand-editing each job.
- Hotfixes deploy directly to QA — a deliberate workflow carve-out so urgent fixes skip the normal promotion path, guarded by branch-name conditions (which themselves needed several fixes — the history shows the iteration).
6. Friction worth keeping (war stories, as seasoning)
The platform's design was shaped as much by what broke as by what was planned. A few of the harder lessons, kept short:
- The arm64 migration was gated by the least portable tool, not by our own code. The default build agent flipped to arm64 in April 2025, was disabled a week later ("needs more work to execute on arm64") because a security / SAST scan step in the pipeline didn't run on arm64, and only completed roughly eleven months later. The build itself ran fine on arm64 early; one third-party scanner held the whole migration for the better part of a year. Sequence an arch migration around the laggard, and be willing to disable-and-revisit rather than block everything.
- Registry migrations ripple into every build variant and into caching. The move off the external artifact registry onto ECR touched PR, prototype, and release flows. An ECR remote-cache change for preview images landed, was reverted the same day, and re-landed differently — the classic "the optimization broke something, back it out, ship the correct version." Migrate the happy path first; treat caching as a separate, revertible change.
- Shared mutable state on a build agent plus concurrency equals corruption. Concurrent pipeline steps logging into ECR could corrupt the shared AWS credentials file on the agent. The first fix covered some call sites; the real fix was finding and guarding all of them. The bug isn't "one bad login," it's "an unguarded write path you didn't enumerate." Grep for every call site, not the one in the stack trace.
- Release-sync bugs are a class, not a one-off. Stages that won't advance past a checkpoint, stray "New release" objects, issues that don't link — all traced back to idempotency, locking, or defensive-read gaps. Each one reinforced the "best-effort, never fails the deploy" contract.
- Death by a thousand cuts in CI. Stale git refs breaking checkouts (fixed with a prune-stale-branch checkout option) and hotfix / stale-branch auto-cleanup workflows — small individually, collectively the bulk of the operational papercuts.
7. The cadence this platform enabled
The cumulative effect of the platform above is the release cadence it made cheap: a move from bi-weekly to daily releases with a <1% rollback rate, on an in-house Jenkins + GitHub Actions toolchain (replacing a six-figure-per-year SaaS suite) running 10K+ monthly pipeline executions. GitOps delivery via Argo CD and the trunk-based workflows sit on top of this pipeline layer; this page is the part of the stack that made "a release is a build, and the build is the truth" operationally real.
8. Honest gaps
Impact numbers are not in the source material: how many services the shared library serves, builds per day, mean pipeline duration over time, and any before/after "releases mislabeled per month" or "manual minutes saved" figure. A headline version of this story ("maintaining CI for a large org for seven years") would need those pulled from Jenkins. They are not in the material this page was distilled from, and nothing here invents them.
Sources
This page was distilled from Harsh's commit history and session logs across the Jenkins shared library, the job-DSL repo, and the release-orchestration work — cross-checked against the in-repo PRD and resume. No internal hostnames, credential identifiers, ticket IDs, or proprietary source code appear; the pipelines and library are described, not copied. Every architectural claim above traces to that material.