|
| 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