Skip to content

Design: Queued CI/CD Adoption

Audience: backend and platform engineers who merge to these repositories; whoever operates the release pipeline. Reading time: ~20 min for the full document, ~4 min for sections 1–5.

  • operations, common-module, and infrastructure move to a GitHub merge queue, with changelog entries authored either in the PR body or as a per-PR file on the branch — never both.
  • The central adaptation: in these repositories CHANGELOG.md is the build-time version source, so assembly must run before the build. changelog-assembly writes the release block and nothing else (DQ-001); the existing build pipeline fires on the resulting assembly commit and keeps owning version, tag, publish, and Release.
  • Assembly covers every merge since the previous assembly commit, producing one release per run rather than one per PR — which makes it self-healing against the race that has already lost changelog entries in documentation.
  • Three repositories gain four workflows, a PR template, a CODEOWNERS file owning every path, and one branch-protection ruleset; two shared actions gain merge_group awareness and an input that lets a repository turn off the introduces-one-new-version check.

This design implements the requirements, which are the authoritative record of what is settled.

TermMeaning
Assembly commitA commit on main whose title starts with chore: assemble CHANGELOG , pushed by arda-changelog-bot. The signal that a release block now exists in CHANGELOG.md.
Changelog entryThe categories and bullets describing one PR’s change, carried by exactly one of two routes: a ## CHANGELOG section in the PR body or an author/assignee comment, or a per-PR changelog file on the branch.
Marked branchA branch whose changelog file carries feature-build: frontmatter. Publishes a prerelease on every push and cannot merge while marked. Narrower than qualify-build-action’s internal sense of “feature branch”, which means any unprotected ref.
Queue buildThe build that runs against the merge queue’s synthetic batch head, on a merge_group event. Never publishes.
Release buildThe build that runs on a push of an assembly commit to main. Publishes, tags, and creates the GitHub Release.
ALLGREEN groupingMerge-queue strategy that merges a batch only when every entry’s checks pass.

This design sits outside the functional decomposition — it changes how artifacts are produced, not what they contain. The Viewpoint Mapping rows for these repositories are the frame of reference.

ViewpointThis design’s position
ProductNo end-user-visible change. The affected persona is the Arda engineer merging to these repositories; the served capability is concurrent, non-conflicting delivery.
Functionaln/a as a domain concern — no Domain, Module, Service, or Endpoint changes. The design operates entirely on the build-and-release surface of three repositories.
ArtifactsGoverns the production of the operations Docker image + Helm chart, the common-module Maven jar, and infrastructure’s CDK deploy. Coordinates, versions, and Release shapes are explicitly unchanged; only the commit that triggers their production moves.
RuntimeIndirect. operations deploys to Alpha002 (dev/stage) and Alpha001 (demo/prod); infrastructure runs amm.sh against all four partitions. Both chains keep their ordering and gating; they fire one commit later than today.
OAMNew failure surface (assembly can fail after a merge has landed, though the range makes it self-healing), new operational controls (manual-changelog, feature-build: frontmatter), and one App identity on three repositories. Detail in §11.
TechnologyInfrastructure layer: GitHub Actions, GitHub rulesets, denisa/clq-action, actions/create-github-app-token. No application-stack change.
MechanismAdded by this designConsumed by this design
API Endpoints / ServicesNone.GitHub REST API — pulls, issue comments, rulesets, team memberships — read through gh and actions/github-script.
ReferencesNone.None.
Data TypesFour textual contracts, each load-bearing between a producer and a consumer: the assembly-commit title (chore: assemble CHANGELOG <semver>), between assembly and each build workflow (DQ-002); the changelog file shape, between authors and assembly; the feature-build: frontmatter key, between authors and qualify-build-action.The ## [x.y.z] - YYYY-MM-DD release-heading format, already shared between clq-action and every CHANGELOG.md in the workspace.
BindingsInstallation tokens for one App — arda-changelog-bot — on three new repositories.The four .github/clq/changemap.json files, verified identical across documentation, operations, common-module, and infrastructure — so version-bump semantics port with no change.

Every merge to main in these three repositories edits CHANGELOG.md. Over the 90 days to 2026-08-04 that was 50 of 50 first-parent commits in operations and 29 of 29 in common-module — not an incidental rate but a structural one, because the changelog is where the version lives and a release is an edit to that file. Two open PRs therefore conflict by construction, and the second to merge always rebases. That is the cost the queued model removes: PRs never touch CHANGELOG.md, so the queue can batch them.

The obstacle is that these repositories are not shaped like the two that already run the model. In documentation and arda-frontend-app, CHANGELOG.md is not a build input — assembly computes the version after the merge and the production build is gated on the resulting commit. Here, gradle.properties carries version=0.0.0 and qualify-build-action derives the version, git tag, Docker image tag, Helm chart version, Maven coordinates, and Release body from the changelog at build time, on the merge commit. Stop editing CHANGELOG.md in PRs and the merge commit still shows the previous release: the tag already exists and the publish fails or silently does not happen.

This design inverts the order. changelog-assembly runs on the merge commit and writes the release block — and only the release block (DQ-001). The build pipeline then fires on the assembly commit, reads the version it finds, and publishes exactly as it does today. CHANGELOG.md stays the single source of version truth with one writer and one reader. Scope is deliberately narrow: what gets published, how it is versioned, and where it deploys are all unchanged. Rollout runs common-moduleoperationsinfrastructure (DQ-005), each step validating the shared actions against a larger blast radius than the last.

#DecisionChosen Option
DQ-001Who owns version, tag, publish, and Release?Assembly writes CHANGELOG.md only
DQ-002Who gates publishing on the assembly commit?The calling workflow
DQ-003How deep does the merge-queue gate run?Full build per batch
DQ-004What replaces feature-branch publishing?A Feature-build: directive in the PR-body section
DQ-005Rollout order across the three repositories?common-moduleoperationsinfrastructure

Full rationale and rejected alternatives in decision-log.md.

  1. CHANGELOG.md must remain the version source. qualify-build-action reads it through clq-action; changing that would move the artifact-publishing path onto untested ground. The design changes who writes the file and when, not what reads it.
  2. Published artifact shapes must be indistinguishable from today’s. Version, tag, image tag, chart version, Maven coordinates, Release title and body.
  3. qualify-build-action supports only push and pull_request. A merge queue emits merge_group; the action must classify it before any of the three repositories can enable a queue.
  4. operations is pinned to gradle-build-pipeline-action@v1.3.7 because @v1.3.8 stopped classifying a push to main as a release. That pin and this design are the same decision; the design must resolve it rather than work around it.
  5. infrastructure reaches production. Its deploy matrix runs amm.sh against Alpha001/prod. It cuts over last, after two proven migrations.
  6. No human bypass. The target ruleset has arda-changelog-bot as sole bypass actor, so assembly must push through an App installation token — GITHUB_TOKEN cannot.
AttributeTargetSatisfied by
Release fidelityByte-identical artifact coordinates and Release shape pre/post migrationDQ-001 — the publish path is untouched
Merge concurrencyTwo independent PRs land in one ALLGREEN batchmerge_queue rule + PRs never touching CHANGELOG.md
Blast-radius containmentA pilot failure costs a version number, not an outageDQ-005 rollout order
AuditabilityEvery release block on main traceable to one PR bodyAssembly is the sole writer of CHANGELOG.md
RecoverabilityA failed assembly is re-runnable without a revertIdempotent assembly; see §9.7

The component diagram below shows the workflow groups of the queued model as they land in each target repository, the two shared actions they call, the two bypass identities, and the repository state they read and write. Green is new, khaki is modified, grey unchanged. The load-bearing relationship is the pair of arrows into qualify-build-action: CHANGELOG.md and changemap.json are its inputs, and changelog-assembly is what now writes the first of them.

PlantUML diagram

The protected branch carries one ruleset, with arda-changelog-bot as its only bypass actor.

RuleParameters
pull_requestrequired_approving_review_count: 0, require_code_owner_review: true, required_review_thread_resolution: true
required_status_checksthe gates; strict_required_status_checks_policy: false
merge_queuegrouping_strategy: ALLGREEN
deletion, non_fast_forward

An earlier version split this into Integrity and Review, so a ReviewOverride App could bypass review without also bypassing the build and the queue. That split served REQ-REV-004 alone, which is withdrawn — the waiver cannot be built, because enqueuePullRequest refuses a pull request awaiting code-owner review no matter who asks. With nothing to bypass narrowly, one ruleset expresses the policy and there is no second bypass list to keep correct.

Two things learned building the split constrain any future attempt. required_review_thread_resolution and require_code_owner_review are parameters of the same pull_request rule, so review cannot be separated from thread resolution across rulesets. And bypass is properly scoped per ruleset — an actor bypassing review was still stopped by the merge-queue rule in another one, so whatever replaces the waiver cannot accidentally acquire the power to skip the queue.

Ownership covers every path, with no unowned subtree. Beyond expressing the policy, this is what stops a pull request rewriting the gate that is gating it: workflows run from the head branch, so a pull request editing .github/workflows/ runs the edited version against itself. Measured — a pull request that neutered its own gate and carried no changelog entry was reported green by it. Owning every path means such a change needs a code-owner approval, which no mechanism can waive.

  • Role in the diagram: Pre-queue gates.

  • Responsibility: establish everything that must hold before a PR may merge — CODEOWNERS resolves; the PR is neither a draft nor a marked feature build; it carries a changelog entry by exactly one of the two routes (REQ-AUTH-007), valid against clq, without editing CHANGELOG.md.

  • Public surface: one required status check named merge-eligibility, on pull_request and merge_group.

  • Members of note: one workflow rather than one per assertion, consolidated during review of the pilot. Three gates shared a trigger that must not drift and duplicated the merge-queue PR resolution between two of them — the dangerous kind of duplication, since fixing one copy and not the other leaves a gate reporting success without evaluating queued entries, the precise hole REQ-GATE-006 exists to close. Three required check names were also three strings that must exist on main and match the ruleset. The logic lives in .github/scripts/ as shell, leaving the workflow a thin trigger.

    Not draft-gated, and it re-evaluates on merge_group rather than auto-passing — the documentation original does both and carries that exposure today. Two assertions exist because GitHub provides neither: a queued PR converted to draft stays queued and merges (failing a required check is also what ejects it), and an unresolvable CODEOWNERS makes require_code_owner_review vacuous rather than stricter.

  • Role in the diagram: Post-merge.
  • Responsibility: on a push to main that is not itself an assembly commit, read the changelog entries for every merge commit since the previous assembly commit, compute one semver from their combined categories, prepend one release block to CHANGELOG.md, validate with clq, remove the changelog files it consumed, and push the result as arda-changelog-bot.
  • Public surface: none — it is triggered by push and observed through the assembly commit it produces.
  • Members of note: the element that diverges most from the documentation original.
    • Tag-creation, GitHub-Release, and push-tag steps are removed; the workflow ends after the commit push (DQ-001).
    • The unit of work is the range, not the triggering commit (REQ-ASM-004). This makes the workflow self-healing: a failed run leaves its work to the next one, so no entry depends on any single run succeeding. It also removes the race that PDEV-694 describes, rather than serialising around it, which means cancel-in-progress: true becomes correct — a later run always covers a superset of an earlier one.
    • Entries arrive by two routes and are merged per category into a single block.
    • Consumed changelog files are deleted in the same commit, which is what keeps them off the tip of main (REQ-AUTH-006). This is the one respect in which assembly touches a path other than CHANGELOG.md.
    • YAML frontmatter is stripped before composing, and that needs a test rather than care: --- is both a frontmatter delimiter and a Markdown horizontal rule, and this system has already leaked an internal delimiter into a published tagged release.
  • Design decisions referenced: DQ-001
  • Role in the diagram: Merge queue.
  • Responsibility: run the repository’s full build against the merge queue’s synthetic batch head, publishing nothing.
  • Members of note: a new merge_group trigger on the existing build job. Requires qualify-build-action to classify merge_group as a test build.
  • Design decisions referenced: DQ-003
  • Role in the diagram: Post-merge.
  • Responsibility: on a push of an assembly commit to main, qualify, build, publish, tag, and create the GitHub Release — exactly as the build job does today, but one commit later.
  • Members of note: gated by a job-level condition on the head-commit title. Everything downstream of the condition is unchanged.
  • Design decisions referenced: DQ-002
  • Role in the diagram: Shared actions.

  • Responsibility: classify the event and ref, read the changelog version, and decide kind (test / publish), version, tag, target, trigger.

  • Public surface: unchanged outputs. One new input classification: merge_grouptrigger = merge_group, kind = test, no version, no tag.

  • Members of note: the change is small and was verified by spike on 2026-08-04. The action is a single composite action.yaml; its qualify-build step is a case "${{ github.event_name }}" over pull_request|push with an explicit *) error "Unsupported event" fallthrough, so merge_group support is one added case branch.

    The define-target step probes gh ruleset check --repo <repo> "${{ github.base_ref || github.ref_name }}" and greps the output for the configured workflow name. Three facts established by spike:

    1. On a merge_group event github.base_ref is empty and github.ref_name is the queue ref, e.g. gh-readonly-queue/main/pr-156-<sha>.
    2. gh ruleset check against that queue ref returns 0 rules apply, so the probe yields target = feature.
    3. The probe requires a bare branch name — refs/heads/main also returns 0 rules apply, so github.event.merge_group.base_ref cannot be passed through unmodified.

    Fact 2 is harmless rather than a defect: nothing on the merge_group path consumes target, because the queue build never publishes. The minimal correct change is therefore the case branch alone, leaving define-target untouched. Rewriting the probe to resolve the real base ref would be strictly more code for no behavioral gain.

    The action gains no knowledge of the assembly-commit convention (DQ-002).

  • Role in the diagram: Shared actions.
  • Responsibility: checkout, build, test, publish, tag, and release a Gradle project, delegating the publish/test decision to qualify-build-action.
  • Members of note: inherits the new classification with no logic change of its own. The operations @v1.3.7 pin retires here — once the publish fires on the assembly commit, @v1.3.8’s behavior is the correct one. Verified by spike: v1.3.7...v1.3.8 is action.yaml -122/+29 plus an actions/checkout 6→7 bump, and its changelog records only “Use shared logic from Arda-cards/qualify-build-action 2”. Retiring the pin is a version bump, not a rewrite.
  • Publish decoupling, verified: gradle-build.sh (89 lines) branches solely on the KIND and VERSION environment variables and reads nothing from the event payload or the trigger classification. This is what makes Constraint 2 achievable — moving when the publish fires cannot change what it publishes.

arda-changelog-bot (new to these repositories)

Section titled “arda-changelog-bot (new to these repositories)”
  • Role in the diagram: Bypass identities.
  • Responsibility: push assembly commits directly to main without any human holding bypass.
  • Members of note: App ID 3683113; permissions Contents: write, Pull requests: read, Metadata: read; credentials in org secrets CHANGELOG_BOT_APP_ID / CHANGELOG_BOT_PRIVATE_KEY. Bypasses both rulesets at always mode, because both carry a pull_request rule and it pushes directly rather than through a PR. The three-step extension procedure — install, grant secret access, add to ruleset bypass — is documented in documentation/knowledge-base/arda-changelog-bot.md.

Behaviors are grouped by pipeline stage. Each cross-links to the Key Element that owns it and maps to entries in Behavior Verification.

Owner: merge-eligibility.

  • Changelog-edit rejection. A PR whose diff modifies CHANGELOG.md fails merge-eligibility with an explanatory error, unless it carries the manual-changelog label — an emergency hatch, not a supported route (§11).
  • Exactly one route. An entry arrives either in the PR body or an author/assignee comment, or as a changelog file on the branch — never both, never neither (REQ-AUTH-007). Both present is an error rather than a precedence question: ambiguity about which entry is authoritative is treated as a defect, not resolved silently.
  • Entry validation. At least one bullet under at least one category valid per the repository’s .github/clq/changemap.json, which is identical across all four repositories. The entry is not a complete changelog and cannot be validated by clq standalone; the gate checks categories, and clq validates the assembled file after the release block is written.
  • The gate evaluates on drafts. It is not draft-gated: a skipped job reports success, which is indistinguishable from having passed (REQ-GATE-006). Running on drafts is also better for the author, who learns the entry is missing while still working.
  • Filename-collision warning. Where another open PR claims the same changelog-file path, merge-eligibility surfaces it. Advisory only — nothing re-runs an open PR’s checks when a later PR claims its name — with the queue as the authoritative backstop, since two PRs adding the same path cannot batch together.
  • Draft refusal. The gate fails when a queued PR is a draft, which is also what ejects it — GitHub keeps a drafted entry and merges it otherwise.
  • Comment-driven amendment. For a body-route PR, posting a new ## CHANGELOG block as a comment supersedes the body — last one wins. For a file-route PR, amendment is an edit to the file; a stray comment carrying an entry would trip the both-present rejection.
  • Owner resolution. The gate fails when CODEOWNERS does not resolve, because an unresolvable owner turns code-owner review off silently rather than making it stricter (REQ-REV-002).

Owner: queue build.

  • Batch formation. The queue forms ALLGREEN batches; entries merge together only when every entry’s checks pass.
  • Full build on the batch head. The build job re-runs against the synthetic merge commit, catching pairs of PRs that compile independently but not together (DQ-003).
  • Non-publishing qualification. qualify-build-action classifies merge_group as a test build, so no tag is cut and nothing is published from the queue.
  • Gate re-evaluation. Every gate re-evaluates against the queued commit rather than auto-passing, resolving the PR number from the gh-readonly-queue/main/pr-N-<sha> ref.

Owner: changelog-assembly.

  • Self-exclusion. The workflow skips when the head commit title already starts with chore: assemble CHANGELOG , so its own push does not retrigger it.
  • Range determination. The unit of work is every merge commit since the previous assembly commit, not the commit that triggered the run (REQ-ASM-004). Whether the queue lands three PRs as one batch or as three merges seconds apart, the run that wins covers all of them and produces one release.
  • Mode selection. Three modes carried over from the reference implementation: normal (merge did not touch CHANGELOG.md), hand-edit (merge added a new ## [x.y.z] - YYYY-MM-DD heading at the top), and skip (any other edit shape).
  • Entry extraction. For each PR in the range, the entry is read from whichever route it used: the ## CHANGELOG section of the body or an author/assignee comment (last one wins), or the changelog file added on its branch.
  • Version computation. The highest-impact category present across all entries in the range determines the bump — Changed/Removed major, Added/Deprecated minor, otherwise patch — applied to the version in the current top release heading.
  • Per-category merge. Where the range covers several PRs, their entries are combined under one heading per category, carrying the bullets from every contributing PR.
  • Release-block prepend and validation. The block is spliced above the first existing ## [ heading and validated with clq-action against changemap.json.
  • Frontmatter stripping. YAML frontmatter is removed before composing, so no directive reaches CHANGELOG.md. --- is both a frontmatter delimiter and a Markdown horizontal rule, and the reference implementation has already leaked an internal delimiter into a published release — this needs a test, not care.
  • Consumed-file removal. The changelog files the run consumed are deleted in the same commit, which is what keeps them off the tip of main.
  • Push as the bot, and stop. The commit is pushed with an arda-changelog-bot installation token. No tag is created and no GitHub Release is made — the divergence from the reference implementation that DQ-001 settles.

Owner: release build, qualify-build-action.

  • Assembly-commit gating. The build job runs on a push to main only when the head commit is an assembly commit. A merge commit produces no build; it produces an assembly run, whose commit produces the build (DQ-002).
  • Version read-back. qualify-build-action reads the version from the release heading assembly just wrote, and returns kind = publish with that version and its v-prefixed tag.
  • Publish, tag, release. Unchanged: image and chart to GHCR/Helm OCI for operations, Maven jar for common-module, ncipollo/release-action for infrastructure.
  • Deploy chain. operations fans out devstagedemoprod at max-parallel: 1; infrastructure fans out its four partitions through amm.yml. Both are downstream of the publish job and inherit its new timing without change.

The activity diagram below traces how a single build workflow classifies every event it can receive after adoption. Four of the leaves carry detail worth stating in prose rather than on the diagram:

  • pull_request runs merge-eligibility and a qualify-build that returns kind = test.
  • merge_group is the new classification: qualify-build treats it as a test build, so the queue never publishes.
  • A merge commit on main deliberately produces no build. changelog-assembly runs on that same push event and pushes the assembly commit, which is what triggers the build.
  • An assembly commit on main is the only path that publishes. The test is on the commit title prefix chore: assemble CHANGELOG (DQ-002).

The feature-branch leaf is where DQ-004 lands.

PlantUML diagram

Owner: qualify-build-action, merge-eligibility.

Terminology. A marked branch is one carrying the feature-build marker. This is narrower than qualify-build-action’s internal sense of “feature branch”, which means any ref that is not release-protected — that is, every working branch. Only a marked branch publishes and is barred from merging.

Today a feature branch publishes by hand-editing CHANGELOG.md to a major.minor.patch-user-issue version, with the changelog header instructing the author to reverse the edit before merging. The new authoring model removes the place that signal lives, so it moves into the changelog file’s frontmatter (DQ-004).

Scope: operations and common-module only. infrastructure is excluded and keeps its current behavior. Verified 2026-08-06: an unprotected branch there runs build, the per-app synth matrix, synth-corporate, and validate-release; qualify-build-action may classify kind = publish, but nothing consumes it, because publish is gated on trigger == 'push_to_release_branch' and deploy needs publish. There is no registry artifact for it to publish. For common-module the capability exists today only as an undocumented side effect of the shared action being generic; it becomes explicit, and it is worth having on purpose — consumers sometimes need to build against an unreleased library change without cutting a real version.

  • Marking. YAML frontmatter in the changelog file carries the suffix explicitly, e.g. feature-build: jmpicnic-1408. The suffix is stated rather than derived, because qualify-build-action’s regex requires two alphanumeric segments and deriving it from the filename would couple two conventions. Marking and unmarking are ordinary file edits, so both work at branch creation or later, and whether or not a PR exists (REQ-FEAT-003).
  • A marked branch requires a changelog file. The body route is unavailable to it, since a marked branch need not have a PR at all — and the file is what gives the version computation its input at push time.
  • Every push builds and publishes. This is why the marker must be readable from the checkout alone, and why it cannot be a PR label.
  • A marked branch cannot merge. A required check fails while the marker is present, replacing today’s “must, of course, be reversed before merging to main” with an enforced condition. Unmarking makes it mergeable; the prohibition is on the marked state, not the branch.
  • Version derivation. The base is computed from the changelog file’s categories against the current CHANGELOG.md head — the same computation assembly performs. An Added entry on a branch off 6.14.0 publishes 6.15.0-jmpicnic-1408-<run>. This sorts before 6.15.0 under semver prerelease ordering, which is correct: it is a prerelease of the release it anticipates.
  • No residue on main. Satisfied by construction rather than discipline — the marker lives in the changelog file, a marked branch cannot merge, and the file is consumed and removed by assembly. There is no state that can survive onto main.
  • Deployment stops at dev, structurally. In operations a feature build deploys to dev and nothing else; the feature path’s matrix contains dev alone, and the other environments are not reachable from it (REQ-FEAT-008). It deliberately does not rely on environment approval gates, which are shared with the ordinary release path — a future decision to relax approvals for normal deploys would otherwise relax them for feature branches by omission. A relaxation that cannot reach feature branches is one that cannot be made accidentally.

The deploy constraint is not hypothetical. operations’ deploy job is gated only on needs.build.outputs.chart_name, and gradle-build.sh emits chart_name whenever KIND = publish and a chart exists — with no distinction between a release publish and a feature publish. A feature publish therefore satisfies the deploy job’s only condition and fans out to dev, stage, demo, and prod. The path is dormant only because feature publishing has produced three tags in the repository’s entire history; publishing on every push to a marked branch would activate it.

The sequence below traces one PR from open to deployed. The two events that matter are the pair of pushes to main: the merge commit triggers assembly and nothing else, and the assembly commit triggers the build. Everything to the right of changelog-assembly behaves exactly as it does today.

PlantUML diagram

Owner: changelog-assembly, release build.

  • Assembly failure cannot lose an entry. Because each run covers everything since the previous assembly commit, a failed run leaves its work to the next one. No entry depends on any single run succeeding, and recovery needs no revert and no hand-edit.

    This is the failure mode the design exists to close, and it is not hypothetical. On 2026-07-23 in documentation, PRs #142, #137, and #135 merged within 80 seconds. Assembly ran for each; the runs for #142 and #137 failed and only #135’s succeeded. The entries for #142 and #137 are absent from CHANGELOG.md and nobody noticed. The cause is the race PDEV-694 describes — concurrent runs each computing a version against a main that moves underneath them. Range-based assembly removes the race rather than serialising around it, and subsumes PDEV-694 for these repositories.

  • Superseded runs are safe to cancel. A later run always covers a superset of an earlier one, so cancel-in-progress: true is correct — the opposite of the reference implementation’s setting, and correct for the opposite reason.

  • Version collision. If a tag for the computed version already exists, the build’s Tag source step fails loudly rather than overwriting — behavior inherited from gradle-build-pipeline-action.

  • Hand-edit path. A manual-changelog PR that adds a top-of-file release heading is honored: assembly extracts the version from it and pushes an empty marker commit carrying the canonical title, so the assembly-commit gate still fires. Any other edit shape skips assembly entirely, which also means no publish — the safe default.

  • Mixed-convention window. Between cutovers, common-module is on the new model while operations still edits CHANGELOG.md directly. The two are independent; the only shared surface is the shared actions, which must keep behaving correctly for a repository that has not yet moved.


Subsections mirror Behavioral Design. Verification for a CI/CD design is necessarily mostly integration-level: the units under test are workflows, and the only faithful fixture is a real repository. The pilot repository is the primary fixture (DQ-005).

Test IDBehavior TestedRequired SetupTest Fixtures
BV-1-01Changelog-edit rejection — a PR touching CHANGELOG.md failsPR on the pilot repo with a one-line changelog editPilot repository
BV-1-02Same, overridden by the manual-changelog labelBV-1-01’s PR, label appliedPilot repository
BV-1-03Entry validation — a PR with neither route failsPR with the section deleted and no changelog filePilot repository
BV-1-04Comment-driven amendment — a later comment supersedes the bodyBV-1-03’s PR plus a comment carrying a valid sectionPilot repository
BV-1-05Label-gated reviewREVIEW-REQUIRED blocks until a human approvesPR with the label, no approvalPilot repository
BV-1-06Exactly one route — a PR with both a body section and a changelog file failsPR carrying bothPilot repository
BV-1-07Exactly one route — the file route alone passesPR with a changelog file and no body sectionPilot repository
BV-1-08Comment scope — a comment from someone other than the author or an assignee is ignoredThird party posts a ## CHANGELOG block on a file-route PRPilot repository
BV-1-09Gates evaluate on drafts — a draft PR with no entry fails rather than reporting a skipped successDraft PR, no entry; inspect the check conclusionPilot repository
BV-1-10Filename-collision warning — two open PRs claiming one path are surfacedTwo PRs adding the same changelog-file pathConcurrent-PR pair
BV-1-11Draft refusal — a queued PR converted to draft is ejected rather than mergedQueue a PR with a slow check, convert to draft while queuedPilot repository
BV-1-12Owner resolution — an unresolvable CODEOWNERS fails the gate rather than silently disabling reviewPoint CODEOWNERS at a team without repository accessPilot repository
Test IDBehavior TestedRequired SetupTest Fixtures
BV-2-01Batch formation — two independent PRs land in one ALLGREEN batchTwo PRs touching disjoint files, both auto-merge enabledConcurrent-PR pair
BV-2-02Non-publishing qualification — the queue build cuts no tag and publishes nothingBV-2-01’s batch; inspect tags and registry before the merge landsConcurrent-PR pair
BV-2-03Full build on the batch head — a semantically conflicting pair is caughtTwo PRs that compile alone but not togetherConflict-pair fixture
BV-2-04Gate re-evaluationmerge-eligibility runs against the queued commit rather than auto-passingAny queued entry; inspect the check runConcurrent-PR pair
Test IDBehavior TestedRequired SetupTest Fixtures
BV-3-01Version computation — each category yields the right bumpFour PRs, one per bump class, merged in sequencePilot repository
BV-3-02Release-block prepend — the block lands above the previous heading and validates with clqAny merged PR; diff CHANGELOG.mdPilot repository
BV-3-03Push as the bot, and stop — assembly creates no tag and no ReleaseMerged PR; assert no new tag exists after assembly and before the buildRelease-shape baseline
BV-3-04Self-exclusion — the assembly commit does not retrigger assemblyObserve workflow runs on the assembly commitPilot repository
BV-3-05Range determination — three merges produce one release block combining all threeThree PRs merged in rapid successionConcurrent-PR pair
BV-3-06Frontmatter stripping — no frontmatter reaches CHANGELOG.mdMerged PR whose changelog file carries frontmatterMarked-branch fixture
BV-3-07Per-category merge — entries from several PRs combine under one heading per categoryThree PRs with overlapping categories, merged togetherConcurrent-PR pair
BV-3-08Consumed-file removal — the changelog file is absent from the tip of main after assemblyFile-route PR; inspect the tree at the assembly commitPilot repository
BV-3-09Two-route extraction — a range mixing body-route and file-route PRs assembles bothOne of each, merged togetherConcurrent-PR pair
BV-3-10Self-healing — an entry survives a failed assembly runForce a run to fail, then merge another PR; assert both entries appearConcurrent-PR pair
Test IDBehavior TestedRequired SetupTest Fixtures
BV-4-01Assembly-commit gating — a merge commit produces no buildMerged PR; assert the build job is skipped on the merge-commit pushPilot repository
BV-4-02Version read-back — the published version equals the assembled oneMerged PR; compare the release heading against the published coordinatesRelease-shape baseline
BV-4-03Publish, tag, release — coordinates and Release shape match the pre-migration baselineOne release before and one after cutoverRelease-shape baseline
BV-4-04Deploy chaindevstagedemoprod fires once, in orderoperations cutover releaseDeploy-chain observation
BV-4-05Same for infrastructure’s four-partition amm.yml matrixinfrastructure cutover releaseDeploy-chain observation
Test IDBehavior TestedRequired SetupTest Fixtures
BV-5-01Marking — a push to a marked branch publishes a suffixed prereleaseBranch whose changelog file carries feature-build: frontmatterMarked-branch fixture
BV-5-02Version derivation — the base is computed from the entry’s categories, and the version matches feature_branch_version_regexBV-5-01 with an Added entry off a known head; inspect the coordinatesMarked-branch fixture
BV-5-03Unmarked branch — a push publishes nothingSame branch with the frontmatter removedMarked-branch fixture
BV-5-04No PR at all — a marked branch with no pull request still publishes on pushMarked branch, no PR openedMarked-branch fixture
BV-5-05A marked branch cannot merge — the required check fails while marked, and passes once unmarkedMarked branch with a PRMarked-branch fixture
BV-5-06Deployment stops at dev — a feature publish reaches dev and no other environmentoperations feature publish; inspect the deploy runsDeploy-chain observation
BV-5-07infrastructure exclusion — a marked branch there publishes and deploys nothingMarked branch on infrastructurePilot repository
Test IDBehavior TestedRequired SetupTest Fixtures
BV-6-01Assembly failure is re-runnableForce a failure mid-assembly, then re-run the workflowPilot repository
BV-6-02Version collision — an existing tag fails the build loudlyPre-create the tag the next release would useRelease-shape baseline
BV-6-03Hand-edit path — a top-of-file release block yields a marker commit and a releasemanual-changelog PR supplying its own blockPilot repository
BV-6-04Hand-edit path — any other edit shape skips assembly and publishes nothingmanual-changelog PR fixing a historical typoPilot repository
BV-6-05Mixed-convention window — a not-yet-migrated repository still builds correctly against the updated shared actionsoperations build on the new action version, before its own cutoverUnmigrated-repo canary

common-module after its cutover. The primary fixture for every behavior that does not require a deploy chain: it carries the full workflow set, publishes a real artifact, and has no environment deploys hanging off its publish, so a failed experiment costs a version number.

Two PRs opened against the pilot repository touching disjoint files, each with a valid ## CHANGELOG section and auto-merge enabled. The fixture for everything queue-related. Extended to three entries for BV-3-05.

Two PRs that each compile in isolation but not together — for example, one renaming an internal function and one adding a caller of the old name. The only fixture that demonstrates what the full-build queue gate (DQ-003) buys over the cheap-gate alternative.

A record of the last pre-cutover release in each repository: version, git tag, Docker image tag, Helm chart version, Maven coordinates, GitHub Release title and body. Captured before the cutover PR merges and compared field by field against the first post-cutover release. This is the fixture that discharges Constraint 2.

The Actions run graph for the first post-cutover release, used to assert that each environment job fired exactly once and in the declared order. For infrastructure this includes confirming that Alpha001/prod fired only after the three preceding partitions.

A branch on the pilot repository whose changelog file carries feature-build: frontmatter. Used for everything in §10.5, and deliberately exercised both with and without an open pull request, because working without one is the property that ruled out every label-based marking scheme.

A build of a repository that has not yet cut over, running against the already-updated shared actions. Guards the mixed-convention window: the shared-action changes must be backward compatible, because operations and infrastructure keep building through them for as long as the rollout takes.


  • Workflow-run history is the primary signal. Three new workflow names appear per repository: Changelog Gate, Review Required Gate, Post-Merge: Changelog Assembly.
  • The assembly commit itself is the observable release marker on main. git log --grep '^chore: assemble CHANGELOG ' enumerates every release.
  • A failed assembly is a failed workflow run on main with no accompanying publish — the shape on-call should recognize.
ControlMechanismEffect when set
manual-changelogPR labelmerge-eligibility accepts a CHANGELOG.md edit; assembly honors a top-of-file release block or skips. Exceptional use only — see below
Merge queueRuleset merge_queue ruleDisabling it reverts to direct merges; PR-body changelogs keep working
check_response_timeout_minutesRuleset parameterHow long the queue waits for the full build before dropping an entry
feature-build: frontmatterChangelog file on a branchPublishes a prerelease on every push and blocks the branch from merging

On manual-changelog. It is an emergency hatch, not a workflow. Reaching for it re-introduces the exact CHANGELOG.md edit this project exists to remove, and a PR carrying it can conflict with any other PR in flight — which is why it must not become a habit. Legitimate uses are narrow: correcting a historical entry, or a release whose notes the assembler genuinely cannot produce. “The PR body was awkward to edit” is not one; amend the entry by posting a ## CHANGELOG comment on the PR instead. Anyone applying the label should expect to justify it in review, and a rising usage rate is a signal that the assembler has a gap worth fixing rather than routing around.

  • Build counts per change, today: operations and common-module carry an unfiltered push: trigger alongside pull_request, so every push to a PR branch fires the build twice — once as push, once as pull_request — plus once more on the merge commit. After adoption the merge-commit build moves to the assembly commit and the queue batch head adds one, so the total rises by one build per batch.
  • That existing double-run is worth removing in the same change: narrowing the push: trigger to branches: [main] halves PR-time build cost and is a prerequisite for the assembly-commit gate to read cleanly. It is a strict improvement independent of this project.
  • The net cost is partially offset by the rebase-and-rebuild cycles the queue eliminates — measurable, since today every second concurrent PR rebases.
  • operations’ build is the expensive one (containerized Postgres integration tests). Queue parameters should start conservative — max_entries_to_build: 1, a generous check_response_timeout_minutes — and tighten with evidence.
  • No runtime cost change. Nothing about the deployed artifacts moves.
  • Assembly failed; merge already on main. Nothing was published and no tag was cut, and the next merge’s assembly will cover the missed one — the range is what makes this self-healing. If nothing further is due to merge, re-run the workflow; it recomputes the same range. If an entry was malformed, fix it and re-run.
  • An unbuilt or bad chart reached dev. It stops there: every environment beyond dev requires human authorisation, and REQ-ROLL-003 keeps that true for the whole migration. To recover, dispatch a deploy of the last healthy tag back to dev; no rollback of main is needed, because the chart is an artifact rather than a branch state.
  • A waiver did nothing. The waiver enqueues rather than merges, and entry to the queue requires green checks. If the checks were red the enqueue failed; the workflow posts the refusal. Fix the checks and re-issue the command — the waiver does not queue up intent.
  • Release landed with no deploy. Check whether the head commit on main is an assembly commit. A merge commit that never produced one means assembly failed or skipped; a skip-mode assembly is silent by design.
  • Tag collision on publish. The build fails at Tag source. Someone hand-edited a version that already shipped, or an assembly ran twice. Inspect CHANGELOG.md’s top heading against git ls-remote --tags.
  • Queue not draining. Entries dropping on timeout means the full build exceeds check_response_timeout_minutes. Raise the timeout or reconsider DQ-003 in favor of tiered gates.

All three repositories additionally move from required_approving_review_count: 1 and no CODEOWNERS, to required_approving_review_count: 0, require_code_owner_review: true, and a * @Arda-cards/engineering @systems-arda CODEOWNERS file owning every path (DQ-006).

The net review requirement is unchanged in strength — one approval, and GitHub already forbids a PR author from approving their own PR — but it must now come from the engineering team or systems-arda rather than from anyone with repository access. The file and the ruleset change must land together; the ruleset change alone drops required review to zero, because with no CODEOWNERS nothing is owned and nothing is required.

RepositoryBeforeAfterCutover position
common-modulebuild on merge commit publishes; no bypass actors; required check buildAssembly then publish; one ruleset; App-only bypass; required checks build, merge-eligibilityFirst
operationsAs above, plus OrganizationAdmin + RepositoryRole 5 bypass and a four-environment deploy chainAs above; bypass reduced to the App; @v1.3.7 pin retiredSecond
infrastructurevalidate-release + publish + four-partition amm.yml; qualify-build-action@v2 called directlyAs above, adapted to ci.yaml; validate-release remains the qualification check nameThird

Paths are repository-relative. Every path below was verified against the repositories at 2026-08-04.

FileConstructContent
.github/workflows/merge-eligibility.yamlworkflow, pull_request + merge_groupNew in all three. A thin trigger: resolves the pull request once — including out of the gh-readonly-queue/main/pr-N-<sha> ref — then runs the three scripts below. Publishes the single required check. Not draft-gated, and it re-evaluates in the queue rather than auto-passing.
.github/scripts/check-codeowners.shscriptFails when repos/{owner}/{repo}/codeowners/errors is non-empty. An unresolvable owner makes require_code_owner_review vacuous rather than stricter, and nothing else reports it (REQ-REV-002). Also run post-merge by changelog-assembly, since owners can break after a merge. Queries the ref under test, not the default branch — corrected 2026-08-07 after the pilot’s own pull request 404’d on it, because operations has no CODEOWNERS on main until that pull request merges. Checking the head ref also turns “a pull request breaks CODEOWNERS” from a post-merge discovery into a pre-merge refusal, and a 404 is treated as the failure it is rather than as an absence of errors.
.github/scripts/check-mergeable.shscriptFails when the pull request is a draft — GitHub keeps a drafted entry queued and merges it, and failing a required check is what ejects it — or when it carries a feature-build: marker (REQ-FEAT-004).
.github/scripts/check-changelog.shscriptRejects a CHANGELOG.md edit without the manual-changelog label, then composes the resolved entry into a candidate release block and validates it with the same clq that guards the real file.
.github/scripts/changelog-entry.shscriptResolves the entry from exactly one of the two routes (REQ-AUTH-007); shared with changelog-assembly, which needs the same resolution per merged pull request. Refusals go to stderr: every caller reads the entry through command substitution, so a message on stdout is captured as though it were the entry and the author is left with a bare exit code — observed and corrected 2026-08-07.

Why one workflow. Three gates shared a trigger that must not drift and duplicated the merge-queue resolution between two of them — fix one copy and not the other and a gate reports success without evaluating queued entries, the hole REQ-GATE-006 exists to close. Three required check names were also three strings that must exist on main and match the ruleset exactly, each a chance at the deadlock REQ-ROLL-005 describes.

Why not a shared action yet. An interface should be shaped by two callers rather than one. common-module’s adoption is the moment to extract these scripts; until then the extraction would be guesswork, and the workflow is already thin enough that it will be mechanical.

FileConstructContent
.github/workflows/changelog-assembly.yamlworkflow Post-Merge: Changelog AssemblyNew in all three. Ported from documentation minus tag-creation, tag-push, and GitHub Release (DQ-001); plus range determination over merge commits since the previous assembly commit, two-route entry extraction, per-category merging, frontmatter stripping, and deletion of consumed changelog files. Concurrency flips to cancel-in-progress: true, which range-based assembly makes correct.
FileConstructContent
operations/.github/workflows/cicd.yamljob buildAdd merge_group to on:; narrow the unfiltered push: trigger to branches: [main] (removes an existing double-run on PR branches); add the assembly-commit condition to the push path; move off the @v1.3.7 pin.
operations/.github/workflows/cicd.yamljob deploySplit by path. The release path keeps the four-environment matrix. The feature path gets a matrix containing dev alone, with the other environments structurally unreachable rather than approval-gated (REQ-FEAT-008). Today’s single if: needs.build.outputs.chart_name condition does not distinguish a release publish from a feature publish, and would fan a feature build out to prod.
common-module/.github/workflows/cicd.yamljob buildSame changes; keep fetch-depth: 0 for the Spotless ratchet (PDEV-1300).
infrastructure/.github/workflows/ci.yamljobs validate-release, publishAdd merge_group; move the publish condition from the merge commit to the assembly commit.
infrastructure/.github/workflows/ci.yamljob all-synth-resultsCandidate required check for the queue — see §13.
operations/.github/workflows/api-tests-local.yamltrigger blockConfirm the push: branches: [main] path still fires as intended when main receives two pushes per change.
FileConstructContent
qualify-build-action/action.yamlstep qualify-build, the case "${{ github.event_name }}"Add merge_group to the existing pull_request|push) branch, replacing the *) error "Unsupported event" fallthrough for that event. A merge_group build then reports merge_group_to_release_branch — or merge_group_to_feature_branch where the destination branch’s ruleset does not require the configured workflow_name check, since the trigger is composed from the event and the resolved target rather than fixed. Either value falls through define-build to kind = test with no change there: neither matches the push_to_release_branch test nor the push test that is_publishable_feature_branch requires.
qualify-build-action/action.yamlstep define-targetChanged — supersedes the earlier “leave untouched”. Resolve the branch as github.event.merge_group.base_ref || github.base_ref || github.ref_name, stripping refs/heads/. Without it a queued build probes the temporary gh-readonly-queue/… ref, finds no rules, and concludes target = feature — which is not harmless as first assessed, because clq-extract takes its mode from target and would validate the changelog in feature mode at the last checkpoint before merge, weaker than the release mode the pull request itself was held to. The fix degrades safely: if the payload field is absent the expression falls through to the previous behaviour.
qualify-build-action/README.mddocsUpdate the trigger and event tables; the current text states only push and pull_request are supported.
gradle-build-pipeline-action/action.yamlcompositeNo change — verified 2026-08-06. Every publish, tag and release step is gated on trigger == 'push_to_release_branch', which a queued build never produces. This repository therefore needs no release of its own: once qualify-build-action 2.1.0 ships, its floating @v2 reference picks the change up.
gradle-build-pipeline-action/gradle-build.shscriptNo change. Verified to branch only on KIND and VERSION. Listed so the implementer does not go looking.

Shared-action consumers and release staging

Section titled “Shared-action consumers and release staging”

Who else runs this code, and how a change reaches them. Verified 2026-08-06.

RepositoryPinReaches qualify-build-action
operationsgradle@v1.3.7v1.3.9transitively, floating @v2
common-modulegradle@v1transitively, floating @v2
accountsgradle@v1transitively, floating @v2
bastiongradle@v1transitively, floating @v2
pdf-rendergradle@v1transitively, floating @v2
qr-lookupgradle@v1transitively, floating @v2
infrastructurequalify@v2directly

The transitive column is the one that matters: gradle-build-pipeline-action/action.yaml references qualify-build-action@v2 from inside its own composite, so an exact pin on the wrapper does not freeze what the wrapper calls. REQ-ROLL-004 is the constraint this imposes; work branches and prerelease versions are the two mechanisms that satisfy it.

operations@v1.3.7 pin was held because v1.3.8 stopped classifying a push to main as a release, silently skipping chart publish and deploy. v1.3.9 corrected that by asking for context:build, which is what the ruleset on both operations and common-module requires; common-module has published every release since on that version. Retiring the pin is therefore a prerequisite of this work rather than part of it, and is staged separately for exactly that reason.

Three runs, in order, each isolating one delta:

Change under testPath exercisedWhat a red result means
Aoperations → released v1.3.9test onlyPre-existing; nothing to do with this project
Boperations → action work branchestest onlyThe merge_group change
CB, plus a feature version in CHANGELOG.mdpublish + deploy to devPublish or deploy regression

A and B both ran green on 2026-08-06. A (PR #263) confirmed the stale pin was the only thing keeping operations off v1.3.9. B (run 31132103910) reported target is feature, trigger is push_to_feature_branch, kind is test, version is undefined — the modified action leaving the existing push path exactly as it was.

A and B stop short of publishing, because a pull-request build only ever yields kind = test and the deploy job is gated on a chart name that a test build never sets. C closes that gap using the repository’s own documented feature-branch capability: a version of the form 7.1.0-jmpicnic-1408 makes the push publish a chart and deploy it to dev, where the run then parks on stage’s required reviewers and is cancelled. dev is restored by dispatching deploy.yaml with the previous chart version. Nothing beyond dev is reachable without a human approving it, which is what REQ-ROLL-003 requires of any new path.

C leaves a Docker image and a Helm chart at the feature version in GPR, which need clearing afterwards. It leaves no git tag: gradle-build-pipeline-action’s Tag source step tags only when the trigger is push_to_release_branch, so a feature build publishes artifacts without marking the history (verified 2026-08-06). C also displaces whatever is running on dev until the restore, and dispatches the api-test suite against it.

A deploy has no concurrency group in cicd.yaml, reusable_deployment.yaml, or deploy.yaml, so two runs reaching the same environment are not serialised — which C proved by colliding with the release merged while it ran. See REQ-PUB-005; the fix lands in Stage 2 and merges before the queue is enabled, so it is proven under today’s traffic first.

C’s own results, 2026-08-06 (run 31132470432): the build published 7.0.1-jmpicnic-1408-31132470432.2251.1 under the modified action, dev deployed successfully, and stage blocked on required reviewers with no action taken — REQ-FEAT-008 demonstrated rather than asserted. The run was cancelled at that gate and dev restored by dispatching deploy.yaml.

FileConstructContent
operations/.github/workflows/reusable_deployment.yamlconcurrency on the deploy jobFolded in, not deferred. A concurrency group keyed deploy-<component>-<purpose> with cancel-in-progress: false (REQ-PUB-005). Placed in the reusable workflow rather than its callers so the automatic chain and deploy.yaml’s dispatch share one group — a manual restore racing an automatic deploy is the same collision.
helm-deploy-pipeline-action/action.yamlprecondition stepREQ-PUB-006 lands here, not in operations — corrected 2026-08-06. The guard must compare the chart version already released in the namespace against the incoming one, and only this action has cluster access: it configures AWS credentials at action.yaml:101 and runs helm upgrade at :263. A step in operations would have nothing to query. This makes it a fourth shared-action repository with its own review requirement, so it should be sequenced early rather than last.
operations/.github/workflows/deploy.yamlforce inputPlumbs the deliberate-rollback escape through to the guard above, since deploy.yaml exists precisely to put an environment back on an earlier version. Added with the guard rather than ahead of it.
infrastructure/.github/workflows/amm.ymlenvironment inputAdd Alpha002/stage to the choice. ci.yaml’s matrix deploys it on every release but no dispatch can reach it, so it has no manual recovery path — which both REQ-FEAT-008 and REQ-PUB-005’s recovery story assume. Absorbs PDEV-1429.
.github/pull_request_template.mdtemplateNew for operations and common-module (neither has one). Carries the ## CHANGELOG placeholder.
infrastructure/.github/PULL_REQUEST_TEMPLATE.mdtemplateModified, not created. Its existing ## CHANGELOG section is a checkbox asserting CHANGELOG.md was updated — the exact opposite of the new convention, and a heading collision with what merge-eligibility parses. Must be replaced with the entry-carrying form.
.github/CODEOWNERSconfigNew in all three; none currently has one, and arda-frontend-app has none either (verified at root, .github/, and docs/). Content is * @Arda-cards/engineering @systems-arda, matching documentation minus its unowned-roadmap exception (DQ-006). Load-bearing: with required_approving_review_count: 0 and require_code_owner_review: true, this file is the review policy and a missing one drops review to zero. Verified 2026-08-06 — @Arda-cards/engineering holds direct push on all three and systems-arda is a User with admin; CODEOWNERS requires direct access, not subteam-inherited. Deliberately flat, with no path-scoped unowned exception: in documentation such an exception is defeated in practice, because a pre-commit hook regenerates the root-level documentation-manifest.yaml on essentially every content change and the default * rule then pulls the PR back under code-owner review regardless. A repository with a generated root-level file cannot have a meaningful unowned subtree unless that file is unowned too.
Branch-protection rulesetGitHub configOne ruleset per repo, amending the existing one (operations 7127991, common-module 3829305, infrastructure 3314240): pull_request (thread resolution, count 0, require_code_owner_review: true), required_status_checks with strict_required_status_checks_policy: false, merge_queue with grouping_strategy: ALLGREEN, deletion, non_fast_forward; bypass arda-changelog-bot at always. Existing human bypass is removed — operations currently carries OrganizationAdmin and RepositoryRole 5. Applied after the change set merges, per REQ-ROLL-005.
CHANGELOG.mdheader proseUpdate the header block in all three: the categories list stays; the feature-branch paragraph is rewritten for the frontmatter marker (DQ-004). Note the asymmetry — operations and infrastructure rewrite an existing paragraph, common-module gains one it never had.
CLAUDE.md, knowledge-base/docsRecord the new convention per repository, following documentation/knowledge-base/pr-body-changelog.md.
FileConstructContent
current-system/oam/configuration/deployment/queued-cicd.mdspecDone. Restructured around the two variants — who owns the release, rather than who runs first, since both variants assemble before they build. Adds per-repository rows for the three new adopters, the CODEOWNERS-fails-open measurement, and a section on cicd-testbed as the reference implementation.
process/craft/deployment-and-release/backend-pr-process.mdhow-toDone. The backend counterpart to frontend-pr-process.md: the two authoring routes, the merge queue, feature builds, and the symptom-to-remedy table.
  • Tiered fast/queue gates. Revisit when: queue latency causes entries to drop on check_response_timeout_minutes, or PR-time feedback becomes the bottleneck. See DQ-003.
  • PDEV-694 external queueing for assembly. The GHA concurrency group stays as-is. Revisit when: ordered assembly demonstrably fails under the increased repository count.
  • arda-frontend-app bypass-list normalization (PDEV-474). Independent of this project, though it shares the App. Revisit when: that ticket is scheduled.
  • workspace/instructions/claude/rules/changelog.md. The rule enumerates which repositories are on PR-body mode and must gain these three — but no workspace worktree exists for this project. Revisit when: the first repository cuts over; the rule is wrong from that moment until updated.
  • Retiring the manual-changelog escape hatch. Kept, but as an emergency hatch reserved for exceptional circumstances rather than a supported workflow (§11). Revisit when: usage rises above the occasional — that would indicate a gap in the assembler being routed around rather than fixed.
  • accounts-component. Explicitly excluded (Miguel, 2026-08-04). It shares the Gradle pipeline and the same changelog shape, so it will keep building through the updated shared actions without adopting the model — which is what makes the unmigrated-repo canary a permanent obligation rather than a transitional one: the shared actions must stay backward compatible indefinitely, not just for the length of the rollout. Revisit when: someone chooses to migrate it.

The requirements are the authoritative record of what is settled and what is not; this section lists only what still blocks design or implementation.

  • infrastructure queue gate composition. The synth matrix fans out per CDK app. Re-running all of it per batch may exceed any workable check_response_timeout_minutes; all-synth-results is the natural aggregating required check, but whether the whole fan-out belongs in the queue is unresolved. Owner: Miguel. Blocking: PDEV-1412’s ruleset change.

  • Queue parameters per repository. max_entries_to_build, max_entries_to_merge, check_response_timeout_minutes. documentation uses 2/3/15; operations’ build is far slower. To be set with evidence from the pilot. Owner: Miguel. Blocking: nothing — defaults can be tightened later.

  • Whether amending an entry should re-run the gate. merge-eligibility triggers on pushes and label changes, so a ## CHANGELOG corrected in the pull-request body — or supplied in a comment, which §9.1 offers as the amendment route — leaves the check red until it is re-run by hand. The reference implementations add pull_request: [edited] and issue_comment and do not have this gap.

    The fix is not a free two-line addition, which is why it is a question rather than a defect. An issue_comment workflow runs from the default branch, so the checked-out tree is main rather than the branch under test — and changelog-entry.sh reads a file-route entry from the working tree. On that event it would find no .changelog/ file and silently fall back to the body route, reporting success on a pull request that is actually carrying both. Closing the gap properly means resolving the entry through the API rather than the checkout, which is a real change to the shared script.

    Meanwhile gh run rerun --failed is sufficient and is documented in the contributor how-to; the gate reads the body at run time, so a re-run sees the correction. Owner: Miguel. Blocking: nothing.

Three platform behaviors this design depends on are documented in principle but untested here. They are exercised on a dedicated testbed repository rather than on the pilot: each concerns ruleset and queue semantics rather than the pilot’s build, and a testbed answers them with synthetic checks in minutes where operations costs 18–27 minutes per iteration.

  1. Ruleset composition. Answered 2026-08-06 — the structure works. Two pull_request rules from different rulesets both apply to main simultaneously and compose to the stricter, with bypass evaluated per ruleset. Measured on cicd-testbed: repos/.../rules/branches/main reports both rules, one with required_approving_review_count: 0 and the other adding require_code_owner_review: true, and the pull request was blocked by the second while the first required nothing.

    The same experiment turned up something the design had asserted but never tested, and which turned out to be worse than asserted — see REQ-REV-002. A CODEOWNERS file whose owner does not resolve is equivalent to no file at all: the identical pull request under the identical rulesets was mergeable with zero reviews while the owning team lacked repository access, and blocked once it was granted. The ruleset advertises require_code_owner_review: true in both states and nothing on the pull request distinguishes them. Hence the CODEOWNERS assertion in merge-eligibility.

  2. Bypass × merge queue. Answered 2026-08-06, and it is why REQ-REV-004 is withdrawn. A bypass actor can neither enqueue a pull request awaiting code-owner review — enqueuePullRequest evaluates the pull request’s state, not the caller’s privileges, with both bypass modes tried — nor merge it directly, because the merge-queue rule belongs to a ruleset it does not bypass. The waiver has no working path.

  3. Draft conversion while queued. Answered 2026-08-06 — it does not. A queued pull request converted to draft stayed at position 1, ran every check, and merged with isDraft: true, after which assembly cut a release from it. REQ-GATE-007 described behaviour GitHub does not have; merge-eligibility now asserts it, and a failing required check is what ejects the entry.

Resolved, retained for traceability:

  • CODEOWNERS ownership rules. Settled as DQ-006, amended 2026-08-06 to * @Arda-cards/engineering @systems-arda.

  • Feature-build signalling. Settled 2026-08-06 as frontmatter in the changelog file, with the suffix stated explicitly. The no-PR case dissolved: a marked branch carries its changelog file regardless of whether a PR exists.

  • merge_group ruleset probe. Answered 2026-08-04, amended and then measured 2026-08-06. The original reading needed two corrections. gh ruleset check also returned 0 rules apply for refs/heads/main, which had cast doubt on the probe itself; that was an artifact of a local token, not of the probe — common-module runs it in CI on every merge and has published every release since v14.0.0 through it. And the feature verdict is not harmless: clq-extract takes its mode from target, so a queued build would validate the changelog in feature mode at the last checkpoint before merge. define-target is therefore changed as well, not just the event case — see Shared actions.

    Measured directly on cicd-testbed (run 31132061181). On a merge_group event github.base_ref is empty, github.ref_name is the queue branch, and github.event.merge_group.base_ref carries refs/heads/main — a full ref, so it needs stripping before the probe. The same run reports 5 rules apply to branch main against 0 rules apply for the queue ref, which is what makes the unfixed expression conclude feature. On a pull_request event both merge_group fields are empty while github.base_ref is main, so the new expression falls through to the term it used before and that path is unchanged by construction.


  • Requirements — the authoritative record of what this design must satisfy, and of what remains open.
  • Decision Log — settled decisions for this design.
  • Goal — project goal this design serves.
  • Queued CI/CD — canonical specification of the model being adopted.
  • Phase 2 — Queued PRs and Tiered Gates — the arda-frontend-app rollout and its tiered-gate design.
  • Backend PR Process — the contributor how-to for the repositories this design covers.
  • Frontend PR Process — its counterpart for arda-frontend-app.
  • Arda-cards/cicd-testbed — the reference implementation, and the findings table recording what each platform experiment established.
  • Viewpoint Mapping — source-to-artifact-to-runtime cross-reference used in §1.
  • documentation/knowledge-base/pr-body-changelog.md (repo-local) — authoring rules and the three assembly modes.
  • documentation/knowledge-base/arda-changelog-bot.md (repo-local) — App identity, secrets, rotation, and the three-step repository-extension procedure.


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