Repository navigation
release: v1.1.0 rehearsal - contract version gate, /health retirement, tag-gate on the real path - #16
Merged
Merged
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
wangrh0909
approved these changes
Sep 27, 2026
Archive the [Unreleased] CHANGELOG body as 1.1.0 and bump the three version declarations to match, so release.yml's version-check has a section to certify when the tag lands. The six `drogon-pay/<version>` package references in the READMEs and the plugin integration guide move to 1.1.0 in the same commit because they document the package this release publishes, not the one before it. The release skill described those six references by literal version, which went stale on every roll; it now names them by position instead. The CHANGELOG link refs still pointed at the pre-rename owner.
`examples/pay-server/openapi.yaml` states a version to every consumer who reads the contract, and nothing compared it to the release: the file still said 1.0.0 while the tree was preparing 1.1.0, which is a contract promising a package that had never shipped. `AGENTS.md` claims the version is "declared three times", so the honest fix is a fourth enforced site, not a documented hole. The reader anchors on the top-level `info:` block and demands exactly one indented `version:` line, and stops at the next top-level key. Matching any `version:` in the document would instead read whichever block came first. Four tamper controls were run against it: agreement, a drifted contract, a duplicate line inside `info:`, and a renamed `info:` block - the last two fail loudly rather than passing on a stale line.
BREAKING: GET /health on the example host now answers 404. The alias carried `Sunset: 2026-08-28`, and every caller inside this repository had already moved to /healthz or /readyz earlier in this cycle, so the window was empty: the route survived only to keep answering probes that should have been told it went away. HealthProbe_RetiredCompatEndpoint_ Answers404 pins the 404, and the contract lost the path and its preflight in the same commit as the registration so the route-parity gate holds both sides. SemVer-wise this stays a MINOR bump: the removed route belongs to examples/pay-server, and the drogon-pay package's API and ABI are untouched. The host's own HTTP surface is what narrowed. The last two callers of the name were docs and the orphan run_docker_tests.sh probe, now /healthz; the ops scripts' comments said "deprecated" about a route that no longer exists.
The guide still told people to clone lucaswang420/drogon-pay. GitHub's rename redirect carries the clone today, but it dies if that name is ever taken, and every other URL in the repository already says voidvec.
`config.json` sets `use_implicit_page`, so an unregistered path 404s from the static fallback even if the controller never linked — the HTTP status alone cannot tell "the alias is gone" from "nothing is registered at all". Assert on `getHandlersInfo()` for all three names, and on the absence of the old `Deprecation` header, before trusting the 404. Co-Authored-By: Qoder <noreply@qoder.com>
… one The first version of the `info:` reader located the top-level block, took it at face value, and matched its `version:` line with a regex that quietly accepted only one spelling. Three shapes survived that: a second top-level `info:` block (this check reads the first, a YAML parser reads the merge), `version: "1.1.0` (nothing matches, so it is invisible rather than wrong), and a legal trailing comment (`version: "1.1.0" # ship it`, rejected by the old regex even though every consumer's parser reads 1.1.0 from it). Capture the scalar whole, require exactly one `info:` block, and validate the value separately, so a wrong shape names itself instead of producing a false pass or a false failure. `agreed_version()` now takes the already-read map: `main` counted sites by calling `declared_versions()` a second time, which re-read four files to print a number it already had. Controls run at this commit: contract drifted -> disagree; second `version:` line -> "found 2"; `info:` renamed -> no block; second `info:` block -> "2 top-level blocks at lines 13, 17"; unbalanced quote -> refused; double-quoted, single-quoted, quoted+comment, plain+comment -> pass; `1.1.0-rc1` and `1.1` -> "does not read as X.Y.Z"; same drift against an LF copy (the CI checkout side) -> disagree. Co-Authored-By: Qoder <noreply@qoder.com>
`Amount.pattern` was double-quoted while carrying `\d` escapes — a double-quoted scalar may not hold an unknown escape, so the load died there — and two `description:` lines put a colon-space inside a plain scalar (`Present with `code: 1` when ...`). The file has been unparseable this way for as long as those lines existed; both guards over it scan line by line, so neither noticed, and one of them now publishes its version. `yaml.safe_load` reads the whole document: 14 paths / 26 operations, matching the counts `check_openapi_routes.py` reaches by regex. Co-Authored-By: Qoder <noreply@qoder.com>
The three places that still pointed at `/health` after 968b525 removed it: `health_check_implementation.md` listed it as a live endpoint with its `Deprecation`/`Sunset` headers, `operations_manual.md` told an operator to read `/health` for database errors (and quoted a `{"database":"error"}` body the controller has never produced), and `deploy.yml`'s ECS health step polled it — which, once the route is gone, fails a rollout that is actually fine. The runbook names the 404 and the test that pins it; the manual points at `/readyz` with the body it really returns; the workflow probes `/readyz`. `actionlint` clean on the workflow edit, and `check_docs_drift.py` passes the seven rules over the docs side. Co-Authored-By: Qoder <noreply@qoder.com>
Making the contract a version site left a line reader standing in for a YAML parser, and review found it green on four files no consumer can load: a tab after `version:` (PyYAML stops on the line, the guard saw 1.1.0), a plain scalar continued by a deeper-indented line (parser reads `1.1.0 9.9.9`), the same continuation across a blank line (parser folds it to `1.1.0\n9.9.9`), and a trailing non-breaking space (not whitespace to YAML, padding to `.strip()`). It also found two false reds: a legal trailing comment refused with a message about the wrong reason, and the first cut of the continuation guard rejecting a deeper-indented comment line, which a parser treats as nothing at all. The reader now accepts only what a parser agrees to and refuses anchors, aliases, tags, block scalars, continuations, escapes inside double quotes, unclosed quotes, doubled `''`, and non-space padding — naming which. `scripts/ci/version_sync_scenarios.py` pins each spelling as a case, runs in `static-analysis` beside the guard, and asserts the one fact the table could otherwise fake: the repo's own contract reads a value inside the agreeing set. Two cases were checked backwards against the pre-fix reader, which accepted `1.1.0\n9.9.9` and rejected the indented comment. AGENTS.md, TECH_SPECS.md, CONTRIBUTING.md and both release-skill twins now say four declarations, and the table's case count is left to the script that prints it. Controls: the guard, both scenario tables, the other four stdlib guards and `actionlint` all exit 0 at this commit. Co-Authored-By: Qoder <noreply@qoder.com>
Both runbooks told an operator that a dead database makes `/readyz` answer 503 on the next poll. It does not necessarily: the controller debounces, so an instance that has already reported `ready` keeps doing so until `FAILURE_THRESHOLD` (3) consecutive probes fail, while one that never succeeded flips on the first. A hung dependency is another wrong answer in the same paragraph — the 1-second deadline records `"timeout"`, not `"db"`, so a probe that names the wrong component sends the operator to the wrong service. The manual also told readers to look for "Database connection failed" in the logs; that string exists nowhere in the code, because the failure callback only appends to `failed` and never logs. CHANGELOG.md carries this round's other two claims: the `/health` entry now dates the deprecation headers (added 2026-05-28 in 373d235, 92 days before the `Sunset` they named) instead of asserting a window nobody can check, and the version-reader hardening from 08b457b gets its entry. Co-Authored-By: Qoder <noreply@qoder.com>
Review of 08b457b found that its own reader disagreed with `yaml.safe_load` in eleven more places. Nine were false passes on files a consumer cannot load: a tab or a non-breaking space padding the `info:` header (the block was located with `rstrip()`, which strips exactly the characters YAML will not accept there), a tab-indented line under the value, junk or a list item at the value's own indent (the continuation test asked only for `indent > 2`, so a sibling positioned like a key and shaped like nothing slipped), a form feed or U+2028 inside the scalar (`str.splitlines()` breaks on both, and neither is a line break to YAML, so the reader saw a clean first line the parser never had), and a second document after `---` (`safe_load` stops there; the check did not notice). One was a version that is not SemVer: `01.1.0` satisfied a `\d+` pattern and four agreeing files would have tagged it. Two were false reds, `info :` and `info: # contract`, both of which load fine. The header is now matched by a regex that allows spaces and a comment and nothing else, line splitting is on `\n` alone, the line after the value must be a sibling key or nothing, a document marker anywhere refuses the file, and `SEMVER_RE` rejects leading zeros — which also tightens the tag shape and the `package.json` field, the other two consumers of that pattern. This commit also retracts two claims made in the last two. 08b457b's message said `version: "1.1.0" # ship it` had been refused before it: it had not, the committed reader handled a spaced comment, and the shape that failed was `"1.1.0"#bumped` with no space; and its "checked backwards" sentence credited a rejection of the indented comment to the pre-fix reader, which accepted that line — the rejection came from that commit's own first cut, which is why the case exists now. The CHANGELOG entry is rewritten to count from the table instead of from memory, and the `/health` entry no longer says the alias predated its headers: 373d235 introduced both in one commit. Controls: 46/46 pinned cases, and each of the eleven replays against `08b457b^` — eight load-failing reads and one `01.1.0` the old reader accepted, two legal files it refused. Guard, both scenario tables, the other four stdlib guards, `actionlint` and a real `yaml.safe_load` of the contract all exit 0. Co-Authored-By: Qoder <noreply@qoder.com>
08b457b's marker rule refused --- and ... anywhere in the contract, which refused two legal single documents: a leading --- and a trailing ... . A gate that rejects valid input gets weakened, so that is the same failure in the other direction the entry documents. The rule now asks what the marker splits: a --- with content above it, or a ... with content below it, starts or ends a document the guard must not read through. Six marker-position spellings are pinned as DOCUMENT_CASES, each expectation cross-checked against yaml.safe_load, and the runner folds both tables through one assert_verdict. Also fixes the tally the runner prints, which still counted only CASES and reported 46/46 while 52 checks ran, and updates the CHANGELOG entry whose replay claim the new cases had aged: replaying the current table against 08b457b^:scripts/check_version_sync.py, the old reader's verdict differs on exactly twenty shapes - the thirteen false passes, the four marker positions a pre-marker reader walked past, and the three refuses of legal spellings. Verified by that replay, not remembered.
Review found the position rule's exact-equality match was too narrow to be a split test: `--- # new contract` is a real marker to YAML (a second document opens, safe_load stops there) but is not equal to `---` after stripping spaces and tabs, so the guard read a version out of a document no consumer receives. The glued spellings are worse, not better: a tab after the marker, `---#x`, `--- x`, `----`, `...foo` and an invisible nbsp or form feed are scanner errors, and the guard walked past each one still reporting a version. The rule now separates candidate (column 0, three dashes or three dots) from clean marker (bare, or space-padded, optionally comment-suffixed - never tab-padded, which YAML itself refuses): candidates that are not clean are refused wherever they stand, clean ones are judged by position as before. Twelve DOCUMENT_CASES pin the classes, each expectation read off yaml.safe_load / safe_load_all. The CHANGELOG replay sentence loses its hardcoded twenty, which the growing table had already aged, and keeps the claim to what stays true at any table size: against 08b457b^ the old reader differs only by being green where the table refuses and refusing the three legal spellings. It also fixes attribution: the blanket marker ban was d2d728c's committed rule, not 08b457b's, and 08b457b's indented-comment false red lived only in its own message's account of its first cut.
…ased Rebasing the release series onto master replayed `prepare v1.1.0`'s section rename against master's newest commits, and its context matched inside [1.1.0]: the four blocks master had added to [Unreleased] (the ledger unique index, the Alipay notification ordering, the time_expire chain, the settled-refund ledger) ended up archived under a release that never contained them. This restores CHANGELOG.md to the collated state of the pre-rebase merge tip, which is byte-identical otherwise.
voidvec
force-pushed
the
release/v1.1.0
branch
from
September 27, 2026 03:25
61ff5b2 to
9ea9187
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The tag gate merged in #14 has only ever run on a pull request. This branch is the
first real
v*push that exercises it end to end:release.yml'sci-gateanddeploy.yml'stag-gateboth call_tag-gate.yml, and neither has yet beenasked to certify a tag on the production path. Preparing the release also forced
the three decisions the previous round deliberately left open.
What
Release mechanics (4 commits)
build(release): prepare v1.1.0— the four version declarations moved to1.1.0, andCHANGELOG.md's[Unreleased]body archived under## [1.1.0] - 2026-09-24soversion-check's section test has something tofind. Six published-package references (
README*,docs/development/plugin_integration.md) follow todrogon-pay/1.1.0.build(release): read the contract's version as a declaration site—examples/pay-server/openapi.yamlsaid1.0.0while the tree prepared1.1.0, because no gate read it. It is now the fourth sitecheck_version_sync.pyenforces, soAGENTS.md/TECH_SPECS.md/ therelease skill say "four declarations" and the contract stops shipping a
version nothing compared.
fix(api)!: retire the /health alias of /readyz— the alias carriedSunset: 2026-08-28and outlived it. Removing a route is breaking, so itwaited for the bump. Package API/ABI is untouched:
git diff master..HEAD -- libs/drogon-pay/is empty; the route lived in the examplehost.
docs(deployment): clone from the renamed owner— the guide still told peopleto clone a
lucaswang420URL that no longer resolves.First review round (4 commits) — pushed after an independent review of the
above found real defects in it:
test(health)—HTTP 404alone could not distinguish "the alias is gone"from "no controller linked at all" (
config.jsonsetsuse_implicit_page), sothe case now asserts registration state via
getHandlersInfo()for all threenames (
/healthunregistered,/healthz+/readyzregistered), plus twotransport
REQUIREs and the 404-and-no-Deprecationresponse checks.build(release)— a second top-levelinfo:block passed the new reader(it took the first and ignored the rest), and
version: "1.1.0matched noregex, so a malformed line was invisible rather than wrong. Both closed;
trailing comments are now accepted instead of failing a legal line.
fix(openapi)— three scalars in the contract broke YAML parsing outright(a double-quoted
\descape, two plain scalars containing:), and had doneso since they were written: both guards over the file scan it line by line.
docs(operations)—health_check_implementation.md,operations_manual.mdanddeploy.yml's ECS health step all still pointed at/healthas live; the same round documents the debounce truth (a hungdependency reports
"timeout"; an instance that never reached ready answers503 on its first failing probe, one that did keeps answering 200 until three
consecutive failures).
Version-guard hardening rounds (5 commits) — making the contract a read site
put a stdlib line-scanner where a YAML parser should be, and four review rounds
each found shapes the previous round misread:
08b457bclosed four false passes (tab afterversion:, plain-scalarcontinuation directly and across a blank, trailing nbsp) and pinned every
spelling as an executable decision table —
scripts/ci/version_sync_scenarios.py, astatic-analysisstep that printsits own count and asserts the live contract reads into the agreeing set.
d2d728cclosed eleven more shapes that review found the table itself pinnedwrong or the reader still read green, and retracted false claims this entry's
first cut made (see Disclosures).
b1b91deoperations docs, as above.84fb572+51af932dealt with document markers. The first rule refused any---/...anywhere, which rejects the legal leading---and trailing...; the position-aware replacement was then shown to match markers toonarrowly —
--- # new contractis a real split (a second document, whichsafe_loadnever reaches) that the exact-equality check walked straight past.Final rule: a column-0
---/...line must be cleanly spelled — spacepadding and a comment only; a tab, a glued
#, a word or a fourth dash is ascanner error, refused wherever it stands — and clean markers are judged by
what content sits on which side. 45 info-block cases + 18 marker cases, each
expectation cross-checked against
yaml.safe_load/safe_load_allout ofband; the CHANGELOG states the disagreement classes as replayable claims, not
remembered numbers.
Verification
check_version_sync(plain and--tag v1.1.0),version_sync_scenarios(64/64),
tag_gate_scenarios(23/23),check_architecture,check_docs_drift(7 rules, incl..claude↔.codexbyte parity),check_test_layout,check_migrations,check_openapi_routes(
--lint-onlyand--routes-only),actionlint— all 0.3c5aff1(git log 3c5aff1..HEAD --name-onlyshows no.cc/.h), so the Windows Release build andctest --test-dir build/windows-msvc -C Release→ 100% (taken with thereworked health case) stands for the whole branch; contract edits since then
are re-validated by the route gate at tip.
check_version_sync.py --tag v1.1.0passing is whatrelease.yml'sversion-checkwill run against the tag.Disclosures (review caught them; history keeps them)
Two committed messages state things that later replays did not reproduce. Both
are corrected in
51af932's message and in the CHANGELOG rather than rewrittenout of history:
d2d728csays "nine" load-failing reads where the replay gives eight plus01.1.0(a read that loads but names a non-SemVer version).84fb572attributes the blanket marker ban to08b457b(it shipped ind2d728c) and says in past tense that the runner "reported 46/46 while 52checks ran" — that mismatch existed only in that round's uncommitted working
tree; no commit ever printed it.
Deliberately not in this PR
Found while sweeping, each a separate concern, none of them reachable by this
release's path:
conanfile.py:26and.github/CODEOWNERSstill name the oldlucaswang420owner — same drift class as the clone URL, different file kinds.
deploy.sh:211,deploy.bat:159andhealthcheck.sh:106poll/api/health, a route that has never existed in this repository, so thosewait loops time out rather than gate on anything.
deploy.yml:148/:200curlstaging-api.example.com/api.example.complaceholders — unless the deployment environment overrides them, those health
gates assert nothing reachable.
scripts/clang_format.py --checkwalks the working tree, so a gitignoredlocal
libs/drogon-pay/src/models_backup/makes it exit 1 on a laptop and passin CI — the inverse of the index-vs-worktree bug the other guards already
fixed. (
clang_tidy_gate.pylikewise needs the Linux preset'scompile_commands.jsonand is not runnable on this laptop.).github/workflows/secrets-scan.ymldeclares nopermissions:block.coverage.ymlrun that measured it.After merge
Tag the master tip as
v1.1.0— that is the event this rehearsal exists toexercise. Tag push is a separate, explicitly authorised step.