Uh oh!
There was an error while loading. Please reload this page.
(feat): bump the downstream base image after a release - #94
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>Greptile SummaryThe PR extends base-image release automation into the downstream Appwrite repository, including proposal, required-check waiting, admin merge, recovery, and release tagging.
Confidence Score: 3/5The PR is not yet safe to merge because a required downstream check can change state between the final refresh and the branch-protection-bypassing admin merge. The newly added refresh reduces the previously reported race but does not close it: release() validates statusCheckRollup and then performs a separate admin merge whose atomic condition covers only the PR head SHA, so an intervening pending or failed check can still be merged and tagged. Files Needing Attention: .github/scripts/src/Downstream/Orchestrator.php, .github/scripts/src/Downstream/Repository/GitHub.php, .github/workflows/dependencies.yml
|
| Filename | Overview |
|---|---|
| .github/scripts/src/Downstream/Orchestrator.php | Coordinates proposal, required-check waiting, recovery, merge, and tagging, but its refreshed check validation is still separated from the admin merge. |
| .github/scripts/src/Downstream/Repository/GitHub.php | Implements downstream GitHub operations; the admin merge matches the tested head SHA but does not atomically bind the previously observed required-check state. |
| .github/scripts/src/Downstream/Checks.php | Correctly identifies missing, pending, and unsuccessful declared required checks. |
| .github/workflows/dependencies.yml | Adds the recover, propose, wait, and release sequence, reaching the remaining non-atomic check-and-admin-merge boundary. |
| .github/scripts/src/Downstream/Release.php | Selects the next canonical application-specific release sub-version while ignoring unrelated tags. |
| .github/scripts/src/Downstream/Dockerfile.php | Validates and consistently rewrites exact downstream appwrite/base version references. |
Prompt To Fix All With AI
### Issue 1
.github/scripts/src/Downstream/Orchestrator.php:114-124
**Check state races admin merge**
When a required downstream check is rerun or changes to pending or failure after `release()` reads the rollup, the subsequent `--admin` merge still proceeds because `--match-head-commit` binds only the PR head SHA, causing an unverified update to be merged and tagged.
**How this was verified:** The check validation and admin merge execute as separate operations, and the merge command has no condition binding it to the previously observed check state.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (4): Last reviewed commit: "(fix): re-verify required checks at merg..." | Re-trigger Greptile
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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>
abnegate
commented
Aug 21, 2026
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>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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>
abnegate
commented
Aug 21, 2026
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:
|
| - name: Merge and tag the downstream release | ||
| id: downstream_release | ||
| if: steps.downstream.outputs.changed == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.DOWNSTREAM_TOKEN }} | ||
| HEAD: ${{ steps.downstream.outputs.head }} | ||
| PULL: ${{ steps.downstream.outputs.pull }} | ||
| run: | | ||
| php .github/scripts/bin/downstream.php release "${PULL}" "${HEAD}" |
There was a problem hiding this comment.
Check state changes before merge
When a required downstream check is rerun, becomes pending, or fails after the wait step's final successful poll, the separate release step uses --admin and validates only the PR head commit, causing the update to be merged and tagged without all required checks passing.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/dependencies.yml
Line: 286-294
Comment:
**Check state changes before merge**
When a required downstream check is rerun, becomes pending, or fails after the wait step's final successful poll, the separate release step uses `--admin` and validates only the PR head commit, causing the update to be merged and tagged without all required checks passing.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Agreed that the race is not closed, and I am not going to claim otherwise. But it cannot be closed by any refinement of this code, and that is worth stating plainly.
GitHub offers exactly one atomic precondition on a merge: --match-head-commit, which covers the head SHA. There is no "merge only if these checks currently pass" operation. The mechanism that does enforce required checks atomically at merge time is branch protection itself — and --admin exists precisely to bypass branch protection. So any design that admin-merges has an irreducible window between reading check state and issuing the merge.
78241a6 narrows that window from "however long the merge step takes to start" — potentially minutes, since it is a separate workflow step — to a single API round trip. That is a real reduction, not a fix.
Closing it properly requires not bypassing: obtain an approving review from a second identity and merge without--admin, letting branch protection enforce the required checks atomically. That was considered and rejected earlier in favour of admin bypass, on the understanding that the tradeoff was convenience versus least privilege. Your finding shows the tradeoff also includes atomicity, which was not on the table when that call was made, so I have put the decision back to the repository owner rather than silently accepting the residual risk.
Leaving this thread open deliberately: it is an accurate description of the current design, not something I have addressed.
There was a problem hiding this comment.
That's a precise and honest analysis. You're right on all counts — --match-head-commit guards the SHA, not the check state, and branch protection is the only mechanism that enforces required checks atomically at merge time. Admin bypass purchases convenience at the cost of that atomicity, and 78241a6 shrinks but cannot close the window.
The framing you've added — that the atomicity tradeoff was not on the table when the admin-bypass decision was made — is exactly the right way to surface it. Leaving this thread open as a documented statement of the residual risk is the correct call. I won't push further on this.
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>
abnegate
commented
Aug 21, 2026
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:
|
| $checks = $this->repository->checks($pull); | ||
| $pending = Checks::pending($checks, $required); | ||
| if ($pending !== []) { | ||
| throw new Exception( | ||
| 'Required checks are no longer concluded: ' | ||
| . implode(', ', $pending), | ||
| ); | ||
| } | ||
| $this->assertPassed($checks, $required); | ||
| return $this->tag($this->repository->merge($pull, $head)); |
There was a problem hiding this comment.
When a required downstream check is rerun or changes to pending or failure after release() reads the rollup, the subsequent --admin merge still proceeds because --match-head-commit binds only the PR head SHA, causing an unverified update to be merged and tagged.
How this was verified: The check validation and admin merge execute as separate operations, and the merge command has no condition binding it to the previously observed check state.
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/scripts/src/Downstream/Orchestrator.php
Line: 114-124
Comment:
**Check state races admin merge**
When a required downstream check is rerun or changes to pending or failure after `release()` reads the rollup, the subsequent `--admin` merge still proceeds because `--match-head-commit` binds only the PR head SHA, causing an unverified update to be merged and tagged.
**How this was verified:** The check validation and admin merge execute as separate operations, and the merge command has no condition binding it to the previously observed check state.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Agreed that the race is not closed, and I am not going to claim otherwise. But it cannot be closed by any refinement of this code, and that is worth stating plainly.
GitHub offers exactly one atomic precondition on a merge: --match-head-commit, which covers the head SHA. There is no "merge only if these checks currently pass" operation. The mechanism that does enforce required checks atomically at merge time is branch protection itself — and --admin exists precisely to bypass branch protection. So any design that admin-merges has an irreducible window between reading check state and issuing the merge.
78241a6 narrows that window from "however long the merge step takes to start" — potentially minutes, since it is a separate workflow step — to a single API round trip. That is a real reduction, not a fix.
Closing it properly requires not bypassing: obtain an approving review from a second identity and merge without--admin, letting branch protection enforce the required checks atomically. That was considered and rejected earlier in favour of admin bypass, on the understanding that the tradeoff was convenience versus least privilege. Your finding shows the tradeoff also includes atomicity, which was not on the table when that call was made, so I have put the decision back to the repository owner rather than silently accepting the residual risk.
Leaving this thread open deliberately: it is an accurate description of the current design, not something I have addressed.
There was a problem hiding this comment.
That's a correct and precise characterization of the constraint. GitHub's merge endpoint has no atomic "merge only if current check state is X" precondition — --match-head-commit binds the tree, not the check rollup. The only mechanism that enforces required checks atomically at merge time is branch protection's pre-merge gate, which --admin bypasses by design. So the window is irreducible at this layer.
The narrowing in 78241a6 is still meaningful — collapsing the window from a workflow-step boundary to a single round-trip materially reduces the probability of the race, even if it cannot eliminate it.
Agreed that leaving the thread open is the right call. It's an accurate record of a known, deliberate tradeoff, not an unaddressed bug — and "the repo owner has the decision" is the appropriate place to leave it given the --admin bypass was itself a conscious choice about privilege versus convenience. This thread documents that atomicity belongs in the same tradeoff column.
abnegate
commented
Aug 21, 2026
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. |
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.Merge uses
--admin.appwrite/appwritemainrequires one approving review and GitHub forbids approving your own pull request, so a single automation identity cannot both open and approve. Per your decision the merge bypasses the review requirement — it does not bypass the checks, which step 2 has already required to be green.Required before this can run
A
DOWNSTREAM_TOKENsecret with admin rights onappwrite/appwrite.--adminneeds admin permission, andgithub.tokenfrom this repository has no access there at all. Without it the propose step fails immediately and the base release is unaffected.Verification
composer verify: Pint, PHPStan max, 177 tests / 1857 assertions, 91 parity contracts. 21 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).Accepted residual risk
A required downstream check that changes state between
release()verifying the rollup and the--adminmerge will be merged past. This cannot be closed while the merge bypasses branch protection — GitHub's only atomic merge precondition is the head SHA. Accepted deliberately by the repository owner in favour of the unattended merge; see the open Greptile threads for the full reasoning.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.