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-module → operations → infrastructureR1
DQ-006What review policy replaces the 1-approval rule?DecidedCODEOWNERS model, mirroring documentationR1
DQ-007How is the deploy overtake hazard addressed?DecidedSerialise only; the ordering hazard leaves the projectR3
DQ-008Which component owns each decision the pipeline makes?DecidedOne owner per concernR4
DQ-009Where does the feature-build marker live?DecidedIn whichever manifest the branch carriesR5
DQ-010How does a caller say there is no marker?DecidedAn explicit opt-out, because empty is an answerR6
DQ-011Who iterates when several manifests are needed?DecidedThe resolver, through its own interfaceR6
DQ-012What shape must a feature-build marker have?Decided<user>-<ticket>, validated where it is readR6
DQ-013How are the merge-queue parameters chosen?DecidedFrom what the queue must tolerate, not from tasteR6
DQ-014Which environments keep a human approval gate?Decidedprod aloneR6
DQ-015Who owns the pipeline’s own machinery?DecidedThe engineering team and the administrative accountR6

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.

Round 3 was a review-driven reversal, recorded 2026-08-17. It withdrew one requirement the project had previously committed to — see Round 3.

Round 4 was driven by what the operations cutover did in production, recorded 2026-08-18. Its first release failed four separate ways, and the common cause was that no component owned exactly one of the decisions the pipeline makes — see Round 4.

Round 5, recorded 2026-08-18, followed from Round 4: once entry resolution is a single component reading both authoring routes, the marker no longer has to live in only one of them — see Round 5.

Round 6, recorded 2026-08-19, is what soaking the pipeline produced. Five of its six decisions came from running the thing rather than from reading it: two contract defects that review had passed, a shape rule for the marker, the queue’s own parameters, and the environment gates. See Round 6.


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-module → operations → infrastructure.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.

Recorded 2026-08-17, driven by Denis Antonioli’s review of PR #37.

DQ-007: How is the deploy overtake hazard addressed?

Section titled “DQ-007: How is the deploy overtake hazard addressed?”

Context: Round 2 produced two paired requirements from the 2026-08-06 incident. REQ-PUB-005 serialises deploys to one environment; REQ-PUB-006 forbids a silent downgrade. They were drafted as a pair because the first does not imply the second, and the distinction is the whole of this decision.

A concurrency group on the deploy job is acquired when that job starts, which is after the build. The slot is therefore handed out in build-completion order. When version N takes 22 minutes to build and N+1 takes 4 — a measured spread on operations, not an estimate — N+1 deploys first and N deploys on top of it. The two runs may never overlap at all, so there is nothing for a concurrency group to serialise. Serialisation and ordering are different properties, and the group provides only the first.

REQ-PUB-006’s remedy, drafted as PR #37, was a precondition inside helm-deploy-pipeline-action: compare the chart version already released in the namespace against the incoming one and refuse to go backwards, with an allow_downgrade input for deliberate rollbacks. Denis requested changes on it, and raised cancel-in-progress on the build path as the cause-level alternative.

OptionDescriptionTrade-offs
ASerialise only. Keep REQ-PUB-005, extend it to accounts, withdraw REQ-PUB-006, and move the ordering hazard to its own ticket.Closes the collision that actually occurred on 2026-08-06 and leaves the rollback path with no added steps — which matters most during an incident, when the operator is under time pressure and will not be reading action inputs. Keeps this project’s scope at the queue rather than at deploy semantics. Costs: the overtake stays open, and the queue raises its rate. Accepted knowingly rather than by omission — the hazard is recorded in PDEV-1647.
BMerge PR #37 as drafted. A monotonicity precondition with an explicit override flag.Closes the hazard outright, and a downgrade becomes deliberate rather than impossible. Costs: the override must be discovered during an outage, which is the worst moment to learn an action has an input; and it addresses the symptom while the obsolete run — the actual defect — proceeds everywhere else. That was Denis’s objection, and it holds.
Ccancel-in-progress: true on the build path. An obsolete run is cancelled when a newer one arrives, so it never reaches the deploy.Attacks the cause rather than the symptom, and needs no new mechanism. Costs: under the assembly-first ordering this project introduces, cancelling a build mid-flight can leave a tag with no artifact behind it — silent and permanent, where a downgrade is at least recoverable by redeploying. It also cannot be applied to the deploy step itself, where a half-applied helm upgrade must never be cancelled.

Recommendation: Option A. It delivers the demonstrated fix, it does not put a gate in front of an emergency rollback, and it keeps a hazard that predates this project from expanding its scope. Option C remains the most promising long-term answer and is recorded on the ticket rather than discarded.

Decision: Option A (Miguel, 2026-08-17). PR #37 is closed unmerged. REQ-PUB-006 is marked Withdrawn rather than deleted, with its reasoning preserved.

Two consequences worth stating plainly:

  • This is an accepted weakening of the position taken on 2026-08-06, which was that the pipeline should be sound when the project closes rather than sound apart from a known silent-corruption path. That earlier reasoning was right about the hazard and wrong about the remedy’s home.
  • accounts is no longer excluded from REQ-PUB-005. The requirement had flagged its exclusion as untenable — “leave a solid pipeline behind” and “do not touch accounts” could not both hold. The exclusion is lifted for the concurrency group and for nothing else.

Applied to:

  • Requirements § REQ-PUB-005 (scope), § REQ-PUB-006 (withdrawn)
  • Design § 12 Implementation Artifacts

Round 4: Separating the pipeline’s concerns

Section titled “Round 4: Separating the pipeline’s concerns”

Recorded 2026-08-18, driven by the operations cutover’s first live release.

DQ-008: Which component owns each decision the pipeline makes?

Section titled “DQ-008: Which component owns each decision the pipeline makes?”

Context: The operations cutover merged on 2026-08-18 and its first assembly failed three times in a row, each for a different reason, none of which any pre-merge check could have caught — assembly runs only after a merge, on main.

The three failures share a cause. Four distinct decisions are made on the way from a merged pull request to a deployed artifact, and no component owns exactly one of them:

#DecisionWhere it lives today
1What is this ref’s changelog entry?changelog-entry.sh, assemble.sh, check-changelog.sh — one repository, three files, coherent
2Is this a feature build, and with what marker?qualify-build-action and check-mergeable.sh and changelog-entry.sh — two repositories, three independent frontmatter parsers
3Which environments does this build reach?cicd.yaml’s matrix, gated on chart_name being non-empty — an implicit side channel
4Who derives the version, tags, releases and publishes?assemble.sh and the build pipeline — both, which is why they collided

Concern 2 is the clearest failure. changelog-entry.sh exists to unify the file and body routes into one entry; every marker reader then bypasses that abstraction and reads the filesystem directly. Two consequences followed immediately:

  • A feature-build: key written in a pull-request body is silently inert. Nothing reads it, nothing rejects it, and check-mergeable.sh will not block the pull request — so an author who believes they have marked a feature branch gets an ordinary build and an unguarded merge, with no signal either way.
  • qualify-build-action guards marker ambiguity by counting files, which is a proxy that fails in both directions: two unmarked entries are fatal, while one marked file plus a marked body is invisible. On 2026-08-18 this blocked every build in operations — the required check could not pass, so the fix for it could not merge.

Concern 4 is the most expensive. Assembly tagged v8.0.0 and created the Release; the build then failed with tag v8.0.0 exists already, leaving a published Release with no artifact behind it and nothing deployed.

The precedent already exists. DQ-002 rejected Option B — teaching qualify-build-action the assembly-commit convention — because it would “couple a generic action, whose contract is ‘analyze the event, the ref, and the changelog’, to one specific post-merge workflow’s commit-message format.” The feature-marker step is that same coupling, already merged: a generic action used by ten repositories encodes one workflow’s .changelog/ frontmatter convention. Applying DQ-002 consistently resolves most of this.

OptionDescriptionTrade-offs
AOne owner per concern. Entry resolution becomes the single component that knows what an entry looks like, emitting text and metadata. Feature-build determination reads that metadata. The deploy scope reads an explicit signal. The build alone tags, releases and publishes.Each decision has one implementation and one place to change. Removes .changelog/ knowledge from the generic action entirely, which is DQ-002 applied consistently rather than selectively. Costs: touches three repositories, and the entry-resolution contract has to be designed rather than grown.
BRepair each defect in place. Make the file count smarter, teach the body route about markers, remove tagging from assembly.Smallest immediate diff, and every fix is local. Costs: leaves three frontmatter parsers that must agree forever, and leaves the generic action coupled to one repository’s convention. The next inconsistency is a matter of time, and common-module and infrastructure inherit all three copies at their cutovers.
CBody route only; retire in-repo authoring. No files, so nothing accumulates and the count check never fires.Costs nothing in code. But it reverses REQ-AUTH-003, which was negotiated in Round 2 specifically so an entry could be written in-repo as work proceeds, and it cannot be total: REQ-FEAT-006 requires a file for feature builds, because frontmatter is the only place a marker can live and a marked branch may have no pull request at all.

Recommendation: Option A, sequenced so that the already-decided part lands first. Removing tag and Release from assembly is not new design — it is DQ-001 finally implemented — and until it lands every release repeats a manual repair.

Decision: Option A (Miguel, 2026-08-18).

Sequencing, and the constraint that governs it: common-module and infrastructure must not cut over until concerns 1 and 2 are consolidated. Their cutovers copy the scripts per repository; doing that now produces three copies of a tangle already diagnosed. The extraction was always scheduled for the common-module cutover (REQ-ROLL-001); that scheduling is now load-bearing rather than an optimisation.

Applied to:


Recorded 2026-08-18, immediately after Round 4 and dependent on it.

DQ-009: Where does the feature-build marker live?

Section titled “DQ-009: Where does the feature-build marker live?”

Context: Round 4 established that entry resolution is one component reading both authoring routes (REQ-FEAT-009). That changes what is possible. The marker was file-only because marker determination read the filesystem and could not see a pull-request body; once a single component resolves the manifest whichever route it took, the body becomes readable and the restriction is no longer forced by the implementation.

The question is therefore reopened on its merits rather than inherited.

OptionDescriptionTrade-offs
AThe marker lives in whichever manifest the branch carries. File or body, and since REQ-AUTH-007 permits exactly one manifest, exactly one marker location exists. A branch with no pull request must use the file, because there is no body to read.Nothing to disambiguate — the marker is wherever the entry already is, so an author never has to learn a second rule about where configuration goes. It also removes the silent-inertness defect by making the body legitimate rather than by rejecting it, which is the better of the two ways to stop a silent failure. Costs: the build runs on push, where there is no pull-request number, so reading a body marker requires resolving branch to pull request — one API call, plus a rule for zero and for more than one open pull request.
BFile-only, and reject a marker found in a body. The position Round 4 assumed.Smallest change, and the build path needs no lookup. Costs: an author who writes the marker where they wrote the entry is told they are wrong, for a reason that is an implementation detail rather than a property of the model. It also keeps two rules where one would do — the entry may live in either place, the marker may not.
CBody-only when a pull request exists, file otherwise, chosen automatically.No author decision at all. Costs: the same branch behaves differently before and after a pull request is opened, and closing one silently changes what the next push builds. A rule conditioned on pull-request existence is harder to hold in the head than either fixed rule.

Recommendation: Option A. The marker belongs with the entry, and the reason it did not was a limitation Round 4 removes.

Decision: Option A (Miguel, 2026-08-18), with the rules stated as:

  1. A pull request carries a file manifest or a body manifest, never both — unchanged, REQ-AUTH-007.
  2. Either manifest may carry a feature-build: marker, and only one manifest exists, so only one marker can.
  3. Publishing from a branch with no pull request requires the file route, because the body does not exist.
  4. Opening a pull request later means keeping the file manifest, or removing it and writing a body one — rule 1 either way.
  5. Closing a pull request takes a body marker with it, and the next push builds ordinarily. Accepted explicitly: the environment keeps what it was last given, and a branch needing its marker to outlive a pull request should use the file.

What this costs, stated rather than discovered: the build gains a branch-to-pull-request resolution it does not have today. It belongs in the resolving component — “the effective entry for this ref” already implies “the pull request for this ref, if any” — and not bolted onto the build. Zero open pull requests means no body marker and an ordinary build; more than one is an error rather than a guess.

Applied to:


Recorded 2026-08-19, after the model was exercised end to end on cicd-testbed and then cut over in operations.

Round 4 separated the pipeline’s concerns; Round 5 placed the marker. What remained could only be settled by running it: two of the decisions below correct contracts that had passed review, and three configure a queue whose behaviour under load was not knowable in advance.

DQ-010: How does a caller say there is no marker?

Section titled “DQ-010: How does a caller say there is no marker?”

Context: REQ-FEAT-009 makes the caller supply the marker. qualify-build-action took it as an input and fell back to deriving one when the input was empty — a reading that seemed obviously right and was not.

An unmarked branch is the ordinary case, so “the caller says there is no marker” and “the caller said nothing” are the same value. Every ordinary build therefore fell through to the derivation the caller had just replaced — and that derivation rejects a changelog directory holding more than one entry file, which is what a merge queue stages on every batch.

Measured on cicd-testbed, 2026-08-19, in the merge group carrying two entries: build passed and qualify failed on the same tree with .changelog holds 2 files.

OptionDescriptionTrade-offs
AAn explicit opt-out input. The caller sets derive_feature_marker: false and the supplied value is taken whatever it is.Says exactly what is meant, and defaults to the old behaviour so unmigrated callers are untouched. Costs an input that exists only until the derivation is deleted.
BA sentinel for “no marker”. A reserved string meaning explicitly-none.No new input. Costs a magic value every caller must know, and one that can appear in real data.
CPass an empty changelog_dir. Works today because the directory test then fails.No code change at all. Costs comprehensibility entirely — it works by accident, and nothing records why.

Recommendation: Option A. The ambiguity is between supplied and unsupplied, so the fix belongs on that axis rather than inside the value.

Decision: Option A (Miguel, 2026-08-19).

The general form, which is the part worth keeping: an empty string is a legal value for most inputs, so it cannot also mean absent. Where a contract needs both, it needs two signals.

Applied to: qualify-build-action (derive_feature_marker), gradle-build-pipeline-action (pass-through), and both consumers.

DQ-011: Who iterates when several manifests are needed?

Section titled “DQ-011: Who iterates when several manifests are needed?”

Context: Assembly needs one answer per merged pull request. The resolver was a composite action, and a uses: step cannot loop — so assembly checked the resolver’s repository out and called the script the action wraps, around a loop of its own.

That is reaching past an interface into an implementation. It also revealed the real defect: the caller with the most demanding need was the one the contract did not cover.

OptionDescriptionTrade-offs
AThe resolver takes a list. prs in, one object per manifest out.The contract covers every caller, and the implementation is free to change. Costs an output shape callers must parse.
BA matrix job per pull request. The workflow fans out and the action runs once per entry.Uses the action as designed. Costs three jobs, artifact plumbing between them, and an empty-matrix edge case, to compose one file.
CKeep the checkout. Document that the script is a supported entry point.No change. Costs the boundary: every internal detail becomes a published interface, and the next caller copies the pattern.

Recommendation: Option A. A component whose contract omits its hardest caller is under-specified, not merely inconvenient.

Decision: Option A (Miguel, 2026-08-19), raised in review on operations#278.

Applied to: synthesize-changelog-entry (prs input, entries output); assembly in operations and cicd-testbed, where discovery, resolution and composition became three steps with one owner each.

DQ-012: What shape must a feature-build marker have?

Section titled “DQ-012: What shape must a feature-build marker have?”

Context: The marker is author-written and becomes part of a version string. feature-build: bad marker with spaces resolved cleanly and would have produced a version no registry accepts.

The worse consequence was quieter. The publishing action’s version pattern requires two alphanumeric segments after the version; a marker failing it produces no feature version, so the build falls through to an ordinary build and says nothing about the marker the author wrote — the same silent-failure family as the body marker of Round 5.

OptionDescriptionTrade-offs
AValidate at the resolver. Reject anything that is not <user>-<ticket>.One implementation, and the error names the reason. Consistent with REQ-FEAT-009. Costs a convention encoded in a shared component.
BValidate in each consumer.Each can apply its own rule. Costs exactly the arrangement Round 4 dismantled.
CDo not validate. Let the version pattern reject it downstream.Nothing to write. Costs the silence above: rejection is invisible and reads as an ordinary build.

Recommendation: Option A, with the offending value reported through a sanitised copy — the raw value is precisely what is not trusted, and it is about to be printed.

Decision: Option A (Miguel, 2026-08-19).

Applied to: synthesize-changelog-entry; REQ-FEAT-003.

DQ-013: How are the merge-queue parameters chosen?

Section titled “DQ-013: How are the merge-queue parameters chosen?”

Context: The queue’s parameters look like preferences. Two of them are not.

The merge method is load-bearing. Assembly finds pending work by reading Merge pull request #N from … out of mainline merge commits. A squash merge produces Title (#N), which that pattern does not match — the entry would be skipped with a warning and silently dropped from the release. Restricting the repository to merge commits makes the invariant structural rather than conventional.

The response timeout must absorb scheduling latency, not build duration. Measured on cicd-testbed, 2026-08-19: a queued entry was ejected after ten minutes because one required check never started — no runner was ever allocated. The other four passed. Re-queued unchanged, the same check started within five seconds and the entry merged in 51 seconds. A merge queue cannot distinguish “the check failed” from “the check never ran”; it ejects on the timeout either way.

That matters in proportion to build cost. A false ejection in operations costs a 20–30 minute rebuild.

OptionDescriptionTrade-offs
ADerive each parameter from what it must tolerate. Timeout from worst-case scheduling plus build; merge method from the parse it protects; grouping from the batching the queue exists to provide.Each value has a reason that survives the person who set it. Costs measurement.
BCopy the reference repository’s values.Free, and proven somewhere. Costs correctness where cost differs: the testbed’s ten-minute timeout suits synthetic checks and produced a false ejection under a real scheduler.

Recommendation: Option A. The values themselves are configuration and will drift; the reasons are the design.

Decision: Option A (Miguel, 2026-08-19). Settled for operations as ALLGREEN grouping, merge commits only, a 60-minute response timeout, a 5-minute gathering wait, and batches of up to three. The live values are in the repository’s ruleset, which is their source of truth.

Why a gathering wait at all: at a 20–30 minute build, pausing briefly to collect companions turns three builds into one. The cost is a few minutes of latency for a solitary pull request, which is small against the build it is waiting for.

Applied to: operations ruleset; REQ-QUEUE-001.

DQ-014: Which environments keep a human approval gate?

Section titled “DQ-014: Which environments keep a human approval gate?”

Context: stage, demo and prod each required a reviewer before deploying. With the pipeline soaking, the question is which of those gates is doing work that nothing else does.

What already stands between a merge and stage is not nothing: the deploy matrix is sequential, the API suite runs against dev and is waited on, and the matrix’s default fail-fast cancels the rest when it fails. The human gate on stage sat behind an automated one.

Against that, approval latency was itself a hazard. Measured 2026-08-18: version N’s stage deploy waited 87 minutes for an approval while N+1 queued behind it — the concurrency group serialises but does not order, so approval delay is what makes an overtake reachable (DQ-007).

OptionDescriptionTrade-offs
AKeep every gate.Maximum ceremony. Costs the latency above, and the gates duplicate a check the pipeline already performs against dev.
Bprod alone keeps its gate.Removes the duplicated ceremony and the main trigger of the ordering hazard, while production still requires a person. Costs: a build reaches demo on the strength of dev’s tests alone, and no environment past dev has post-deploy verification.
CRemove every gate.Fully automatic. Rejected: production authorisation is not given up while the machinery is still being proven.

Recommendation: Option B, with the residual risk stated rather than discovered — stage, demo and prod receive no post-deploy verification, so dev’s API run is the only automated evidence behind all three.

Decision: Option B (Miguel, 2026-08-19).

What made this safe to decide separately: REQ-FEAT-008 requires the feature path to reach dev by construction rather than by approval gate, precisely so that relaxing these gates could not reach marked branches by omission. That requirement was written for this decision and held.

Applied to: operations environments stage and demo.

DQ-015: Who owns the pipeline’s own machinery?

Section titled “DQ-015: Who owns the pipeline’s own machinery?”

Context: CODEOWNERS owns every path, including .github/, so the rules can only be changed by the process the rules describe. With the engineering team as sole owner, a change authored by the only available reviewer has nobody able to approve it — an author cannot approve their own pull request. That is not hypothetical; it blocked this project’s own work.

OptionDescriptionTrade-offs
ATeam plus the administrative account. Mirrors what operations already listed.Always leaves an approver. The account is tightly controlled and used deliberately by a person. Costs: an escape hatch is now a routine approver, so its use must stay visible.
BTeam alone.Simplest policy. Costs a deadlock whenever the team’s available reviewer is the author.
CLeave .github/ unowned.No deadlock. Rejected outright: it removes review from exactly the machinery that enforces review everywhere else.

Recommendation: Option A.

Decision: Option A (Miguel, 2026-08-19). No automated process may use the account; it is an administrative identity used deliberately by a person, which is REQ-REV-003 applied rather than relaxed.

Applied to: cicd-testbed CODEOWNERS, matching operations.



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