Skip to content

Decision Log: Queued CI/CD Adoption

Tracks the decisions that shape the adoption of the Queued CI/CD model in operations, common-module, and infrastructure — covering the division of labour between changelog assembly and the build pipeline, the publish trigger, merge-queue gate depth, the feature-build path, and rollout order.

#QuestionStatusDecisionRound
DQ-001Who owns version, tag, publish, and Release?DecidedAssembly writes CHANGELOG.md onlyR1
DQ-002Who gates publishing on the assembly commit?DecidedThe calling workflowR1
DQ-003How deep does the merge-queue gate run?DecidedFull build per batchR1
DQ-004What replaces feature-branch publishing?DecidedA Feature-build: directive in the PR-body sectionR1
DQ-005Rollout order across the three repositories?Decidedcommon-moduleoperationsinfrastructureR1
DQ-006What review policy replaces the 1-approval rule?DecidedCODEOWNERS model, mirroring documentationR1

Round 2 was an in-person design review with Denis Antonioli, recorded 2026-08-06. It amended three of the decisions above and settled a further set that are recorded as requirements rather than as DQs — see Round 2 for the index.


DQ-001: Who owns version, tag, publish, and Release?

Section titled “DQ-001: Who owns version, tag, publish, and Release?”

Context: This is the decision the whole project turns on. In documentation and arda-frontend-app, CHANGELOG.md is not a build input — changelog-assembly computes the version, prepends the block, tags, and creates the Release, and the production build is gated on the resulting assembly commit. In the three target repositories, CHANGELOG.md is the version: gradle.properties carries version=0.0.0, and qualify-build-action derives the version, tag, image tag, chart version, Maven coordinates, and Release body from the changelog at build time. Something has to give.

OptionDescriptionTrade-offs
AAssembly writes the release block only. changelog-assembly computes the semver, prepends the block, and pushes as arda-changelog-bot. Version derivation, tagging, publishing, and the GitHub Release stay with qualify-build-action / gradle-build-pipeline-action, which now fire on the assembly commit.CHANGELOG.md remains the single source of version truth, read by exactly one consumer. The Gradle and CDK publish paths are untouched, so artifact coordinates cannot drift. Smallest diff to the shared actions. Costs: the version is computed by assembly and re-read by qualification, so the two must agree on the heading format.
BFull port of the arda-frontend-app model. Assembly computes the version, tags, and creates the Release; the build pipeline is fed the version through an explicit input and no longer reads the changelog.Maximum uniformity — one assembly workflow shape across all five repositories, and PDEV-694’s queueing fix would land once. Costs: qualify-build-action and gradle-build-pipeline-action both need a new version-input path; the Helm/Docker/Maven publish coordinates move from a proven path to a new one; a bug here mis-tags a production artifact.
CMerge queue without PR-body changelogs. Enable the queue, keep direct CHANGELOG.md editing.Nearly free. But it does not solve the problem: two queued PRs that both add a release block still conflict, and ALLGREEN batching cannot form. Measured at 100% of merges in both Kotlin repos, this is the entire motivation.

Recommendation: Option A — it keeps one source of version truth and one writer, and it leaves the artifact-publishing path (the part with real blast radius) exactly as it is today.

Decision: Option A. Assembly writes the CHANGELOG.md release block and nothing else.

Amended in Round 2: assembly also removes the changelog files it consumed, in the same commit. That is what keeps them off the tip of main, and it is the one respect in which assembly touches a path other than CHANGELOG.md. The division of labour with the build pipeline is unchanged.

Applied to:

  • Design § 3 Overview, § 8 Structural Design, § 9.3 Assembly
  • Requirements § REQ-ASM-001

DQ-002: Who gates publishing on the assembly commit?

Section titled “DQ-002: Who gates publishing on the assembly commit?”

Context: Under DQ-001 the publish must fire on the assembly commit rather than the merge commit. qualify-build-action currently classifies any push to a protected branch as push_to_release_branch, which would now fire on the merge commit — before the changelog has been assembled — and see a version that is already released.

OptionDescriptionTrade-offs
AThe calling workflow gates. Each repository’s cicd.yaml / ci.yaml adds a job-level condition on the head commit title (chore: assemble CHANGELOG ). qualify-build-action gains only merge_group classification.Mirrors what documentation’s publish-docs.yml already does, so the convention is proven. Each repository controls its own trigger, which matters because infrastructure’s workflow shape differs. Keeps a generic build-qualification action free of a changelog-assembly convention. Costs: the condition is repeated in three workflows and must stay in sync with the commit-title prefix.
Bqualify-build-action gates. The action learns the assembly-commit convention and returns push_to_release_branch only for those commits.Callers change nothing, and the rule lives in one place. Costs: couples a generic action — whose contract is “analyze the event, the ref, and the changelog” — to one specific post-merge workflow’s commit-message format. Any repository adopting the action without changelog assembly would then need an opt-out.
CAssembly dispatches the build. changelog-assembly fires a repository_dispatch carrying the version after pushing.Explicit hand-off with no commit-title sniffing, and the version travels with the event. Costs: loses the natural push trigger, adds token plumbing, and makes a re-run of the build require a manual dispatch rather than a re-run of a push-triggered workflow.

Recommendation: Option A — the convention is already load-bearing in documentation, and the three target workflows differ enough that per-repository control is an asset rather than duplication.

Decision: Option A. The calling workflow gates on the assembly-commit title; the shared action gains only merge_group classification.

Applied to:

  • Design § 9.4 Build and publish, § 12 Implementation Artifacts

DQ-003: How deep does the merge-queue gate run?

Section titled “DQ-003: How deep does the merge-queue gate run?”

Context: GitHub re-runs required checks against the queue’s synthetic merge commit. For documentation that is a link check taking a couple of minutes. For operations the build job is a full Gradle build with containerized Postgres integration tests. How much of it should run per queue entry determines the check_response_timeout_minutes the ruleset can carry and how quickly the queue drains.

OptionDescriptionTrade-offs
AFull build per batch. The queue re-runs the same build job on the batch head.With ALLGREEN grouping the batch head is built once, not once per PR, so cost scales with batches rather than entries. Catches the case the queue exists to catch: two PRs that each compile alone but not together. No new workflow machinery. Costs: a long check_response_timeout_minutes, and a batch failure drops entries that were individually fine.
BTiered fast/queue gates. Compile + unit tests on PR push; full build + integration tests in the queue. Mirrors arda-frontend-app Phase 2.Faster PR-time feedback and a queue gate that is genuinely the heavier one. Costs: splitting a single build job that today is one call to a shared action; the split has to be maintained in two repositories with different build shapes.
CCheap gates only in the queue. Only changelog-check and review-required-gate re-run; the full build stays a PR-time check with strict_required_status_checks_policy: false. What documentation does today.Fastest possible queue. Costs: a batch is never built together, so semantic conflicts between concurrently-merged PRs reach main unbuilt — a real risk in a Kotlin service where two PRs can independently compile and jointly fail.

Recommendation: Option A — the throughput win this project is chasing comes from eliminating rebase churn, not from parallel builds, so the simplest correct gate is the right first move. Option B stays available if queue latency proves painful.

Decision: Option A. Full build per batch, with the queue parameters tuned per repository.

Applied to:

  • Design § 9.2 Queue, § 11 Operations Impact, § 13 Open Questions

DQ-004: What replaces feature-branch publishing?

Section titled “DQ-004: What replaces feature-branch publishing?”

Context: qualify-build-action supports a feature-branch publish path: a branch whose CHANGELOG.md version matches major.minor.patch-user-issue publishes a build tagged with that version plus the run identity. The operations changelog header documents the workflow — “To deploy a feature branch, append user and ticket to version… This must, of course, be reversed before merging to main.” Under PR-body changelogs the author no longer edits CHANGELOG.md, so the signal has nowhere to live. Usage is thin: three such tags exist in operations across its whole history, the most recent from the 2.x era.

The capability is in scope for all three repositories, common-module included. Because qualify-build-action is generic, a common-module branch with a suffixed version already publishes today — but its changelog header does not document the workflow, so the capability exists there by accident rather than by design. The chosen option makes it explicit and supported everywhere, which matters most for common-module: consumers occasionally need to build against an unreleased library change, and a suffixed publish is how they do that without cutting a real version.

OptionDescriptionTrade-offs
AA Feature-build: directive in the PR-body ## CHANGELOG section. The author writes a directive line (e.g. Feature-build: jmpicnic-1408) inside the same block they already use for the changelog entry. On a push to a feature branch, the build workflow resolves the branch’s open PR, reads the directive, and publishes x.y.z-user-ticket. changelog-check tolerates the line; assembly strips it before writing the release block.Operators signal in the same place they signal today — the outgoing changelog block — so the muscle memory carries over with the smallest possible change. Removes the “reverse before merging” footgun entirely: the directive lives in the PR body and is simply never assembled. Costs: assembly gains a stripping rule; the build path gains a GitHub API lookup to resolve the PR; a branch with no open PR has nowhere to read the directive from.
BRetire it. Drop the capability and the paragraph from all three changelog headers.Simplest. Removes a footgun and a code path. Costs: takes away a capability that, however rarely used, has no replacement — and the decision is easy to make and hard to notice was wrong until someone needs it.
CReplace with workflow_dispatch. A manual dispatch takes an explicit version input.Clearest intent, no changelog involvement, works with no PR open. Costs: a new input path through both shared actions, and a UI flow that nobody currently has in their fingers.
DKeep via the manual-changelog label. The branch hand-edits CHANGELOG.md with the suffixed version and labels the PR so the check accepts it.Preserves today’s flow byte for byte. Costs: keeps the footgun, and re-introduces exactly the CHANGELOG.md edit the project exists to remove — on the branches most likely to be long-lived.

Recommendation: Option A — it is the least change for the humans and the largest reduction in footgun, and it keeps the capability rather than betting that nobody needs it.

Decision: Option A in principle — the signal stays with the changelog entry rather than moving to a label, a dispatch, or a CHANGELOG.md edit.

Superseded in detail by Round 2. The mechanism is now YAML frontmatter in the changelog file, not a directive line in the PR body:

---
feature-build: jmpicnic-1408
---

Three things changed with it. The suffix is stated explicitly, because qualify-build-action’s regex requires two alphanumeric segments and deriving it from the filename would couple two conventions. The no-open-PR question dissolved — a marked branch carries its changelog file whether or not a PR exists, which is exactly why the file route is mandatory for it. And the scope narrowed to operations and common-module; infrastructure is excluded, because nothing there consumes a feature publish and there is no registry artifact to produce.

A separate marker file was considered and rejected: once a marked branch must carry a changelog file, a second file is redundant state that can disagree with the first.

Applied to:

  • Design § 9.5 Feature builds, § 12 Implementation Artifacts
  • Requirements § REQ-FEAT-001 through REQ-FEAT-008

DQ-005: Rollout order across the three repositories?

Section titled “DQ-005: Rollout order across the three repositories?”

Context: The three repositories share the shared-action changes but differ sharply in blast radius. common-module publishes a library. operations publishes an image and chart that deploy to four partitions. infrastructure runs amm.sh against four partitions including production.

OptionDescriptionTrade-offs
Acommon-moduleoperationsinfrastructure.Each step validates the shared actions against a larger blast radius than the last. A mistake in the pilot costs a version number, not an outage. common-module is also the highest-value pilot on its own terms: 29 of 29 merges touched CHANGELOG.md, and its changelog is what consumers read to decide whether to upgrade. Costs: the longest period of mixed conventions across the backend repositories.
Boperations first.Fastest payoff — 50 merges in 90 days, every one touching CHANGELOG.md. Costs: validates the shared actions for the first time against the repository whose publish feeds a four-partition deploy chain.
CAll three in one cutover.Shortest period of mixed conventions. Costs: the shared-action changes, three rulesets, and three workflow rewirings all land untested together, against production-bearing pipelines.

Recommendation: Option A — the pilot is nearly free and the sequence buys two rounds of evidence before the production-bearing repositories move.

Decision: Option A.

Applied to:

  • Goal § Repositories
  • Design § 11 Operations Impact

DQ-006: What review policy replaces the 1-approval rule?

Section titled “DQ-006: What review policy replaces the 1-approval rule?”

Context: All three repositories currently require one approving review with no CODEOWNERS file. The queued model as run in documentation pairs required_approving_review_count: 0 with require_code_owner_review: true, which makes CODEOWNERS the entire review policy. Adopting the ruleset shape without also adding the file would silently reduce required review to zero — the single most dangerous way to get this migration wrong.

The obvious precedent turned out not to exist. arda-frontend-app has no CODEOWNERS file at any of the three valid locations (root, .github/, docs/), and its ruleset runs require_code_owner_review: false with one required approval — so despite running the queued model, it is on the count-based policy, not the ownership-based one. The only real CODEOWNERS in the workspace is documentation’s.

OptionDescriptionTrade-offs
AMirror documentation. Default owner @Arda-cards/engineering on *. Drop @systems-arda (a service account for system-driven documentation changes, with no backend equivalent) and drop the unowned-roadmap exception (no equivalent low-stakes path in these repositories).One consistent policy across the repositories running the queued model, and review requirements become visible in a reviewed file rather than buried in ruleset JSON. Verified: the engineering team holds direct push access on all three repositories, which CODEOWNERS requires — subteam-inherited access does not satisfy it. Costs: every PR now needs an @Arda-cards/engineering approval, where the count-based rule accepted an approval from anyone with access.
BKeep one required approval, no CODEOWNERS. Change only the merge_queue rule and the required-status-check list.Smallest diff, and matches what arda-frontend-app actually does. Costs: leaves the two queued repositories on divergent review models, and forgoes path-scoped ownership for risk surfaces such as migrations and workflows.
CPath-scoped owners per repository. Tighter ownership on migrations, CI workflows, and Helm charts than on application code.Most control, and best matches the real risk distribution. Costs: requires a per-path, per-repository ownership decision that nobody has made yet, and stalls three ruleset changes behind it.

Recommendation: Option A — it is the smallest step that keeps the ownership model honest, and Option C remains reachable later by editing one file per repository rather than by revisiting the ruleset.

Decision: Option A.

Amended in Round 2: @systems-arda is retained as a co-owner, so the file matches documentation exactly: * @Arda-cards/engineering @systems-arda. Verified 2026-08-06 that systems-arda is a User account with admin on all three repositories, and that it is reserved for DevOps engineers under tight constraints — no automated process may use it (REQ-REV-003). That ruling matters because a workflow approving as systems-arda would otherwise have been the obvious way to build a review waiver.

Applied to:

  • Design § 8 Structural Design, § 11 Migration matrix, § 12 Implementation Artifacts
  • Requirements § REQ-REV-001, REQ-REV-003

Held with Denis Antonioli, recorded 2026-08-06. It reopened the authoring model, the assembly unit of work, the feature-build mechanism, and the review policy.

The decisions are recorded as requirements rather than as new DQs, because they were settled in conversation with their alternatives weighed there rather than in writing, and requirements.md carries each one with its rationale. This section is the index.

AreaOutcomeRequirements
AuthoringAn entry may be written in-repo as a per-PR changelog file, or in the PR body — exactly one, never both, never neither. This closes the gap Denis raised on PR #157: the original design traded authoring experience for merge concurrency without weighing the trade.REQ-AUTH-003, 006, 007
AssemblyOne release per assembly, covering every merge since the previous assembly commit — rather than one release per PR. Makes assembly self-healing and subsumes PDEV-694 for these repositories.REQ-ASM-004, 005
GatesA required gate must never report success without evaluating; the cheap gates stop being draft-gated.REQ-GATE-005, 006, 007
Feature buildsFrontmatter marker; marked branches cannot merge; deployment reaches dev and no further, structurally rather than by approval gate.REQ-FEAT-001 through 008
ReviewOwnership-based review with a code-owner waiver, implemented as a two-ruleset split plus a gated ReviewOverride App. Recorded as an accepted weakening of review rigor.REQ-REV-001, 003, 004
Release granularityA release may cover several PRs. Fewer production deploys; coarser rollback. Accepted explicitly.REQ-PUB-004

Two evidence findings from the review drove more than one of these:

  • Concurrent assemblies have already lost changelog entries. On 2026-07-23 in documentation, PRs #142, #137, and #135 merged within 80 seconds; two of the three assembly runs failed and their entries are absent from CHANGELOG.md. This is why REQ-ASM-004 exists.
  • A feature publish in operations would fan out to production. The deploy job is gated only on chart_name, which gradle-build.sh emits for any publish. This is why REQ-FEAT-008 is structural rather than procedural.


Copyright: (c) Arda Systems 2025-2026, All rights reserved