Backend PR Process
This guide explains how to create, review, and merge a pull request against operations, common-module, or infrastructure. It is the backend counterpart to the Frontend PR Process, and the human companion to the pr-steward agent skill.
For the model these repositories run and why the backend variant differs, see Queued CI/CD. For the design rationale, see Queued CI/CD Adoption.
Status
Section titled “Status”The conventions below are being rolled out per repository, common-module first. Until a repository has cut over it still uses the old model — edit CHANGELOG.md in the PR, no merge queue. Check whether the repository has a .changelog/ directory: if it does, this guide applies; if it does not, Release Lifecycle still describes what to do.
The one thing that is different from the frontend
Section titled “The one thing that is different from the frontend”In these repositories CHANGELOG.md is the version. gradle.properties carries version=0.0.0 and everything downstream — the git tag, the Docker image tag, the Helm chart version, the Maven coordinates, the GitHub Release body — is derived from the top release heading in that file at build time.
That has one consequence you will feel: the build that publishes your change runs one commit later than the merge. Your PR merges, changelog-assembly writes the release block and pushes an assembly commit, and that commit is what builds, publishes, tags, and deploys. If you go looking for the build on your merge commit, you will not find one, and that is correct.
- Branch from latest
mainas<github-username>/<description>. - Make your change. Run the local gate before pushing.
- Open a PR. Fill in the
## CHANGELOGsection, or add a file under.changelog/— one or the other, never both, never neither. merge-eligibilityandbuildrun. Fix anything red.- Get a code-owner approval and resolve every review thread.
- Enable auto-merge → the PR enters the merge queue.
- The queue re-runs the full build against the batch head and merges.
- Post-merge: assembly writes
CHANGELOG.md, the assembly commit builds and publishes, andoperationsdeploys todev.
Step-by-step
Section titled “Step-by-step”1. Branch from latest main
Section titled “1. Branch from latest main”git -C <workspace>/operations fetch origin maingit -C <workspace>/operations checkout -b <username>/<description> origin/mainFor project work, prefer a worktree under projects/<project-name>-worktrees/ rather than touching the main clone. Agent branches take the form <username>-ag/<description>-<ticket>.
2. Make the change
Section titled “2. Make the change”Follow the repo conventions in the repository’s own CLAUDE.md and its knowledge-base/. For operations in particular: modules share state through injected Kotlin services rather than cross-module database access, and inter-module communication goes through gRPC/Protobuf.
3. Pre-push gate
Section titled “3. Pre-push gate”Run the local gate before every push, not just the first.
./gradlew check # tests + static analysismake clqLint # CHANGELOG format./gradlew check has two known blind spots: it does not generate the OpenAPI document and it does not exercise runtime DBIO wiring. A dependency bump is not verified until the local api-tests suite has run against it.
4. Write the changelog entry
Section titled “4. Write the changelog entry”Exactly one of two routes. Both present is rejected; neither is rejected.
Route A — the PR body. The PR template scaffolds it:
## CHANGELOG
### Fixed- Item lookup no longer returns soft-deleted supplies.Route B — a file under .changelog/. Name it with a prefix of your own; your GitHub username is the obvious choice.
cat > .changelog/jmpicnic-item-lookup.md <<'EOF'### Fixed- Item lookup no longer returns soft-deleted supplies.EOFNames are author-chosen rather than fixed so that two PRs in one merge batch cannot collide on the same path — a fixed name would guarantee the collision the queue exists to avoid.
Use Route B when the entry belongs with the code (a large change developed over several sessions), or when you want a feature build — see below. Use Route A otherwise; it is one less file to remember.
Categories
Section titled “Categories”| Category | SemVer bump | Use for |
|---|---|---|
Changed | major | API-breaking changes to existing functionality |
Removed | major | Removed functionality |
Added | minor | New features |
Deprecated | minor | Features marked for future removal |
Fixed | patch | Bugfixes, and all internal work |
Security | patch | Vulnerability fixes |
Be deliberate: the category is what decides the version bump. Changed and Removed are for API-breaking changes only — internal refactors, however large, are Fixed. Order sections larger-impact-first within an entry.
Write what the change enables or fixes, not what files moved. A changelog entry is read by someone deciding whether to upgrade.
Do not edit CHANGELOG.md
Section titled “Do not edit CHANGELOG.md”The file is assembly’s output, not its input. merge-eligibility rejects a PR that touches it. If you genuinely need a hand-edit — correcting a historical entry — apply the manual-changelog label, and expect to justify it in review.
5. Open the PR
Section titled “5. Open the PR”Fill in the template: What and why, Closes (only Linear IDs this PR actually resolves — any PDEV-XXX in the body transitions that issue regardless of keyword), and CHANGELOG.
If you are an agent or contributing on someone’s behalf, append the attribution block to the PR body and to every comment:
> [!note]> Authored by <Tool Name> for <github-username>6. Watch the checks
Section titled “6. Watch the checks”| Required check | What it does |
|---|---|
merge-eligibility | One check covering four assertions: CODEOWNERS resolves; the PR is not a draft; it is not a marked feature build; it carries exactly one valid changelog entry and does not edit CHANGELOG.md. |
build | The full Gradle build — compile, test, static analysis, containerized integration tests. |
merge-eligibility runs on drafts. That is deliberate: a skipped job publishes a successful check run, which GitHub cannot distinguish from a passing one, so the gate never skips. It also means you learn your entry is missing while you are still working, which is the better time.
The build on a pull request is a test build. It publishes nothing and cuts no tag.
7. Review
Section titled “7. Review”The review policy is ownership-based: required_approving_review_count: 0 with require_code_owner_review: true, and a CODEOWNERS file that owns every path. In practice that means one approval, from @Arda-cards/engineering or systems-arda, on every PR.
Owning every path is not bureaucracy. Workflows run from the head branch, so a PR that edits .github/workflows/ runs the edited version against itself — a PR that neuters its own gate is reported green by it. An owned path is the only thing that stops that, and no mechanism can waive it.
Address every reviewer comment and resolve every thread before merge — thread resolution is enforced by the ruleset.
8. Enter the merge queue
Section titled “8. Enter the merge queue”Once approved and green:
gh pr merge <num> --autoThe queue forms ALLGREEN batches and re-runs the full build against the synthetic batch head, which is what catches two PRs that compile independently but not together. merge-eligibility re-evaluates against the queued commit rather than passing through.
Do not push to a branch that is in the queue. And note that converting a queued PR back to draft does not remove it — GitHub keeps it and will merge it. merge-eligibility fails on a draft precisely so that the failure ejects the entry; that is the only lever available.
Possible outcomes
Section titled “Possible outcomes”- Success — the PR merges. On to post-merge.
- Batch failure — GitHub bisects; the offending PR is removed and yours re-enters automatically.
- Ejection — your PR caused the failure. It is removed and reopened. Fix, re-approve, re-enter.
9. Post-merge
Section titled “9. Post-merge”Two pushes to main follow, in order.
-
The merge commit triggers
changelog-assemblyand nothing else. It collects the entries for every merge since the last release tag, computes one semver from their combined categories, prepends one release block toCHANGELOG.md, deletes any.changelog/files it consumed, and pushes asarda-changelog-bot.The range is what makes this safe: if a run fails, its work is still pending and the next run collects it. Nothing depends on a single run succeeding.
-
The assembly commit triggers the build.
qualify-build-actionreads the version out of the release heading assembly just wrote, and the build publishes the image and chart, cuts thevx.y.ztag, and creates the GitHub Release. Foroperations,deploythen fans outdev→stage→demo→prodat one at a time, with every environment pastdevbehind an approval gate.
If several PRs merged close together, they share one release block and one version. That is intended — a release reads as a description of the release rather than as a list of pull requests.
Publishing a prerelease from your branch
Section titled “Publishing a prerelease from your branch”operations and common-module only. Useful when a consumer needs to build against an unreleased change, or when you want the change on dev before it merges.
Add feature-build: frontmatter to your .changelog/ file, naming the suffix explicitly:
---feature-build: jmpicnic-1408---
### Added- Item scanning by GTIN.Then:
- Every push to the branch publishes. An
Addedentry off6.14.0publishes6.15.0-jmpicnic-1408-<run>, which sorts before6.15.0under semver prerelease ordering — correct, since it is a prerelease of the release it anticipates. operationsdeploys it todevand nowhere else. Structurally: the feature path’s deploy matrix containsdevalone. It deliberately does not rely on the approval gates, which are shared with the release path.- A marked branch cannot merge.
merge-eligibilityfails while the marker is present. Delete the frontmatter to make it mergeable. - No PR is required. The marker lives in a file on the branch, so this works whether or not a pull request exists — which is why it is not a label.
Nothing survives onto main: the branch cannot merge while marked, and assembly deletes the file.
To put dev back on the released version afterwards, dispatch deploy.yaml with the previous chart version.
When things go wrong
Section titled “When things go wrong”| Symptom | What to do |
|---|---|
merge-eligibility fails with “no changelog entry found” | Add a ## CHANGELOG section to the PR body, or a file under .changelog/. Then re-run the check — see the row below. |
| I fixed the entry in the PR body but the check is still red | The gate does not re-run on a body edit or a new comment; it triggers on pushes and label changes only. Re-run it (gh run rerun <id> --failed) or push. The re-run reads the current body, so nothing else is needed. |
merge-eligibility fails with “both a .changelog/ file and a ## CHANGELOG section are present” | Pick one. Ambiguity about which entry is authoritative is treated as a defect, not resolved silently. |
merge-eligibility fails with “CODEOWNERS has N unresolved owner(s)“ | Someone renamed a team or lost repository access. Until it resolves, code-owner review is not being enforced at all — this is why the check exists. Fix the file or restore the access. |
| The entry needs changing after the PR is open | Body route: post a comment with a new ## CHANGELOG section — last one wins, and only the author or an assignee counts. File route: edit the file. |
| No build ran on my merge commit | Correct. The build runs on the assembly commit that follows. If no assembly commit appeared, assembly failed or skipped — check the run on main. |
| Assembly failed | Nothing was published and no tag was cut. The next merge’s assembly covers yours too. If nothing further is due, re-run the workflow; it recomputes the same range. |
The build fails at Tag source | The version already shipped. Someone hand-edited a version, or an assembly ran twice. Compare CHANGELOG.md’s top heading against git ls-remote --tags. |
An unbuilt or bad chart reached dev | It stops there. Dispatch deploy.yaml with the last healthy chart version; no rollback of main is needed, because the chart is an artifact rather than a branch state. |
Push rejected with branch is in merge queue | Wait for the queue. If the PR is ejected, push the fix to the reopened branch. |
| Queue entries dropping on timeout | The full build is exceeding check_response_timeout_minutes. Raise it. |
Appendix — agent companions
Section titled “Appendix — agent companions”| Agent skill | Source | What it automates |
|---|---|---|
pr-steward | workspace/instructions/claude/skills/pr-steward/SKILL.md | Watches checks, triages reviewer comments, replies and resolves threads, monitors merge_group runs through to merge or ejection. |
changelog rule | workspace/instructions/claude/rules/changelog.md | Which repositories use which changelog model, and what a good entry says. |
The skill never enters the merge queue or merges autonomously — both require an explicit affirmative response in the same session as the request.
Copyright: © Arda Systems 2025-2026, All rights reserved