Requirements: Queued CI/CD Adoption
The requirements the design implements, extracted from it so they can be reviewed on their own terms. Each carries a Source pointing at the design section or decision that satisfies it, and a Status.
Two status values are in use:
- Settled — agreed, and the design implements it.
- Open — genuinely undecided. These are where a review discussion should land; the design either does not satisfy them yet or satisfies them in a way that is still contested.
Requirements marked Open are not gaps in the writing. They are the live questions.
Provenance. Requirements sourced to Review with Denis Antonioli come from an in-person design review recorded on 2026-08-06, not from the decision log. Several of them supersede earlier positions; where that happens the requirement says so. IDs are permanent — a withdrawn requirement is marked Withdrawn rather than deleted or renumbered.
Summary
Section titled “Summary”| # | Requirement | Source | Status |
|---|---|---|---|
| REQ-AUTH-001 | An entry can be recorded without editing CHANGELOG.md | DQ-001 | Settled |
| REQ-AUTH-002 | Concurrent PRs never conflict on changelog content | Design §3 | Settled |
| REQ-AUTH-003 | The entry can be written in-repo as work proceeds | Review with Denis Antonioli | Settled |
| REQ-AUTH-004 | An entry can be amended after the PR is open | Review with Denis Antonioli | Settled |
| REQ-AUTH-005 | Hand-editing CHANGELOG.md is exceptional, not a workflow | Design §11 | Settled |
| REQ-AUTH-006 | The PR-changelog file: one per PR, author-named, not kept on main | Review with Denis Antonioli | Settled |
| REQ-AUTH-007 | Exactly one of the two routes — both or neither is rejected | Review with Denis Antonioli | Settled |
| REQ-GATE-001 | PRs editing CHANGELOG.md are rejected | Design §9.1 | Settled |
| REQ-GATE-002 | PRs without exactly one valid entry are rejected | Review with Denis Antonioli | Settled |
| REQ-GATE-003 | REVIEW-REQUIRED forces a human approval | Design §9.1 | Withdrawn — redundant once every path is owned |
| REQ-GATE-004 | Filename collisions warn early rather than failing in the queue | Review with Denis Antonioli | Settled |
| REQ-GATE-005 | The queue never merges a draft pull request | Review with Denis Antonioli | Settled |
| REQ-GATE-006 | A required gate never reports success without evaluating | Review with Denis Antonioli | Settled |
| REQ-GATE-007 | A pull request returned to draft leaves the queue | Review with Denis Antonioli | Settled |
| REQ-QUEUE-001 | Independent PRs merge in one ALLGREEN batch | Design §9.2 | Settled |
| REQ-QUEUE-002 | The batch head is built before it merges | DQ-003 | Settled |
| REQ-QUEUE-003 | The queue publishes nothing | Design §9.2 | Settled |
| REQ-ASM-001 | Assembly writes the release block and consumes the entry | Review with Denis Antonioli | Settled |
| REQ-ASM-002 | The version is computed from categories | Review with Denis Antonioli | Settled |
| REQ-ASM-003 | Assembly pushes with no human bypass | Design §8 | Settled |
| REQ-ASM-004 | One release per assembly, covering every pending merge | Review with Denis Antonioli | Settled |
| REQ-ASM-005 | Assembly is self-healing — a failed run cannot lose an entry | Review with Denis Antonioli | Settled |
| REQ-PUB-001 | Publishing fires on the assembly commit only | DQ-002 | Settled |
| REQ-PUB-002 | Artifact coordinates and Release shape are unchanged | Design §5 | Settled |
| REQ-PUB-003 | Deploy chains fire once per release, in order | Review with Denis Antonioli | Settled |
| REQ-PUB-004 | Release granularity is the assembly, not the PR | Review with Denis Antonioli | Settled |
| REQ-PUB-005 | Deploys to one environment are serialised | Incident 2026-08-06 | Settled |
| REQ-PUB-006 | An environment is never silently downgraded | Incident 2026-08-06 | Settled |
| REQ-FEAT-001 | Feature builds apply to operations and common-module only | Review with Denis Antonioli | Settled |
| REQ-FEAT-002 | Feature builds leave no residue on main | DQ-004 | Settled |
| REQ-FEAT-003 | A frontmatter marker in the changelog file signals a feature build | Review with Denis Antonioli | Settled |
| REQ-FEAT-004 | A marked branch cannot merge while it is marked | Review with Denis Antonioli | Settled |
| REQ-FEAT-005 | Every push to a marked branch builds and publishes | Review with Denis Antonioli | Settled |
| REQ-FEAT-006 | A marked branch requires a changelog file | Review with Denis Antonioli | Settled |
| REQ-FEAT-007 | The feature version is a prerelease of the eventual version | Review with Denis Antonioli | Settled |
| REQ-FEAT-008 | Feature deployment reaches dev and no further, by construction | Review with Denis Antonioli | Settled |
| REQ-REV-001 | Review is ownership-based; * @Arda-cards/engineering @systems-arda | Review with Denis Antonioli | Settled |
| REQ-REV-002 | Review strength never transiently drops to zero | DQ-006 | Settled |
| REQ-REV-003 | systems-arda is human-only; no automated process uses it | Review with Denis Antonioli | Settled |
| REQ-REV-004 | Review with Denis Antonioli | Withdrawn — unbuildable, measured 2026-08-06 | |
| REQ-ROLL-001 | operations is the pilot; cutover order follows from it | Review with Denis Antonioli | Settled |
| REQ-ROLL-003 | Production authorisation is preserved throughout the migration | Review with Denis Antonioli | Settled |
| REQ-ROLL-004 | Shared-action changes reach unmigrated repositories only by deliberate act | Verification 2026-08-06 | Settled |
| REQ-ROLL-005 | A check becomes required only after it exists on main | Testbed incident 2026-08-06 | Settled |
| REQ-ROLL-002 | Unmigrated repositories keep building | Design §12 | Settled |
| REQ-DOC-001 | The shared model documents the new variant | Design §12 | Settled |
| REQ-DOC-002 | Agent rules track which repos use which model | Design §12 | Open |
Authoring
Section titled “Authoring”REQ-AUTH-001: Changelog entry without editing CHANGELOG.md
Section titled “REQ-AUTH-001: Changelog entry without editing CHANGELOG.md”A developer shall be able to record a changelog entry for a change without editing CHANGELOG.md in the pull request. CHANGELOG.md shall have exactly one writer — the post-merge assembly workflow.
Source: DQ-001 · Status: Settled
REQ-AUTH-002: Concurrent PRs do not conflict on changelog content
Section titled “REQ-AUTH-002: Concurrent PRs do not conflict on changelog content”Two pull requests open at the same time shall not conflict with one another on changelog content, and shall be mergeable in either order without either being rebased for that reason.
This is the requirement that motivates the project. Over the 90 days to 2026-08-04, CHANGELOG.md was touched by 50 of 50 first-parent commits on operations main and 29 of 29 on common-module — so today every pair of concurrent PRs conflicts by construction.
The two routes achieve this differently, and the difference is worth stating plainly. The PR-body route is conflict-free by construction — nothing in the repository is written. The file route is conflict-free by convention: author-chosen names (REQ-AUTH-006) are unique in practice but not by construction, so a residual collision case survives. It is bounded — REQ-GATE-004 warns early, and the merge queue fails loudly rather than silently — but this requirement is met absolutely only on the body route.
Source: Design §3, Review with Denis Antonioli · Status: Settled
REQ-AUTH-003: In-repo authoring as work proceeds
Section titled “REQ-AUTH-003: In-repo authoring as work proceeds”A developer shall be able to write the changelog entry in the repository, in their editor, as the work proceeds, and have it reviewed as part of the code diff — rather than composing it in a pull-request description after the fact.
This is satisfied by the PR-changelog file route of REQ-AUTH-006. Both routes remain available and the author chooses per pull request (REQ-AUTH-007); neither is privileged.
Raised in review of PR #157, where the original design routed every entry through the PR body — a trade of authoring experience for merge concurrency that had not been explicitly weighed. The file route closes the gap without reopening the conflict class.
Source: Review with Denis Antonioli · Status: Settled
REQ-AUTH-004: Amendment after the PR is open
Section titled “REQ-AUTH-004: Amendment after the PR is open”A developer shall be able to correct or extend a changelog entry after the pull request is open, without force-pushing a commit solely to change the entry.
The mechanism follows the route in use:
- PR-body route — post a comment containing a replacement entry. The last one found wins.
- File route — edit the file on the branch. This is an ordinary commit, reviewed like any other change.
A comment carrying a changelog entry on a pull request that also has a PR-changelog file trips the both-present rejection of REQ-AUTH-007. Amending a file-route pull request is done by editing the file, not by commenting.
Source: Review with Denis Antonioli · Status: Settled
REQ-AUTH-005: Hand-editing is exceptional
Section titled “REQ-AUTH-005: Hand-editing is exceptional”An escape hatch shall exist for the rare change that genuinely needs to write CHANGELOG.md directly — correcting a historical entry, or a release whose notes the assembler cannot produce. It shall be reserved for exceptional circumstances and shall not become a routine authoring route. A rising usage rate shall be read as a gap in the assembler rather than as a reason to keep using the hatch.
Source: Design §11 · Status: Settled
REQ-AUTH-006: The PR-changelog file
Section titled “REQ-AUTH-006: The PR-changelog file”A pull request may carry at most one PR-changelog file, in a designated directory, holding that pull request’s changelog entry.
- The author names it. Names are chosen by the author, not derived. Engineers are expected to adopt a personal prefix — their GitHub username or similar — so that names are unique in practice.
- It never reaches the tip of
main. The file is merged along with the branch and is therefore present inmain’s history at the merge commit; it is removed by the assembly commit, in the same commit that writes the release block (REQ-ASM-001). “Never onmain” means the tip, evaluated at the moment theCHANGELOG.mdof record is written. - Its content is an entry, not a release block. Categories and bullets, with no
## [x.y.z]heading — the version is computed, never authored (REQ-ASM-002). The entry may be preceded by optional YAML frontmatter carrying directives; the only directive currently defined is the feature-build marker of REQ-FEAT-003. - Frontmatter never reaches
CHANGELOG.md. Assembly strips it before composing the release block. This needs an explicit test rather than care: frontmatter is delimited by---, which is also a Markdown horizontal rule, and this system has already leaked an internal delimiter into a published tagged release (see An internal delimiter leaks into published output).
The directory is .changelog/, a fixed and discoverable path so the gates can scope to it.
Point 2 reverses the earlier position that assembly never deletes files. That position was reasoned for an accumulate-forever fragment directory; a file that must not survive on main requires deletion. The reversal is deliberate and comes from the in-person review.
Source: Review with Denis Antonioli · Status: Settled
REQ-AUTH-007: Exactly one route
Section titled “REQ-AUTH-007: Exactly one route”A pull request shall carry its changelog entry by exactly one of the two routes — the PR body or an author/assignee comment, or a PR-changelog file.
- Both present — the pull request is rejected. There is no precedence rule and no silent winner; ambiguity about which entry is authoritative is treated as an error.
- Neither present — the pull request is rejected.
Comments count when written by the pull request’s author or an assignee. Comments from anyone else are ignored, so that a reviewer quoting an entry cannot alter or reject the pull request.
This widens the existing implementation, which reads author comments only. It is a change to the assembly and gate code rather than a configuration change, and it does not affect approval requirements.
Source: Review with Denis Antonioli · Status: Settled
REQ-GATE-001: Reject CHANGELOG.md edits
Section titled “REQ-GATE-001: Reject CHANGELOG.md edits”A pull request whose diff modifies CHANGELOG.md shall fail a required status check with an explanatory error, unless it carries the exceptional-use label of REQ-AUTH-005.
Source: Design §9.1 · Status: Settled
REQ-GATE-002: Require a valid entry
Section titled “REQ-GATE-002: Require a valid entry”A pull request shall fail a required status check unless it carries a changelog entry by exactly one of the two routes (REQ-AUTH-007), with at least one bullet under at least one category valid for the repository as defined by its .github/clq/changemap.json.
Validity is judged on the categories and on the composed result: the entry itself is not a complete changelog and cannot be validated by clq standalone, because it carries no version heading. The gate checks the categories; clq validates the assembled CHANGELOG.md after the release block is written.
The four changemap files across documentation, operations, common-module, and infrastructure are already identical, so category semantics are uniform.
Source: Review with Denis Antonioli · Status: Settled
REQ-GATE-003: Label-gated human review — WITHDRAWN
Section titled “REQ-GATE-003: Label-gated human review — WITHDRAWN”Withdrawn 2026-08-06, as a consequence of withdrawing REQ-REV-004.
The requirement was that a designated label block a pull request until a human — non-bot — approving review exists. It is inherited from documentation, where it earns its place because that repository’s CODEOWNERS deliberately leaves a subtree unowned, so some pull requests genuinely need no review and an author may want to demand one.
These repositories own every path (REQ-REV-001), so every pull request already requires a code-owner approval, and a code owner is by definition human — CODEOWNERS does not accept Apps. The label would demand a human where a human is already demanded. Retaining it would mean maintaining a gate that can never change an outcome, which is worse than not having it: a check nobody has seen fail is a check nobody trusts.
It becomes worth reinstating the moment any of these repositories introduces an unowned path.
Source: Design §9.1 · Status: Withdrawn
REQ-GATE-004: Early warning on filename collision
Section titled “REQ-GATE-004: Early warning on filename collision”Where two open pull requests carry PR-changelog files at the same path, this should be surfaced on the pull request rather than discovered in the merge queue.
Author-chosen names make collisions possible. They are not a correctness risk: two pull requests adding the same path produce an add/add conflict, so the merge queue cannot form a batch containing both and the condition fails loudly rather than silently picking a winner. The gate exists to convert a confusing late failure into a clear early one.
The check is inherently advisory — nothing re-runs an open pull request’s checks when a later pull request claims the same name, so a stale pass is possible. The merge queue remains the authoritative backstop.
Source: Review with Denis Antonioli · Status: Settled
REQ-GATE-005: The queue never merges a draft pull request
Section titled “REQ-GATE-005: The queue never merges a draft pull request”A draft pull request shall not be merged and shall not enter the merge queue.
This is a platform guarantee rather than something to build. GitHub’s documentation states that “No one can merge the pull request until you mark the pull request as ready for review again”, and queue entry is a merge action. It is recorded as an invariant because it is an assumption the rest of the model rests on, and the way it would be broken is by a future configuration change that nobody recognised as load-bearing.
Recorded belt-and-braces; no incident prompted it.
Source: Review with Denis Antonioli · Status: Settled
REQ-GATE-006: A required gate never reports success without evaluating
Section titled “REQ-GATE-006: A required gate never reports success without evaluating”No required status check shall be conditioned such that it reports success without having actually evaluated the pull request.
GitHub treats a job skipped by a job-level if as passing: the check “will report its status as ‘Success’” and “will not prevent a pull request from merging, even if it is a required check.” A gate carrying a draft condition therefore reports green on a draft while having examined nothing.
That matters most where the merge queue does not re-evaluate the gate. merge-eligibility auto-passes on merge_group by design, so pull-request time is its only real evaluation. If it is skipped-as-success on a draft, and the pull request is enqueued in the window between being marked ready and the re-triggered runs completing — required-check evaluation reads the latest run per name, which is still the skipped one — then the changelog requirement is never evaluated at all, and the failure surfaces post-merge as an assembly that cannot find an entry.
The consequent rule:
- Cheap gates are not draft-gated.
merge-eligibilityandreview-required-gaterun from the moment a pull request exists. This is better for the author as well — a missing changelog entry is reported while the work is in progress rather than at the end. - The expensive build may remain draft-gated. Skipped-as-success is harmless there, because the merge queue re-runs the build on the batch head (REQ-QUEUE-002).
The general test: a gate may be draft-gated only if the merge queue independently re-evaluates it.
Scope note. This requirement covers the three repositories in this project. The same exposure exists in documentation and arda-frontend-app today — merge-eligibility is draft-gated in both — and is a pre-existing defect rather than one this project introduces. Fixing it there needs its own ticket; see Out of scope.
Source: Review with Denis Antonioli · Status: Settled
REQ-GATE-007: A pull request returned to draft leaves the queue
Section titled “REQ-GATE-007: A pull request returned to draft leaves the queue”A pull request converted back to draft while queued shall not merge.
GitHub’s merge-queue documentation does not state what happens in this case, and the behavior is not assumed here. Verification is a named pilot task: convert a queued pull request back to draft on common-module and observe whether the queue ejects it. If the platform does not eject it, this requirement needs a mitigation and should be reopened.
Source: Review with Denis Antonioli · Status: Settled — behavior unverified, pilot task assigned.
Merge queue
Section titled “Merge queue”REQ-QUEUE-001: ALLGREEN batching
Section titled “REQ-QUEUE-001: ALLGREEN batching”Two or more independent pull requests, each with passing checks, shall be capable of merging together in a single batch.
Source: Design §9.2 · Status: Settled
REQ-QUEUE-002: The batch is built before it merges
Section titled “REQ-QUEUE-002: The batch is built before it merges”The combined state of a batch shall be built before the batch merges, so that two changes which each build in isolation but not together are caught before reaching main.
Source: DQ-003 · Status: Settled
REQ-QUEUE-003: The queue never publishes
Section titled “REQ-QUEUE-003: The queue never publishes”No build triggered by the merge queue shall publish an artifact, create a tag, or create a release.
Source: Design §9.2 · Status: Settled
Assembly
Section titled “Assembly”REQ-ASM-001: What assembly writes
Section titled “REQ-ASM-001: What assembly writes”The assembly workflow shall, in a single commit, prepend a release block to CHANGELOG.md and remove the PR-changelog files it consumed. It shall do nothing else: version derivation for the build, tagging, artifact publication, and GitHub Release creation remain with the existing build pipeline.
Removing consumed files is what keeps REQ-AUTH-006 true. It is the one respect in which assembly touches a path other than CHANGELOG.md.
Source: DQ-001, Review with Denis Antonioli · Status: Settled
REQ-ASM-002: The version is computed, not chosen
Section titled “REQ-ASM-002: The version is computed, not chosen”The release version shall be computed mechanically from the categories present across all entries the assembly covers — highest impact wins — applied to the version in the current top release heading. No human shall choose a version number in the ordinary path.
Where an assembly covers several pull requests, their entries are merged per category: one heading per category, carrying the bullets from every contributing pull request.
Source: Review with Denis Antonioli · Status: Settled
REQ-ASM-003: No human bypass
Section titled “REQ-ASM-003: No human bypass”No human account shall hold ruleset bypass. Bypass shall be held only by purpose-scoped GitHub Apps, each granted the narrowest bypass_mode that lets it do its job.
Assembly pushes to the protected branch as arda-changelog-bot, which needs always mode because it pushes a commit directly rather than through a pull request.
An earlier form of this requirement was relaxed to accommodate a second bypass App for REQ-REV-004. With that requirement withdrawn, sole bypass actor is accurate again: arda-changelog-bot is the only actor bypassing the only ruleset.
Source: Design §8, Review with Denis Antonioli · Status: Settled
REQ-ASM-004: One release per assembly, covering every pending merge
Section titled “REQ-ASM-004: One release per assembly, covering every pending merge”An assembly shall cover every merge commit landed since the last release tag, and shall produce exactly one release block, one version, one tag, and one GitHub Release for that set.
The unit of work is the range, not the merge commit that triggered the run. This is what makes “one version per successful queue run” achievable: whether the queue lands three pull requests as one batch or as three merges seconds apart, the assembly that runs covers all of them.
The boundary is the tag, not the previous assembly commit — a correction made 2026-08-06 after the first working implementation failed on it. In steady state the two readings agree, because assembly is what creates the tag. They diverge exactly once, at adoption: a repository migrating to this model has no assembly commit, so that reading makes the first range the entire history, and assembly tries to collect entries from merges that predate the gate requiring them. It fails, and every subsequent run fails the same way. The last hand-made release tag is the right boundary on the first run and stays right afterwards. All three migrating repositories already tag their releases, so none needs preparing.
This supersedes the earlier requirement that each merge produce its own release block in landing order. That formulation assumed one assembly per merge commit, which is the arrangement that produced the failure recorded under REQ-ASM-005.
Source: Review with Denis Antonioli · Status: Settled
REQ-ASM-005: Assembly is self-healing
Section titled “REQ-ASM-005: Assembly is self-healing”A failed assembly shall not be able to lose a changelog entry. Because each assembly covers everything since the last release tag, a run that fails leaves its work to the next one; no entry depends on any single run succeeding.
It shall also remain true that a failed assembly publishes and tags nothing, and that re-running it requires no revert and no hand-edit.
This requirement exists because the failure is not hypothetical. On 2026-07-23 in documentation, pull requests #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 no one noticed. The cause is the race that PDEV-694 describes: concurrent runs each compute a version against a main that moves underneath them.
Range-based assembly removes the race rather than serializing around it, and subsumes PDEV-694 for these repositories.
Source: Review with Denis Antonioli · Status: Settled
Publishing
Section titled “Publishing”REQ-PUB-001: Publishing fires on the assembly commit
Section titled “REQ-PUB-001: Publishing fires on the assembly commit”An artifact shall be published only from a commit whose CHANGELOG.md already carries the release block for that version. A merge commit shall publish nothing.
Source: DQ-002 · Status: Settled
REQ-PUB-002: Artifact shape is unchanged
Section titled “REQ-PUB-002: Artifact shape is unchanged”The version, git tag, Docker image tag, Helm chart version, Maven coordinates, and GitHub Release title and body produced by a release shall be indistinguishable from those produced before the migration. Verification is a field-by-field comparison of the last pre-cutover release against the first post-cutover one.
Source: Design §5, §10.4 · Status: Settled
REQ-PUB-003: Deploy chains are unchanged
Section titled “REQ-PUB-003: Deploy chains are unchanged”Each repository’s environment deploy chain shall fire exactly once per release, in its existing order — dev → stage → demo → prod for operations, and the four-partition sequence for infrastructure.
“Per release” means per assembly, not per pull request (REQ-PUB-004). The order and gating of the chain are unchanged; only how often it runs changes.
Source: Review with Denis Antonioli · Status: Settled
REQ-PUB-004: Release granularity is the assembly, not the PR
Section titled “REQ-PUB-004: Release granularity is the assembly, not the PR”A release may cover more than one pull request. This is an accepted change in operational granularity, ruled on explicitly rather than inherited.
What improves: fewer production deploys, and the deploy chain runs once per assembly rather than once per merge.
What worsens: a release bundles N changes, so rolling back reverts all of them, and release-to-pull-request traceability becomes one-to-many. The release notes name every contributing pull request, so the mapping stays recoverable even though it is no longer one-to-one.
This is not a weakening of review or approval rigor — nothing about what must be approved changes. It is a coarsening of the release unit.
Source: Review with Denis Antonioli · Status: Settled
REQ-PUB-005: Deploys to one environment are serialised
Section titled “REQ-PUB-005: Deploys to one environment are serialised”Two workflow runs shall not deploy to the same environment concurrently. Serialisation shall cover the automatic chain and the dispatch path alike, since a manual restore racing an automatic deploy is the same collision.
This is a prerequisite of the queue rather than an improvement alongside it. max-parallel: 1 orders dev → stage → demo → prod within one run; nothing orders anything across runs, and environment: gates approval rather than concurrency. Today merges are spaced far enough apart that overlap is an accident. The queue’s whole purpose is to make back-to-back merges normal, so overlap becomes the expected case — the hazard is pre-existing, and what the queue changes is its rate.
Demonstrated 2026-08-06, not theorised. A feature build and a release build reached dev-operations at the same time and the release deploy died with UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress. That failure took the rest of the chain with it, leaving operations 7.0.1 published but deployed nowhere. The collision was induced by a pilot experiment running alongside a merge, but the mechanism belongs to the repository.
The minimum satisfying change is a concurrency group on the deploy job keyed by component and purpose — so dev does not block stage, and other components do not block operations — with cancel-in-progress: false, because a half-applied helm upgrade must never be cancelled. GitHub keeps at most one pending run per group and cancels an older pending one when a newer arrives, so overlapping deploys resolve to latest-wins.
What that does not fix: an older version whose build finished late, with no overlap at all, still lands last and silently downgrades the environment. Guarding against that needs a monotonicity check with force semantics, because deploy.yaml exists precisely to roll an environment back. That is deliberately out of scope here — it protects against a failure mode that predates the queue and outlives it, and its design turns on what a deploy is allowed to do.
Scope note: reusable_deployment.yaml exists only in operations and accounts. accounts is excluded from this project by instruction and so keeps the hazard — an exclusion worth restating, because “leave a solid pipeline behind” and “do not touch accounts” cannot both hold for that repository. bastion, pdf-render and qr-lookup use deployment_requests_approval.yaml, a different mechanism not examined here. common-module publishes a library and never deploys.
Source: Incident 2026-08-06 · Status: Settled
REQ-PUB-006: An environment is never silently downgraded
Section titled “REQ-PUB-006: An environment is never silently downgraded”A deploy shall not replace a deployed version with an older one unless a human asks for it explicitly. Where it happens by accident the deploy shall fail rather than proceed.
REQ-PUB-005 closes the case where two deploys overlap. It does not close the case where they do not: version N and N+1 both build, N+1 lands first because its build was faster, and N lands afterwards — leaving the environment on the older version with both runs green and nothing reporting it. Build duration varies by a factor of five on this repository (REQ-ROLL-001), so the overtaking is not hypothetical.
The satisfying change is a precondition on the deploy: compare the chart version already released in the namespace against the incoming one and refuse to go backwards. It requires a force input, because deploy.yaml exists precisely to put an environment back on an earlier version and that must stay possible — the requirement is that a downgrade be deliberate, not that it be impossible. The dispatch path sets force; the automatic chain never does.
Rolling this into the project rather than deferring it is a deliberate choice (Miguel, 2026-08-06): the pipeline should be sound when the project closes rather than sound apart from a known silent-corruption path.
Source: Incident 2026-08-06 · Status: Settled
Feature builds
Section titled “Feature builds”Terminology. In this section a marked branch is one carrying the feature-build marker of REQ-FEAT-003. 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, including the ones that become ordinary pull requests. The two must not be conflated: every branch is a feature branch to the action; only a marked branch publishes and is barred from merging.
REQ-FEAT-001: Where feature builds apply
Section titled “REQ-FEAT-001: Where feature builds apply”Feature builds shall be supported in operations and common-module. infrastructure is excluded and retains its current behavior.
For common-module the capability exists today only as an undocumented side effect of the shared action being generic; it becomes explicit and supported.
For infrastructure the exclusion is a description of reality rather than a restriction. Verified 2026-08-06: an unprotected branch there runs build, the full per-app synth matrix, synth-corporate, and validate-release; qualify-build-action may classify the build as kind = publish, but nothing consumes that classification, because publish is gated on trigger == 'push_to_release_branch' and deploy needs publish. There is no registry artifact for infrastructure to publish — the meaningful pre-merge signal is the synth, which already runs on every push.
Source: Review with Denis Antonioli · Status: Settled
REQ-FEAT-002: Feature builds leave no residue on main
Section titled “REQ-FEAT-002: Feature builds leave no residue on main”Requesting a feature build shall not require any edit that must be reversed before merging, and nothing about the request shall reach CHANGELOG.md.
This is satisfied by construction rather than by discipline: the marker lives in the changelog file, a marked branch cannot merge (REQ-FEAT-004), and the changelog file is consumed and removed by assembly (REQ-ASM-001). There is no state that can survive onto main.
Source: DQ-004 · Status: Settled
REQ-FEAT-003: Feature-build signalling
Section titled “REQ-FEAT-003: Feature-build signalling”A branch shall be marked for feature builds by YAML frontmatter in its changelog file, carrying the version suffix explicitly:
---feature-build: jmpicnic-1408---
### Fixed
- ...The suffix is stated rather than derived. qualify-build-action’s feature-version regex requires two alphanumeric segments after the version, so the suffix is <user>-<ticket> shaped; deriving it from the filename would couple two independent conventions.
Marking and unmarking are ordinary edits to a file already on the branch, so both work at branch creation or at any later time, and both work whether or not a pull request exists. Unmarking is removing the frontmatter.
A separate marker file was considered and rejected. Once a marked branch is required to carry a changelog file (REQ-FEAT-006), a second file is redundant state that can disagree with the first, and it admits a reachable condition where the marker exists and the changelog does not. The cost of frontmatter is that merge-eligibility is no longer visible from the file list — a reader must look inside the file — which the required check of REQ-FEAT-004 reports.
Source: Review with Denis Antonioli · Status: Settled
REQ-FEAT-004: A marked branch cannot merge
Section titled “REQ-FEAT-004: A marked branch cannot merge”While a branch carries the feature-build marker, its pull request shall be ineligible to merge, enforced by a required check rather than by convention.
Unmarking makes it mergeable. This replaces today’s instruction in the changelog headers — “This must, of course, be reversed before merging to main” — with an enforced condition, removing the class of mistake where a suffixed version reaches main.
The prohibition is on the marked state, not on the branch permanently. A branch is expected to be unmarked and then merged in the ordinary way.
Source: Review with Denis Antonioli · Status: Settled
REQ-FEAT-005: Every push builds and publishes
Section titled “REQ-FEAT-005: Every push builds and publishes”Every push to a marked branch shall produce a build and a publish, whether or not a pull request is associated with the branch.
This is why the marker cannot be a pull-request label and why it must be readable from the checkout alone.
Source: Review with Denis Antonioli · Status: Settled
REQ-FEAT-006: A marked branch requires a changelog file
Section titled “REQ-FEAT-006: A marked branch requires a changelog file”A marked branch shall carry its changelog entry as a file. The pull-request-body route is not available to it, because a marked branch need not have a pull request at all.
This also gives REQ-FEAT-007 the input it needs: the categories that determine the version are on the branch, readable at push time.
Source: Review with Denis Antonioli · Status: Settled
REQ-FEAT-007: The feature version is a prerelease of the version the change will become
Section titled “REQ-FEAT-007: The feature version is a prerelease of the version the change will become”The published feature version shall be the version the change would receive if it merged, suffixed with the marker and the run identity.
The base is computed from the changelog file’s categories against the current CHANGELOG.md head — the same computation REQ-ASM-002 performs at assembly. An Added entry on a branch off 6.14.0 publishes 6.15.0-jmpicnic-1408-<run>.
This ordering is correct under semver: 6.15.0-jmpicnic-1408 sorts before 6.15.0, so a feature build reads as a prerelease of the release it anticipates. Deriving the base from the current head instead would sort the build before the release it is already ahead of.
Source: Review with Denis Antonioli · Status: Settled
REQ-FEAT-008: Feature deployment reaches dev and no further, by construction
Section titled “REQ-FEAT-008: Feature deployment reaches dev and no further, by construction”In operations, a feature build shall deploy to dev and to no other environment. Reaching any further environment shall require a positive human action, such as an explicit workflow_dispatch.
This shall be structural, not procedural. The feature path’s deploy matrix contains dev alone; the other environments are not reachable from it. It shall not rely on environment approval gates, because those 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, which is precisely the failure this requirement exists to prevent. A relaxation that cannot reach feature branches is one that cannot be made accidentally.
Scoped to operations. common-module has no deploy chain, and infrastructure is excluded by REQ-FEAT-001.
This 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-branch publish therefore satisfies the deploy job’s only condition and fans out to dev, stage, demo, and prod. The path is dormant today only because feature publishing has produced three tags in the repository’s entire history; REQ-FEAT-005 would activate it on every push.
Source: Review with Denis Antonioli · Status: Settled
Review policy
Section titled “Review policy”REQ-REV-001: Ownership-based review
Section titled “REQ-REV-001: Ownership-based review”Review requirements shall be expressed as code ownership rather than as an approval count, so that the policy lives in a reviewed file in the repository rather than in ruleset configuration.
The CODEOWNERS file for all transitioned repositories shall initially be:
* @Arda-cards/engineering @systems-ardamatching documentation. required_approving_review_count is 0 and require_code_owner_review is true, so CODEOWNERS is the whole policy — with * owned, every pull request needs an approval from the engineering team or systems-arda.
Any user or team named as an owner must hold direct write access; subteam-inherited access does not satisfy CODEOWNERS. Verified 2026-08-06: @Arda-cards/engineering holds direct push on all three repositories, and systems-arda is a User account with admin on all three.
Note what this does not change: today’s required_approving_review_count: 1 already requires a second human, because GitHub does not permit a pull-request author to approve their own pull request. Confirmed empirically across the last 60 closed pull requests in each of documentation, operations, and common-module — zero were approved by their author. What changes is who may be that second human: from anyone with write access, to the engineering team or systems-arda. That narrowing is a strengthening. It also narrows the pool of possible second approvers, which is the cost now paid in full: REQ-REV-004 would have softened it and cannot be built.
Source: DQ-006, Review with Denis Antonioli · Status: Settled
REQ-REV-002: Review strength never drops to zero
Section titled “REQ-REV-002: Review strength never drops to zero”The migration shall not, at any point, leave a repository requiring no review. The CODEOWNERS file and the ruleset change shall land together — the ruleset change alone reduces the required approval count to zero with nothing to replace it.
Landing them together is necessary but not sufficient, which measurement on 2026-08-06 made plain. A CODEOWNERS file whose owner does not resolve is equivalent to no file at all, and the equivalence is silent. The same pull request, under the same rulesets, differing only in whether the owning team had repository access:
| CODEOWNERS | Outcome |
|---|---|
| unresolvable | CLEAN — mergeable with zero reviews |
| resolvable | BLOCKED, review requested |
GitHub reports the fault only through repos/{owner}/{repo}/codeowners/errors; nothing on the pull request distinguishes the two states, and the ruleset continues to advertise require_code_owner_review: true throughout. So review can be turned off by a team rename, a permission change, or a typo, with no signal.
A repository therefore shall carry a required check that fails when CODEOWNERS does not resolve. Verifying access once at cutover is not enough: the condition can become true long afterwards, and the whole point of required_approving_review_count: 0 is that CODEOWNERS is the only thing left holding the policy up.
Source: DQ-006, strengthened by measurement 2026-08-06 · Status: Settled
REQ-REV-003: systems-arda is a human escape hatch
Section titled “REQ-REV-003: systems-arda is a human escape hatch”systems-arda is an administrative escape hatch for DevOps engineers, subject to tight constraints on its use. No automated process shall use it.
This is recorded because the obvious implementation of a review waiver — a workflow submitting an approving review as systems-arda — is now explicitly forbidden. systems-arda is a User with admin on every repository and a listed code owner, so a credential for it would satisfy code-owner review on any pull request, with no audit trail distinguishing a genuine DevOps intervention from an automated convenience.
Source: Review with Denis Antonioli · Status: Settled
REQ-REV-004: Code-owner review waiver — WITHDRAWN
Section titled “REQ-REV-004: Code-owner review waiver — WITHDRAWN”Withdrawn 2026-08-06. The mechanism cannot be built, and the project no longer needs it (Miguel).
The requirement was that a code owner could waive the code-owner review requirement on a single pull request by an authenticated act recording a categorised reason. It was designed around two composing rulesets and a ReviewOverride App holding bypass on the review one. Measurement showed that arrangement has no working path:
| Attempt | Result |
|---|---|
enqueuePullRequest as a bypass actor | Refused — “Waiting on code owner review from Arda-cards/engineering”, with both pull_request and always bypass modes |
| Direct merge as the same actor | Refused — “Changes must be made through the merge queue” |
The first refusal is the fatal one: enqueuePullRequest evaluates the pull request’s own review state and ignores the caller’s privileges entirely. Bypass appears to apply at merge time, and under a merge queue nobody merges directly, so it never applies at all. A purpose-built App with pull_requests: write would meet the same refusal, because the refusal is not about permissions.
The second refusal is worth keeping in mind for any future design: it shows bypass is properly scoped per ruleset. The actor bypassing the review ruleset was still stopped by the merge-queue rule belonging to the other one. A waiver could never have skipped the queue — the guarantee held; it was the entry path that did not exist.
Withdrawing this removes, in one step: the ReviewOverride App and the browser-only manifest flow needed to create it, the two-ruleset split (whose only purpose was to give the bypass somewhere narrow to apply), the /waive-review workflow, and REQ-GATE-003’s review-required-gate, which demanded a human where code-owner review already demands one.
The accepted cost. An urgent fix now waits for a second human, with no in-band escape. That was the waiver’s entire justification. systems-arda remains what REQ-REV-003 says it is — an admin escape hatch with tight conditions, not a workflow.
Source: Review with Denis Antonioli; withdrawn on measurement 2026-08-06 · Status: Withdrawn
Rollout
Section titled “Rollout”REQ-ROLL-001: operations is the pilot; the cutover order follows from it
Section titled “REQ-ROLL-001: operations is the pilot; the cutover order follows from it”operations is the pilot repository. The order in which the three repositories then cut over is decided from what the pilot teaches, not fixed in advance.
This supersedes the original blast-radius ordering, which put common-module first on the grounds that a mistake there costs a version number rather than an outage (Miguel, 2026-08-06). Piloting on operations instead means the unverified platform behaviours are exercised on the repository whose publish feeds a four-environment deploy chain. That is deliberate: operations is where the cost is — 50 of 50 merges touching CHANGELOG.md — and validating the hardest case first makes the other two straightforward.
Two facts make it workable. operations’ build job runs anywhere from 4 to 22 minutes, and the spread is not explained by what the build produces. Measured 2026-08-06: five consecutive runs took 4.0, 4.0, 4.0, 4.8 and 5.5 minutes, and the last two of those published — so publishing costs roughly a minute, not sixteen. Earlier main runs took 19, 22, 19, 20 and 18 minutes. The difference tracks how much of the tree changed: the fast runs all carried near-trivial diffs against a warm cache, while the slow ones followed substantive Kotlin changes. That correlation is plausible but unconfirmed, so the driver should be treated as unknown.
The consequence for check_response_timeout_minutes is to size for the slow case. A queued batch follows substantive changes by definition — that is what people merge — so the 4-minute figure is the one that does not generalise. documentation’s 15 would not be safe here; something at or above 35 is. The pilot’s own queue runs should confirm it.
And REQ-ROLL-003 keeps production authorisation intact for the whole migration, which is what makes piloting on a production-bearing repository acceptable rather than reckless.
Source: DQ-005, superseded by review · Status: Settled
REQ-ROLL-003: Production authorisation is preserved throughout
Section titled “REQ-ROLL-003: Production authorisation is preserved throughout”No stage of the migration shall remove or weaken the human authorisation required to deploy to production. Where a stage introduces a new path that could reach production, that path shall be fail-closed — requiring positive human action to proceed — until the machinery is tested and verified.
The constraint is on the migration as a whole rather than on any one change, because the failure mode is cumulative: each stage individually looks safe, and authorisation is lost by omission across them. A dead-man switch is the acceptable transitional form; silently inheriting an existing approval gate is not, since that gate may be relaxed later for unrelated reasons (REQ-FEAT-008 makes the same argument for feature branches).
Source: Review with Denis Antonioli · Status: Settled
REQ-ROLL-004: Shared-action changes reach unmigrated repositories only by deliberate act
Section titled “REQ-ROLL-004: Shared-action changes reach unmigrated repositories only by deliberate act”No change to a shared build action shall reach a repository outside this project as a side effect of publishing it. Until the pilot has proven a change, it shall be exercised through a ref that only the pilot resolves — a work branch or an exact version — never through a moving major tag.
The obligation exists because the consumption graph makes the default the opposite. Six repositories consume gradle-build-pipeline-action; five of them (common-module, accounts, bastion, pdf-render, qr-lookup) float on @v1. infrastructure consumes qualify-build-action@v2 directly, and gradle-build-pipeline-action itself references qualify-build-action@v2 from inside its own composite — so every consumer picks up a qualify-build-action change the moment v2 moves, including operations, whose exact pin buys it no insulation at all.
The release machinery moves those tags automatically: denisa/semantic-tag-helper in mode: set creates the full version tag and force-moves every alias derived from it, so publishing v1.4.0 moves v1 in the same job. There is no input to suppress it. Two mechanisms satisfy this requirement without modifying that machinery. A work branch publishes nothing — both action repositories scope their only ruleset to ~DEFAULT_BRANCH, so a branch push skips validate-release and with it the tag job (verified on runs 28301808312 and 30806077429). A prerelease version publishes an installable tag whose aliases stop at the prerelease segment: v1.4.0-rc.1 moves v1.4.0-rc and leaves v1.4 and v1 untouched.
This is what makes REQ-ROLL-002 enforceable rather than aspirational. Backward compatibility is still required — the two are complementary, since the moment a floating tag does move, compatibility is the only thing protecting the other five repositories. accounts is the sharpest case: it is explicitly out of scope, yet moving v1 changes its build with no pull request and no review.
Source: Verification 2026-08-06 · Status: Settled
REQ-ROLL-005: A check becomes required only after it exists on main, and open pull requests are refreshed
Section titled “REQ-ROLL-005: A check becomes required only after it exists on main, and open pull requests are refreshed”A status check shall be added to a ruleset’s required list only once the workflow producing it is on the default branch. After the ruleset changes, every open pull request shall be updated from main before it can merge.
Both halves are necessary, and getting either wrong stops the repository rather than slowing it. A required check that no branch can produce never reports, and a check that never reports is indistinguishable from one still running: the pull request waits forever. The fix needs org-admin access to the ruleset, so it is not something the blocked engineers can resolve themselves.
Demonstrated 2026-08-06 on the testbed, by doing it wrong. Two gates were made required while each still lived on its own unmerged pull request, so every pull request lacked at least one required check and none could merge — including the two carrying the gates. Recovery meant rolling the ruleset back, merging both, and re-adding.
The second half is the subtler one. Even once the workflows are on main, a pull request branched before that point still cannot produce the new checks, so it stays blocked until refreshed. On a repository with a dozen open pull requests this is invisible until someone tries to merge one, and the remedy — update every branch — is nobody’s obvious next step.
This is the concrete reason the cutover is two acts rather than one: merge the change set, then flip the ruleset. The ordering is not a preference about observability, it is what keeps the repository working.
Source: Testbed incident 2026-08-06 · Status: Settled
REQ-ROLL-002: Unmigrated repositories keep building
Section titled “REQ-ROLL-002: Unmigrated repositories keep building”Changes to the shared build actions shall remain backward compatible for repositories that have not adopted the model. Because accounts-component is explicitly excluded from this project, this is a standing obligation rather than a transitional one.
Source: Design §12 · Status: Settled
Documentation
Section titled “Documentation”REQ-DOC-001: The shared model is documented
Section titled “REQ-DOC-001: The shared model is documented”The Queued CI/CD reference shall describe the variant these repositories adopt — assembly before build, and the split of responsibilities between assembly and the build pipeline — alongside the existing content-and-frontend variant. A contributor how-to shall exist for the backend repositories, as Frontend PR Process does for the frontend.
Source: Design §12 · Status: Settled
REQ-DOC-002: Agent rules track the repository list
Section titled “REQ-DOC-002: Agent rules track the repository list”The workspace changelog rule enumerates which repositories use which model, and agents rely on it to decide whether to edit CHANGELOG.md. It shall be updated as each repository cuts over.
No workspace worktree exists for this project, so this is currently unowned — and the rule becomes wrong the moment the first repository migrates.
Source: Design §12 · Status: Open — needs an owner.
Folded in: defects this project now fixes
Section titled “Folded in: defects this project now fixes”Defects found while deriving these requirements. The project does not spawn follow-up tickets for them; anything that lands in operations, common-module, infrastructure, or the shared actions is fixed here, so that what the project leaves behind is sound rather than sound-except-for-a-list (Miguel, 2026-08-06).
amm.yml cannot dispatch to Alpha002/stage — did not hold
Section titled “amm.yml cannot dispatch to Alpha002/stage — did not hold”amm.yml cannot dispatch to Alpha002/stageRecorded as a defect and withdrawn 2026-08-07 on re-checking: Alpha002/stage is present in amm.yml’s workflow_dispatch options on main, alongside the other three partitions. PDEV-1429 was cancelled 43 minutes after it was filed.
The reasoning that made it seem load-bearing still stands and is worth keeping: REQ-FEAT-008 names workflow_dispatch as the promotion mechanism, and REQ-PUB-005’s recovery story assumes every environment can be put back by hand. Both hold — the option that would have broken them is there. No change to infrastructure is needed.
An internal delimiter leaks into published output
Section titled “An internal delimiter leaks into published output”The 15.0.0 release block in documentation’s CHANGELOG.md ends with a literal ---comment--- line — the separator the assembly script uses to join the pull-request body with author comments, left behind in the extracted section. The assembly this project builds is derived from that script, so the defect would be inherited rather than encountered. It shall not be: assembly output shall contain no marker that exists only for the parser’s benefit, and the pilot shall check the composed block for one before the first release.
Repairing the already-published documentation block is a separate matter — it is a tagged historical release, and rewriting it is not this project’s business.
Out-of-scope observations
Section titled “Out-of-scope observations”Defects in the two reference implementations. They affect documentation and arda-frontend-app, which this project does not migrate and whose pipelines it does not own. The requirements above make the three target repositories immune by construction; the reference repositories stay exposed until they adopt the same shape, which is a separate decision.
Skipped required checks report success on drafts
Section titled “Skipped required checks report success on drafts”merge-eligibility is draft-gated in both documentation and arda-frontend-app, so it reports success on a draft without evaluating anything, and it auto-passes on merge_group. The window described in REQ-GATE-006 is therefore open in both repositories today. The fix is the same one this project adopts: stop draft-gating the cheap gates.
Concurrent assemblies lose changelog entries
Section titled “Concurrent assemblies lose changelog entries”On 2026-07-23 in documentation, pull requests #142, #137, and #135 merged within 80 seconds. Assembly ran for each; the runs for #142 and #137 failed and only #135’s succeeded, so two entries never reached CHANGELOG.md and nothing reported the loss. This is the race PDEV-694 describes. Range-based assembly (REQ-ASM-004) removes it for the three repositories here; documentation and arda-frontend-app remain exposed until they adopt the same shape.
References
Section titled “References”- Goal — why the project exists.
- Design — how these requirements are met.
- Decision Log — the decisions behind the design.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved