Skip to content

Commit 27152d1

Browse files
committed
chore: apply generated middleware pipeline patch
1 parent daa4e9f commit 27152d1

1 file changed

Lines changed: 294 additions & 0 deletions

File tree

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
name: Apply generated middleware pipeline patch
2+
3+
on:
4+
push:
5+
branches:
6+
- codex/generated-middleware-pipeline
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
patch:
13+
runs-on: ubuntu-latest
14+
timeout-minutes: 30
15+
steps:
16+
- uses: actions/checkout@v6
17+
with:
18+
fetch-depth: 0
19+
- uses: actions/setup-go@v6
20+
with:
21+
go-version-file: go.mod
22+
cache: true
23+
- name: Apply source and test changes
24+
shell: bash
25+
run: |
26+
python3 - <<'PY'
27+
from pathlib import Path
28+
29+
def replace_once(path: str, old: str, new: str) -> None:
30+
file = Path(path)
31+
text = file.read_text()
32+
count = text.count(old)
33+
if count != 1:
34+
raise SystemExit(f"{path}: expected one match, found {count}\n--- needle ---\n{old}")
35+
file.write_text(text.replace(old, new, 1))
36+
37+
replace_once(
38+
"internal/appgen/source_middleware.go",
39+
'''func registeredMiddlewaresDecl() ast.Decl {''',
40+
'''func applyRegisteredMiddlewaresExpr(handler ast.Expr) ast.Expr {
41+
\treturn &ast.CallExpr{
42+
\t\tFun: sel("gowdkruntime", "ApplyMiddlewares"),
43+
\t\tArgs: []ast.Expr{
44+
\t\t\thandler,
45+
\t\t\tcall(id("registeredMiddlewares")),
46+
\t\t},
47+
\t\tEllipsis: token.Pos(1),
48+
\t}
49+
}
50+
51+
func registeredMiddlewaresDecl() ast.Decl {''',
52+
)
53+
54+
replace_once(
55+
"internal/appgen/source_lifecycle.go",
56+
'''\t\t&ast.IfStmt{
57+
\t\t\tCond: notNil("err"),
58+
\t\t\tBody: block(&ast.ReturnStmt{Results: []ast.Expr{id("nil"), id("err")}}),
59+
\t\t},
60+
\t\tdefine([]ast.Expr{id("values")}, &ast.CompositeLit{''',
61+
'''\t\t&ast.IfStmt{
62+
\t\t\tCond: notNil("err"),
63+
\t\t\tBody: block(&ast.ReturnStmt{Results: []ast.Expr{id("nil"), id("err")}}),
64+
\t\t},
65+
\t\tdefine([]ast.Expr{id("handler")}, applyRegisteredMiddlewaresExpr(id("mux"))),
66+
\t\tdefine([]ast.Expr{id("values")}, &ast.CompositeLit{''',
67+
)
68+
replace_once(
69+
"internal/appgen/source_lifecycle.go",
70+
'''\t\t\t\tkeyValue("Handler", id("mux")),''',
71+
'''\t\t\t\tkeyValue("Handler", id("handler")),''',
72+
)
73+
74+
replace_once(
75+
"internal/appgen/source.go",
76+
'''func handlerDecl() ast.Decl {
77+
\treturn funcDecl("Handler", nil, []*ast.Field{
78+
\t\t{Type: sel("http", "Handler")},
79+
\t\t{Type: id("error")},
80+
\t}, []ast.Stmt{
81+
\t\t&ast.ReturnStmt{Results: []ast.Expr{call(sel("ServeMux"))}},
82+
\t})
83+
}''',
84+
'''func handlerDecl() ast.Decl {
85+
\treturn funcDecl("Handler", nil, []*ast.Field{
86+
\t\t{Type: sel("http", "Handler")},
87+
\t\t{Type: id("error")},
88+
\t}, []ast.Stmt{
89+
\t\tdefine([]ast.Expr{id("mux"), id("err")}, call(id("newServeMux"), call(sel("gowdkruntime", "InstanceIdentity")))),
90+
\t\t&ast.IfStmt{
91+
\t\t\tCond: notNil("err"),
92+
\t\t\tBody: block(&ast.ReturnStmt{Results: []ast.Expr{id("nil"), id("err")}}),
93+
\t\t},
94+
\t\t&ast.ReturnStmt{Results: []ast.Expr{
95+
\t\t\tapplyRegisteredMiddlewaresExpr(id("mux")),
96+
\t\t\tid("nil"),
97+
\t\t}},
98+
\t})
99+
}''',
100+
)
101+
102+
replace_once(
103+
"internal/appgen/source.go",
104+
'''func serveMuxDecl(options Options, embedded bool) ast.Decl {
105+
\treturn funcDecl("ServeMux", nil, []*ast.Field{
106+
\t\t{Type: &ast.StarExpr{X: sel("http", "ServeMux")}},
107+
\t\t{Type: id("error")},
108+
\t}, []ast.Stmt{
109+
\t\t&ast.ReturnStmt{Results: []ast.Expr{call(id("newServeMux"), call(sel("gowdkruntime", "InstanceIdentity")))}},
110+
\t})
111+
}''',
112+
'''func serveMuxDecl(options Options, embedded bool) ast.Decl {
113+
\treturn funcDecl("ServeMux", nil, []*ast.Field{
114+
\t\t{Type: &ast.StarExpr{X: sel("http", "ServeMux")}},
115+
\t\t{Type: id("error")},
116+
\t}, []ast.Stmt{
117+
\t\tdefine([]ast.Expr{id("routes"), id("err")}, call(id("newServeMux"), call(sel("gowdkruntime", "InstanceIdentity")))),
118+
\t\t&ast.IfStmt{
119+
\t\t\tCond: notNil("err"),
120+
\t\t\tBody: block(&ast.ReturnStmt{Results: []ast.Expr{id("nil"), id("err")}}),
121+
\t\t},
122+
\t\tdefine([]ast.Expr{id("mux")}, call(sel("http", "NewServeMux"))),
123+
\t\texprStmt(call(selExpr(id("mux"), "Handle"), stringLit("/"), applyRegisteredMiddlewaresExpr(id("routes")))),
124+
\t\t&ast.ReturnStmt{Results: []ast.Expr{id("mux"), id("nil")}},
125+
\t})
126+
}''',
127+
)
128+
129+
replace_once(
130+
"internal/appgen/source.go",
131+
'''\tif embedded {
132+
\t\tstmts = append(stmts, exprStmt(call(selExpr(id("mux"), "Handle"), stringLit("/"), &ast.CallExpr{
133+
\t\t\tFun: sel("gowdkruntime", "ApplyMiddlewares"),
134+
\t\t\tArgs: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: &ast.CompositeLit{
135+
\t\t\t\tType: sel("gowdkruntime", "Handler"),
136+
\t\t\t\tElts: embeddedHandlerFields(options, id("identity")),
137+
\t\t\t}}, call(id("registeredMiddlewares"))},
138+
\t\t\tEllipsis: token.Pos(1),
139+
\t\t})))
140+
\t} else {
141+
\t\tstmts = append(stmts, exprStmt(call(selExpr(id("mux"), "Handle"), stringLit("/"), backendOnlyHandlerExpr(options))))
142+
\t}''',
143+
'''\tif embedded {
144+
\t\tstmts = append(stmts, exprStmt(call(selExpr(id("mux"), "Handle"), stringLit("/"), &ast.UnaryExpr{
145+
\t\t\tOp: token.AND,
146+
\t\t\tX: &ast.CompositeLit{
147+
\t\t\t\tType: sel("gowdkruntime", "Handler"),
148+
\t\t\t\tElts: embeddedHandlerFields(options, id("identity")),
149+
\t\t\t},
150+
\t\t})))
151+
\t} else {
152+
\t\tstmts = append(stmts, exprStmt(call(selExpr(id("mux"), "Handle"), stringLit("/"), backendOnlyHandlerExpr(options))))
153+
\t}''',
154+
)
155+
156+
replace_once(
157+
"internal/appgen/source.go",
158+
'''func backendOnlyHandlerExpr(options Options) ast.Expr {
159+
\thandler := backendOnlyBaseHandlerExpr(options)
160+
\tif headers := securityHeadersExpr(options); headers != nil {
161+
\t\thandler = call(sel("http", "HandlerFunc"), backendOnlySecurityHeadersHandlerFunc(handler, headers))
162+
\t}
163+
\treturn &ast.CallExpr{
164+
\t\tFun: sel("gowdkruntime", "ApplyMiddlewares"),
165+
\t\tArgs: []ast.Expr{handler, call(id("registeredMiddlewares"))},
166+
\t\tEllipsis: token.Pos(1),
167+
\t}
168+
}''',
169+
'''func backendOnlyHandlerExpr(options Options) ast.Expr {
170+
\thandler := backendOnlyBaseHandlerExpr(options)
171+
\tif headers := securityHeadersExpr(options); headers != nil {
172+
\t\thandler = call(sel("http", "HandlerFunc"), backendOnlySecurityHeadersHandlerFunc(handler, headers))
173+
\t}
174+
\treturn handler
175+
}''',
176+
)
177+
178+
replace_once(
179+
"internal/appgen/appgen_test.go",
180+
'''\t\t`mux.Handle("/", gowdkruntime.ApplyMiddlewares(&gowdkruntime.Handler{`,''',
181+
'''\t\t`handler := gowdkruntime.ApplyMiddlewares(mux, registeredMiddlewares()...)`,
182+
\t\t`Handler: handler, Mux: mux`,
183+
\t\t`return gowdkruntime.ApplyMiddlewares(mux, registeredMiddlewares()...), nil`,
184+
\t\t`mux.Handle("/", &gowdkruntime.Handler{`,''',
185+
)
186+
187+
replace_once(
188+
"internal/appgen/appgen_test.go",
189+
'''\tassertSourceOrder(t, source,
190+
\t\t`mux.Handle("/sitemap.xml", gowdkseo.Handler`,
191+
\t\t`mux.Handle("/", gowdkruntime.ApplyMiddlewares`,
192+
\t)
193+
}''',
194+
'''\tassertSourceOrder(t, source,
195+
\t\t`mux.Handle("/sitemap.xml", gowdkseo.Handler`,
196+
\t\t`mux.Handle("/", &gowdkruntime.Handler`,
197+
\t)
198+
\tfor _, want := range []string{
199+
\t\t`handler := gowdkruntime.ApplyMiddlewares(mux, registeredMiddlewares()...)`,
200+
\t\t`mux.Handle("/", gowdkruntime.ApplyMiddlewares(routes, registeredMiddlewares()...))`,
201+
\t} {
202+
\t\tif !strings.Contains(source, want) {
203+
\t\t\tt.Fatalf("expected generated middleware pipeline to contain %q:\\n%s", want, source)
204+
\t\t}
205+
\t}
206+
\tif strings.Contains(source, `mux.Handle("/", gowdkruntime.ApplyMiddlewares(&gowdkruntime.Handler`) {
207+
\t\tt.Fatalf("generated root route must stay unwrapped until the final mux is composed:\\n%s", source)
208+
\t}
209+
}''',
210+
)
211+
212+
replace_once(
213+
"runtime/app/lifecycle_test.go",
214+
'''\t"net/http"\n''',
215+
'''\t"net/http"\n\t"net/http/httptest"\n''',
216+
)
217+
replace_once(
218+
"runtime/app/lifecycle_test.go",
219+
'''func TestRunIgnoresNilAndNoOpServices(t *testing.T) {''',
220+
'''func TestMiddlewareWrappedMuxIncludesRoutesMountedAfterComposition(t *testing.T) {
221+
\tmux := http.NewServeMux()
222+
\tvar calls atomic.Int32
223+
\thandler := ApplyMiddlewares(mux, func(next http.Handler) http.Handler {
224+
\t\treturn http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
225+
\t\t\tcalls.Add(1)
226+
\t\t\tresponse.Header().Set("X-GOWDK-Middleware", "applied")
227+
\t\t\tnext.ServeHTTP(response, request)
228+
\t\t})
229+
\t})
230+
\tmux.HandleFunc("/service", func(response http.ResponseWriter, _ *http.Request) {
231+
\t\tresponse.WriteHeader(http.StatusNoContent)
232+
\t})
233+
234+
\tresponse := httptest.NewRecorder()
235+
\thandler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/service", nil))
236+
\tif response.Code != http.StatusNoContent {
237+
\t\tt.Fatalf("status = %d, want %d", response.Code, http.StatusNoContent)
238+
\t}
239+
\tif got := response.Header().Get("X-GOWDK-Middleware"); got != "applied" {
240+
\t\tt.Fatalf("middleware header = %q, want applied", got)
241+
\t}
242+
\tif got := calls.Load(); got != 1 {
243+
\t\tt.Fatalf("middleware calls = %d, want 1", got)
244+
\t}
245+
}
246+
247+
func TestRunIgnoresNilAndNoOpServices(t *testing.T) {''',
248+
)
249+
250+
replace_once(
251+
"docs/reference/hooks.md",
252+
'''Register middleware before calling `Handler()` or `ServeMux()`. Middleware runs
253+
in registration order; a middleware that does not call `next` owns the response
254+
and skips generated headers, metrics, static serving, and request-time route
255+
dispatch for that request. App-owned startup code can still wrap the returned
256+
handler with ordinary middleware:''',
257+
'''Register middleware before calling `App()`, `Handler()`, or `ServeMux()`.
258+
Middleware runs in registration order; a middleware that does not call `next`
259+
owns the response and skips generated headers, metrics, static serving, and
260+
request-time route dispatch for that request.
261+
262+
`App()` snapshots the registered chain around its raw application mux. Routes
263+
mounted by lifecycle services before server startup therefore pass through the
264+
same middleware as health, static, backend, dynamic sitemap, and realtime
265+
routes. `ServeMux()` mounts the generated route graph behind the same finalized
266+
wrapper; routes added directly to that returned mux afterward are caller-owned
267+
and need their own middleware policy.
268+
269+
App-owned startup code can still wrap the returned handler with ordinary
270+
middleware:''',
271+
)
272+
PY
273+
274+
gofmt -w \
275+
internal/appgen/source.go \
276+
internal/appgen/source_lifecycle.go \
277+
internal/appgen/source_middleware.go \
278+
internal/appgen/appgen_test.go \
279+
runtime/app/lifecycle_test.go
280+
281+
- name: Regenerate appgen golden output
282+
run: go test ./internal/appgen -update
283+
- name: Run focused tests
284+
run: go test ./internal/appgen ./runtime/app
285+
- name: Validate and commit
286+
shell: bash
287+
run: |
288+
git diff --check
289+
rm .github/workflows/codex-generated-middleware-patch.yml
290+
git config user.name "github-actions[bot]"
291+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
292+
git add -A
293+
git commit -m "fix(appgen): wrap the finalized route graph with middleware"
294+
git push origin HEAD:codex/generated-middleware-pipeline

0 commit comments

Comments
 (0)