Skip to content

release: v1.1.0 rehearsal - contract version gate, /health retirement, tag-gate on the real path - #16

Merged
wangrh0909 merged 14 commits into
masterfrom
release/v1.1.0
Sep 27, 2026
Merged

wangrh0909 merged 14 commits into
masterfrom
release/v1.1.0

Conversation

@voidvec

@voidvec voidvec commented Sep 24, 2026

Copy link
Copy Markdown
Owner

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's ci-gate and
deploy.yml's tag-gate both call _tag-gate.yml, and neither has yet been
asked 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 to
    1.1.0, and CHANGELOG.md's [Unreleased] body archived under
    ## [1.1.0] - 2026-09-24 so version-check's section test has something to
    find. Six published-package references (README*,
    docs/development/plugin_integration.md) follow to drogon-pay/1.1.0.
  • build(release): read the contract's version as a declaration site —
    examples/pay-server/openapi.yaml said 1.0.0 while the tree prepared
    1.1.0, because no gate read it. It is now the fourth site
    check_version_sync.py enforces, so AGENTS.md / TECH_SPECS.md / the
    release skill say "four declarations" and the contract stops shipping a
    version nothing compared.
  • fix(api)!: retire the /health alias of /readyz — the alias carried
    Sunset: 2026-08-28 and outlived it. Removing a route is breaking, so it
    waited for the bump. Package API/ABI is untouched: git diff master..HEAD -- libs/drogon-pay/ is empty; the route lived in the example
    host.
  • docs(deployment): clone from the renamed owner — the guide still told people
    to clone a lucaswang420 URL 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 404 alone could not distinguish "the alias is gone"
    from "no controller linked at all" (config.json sets use_implicit_page), so
    the case now asserts registration state via getHandlersInfo() for all three
    names (/health unregistered, /healthz + /readyz registered), plus two
    transport REQUIREs and the 404-and-no-Deprecation response checks.
  • build(release) — a second top-level info: block passed the new reader
    (it took the first and ignored the rest), and version: "1.1.0 matched no
    regex, 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 \d escape, two plain scalars containing : ), and had done
    so since they were written: both guards over the file scan it line by line.
  • docs(operations) — health_check_implementation.md,
    operations_manual.md and deploy.yml's ECS health step all still pointed at
    /health as live; the same round documents the debounce truth (a hung
    dependency reports "timeout"; an instance that never reached ready answers
    503 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:

  • 08b457b closed four false passes (tab after version:, plain-scalar
    continuation directly and across a blank, trailing nbsp) and pinned every
    spelling as an executable decision table —
    scripts/ci/version_sync_scenarios.py, a static-analysis step that prints
    its own count and asserts the live contract reads into the agreeing set.
  • d2d728c closed eleven more shapes that review found the table itself pinned
    wrong or the reader still read green, and retracted false claims this entry's
    first cut made (see Disclosures).
  • b1b91de operations docs, as above.
  • 84fb572 + 51af932 dealt 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 too
    narrowly — --- # new contract is a real split (a second document, which
    safe_load never reaches) that the exact-equality check walked straight past.
    Final rule: a column-0 ---/... line must be cleanly spelled — space
    padding and a comment only; a tab, a glued #, a word or a fourth dash is a
    scanner 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_all out of
    band; the CHANGELOG states the disagreement classes as replayable claims, not
    remembered numbers.

Verification

  • Full stdlib guard sweep at tip, each checked with its true exit code:
    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↔.codex byte parity),
    check_test_layout, check_migrations, check_openapi_routes
    (--lint-only and --routes-only), actionlint — all 0.
  • No C++ changed after 3c5aff1 (git log 3c5aff1..HEAD --name-only shows no
    .cc/.h), so the Windows Release build and
    ctest --test-dir build/windows-msvc -C Release → 100% (taken with the
    reworked 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.0 passing is what release.yml's
    version-check will 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 rewritten
out of history:

  • d2d728c says "nine" load-failing reads where the replay gives eight plus
    01.1.0 (a read that loads but names a non-SemVer version).
  • 84fb572 attributes the blanket marker ban to 08b457b (it shipped in
    d2d728c) and says in past tense that the runner "reported 46/46 while 52
    checks 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:26 and .github/CODEOWNERS still name the old lucaswang420
    owner — same drift class as the clone URL, different file kinds.
  • deploy.sh:211, deploy.bat:159 and healthcheck.sh:106 poll
    /api/health, a route that has never existed in this repository, so those
    wait loops time out rather than gate on anything.
  • deploy.yml:148/:200 curl staging-api.example.com / api.example.com
    placeholders — unless the deployment environment overrides them, those health
    gates assert nothing reachable.
  • scripts/clang_format.py --check walks the working tree, so a gitignored
    local libs/drogon-pay/src/models_backup/ makes it exit 1 on a laptop and pass
    in CI — the inverse of the index-vs-worktree bug the other guards already
    fixed. (clang_tidy_gate.py likewise needs the Linux preset's
    compile_commands.json and is not runnable on this laptop.)
  • .github/workflows/secrets-scan.yml declares no permissions: block.
  • The coverage ratchet's baseline is only as honest as the one green
    coverage.yml run that measured it.

After merge

Tag the master tip as v1.1.0 — that is the event this rehearsal exists to
exercise. Tag push is a separate, explicitly authorised step.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

voidvec and others added 14 commits September 27, 2026 11:20
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.
@wangrh0909
wangrh0909 merged commit 3ee02f9 into master Sep 27, 2026
9 checks passed
@voidvec
voidvec deleted the release/v1.1.0 branch September 27, 2026 06:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants