-
Notifications
You must be signed in to change notification settings - Fork 957
docs(devlog): record the release-readiness train and its outcome #2974
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
01b2669
docs(devlog): plan the green-PR merge train with overlap-derived orde…
lidge-jun 123e1ed
docs(devlog): plan the release-readiness train from the 260830 snapshot
lidge-jun 42d4818
docs(devlog): correct the train plan against two audited blockers
lidge-jun e3c5f70
docs(devlog): record the release-readiness train outcome
lidge-jun File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # 260829 — Green-PR merge train | ||
|
|
||
| Eight rebased pull requests reached a fully green test matrix on `dev@e546c160b` and are | ||
| candidates to land. This unit records why each one is safe to merge, the order the merges | ||
| must happen in, and the two integration designs that have to be built before their PRs can | ||
| land at all. | ||
|
|
||
| ## Why this needs a written analysis rather than eight merge clicks | ||
|
|
||
| The eight diffs are not independent. Five pairs touch the same file, and three of those | ||
| pairs touch `src/config.ts` — the shared config parser every provider path reads. Merging | ||
| in arrival order would produce conflicts that a later merge resolves blindly, which is the | ||
| failure mode that produced the #2850 → #2851 follow-up: a merge that looked clean and | ||
| needed a security repair one hour later. | ||
|
|
||
| A second reason is drift. `dev` moved from `e546c160b` to `8d1dc1f5d` while this set was | ||
| being prepared (#2861, #2862, #2865, #2868, #2869). Every green result recorded earlier | ||
| belongs to the head that produced it, not to the head the merge will land on. | ||
|
|
||
| ## Regression-impact inventory | ||
|
|
||
| Source files each PR touches, ignoring docs and tests: | ||
|
|
||
| | PR | Subject | `src/` surface | | ||
| |---|---|---| | ||
| | #2365 | usage cache metrics | `usage/summary.ts` | | ||
| | #2429 | `test:changed` local check | `AGENTS.md` only | | ||
| | #1756 | Grok per-model reasoning effort | `grok/{catalog,effort,inject,models}.ts`, `server/index.ts` | | ||
| | #2050 | combo routing strategies | `combos/*`, `cli/*`, `providers/quota*.ts`, `router.ts`, `types/config.ts` | | ||
| | #2827 | trusted Responses request id | `server/index.ts`, `server/request-log.ts` | | ||
| | #2364 | Vercel AI Gateway routing | `adapters/openai-chat.ts`, `config.ts`, `providers/vercel-gateway-routing.ts`, `server/auth-cors.ts`, `types.ts`, `types/provider.ts` | | ||
| | #2712 | xAI `x_search` opt-in | `adapters/{openai-responses,xai-web-search}.ts`, `config.ts`, `server/auth-cors.ts`, `server/responses/core.ts`, `types/provider.ts` | | ||
| | #2854 | blocked-model redirection | `config.ts`, `lib/shadow-call.ts`, `router.ts`, `types/config.ts` | | ||
|
|
||
| ## Overlap matrix | ||
|
|
||
| Computed by intersecting the `src/` file sets, not by reading titles: | ||
|
|
||
| ``` | ||
| #1756 x #2827 src/server/index.ts | ||
| #2050 x #2854 src/router.ts, src/types/config.ts | ||
| #2364 x #2712 src/config.ts, src/server/auth-cors.ts, src/types/provider.ts | ||
| #2364 x #2854 src/config.ts | ||
| #2712 x #2854 src/config.ts | ||
| ``` | ||
|
|
||
| Collision degree per PR: `#2854=3`, `#2364=2`, `#2712=2`, `#1756=1`, `#2050=1`, | ||
| `#2827=1`, `#2365=0`, `#2429=0`. | ||
|
|
||
| ## Derived merge order | ||
|
|
||
| Ascending collision degree, so each merge lands against the largest possible amount of | ||
| already-settled `dev`, and the diff most likely to conflict resolves last against a tree | ||
| that already contains everything it must coexist with: | ||
|
|
||
| ``` | ||
| #2365 -> #2429 -> #1756 -> #2050 -> #2827 -> #2364 -> #2712 -> #2854 | ||
| ``` | ||
|
|
||
| `#2854` merging last is the load-bearing part of this order. It touches `config.ts` | ||
| alongside both #2364 and #2712, and `router.ts` alongside #2050 — it is the only PR that | ||
| collides with more than one other cluster, so it is the only one whose conflicts are | ||
| cheaper to resolve once rather than three times. | ||
|
|
||
| Waves, because `dev` CI is the gate and a wave is the smallest useful unit to verify: | ||
|
|
||
| - **Wave A** — `#2365`, `#2429`, `#1756`: zero or single collisions, no shared config surface. | ||
| - **Wave B** — `#2050`, `#2827`: single collisions each. | ||
| - **Wave C** — `#2364`, `#2712`, `#2854`: the `config.ts` / `auth-cors.ts` cluster. | ||
|
|
||
| **Corrected after audit.** An earlier draft claimed wave B's collisions were "already | ||
| settled by wave A". They are not: `#2050` collides with `#2854`, which is in wave C, and | ||
| `#2827` collides with `#1756` in wave A. Only `#2827`'s is settled by A. `#2050` is placed | ||
| in B because its one collision partner merges later, so `#2854` absorbs the resolution — | ||
| which is the same reason `#2854` is last. | ||
|
|
||
| ## Per-merge mechanics (added after audit) | ||
|
|
||
| Merge order alone does not make a later PR land against settled `dev`; it only decides who | ||
| resolves the conflict. All eight heads currently share merge base `e546c160b`, and `dev` is | ||
| already five commits past it, so each merge must carry its own freshness step: | ||
|
|
||
| 1. Rebase the PR onto the then-current `dev`. | ||
| 2. Push and let CI run on that exact head. | ||
| 3. Merge only on a green technical matrix. | ||
| 4. Re-read `dev` CI before starting the next merge. | ||
|
|
||
| Skipping step 1 would also drift heads past the repository's ten-commit readiness | ||
| allowance as the train advances, so the freshness step is a gate requirement and not only | ||
| a correctness preference. | ||
|
|
||
| #2429 and #2827 cannot enter their wave until the two designs below are built. | ||
|
|
||
| ## What this unit does not cover | ||
|
|
||
| Five rebased PRs are excluded because CI found real defects in them, not stale-base | ||
| artifacts: #2716 (display name leaks into the opencode selector), #2351 (management route | ||
| not declared in the registry), #2213 (xAI wire defaults), #2496 (residual failures), and | ||
| #1829 (macOS launcher flake, unrelated to its own diff). They stay open. | ||
79 changes: 79 additions & 0 deletions
79
devlog/_plan/260829_green_pr_merge_train/010_wp1_2429_privacy_scan.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # wp1 — #2429: the privacy scanner rejects its own test fixture | ||
|
|
||
| ## Symptom | ||
|
|
||
| `gates` fails on #2429's head. The failing step is `Privacy scan`, not a test: | ||
|
|
||
| ``` | ||
| Privacy scan failed: | ||
| tests/test-runner.test.ts:42 email: test<at>opencodex.invalid | ||
| error: script "privacy:scan" exited with code 1 | ||
| ``` | ||
|
|
||
| Every test shard, `macos`, and all three `npm-global` matrices pass. The only red checks | ||
| are `gates` and the two draft-checklist gates (`hygiene`, `enforce-target`), which are | ||
| process gates rather than code failures. | ||
|
|
||
| ## Cause | ||
|
|
||
| The PR's test helper commits a fixture repository and needs a git identity to do it: | ||
|
|
||
| ```ts | ||
| runGit( | ||
| cwd, | ||
| "-c", "user.name=OpenCodex Test", | ||
| "-c", "user.email=test<at>opencodex.invalid", | ||
| "commit", "-m", message, | ||
| ); | ||
| ``` | ||
|
|
||
| `scripts/privacy-scan.ts` matches `/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi` across the | ||
| tree. The fixture address satisfies that pattern, and `.invalid` is not on the | ||
| allow-list, so the scanner is behaving correctly — the literal really is an email-shaped | ||
| string in a tracked file. | ||
|
|
||
| (This document writes the address as `test<at>opencodex.invalid` for exactly the same | ||
| reason the fix exists: quoting the literal verbatim would make this file trip the scanner | ||
| too. It did, on the first commit of this unit.) | ||
|
|
||
| This is not a false positive worth loosening the scanner for. The scanner's value comes | ||
| from having almost no exceptions; every added exception is a hole someone's real address | ||
| can later fall through. | ||
|
|
||
| ## Design | ||
|
|
||
| Use the idiom the repository already uses for exactly this problem. `privacy-scan.ts` and | ||
| its own fixtures avoid self-matching by never writing an email as one literal: | ||
|
|
||
| ```ts | ||
| ["1", "gmail.com"].join("@") | ||
| ["stranger", "third-party.example.org"].join("@") | ||
| ``` | ||
|
|
||
| So the fix is a join at the call site: | ||
|
|
||
| ```ts | ||
| const TEST_COMMIT_EMAIL = ["test", "opencodex.invalid"].join("@"); | ||
| ``` | ||
|
|
||
| The value handed to git is byte-identical, so the fixture commits exactly as before and no | ||
| test expectation changes. The scanner no longer sees an email literal because there is no | ||
| longer one in the source. | ||
|
|
||
| ### Rejected alternatives | ||
|
|
||
| - **Add `tests/test-runner.test.ts` to the scanner's allow-list.** The allow-list currently | ||
| holds two narrowly-argued entries (`a@b.com` in tests, a URL-userinfo fixture that reads | ||
| as `pw@host`). Adding a whole file would exempt every future email added to it. | ||
| - **Allow the `.invalid` TLD globally.** `.invalid` is reserved and safe in principle, but | ||
| the exemption would apply repository-wide and the scanner's job is to be boring, not | ||
| clever. | ||
| - **Drop the git identity and rely on ambient config.** CI runners have no global | ||
| `user.email`, so the fixture commit would fail. The identity is load-bearing. | ||
|
|
||
| ## Verification | ||
|
|
||
| - `bun run privacy:scan` exits 0 locally. | ||
| - `bun x tsc --noEmit` clean. | ||
| - `gates` returns success on the pushed head. | ||
| - No local full-suite run: the user has forbidden it, and CI covers the shards. |
114 changes: 114 additions & 0 deletions
114
devlog/_plan/260829_green_pr_merge_train/020_wp2_2827_expose_header.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # wp2 — #2827: the request id a browser cannot read | ||
|
|
||
| ## Symptom | ||
|
|
||
| #2827 is green across every check. The defect is not a failing test — it is a feature that | ||
| silently does nothing for its stated consumer, found by review rather than by CI. | ||
|
|
||
| The PR adds a response header carrying the request-log id: | ||
|
|
||
| ```ts | ||
| const REQUEST_LOG_ID_RESPONSE_HEADER = "x-opencodex-request-id"; | ||
|
|
||
| function withRequestLogId(response: Response, requestId: string): Response { | ||
| const headers = new Headers(response.headers); | ||
| headers.set(REQUEST_LOG_ID_RESPONSE_HEADER, requestId); | ||
| return new Response(response.body, { status: response.status, statusText: response.statusText, headers }); | ||
| } | ||
| ``` | ||
|
|
||
| ## Cause | ||
|
|
||
| `corsHeaders()` in `src/server/auth-cors.ts` emits `Access-Control-Allow-Origin`, | ||
| `-Allow-Methods`, `-Allow-Headers`, and `Vary` — but no `Access-Control-Expose-Headers`. | ||
|
|
||
| The CORS default is that cross-origin JavaScript may read only the seven CORS-safelisted | ||
| response headers. A custom `x-` header is not one of them, so `response.headers.get( | ||
| "x-opencodex-request-id")` returns `null` in a browser even though the header is on the | ||
| wire and visible in devtools. | ||
|
|
||
| The tests pass because they call the handler directly. Server-side fetches see every header; | ||
| the restriction is enforced by the browser, and nothing in the suite is a browser. This is | ||
| the same shape of gap as a feature guarded by a flag no test sets — the code is right and | ||
| unreachable. | ||
|
|
||
| `-Allow-Headers` does not help: it governs what the **request** may send, not what the | ||
| **response** may reveal. | ||
|
|
||
| ## Design | ||
|
|
||
| Add the response-header allow-list next to the request one, naming exactly the header this | ||
| proxy adds: | ||
|
|
||
| ```ts | ||
| "Access-Control-Expose-Headers": REQUEST_LOG_ID_RESPONSE_HEADER, | ||
| ``` | ||
|
|
||
| Two constraints on how: | ||
|
|
||
| 1. **The constant moves to `auth-cors.ts` and `server/index.ts` imports it.** Two string | ||
| literals that must agree will eventually disagree; the header name has one owner. | ||
| 2. **`Vary` does not change.** `Expose-Headers` here is a constant, not a function of the | ||
| request, so it introduces no new cache dimension. Adding it to `Vary` would fragment the | ||
| cache for no reason. | ||
|
|
||
| ### Scope note | ||
|
|
||
| **Corrected after audit.** The first draft said `managementCorsHeaders()` was "separate and | ||
| not touched". That was wrong, and the audit caught it: | ||
|
|
||
| ```ts | ||
| export function managementCorsHeaders(req?: Request, config?: OcxConfig): Record<string, string> { | ||
| const headers = corsHeaders(); // <- inherits everything, including a new Expose-Headers | ||
| ... | ||
| } | ||
| ``` | ||
|
|
||
| Adding the key inside `corsHeaders()` would therefore have propagated it to every | ||
| management response, which is the opposite of the scope the design claimed. The two | ||
| options are to set it only in the data-plane wrapper, or to add it in `corsHeaders()` and | ||
| strip it in `managementCorsHeaders()`. | ||
|
|
||
| Take the first. `withCors()` is the data-plane wrapper and the only path that serves | ||
| `/v1/responses`, so exposing the header there grants exactly the reach the feature needs. | ||
| Stripping a key the shared helper just added would leave two places that must stay in | ||
| agreement about a header neither of them owns. | ||
|
|
||
| Concretely: `corsHeaders()` is left alone, and `withCors()` sets | ||
| `Access-Control-Expose-Headers: x-opencodex-request-id` after copying the shared keys. | ||
|
|
||
| ## Regression test | ||
|
|
||
| The existing suite cannot catch this class of defect, so the test asserts the header | ||
| contract directly rather than the behavior of a browser we do not have: | ||
|
|
||
| - `withCors(new Response(...), req, policy)` output contains `Access-Control-Expose-Headers` | ||
| naming `x-opencodex-request-id`. The assertion targets the wrapper, not `corsHeaders()`, | ||
| because the amended design deliberately leaves the shared helper untouched. | ||
| - The exposed name matches the header `withRequestLogId` actually sets — one assertion | ||
| comparing the two, so a future rename of either side fails here instead of shipping a | ||
| header nobody can read. | ||
| - `managementCorsHeaders()` output does NOT contain the key. This assertion is the one that | ||
| would have failed under the original design, so it is the reason the test exists. | ||
|
|
||
| ## Wrapper order (verified) | ||
|
|
||
| The route composes the two wrappers as: | ||
|
|
||
| ```ts | ||
| return withRequestLogId( | ||
| withCors(responseWithDeferredRequestLog(response, requestId, start, logCtx), req, policy), | ||
| requestId, | ||
| ); | ||
| ``` | ||
|
|
||
| `withCors()` runs first and `withRequestLogId()` wraps its result, copying headers through | ||
| `new Headers(response.headers)`. So an expose header set inside `withCors()` survives onto | ||
| the final response. Checked on #2827's head at `src/server/index.ts:1397` and `:1441`; no | ||
| success path carrying the request-id header bypasses `withCors()`. | ||
|
|
||
| ## Verification | ||
|
|
||
| - `bun x tsc --noEmit` clean. | ||
| - Focused run of the CORS and request-log tests only. | ||
| - CI green on the pushed head, including `gates`. |
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this restored unit to
_finand add its terminal disposition: a repository-wide history check shows that all eight candidates named here (#2365, #2429, #1756, #2050, #2827, #2364, #2712, and #2854) landed before this commit, so introducing their historical plan under_planincorrectly presents completed work as an open unit.AGENTS.md reference: AGENTS.md:L83-L86
Useful? React with 👍 / 👎.