Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
backend/internal/httpd/apispec/openapi.yaml text eol=lf
38 changes: 38 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
pull_request:
paths:
- "backend/**"
- "frontend/**"
- ".github/workflows/go.yml"

permissions:
Expand Down Expand Up @@ -68,3 +69,40 @@ jobs:
working-directory: backend
# Blocking on the full ruleset: the tree is clean at zero findings, so
# any new issue fails CI rather than being grandfathered.

# gen-verify regenerates the code-first artifacts (openapi.yaml from Go, then
# the frontend TS types from that spec) and fails if the committed copies are
# stale — i.e. someone changed a Go contract type without running
# `go generate ./...` + `npm run gen:api`.
gen-verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: backend/go.mod
cache: false

- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Generate OpenAPI from Go
working-directory: backend
run: go generate ./...

- name: Generate TypeScript from OpenAPI
working-directory: frontend
run: |
npm ci
npm run gen:api

- name: Fail on stale generated files
run: |
if ! git diff --exit-code; then
echo "::error::Generated files are stale. Run 'go generate ./...' in backend and 'npm run gen:api' in frontend, then commit."
exit 1
fi
26 changes: 26 additions & 0 deletions backend/cmd/genspec/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Command genspec writes the code-first OpenAPI document produced by
// apispec.Build() to disk. It is invoked via `go generate` (see
// internal/httpd/apispec/gen.go); the output openapi.yaml is committed and
// embedded by the apispec package.
package main

import (
"flag"
"log"
"os"

"github.com/aoagents/agent-orchestrator/backend/internal/httpd/apispec/specgen"
)

func main() {
out := flag.String("out", "openapi.yaml", "output path for the generated OpenAPI document")
flag.Parse()

doc, err := specgen.Build()
if err != nil {
log.Fatalf("genspec: build openapi: %v", err)
}
if err := os.WriteFile(*out, doc, 0o600); err != nil {
log.Fatalf("genspec: write %s: %v", *out, err)
}
}
4 changes: 4 additions & 0 deletions backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ require (
github.com/go-chi/chi/v5 v5.1.0
github.com/pressly/goose/v3 v3.27.1
github.com/spf13/cobra v1.10.1
github.com/swaggest/jsonschema-go v0.3.79
github.com/swaggest/openapi-go v0.2.61
golang.org/x/sys v0.43.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.51.0
Expand All @@ -23,8 +25,10 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/swaggest/refl v1.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.20.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
Expand Down
22 changes: 22 additions & 0 deletions backend/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
github.com/bool64/dev v0.2.43 h1:yQ7qiZVef6WtCl2vDYU0Y+qSq+0aBrQzY8KXkklk9cQ=
github.com/bool64/dev v0.2.43/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
Expand All @@ -15,6 +19,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
Expand All @@ -30,6 +36,8 @@ github.com/pressly/goose/v3 v3.27.1/go.mod h1:maruOxsPnIG2yHHyo8UqKWXYKFcH7Q76cs
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
Expand All @@ -38,6 +46,18 @@ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
github.com/swaggest/jsonschema-go v0.3.79 h1:0TOShCbAJ9Xjt1e2W83l+QtMQSG2pbun2EkiYTyafCs=
github.com/swaggest/jsonschema-go v0.3.79/go.mod h1:GqVmJ+XNLeUHhFIhHNKc+C68euxfrl3a3aoZH4vTRl0=
github.com/swaggest/openapi-go v0.2.61 h1:psc+LE7pWhEjmJpmkti9tUmBPkkobdUNflBf5Ps6JSc=
github.com/swaggest/openapi-go v0.2.61/go.mod h1:786CwSwleh1IorB0nfwYGESWf83JgQh6fBc1PeJe4Iw=
github.com/swaggest/refl v1.4.0 h1:CftOSdTqRqs100xpFOT/Rifss5xBV/CT0S/FN60Xe9k=
github.com/swaggest/refl v1.4.0/go.mod h1:4uUVFVfPJ0NSX9FPwMPspeHos9wPFlCMGoPRllUbpvA=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
Expand All @@ -50,6 +70,8 @@ golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
Expand Down
8 changes: 7 additions & 1 deletion backend/internal/httpd/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/controllers"
"github.com/aoagents/agent-orchestrator/backend/internal/httpd/envelope"
"github.com/aoagents/agent-orchestrator/backend/internal/project"
"github.com/aoagents/agent-orchestrator/backend/internal/session"
)

// APIDeps bundles every Manager the API layer's controllers depend on.
Expand All @@ -19,13 +20,15 @@ import (
// registered but returns the OpenAPI-backed 501 response.
type APIDeps struct {
Projects project.Manager
Sessions *session.Manager
}

// API owns one controller per resource and is the single Register call the
// router invokes to mount the /api/v1 surface.
type API struct {
cfg config.Config
projects *controllers.ProjectsController
sessions *controllers.SessionsController
}

// NewAPI constructs the API surface from its dependencies. cfg carries the
Expand All @@ -37,6 +40,9 @@ func NewAPI(cfg config.Config, deps APIDeps) *API {
projects: &controllers.ProjectsController{
Mgr: deps.Projects,
},
sessions: &controllers.SessionsController{
Mgr: deps.Sessions,
},
}
}

Expand All @@ -55,7 +61,7 @@ func (a *API) Register(root chi.Router) {
r.Group(func(r chi.Router) {
r.Use(middleware.Timeout(timeout))
a.projects.Register(r)
// Sibling REST controllers plug in here.
a.sessions.Register(r)
})
// Surfaces that intentionally bypass the REST timeout register at this level.
})
Expand Down
5 changes: 5 additions & 0 deletions backend/internal/httpd/apispec/apispec.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ func New(yamlBytes []byte) (*Spec, error) {
return &Spec{doc: doc}, nil
}

// Embedded returns the raw bytes of the committed, embedded openapi.yaml. The
// code-first generator's drift and route-parity tests compare against it
// (specgen.Build() must equal these bytes); ServeYAML writes the same bytes.
func Embedded() []byte { return openapiYAML }

// Operation returns the spec slice for a single (method, path) pair, ready
// to be JSON-serialised. The slice is the OpenAPI Operation object (the
// inner block under e.g. paths./projects.get), with parent path-level
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/httpd/apispec/gen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package apispec

// openapi.yaml is generated from Go (see build.go) — do not edit it by hand.
// Regenerate with `go generate ./...` from the backend module root.
//
//go:generate go run ../../../cmd/genspec -out openapi.yaml
Loading
Loading