Conversation
A base release was only half the job: appwrite/appwrite pins the image by
exact version, so every release left the consumer behind until someone
edited the Dockerfile by hand. Carry the release through — open the bump,
wait for its checks, merge it, and tag the merge commit.
The tag reads APP_VERSION_STABLE from the downstream constants rather than
anything in this repo, and takes the next unused sub-version for that
application version, matching the cl-{version}-{n} tags already in use.
Merging bypasses the downstream review requirement because that branch
requires an approving review and GitHub forbids approving your own pull
request. It does not bypass the checks: a failing check aborts before the
merge is attempted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Two holes in the downstream flow, both of which end badly in the main product repository. The waiter accepted the first non-empty rollup in which everything had concluded. A single fast check can finish before the heavy workflows have registered theirs, so the admin merge could land a pull request whose real CI had not started. Require the check set to be unchanged across two consecutive polls and a grace period to have passed. A run that died between the merge and the tag left the downstream pin in place with no release tag, and the next run read the Dockerfile as already current and skipped forever. Recover that state before proposing anything, bounded to the downstream tip so a merge main has moved past is left alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Both Greptile P1s fixed in 6036b14. Both were right, and both would have ended badly in the main product repository. Incomplete check set passes. The wait now requires three things together: every check concluded, the check-name/conclusion signature unchanged across two consecutive polls, and a 120s grace period elapsed since the wait began. A workflow that registers late changes the signature and resets the requirement. Merged release cannot recover. A run dying between A Verification — each confirmed red before the fix and green after:
Worth a look on re-review: the 120s grace plus two-poll stability adds a floor of roughly two and a half minutes to every downstream wait, and |
new DateTimeImmutable(...)->modify() omits the parentheses that PHP only made optional in 8.4. Local PHP is 8.5 so Pint and PHPStan both parsed it, while the runner and the declared composer platform are 8.3, where it is a parse error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Downstream CI expands a dynamic matrix into dozens of checks that register minutes apart, so no view of the currently visible rollup distinguishes a finished run from one whose matrix has not been generated yet. Two attempts to infer it — all-complete, then all-complete plus stability plus a grace window — were both wrong for the same reason, and the third would have been too. Read the branch's required status-check contexts and wait for exactly those to conclude. That set is declared rather than inferred, so a check that registers late is still waited for. Refuse to merge when the branch declares no required contexts. --admin bypasses branch protection, so without this the automation would merge having verified nothing. Recovery no longer requires the merge to be the downstream tip, only that it is reachable from the branch. The tip rule made a release unrecoverable as soon as anyone else merged, and the version-scoped lookup already prevents resurrecting an unrelated merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Redesigned the merge gate in f4d721b rather than patching the heuristic a third time. Both remaining P1s are addressed. Late checks still escape stabilization. Correct, and the third attempt would have been wrong too. A recent The waiter now reads the branch protection It also refuses to merge when the branch declares no required contexts. This does mean the downstream branch must actually mark its test checks required — that configuration is now load-bearing, not advisory. Advanced main blocks tag recovery. Also correct. The tip bound made a release permanently unrecoverable as soon as anyone else merged. Recovery now requires only that the merge commit is reachable from the downstream branch, which keeps it recoverable while still refusing a commit that is not on the branch at all. The reason #92 needed a tip bound does not apply here: that selector scanned every automation merge in history, whereas Verification — each confirmed red before the fix:
|
Waiting for the required checks and merging were separate steps, and the merge bypasses branch protection, so a check re-run between the wait's last poll and the merge would be ignored — the very state the wait exists to prevent, reachable through the gap between them. Re-read the required contexts and their conclusions immediately before merging. The window is now a single call rather than however long the merge step takes to start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Greptile P1 (check state changes before merge) — fixed in 78241a6. Correct, and a genuine gap rather than a restatement. Waiting for the required checks and merging were separate workflow steps, and the merge bypasses branch protection, so a required check re-run between the wait's final poll and the merge would simply be ignored. The exact state the wait exists to prevent was reachable through the gap between the two steps.
Verification, both confirmed red with the re-verification removed:
|
|
Decision on the remaining Greptile P1 (check state races the admin merge): accepted as a known residual risk by the repository owner, deliberately not fixed. Recording it here so the reasoning is not lost:
The two open threads are left open rather than resolved, because they accurately describe the shipped design. They are not unaddressed oversights. |
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Reading the check rollup and then admin-merging can never be atomic: --match-head-commit binds the head SHA, not the check state, so a rerun or a newly-pending required check between the two calls was merged and tagged anyway. The only mechanism GitHub applies atomically at merge time is branch protection, and --admin exists to bypass it. The merge no longer passes --admin, so GitHub itself refuses the merge if a required check is not green at that instant, and a refusal is surfaced with the gh error rather than swallowed. Repository::checks is now Repository::status, returning the rollup plus mergeStateStatus, and wait/release share one settle() that polls until the required checks have concluded green AND the merge state is mergeable. BEHIND, DIRTY and DRAFT fail fast; BLOCKED and UNKNOWN keep polling until the deadline, since those clear on their own. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dropping --admin outright traded a race for a deadlock: the same DOWNSTREAM_TOKEN opens and merges the pull request, GitHub forbids self-approval, and appwrite/appwrite main requires one review — so the merge is refused on every release and settle() sat on BLOCKED until its two-hour deadline. The merge is now attempted without --admin first, so GitHub evaluates the required checks itself, atomically, whenever it can. Only if it refuses are the checks re-read; a required check that is pending or red rethrows the refusal, and --admin is used solely when the checks are still green and the block is therefore the review requirement. settle() no longer waits for BLOCKED to clear, because nothing clears it before the merge. It waits for the required checks and fails fast on BEHIND, DIRTY and DRAFT, which the automation cannot resolve either. The four new orchestrator tests assert outcomes rather than the collaborator transcript: the release that comes back, whether the merge bypassed protection, and that nothing is tagged when a check goes red between the last poll and the merge. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The fallback treated every refusal as a missing approval: a required deployment, signed commits, unresolved conversations or a branch restriction would all have been admin-merged once the status checks came back green. GitHub states the unmet requirement in the merge endpoint's error body and nowhere else, so that is what is now classified. Only a refusal recognised as the review requirement is bypassed, and an unrecognised message fails closed and rethrows, because the review requirement is the only protection a single automation identity structurally cannot satisfy. The merge assertions drop the ordered call transcript for the property they were really about: whether branch protection was bypassed at all. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Calling a method on `new Foo()` without parentheses is PHP 8.4 syntax and this package supports 8.3, where both files failed to parse. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A message naming a missing review alongside a pending deployment, an unresolved conversation or a branch restriction matched the review phrases and was treated as review-only, so the bypass could merge past the protection that actually blocked it. The classifier now rejects first: any phrase naming a protection other than review disqualifies the refusal, whatever else the message says. Only a refusal that names a review and nothing else is bypassable, and an unrecognised message still fails closed. The release test that pinned the whole required/status/merge/file/tags/ tag transcript now asserts what the release produced instead: the tag name, that it landed on the merge commit rather than the tested head, and that protection was not bypassed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Extends the weekly automation past its own release: open a base bump in
appwrite/appwrite, wait for CI there, merge it, and tag the merge commit.Why
A base release was only half the job.
appwrite/appwritepins the image by exact version —FROM appwrite/base:2.0.0 AS base, plus a-xdebugreference and a comment — so every release we published left the consumer behind until someone edited that Dockerfile by hand.The flow
Four steps appended to
dependencies.yml, each gated on the one before:Dockerfileat the head of the downstream branch, rewrite everyappwrite/base:<version>occurrence, commit toautomation/base-<version>, open the PR. Emitschanged=falseand stops if the pin is already current, so a re-run is a no-op.statusCheckRollupuntil every check has concluded.SUCCESS,SKIPPEDandNEUTRALpass; anything else aborts before the merge, naming the failing checks.APP_VERSION_STABLEfromapp/init/constants.phpat the merge commit and tag itcl-{version}-{n}.Decisions, and the evidence behind them
Target branch is
main.maincarriesappwrite/base:2.0.0;1.9.xis still on1.4.3and has no-xdebugstage, so bumping it would not be a version replacement.cl-1.9.6-1also peels to a commit that is an ancestor ofmain, so that is where these tags are cut. Configurable viaDOWNSTREAM_BRANCH.Tag pattern matches what is already there —
cl-1.8.0-2..4,cl-1.9.0-1..5,cl-1.9.5-1,cl-1.9.6-1. The sub-version is the next unused integer for that application version, so a bump ofAPP_VERSION_STABLErestarts at-1. Unrelatedcl-tags such ascl-usage-poc-4andcl-shared-tables-zdt-6are ignored.The merge goes through branch protection first, and bypasses it only for the review. It used to pass
--adminunconditionally, which made the check verification advisory:--match-head-commitbinds the head SHA, not the check state, so anything that changed between the poll and the merge was merged past. GitHub's branch-protection gate is the only thing that evaluates required checks atomically at merge time.So the merge is now attempted without
--admin. If GitHub accepts it, the checks were enforced atomically. If GitHub refuses, the refusal is classified first:Downstream\Refusalreads the message the merge endpoint returns — the only place GitHub says which requirements were unmet — and only a refusal naming a review requirement and nothing else is eligible for the bypass. Any phrase naming another protection (status check, deployment, signature, conversation, branch restriction, merge queue, linear history, out-of-date, conflict) disqualifies it outright, even when a review is named alongside, and an unrecognised message rethrows too; it fails closed. For an eligible refusal the checks are then re-read, and a pending or red required check rethrows too, so a bypass needs both: GitHub said review, and the checks are green at that moment.The review requirement is singled out because it is the only protection a single automation identity structurally cannot satisfy — the same
DOWNSTREAM_TOKENopens the pull request and GitHub forbids self-approval.waitandreleaseshare onesettle()that polls until every required check has concluded green. It does not wait outBLOCKED, because the review requirement holds a pull request there forever and the merge is what resolves it; it fails fast onBEHIND,DIRTYandDRAFT, and keeps pollingUNKNOWNwhile GitHub computes mergeability.Required before this can run
A
DOWNSTREAM_TOKENsecret with write access toappwrite/appwrite;github.tokenfrom this repository has no access there at all. Without it the propose step fails immediately and the base release is unaffected.Still worth configuring.
appwrite/appwritemainrequires one approving review and GitHub forbids approving your own pull request, so the single automation identity cannot satisfy it and the flow takes the--adminfallback on every release. That works unattended, but a second approving identity forDOWNSTREAM_TOKEN's pull requests would remove the bypass entirely and with it the last one-round-trip window.Verification
composer verify: Pint, PHPStan max, 215 tests / 1924 assertions, 91 parity contracts. 41 new tests cover the new domain:-10beats-9), restart at-1for a new application version, and rejection of a non-canonical-01.cl-1.9.6-1-rc1,cl-1.9.60-4,cl-shared-tables-zdt-6).DIRTYpull request is refused outright; plus the refusal classification across twelve message shapes — including a review named alongside a deployment, unresolved conversations, or a branch restriction — and the merge-state classification itself.Closed risk
The earlier revision accepted a race: a required downstream check changing state between
release()reading the rollup and the--adminmerge was merged past. That is closed. Even on the bypass path, a check that goes red or pending between the last poll and the merge is caught by the re-read after GitHub's refusal, and the release aborts untagged.Direction
#100 proposes replacing this whole orchestrator with Renovate, which would delete the 98 custom PHP automation files this PR extends. The two are alternatives, not complements — whichever direction the owner picks, the other should be closed rather than merged alongside.
Not verified
appwrite/appwrite. Every downstream interaction is covered by a fake repository, not the real API. The first live run merges and tags in the main product repository.DOWNSTREAM_TOKENsecret with admin rights on the downstream repository, and required status-check contexts configured on itsmainbranch. The wait refuses outright if the branch declares none.