From f0a95eebe342495cf355b7e0e5b3f0ef5ede4106 Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 31 Jul 2026 16:16:27 +0200 Subject: [PATCH] refactor: serve the api specs as plain json (#38715) The api specs were Go templates whose committed form was not a valid swagger document, so `swagger-validate`, `generate-openapi.go` and `.spectral.yaml` each worked around it. They are now plain json, substituted at serve time. Renaming them off `.tmpl` also stops `make fmt` rewriting them, which used to bump their mtime and silently skip the next `make generate-swagger`. Also enables stricter spectral linting: extends `lint-swagger` to the OpenAPI 3 spec, turns on `openapi-tags`, `operation-singular-tag` and `operation-tag-defined`, adds a top-level `tags` array with descriptions to the swagger input, and drops the redundant `repository` tag from `POST /user/repos`. --------- Signed-off-by: silverwind Co-authored-by: wxiaoguang Co-authored-by: Giteabot --- .editorconfig | 2 +- .gitattributes | 3 +- .github/workflows/files-changed.yml | 3 +- .spectral.yaml | 5 --- Makefile | 15 +++---- build/generate-openapi.go | 43 ++++--------------- build/openapi3gen/convert.go | 5 ++- routers/api/v1/repo/repo.go | 2 +- routers/web/auth/oauth2_provider.go | 1 + routers/web/swagger_json.go | 32 +++++++++----- tailwind.config.ts | 1 - templates/swagger/v1-input.json | 17 ++++++++ ...3_json.tmpl => v1-openapi3.generated.json} | 43 +++++++++++++++++-- ...v1_json.tmpl => v1-swagger.generated.json} | 43 +++++++++++++++++-- templates/swagger/v1_input.json | 6 --- tests/integration/links_test.go | 36 ++++++++++++++++ 16 files changed, 178 insertions(+), 79 deletions(-) create mode 100644 templates/swagger/v1-input.json rename templates/swagger/{v1_openapi3_json.tmpl => v1-openapi3.generated.json} (99%) rename templates/swagger/{v1_json.tmpl => v1-swagger.generated.json} (99%) delete mode 100644 templates/swagger/v1_input.json diff --git a/.editorconfig b/.editorconfig index 703a834818..d095d8435f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,7 +18,7 @@ indent_style = tab [templates/custom/*.tmpl] insert_final_newline = false -[templates/swagger/*_json.tmpl] +[templates/swagger/*.generated.json] indent_style = space insert_final_newline = false diff --git a/.gitattributes b/.gitattributes index 3ddb8f641b..9cb8030976 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,8 +3,7 @@ *.pb.go linguist-generated /assets/*.json linguist-generated /public/assets/img/svg/*.svg linguist-generated -/templates/swagger/v1_json.tmpl linguist-generated -/templates/swagger/v1_openapi3_json.tmpl linguist-generated +/templates/swagger/*.generated.json linguist-generated /options/fileicon/** linguist-generated /vendor/** -text -eol linguist-vendored /web_src/js/vendor/** -text -eol linguist-vendored diff --git a/.github/workflows/files-changed.yml b/.github/workflows/files-changed.yml index 7a69f91517..9da47d3a4b 100644 --- a/.github/workflows/files-changed.yml +++ b/.github/workflows/files-changed.yml @@ -113,8 +113,7 @@ jobs: - "Dockerfile.rootless" swagger: - - "templates/swagger/v1_json.tmpl" - - "templates/swagger/v1_input.json" + - "templates/swagger/*.json" - "Makefile" - "package.json" - "pnpm-lock.yaml" diff --git a/.spectral.yaml b/.spectral.yaml index e547eea57d..54e5f1c006 100644 --- a/.spectral.yaml +++ b/.spectral.yaml @@ -4,9 +4,4 @@ rules: info-contact: off oas2-api-host: off oas2-parameter-description: off - oas2-schema: off - oas2-valid-schema-example: off - openapi-tags: off operation-description: off - operation-singular-tag: off - operation-tag-defined: off diff --git a/Makefile b/Makefile index ebcfd4819e..7cd744ba23 100644 --- a/Makefile +++ b/Makefile @@ -150,10 +150,10 @@ GO_SOURCES += $(GENERATED_GO_DEST) ESLINT_CONCURRENCY ?= 2 ESLINT_ARGS := --color --max-warnings=0 --concurrency $(ESLINT_CONCURRENCY) -SWAGGER_SPEC := templates/swagger/v1_json.tmpl -SWAGGER_SPEC_INPUT := templates/swagger/v1_input.json SWAGGER_EXCLUDE := gitea.dev/sdk -OPENAPI3_SPEC := templates/swagger/v1_openapi3_json.tmpl +SWAGGER_SPEC_INPUT := templates/swagger/v1-input.json +SWAGGER_SPEC := templates/swagger/v1-swagger.generated.json +OPENAPI3_SPEC := templates/swagger/v1-openapi3.generated.json TEST_MYSQL_HOST ?= mysql:3306 TEST_MYSQL_DBNAME ?= testgitea @@ -247,13 +247,10 @@ swagger-check: generate-swagger .PHONY: swagger-validate swagger-validate: ## check if the swagger spec is valid - @# swagger "validate" requires that the "basePath" must start with a slash, but we are using Golang template "{{...}}" - @$(SED_INPLACE) -E -e 's|"basePath":( *)"(.*)"|"basePath":\1"/\2"|g' './$(SWAGGER_SPEC)' # add a prefix slash to basePath + @# ensure no warnings @output="$$($(GO) run $(SWAGGER_PACKAGE) validate './$(SWAGGER_SPEC)' 2>&1)"; status=$$?; \ - $(SED_INPLACE) -E -e 's|"basePath":( *)"/(.*)"|"basePath":\1"\2"|g' './$(SWAGGER_SPEC)'; \ printf '%s\n' "$$output" | grep -v '^go: '; \ - [ $$status -eq 0 ] || exit $$status; \ - case "$$output" in *WARNING:*) exit 1;; esac + case "$$output" in *WARNING:*) exit 1;; esac; exit $$status .PHONY: generate-openapi3 generate-openapi3: $(OPENAPI3_SPEC) ## generate the OpenAPI 3.0 spec from the Swagger 2.0 spec @@ -317,7 +314,7 @@ lint-css-fix: node_modules ## lint css files and fix issues .PHONY: lint-swagger lint-swagger: node_modules ## lint swagger files - pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC) + pnpm exec spectral lint -q -F hint $(SWAGGER_SPEC) $(OPENAPI3_SPEC) .PHONY: lint-md lint-md: node_modules ## lint markdown files diff --git a/build/generate-openapi.go b/build/generate-openapi.go index f0cdd86634..dd73043a6f 100644 --- a/build/generate-openapi.go +++ b/build/generate-openapi.go @@ -10,7 +10,7 @@ // cleaner SDK output with proper enum types instead of anonymous strings. // // Run: go run build/generate-openapi.go -// Output: templates/swagger/v1_openapi3_json.tmpl +// Output: templates/swagger/v1-openapi3.generated.json //go:build ignore @@ -21,35 +21,21 @@ import ( "fmt" "log" "os" - "regexp" "sort" "strings" "gitea.dev/build/openapi3gen" - - "github.com/getkin/kin-openapi/openapi3" ) const ( - swaggerSpecPath = "templates/swagger/v1_json.tmpl" - openapi3OutPath = "templates/swagger/v1_openapi3_json.tmpl" - - appSubUrlVar = "{{.SwaggerAppSubUrl}}" - appVerVar = "{{.SwaggerAppVer}}" - - appSubUrlPlaceholder = "GITEA_APP_SUB_URL_PLACEHOLDER" - appVerPlaceholder = "0.0.0-gitea-placeholder" + swaggerSpecPath = "templates/swagger/v1-swagger.generated.json" + openapi3OutPath = "templates/swagger/v1-openapi3.generated.json" ) -var ( - appSubUrlRe = regexp.MustCompile(regexp.QuoteMeta(appSubUrlVar)) - appVerRe = regexp.MustCompile(regexp.QuoteMeta(appVerVar)) - - enumScanDirs = []string{ - "modules/structs", - "modules/commitstatus", - } -) +var enumScanDirs = []string{ + "modules/structs", + "modules/commitstatus", +} func main() { astEnumMap, err := openapi3gen.ScanSwaggerEnumTypes(enumScanDirs) @@ -68,28 +54,17 @@ func main() { log.Fatalf("reading swagger spec: %v", err) } - cleaned := appSubUrlRe.ReplaceAll(data, []byte(appSubUrlPlaceholder)) - cleaned = appVerRe.ReplaceAll(cleaned, []byte(appVerPlaceholder)) - - oas3, err := openapi3gen.Convert(cleaned, astEnumMap) + oas3, err := openapi3gen.Convert(data, astEnumMap) if err != nil { log.Fatalf("converting to openapi 3.0: %v", err) } - oas3.Servers = openapi3.Servers{ - {URL: appSubUrlPlaceholder + "/api/v1"}, - } - out, err := json.MarshalIndent(oas3, "", " ") if err != nil { log.Fatalf("marshaling openapi 3.0: %v", err) } - result := strings.ReplaceAll(string(out), appSubUrlPlaceholder, appSubUrlVar) - result = strings.ReplaceAll(result, appVerPlaceholder, appVerVar) - result = strings.TrimSpace(result) - - if err := os.WriteFile(openapi3OutPath, []byte(result), 0o644); err != nil { + if err := os.WriteFile(openapi3OutPath, out, 0o644); err != nil { log.Fatalf("writing openapi 3.0 spec: %v", err) } diff --git a/build/openapi3gen/convert.go b/build/openapi3gen/convert.go index d1ee1a3a68..922e0558dc 100644 --- a/build/openapi3gen/convert.go +++ b/build/openapi3gen/convert.go @@ -23,8 +23,8 @@ import ( var rxDeprecated = regexp.MustCompile(`(?i)(?:^|[\n.;])\s*deprecated\b`) // Convert parses a Swagger 2.0 spec and returns an OAS3 spec, applying -// Gitea-specific post-processing: file-schema fixups, URI formats, -// deprecated flags, and shared-enum extraction. +// Gitea-specific post-processing: server URL, file-schema fixups, URI +// formats, deprecated flags, and shared-enum extraction. // // astEnumMap is a value-set-key → Go-type-name(s) map (built by // ScanSwaggerEnumTypes). When a value set is shared by multiple Go types, @@ -42,6 +42,7 @@ func Convert(swaggerJSON []byte, astEnumMap map[string][]string) (*openapi3.T, e return nil, fmt.Errorf("converting to openapi 3.0: %w", err) } + oas3.Servers = openapi3.Servers{{URL: swagger2.BasePath}} fixFileSchemas(oas3) addURIFormats(oas3) addDeprecatedFlags(oas3) diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index e12f983615..f42f6c7e8a 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -277,7 +277,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre // Create one repository of mine func Create(ctx *context.APIContext) { - // swagger:operation POST /user/repos repository user createCurrentUserRepo + // swagger:operation POST /user/repos user createCurrentUserRepo // --- // summary: Create a repository // consumes: diff --git a/routers/web/auth/oauth2_provider.go b/routers/web/auth/oauth2_provider.go index a9b8b63940..755afb14c7 100644 --- a/routers/web/auth/oauth2_provider.go +++ b/routers/web/auth/oauth2_provider.go @@ -482,6 +482,7 @@ func OIDCWellKnown(ctx *context.Context) { ctx.Data["OidcIssuer"] = jwtRegisteredClaims.Issuer // use the consistent issuer from the JWT registered claims ctx.Data["OidcBaseUrl"] = strings.TrimSuffix(setting.AppURL, "/") ctx.Data["SigningKeyMethodAlg"] = oauth2_provider.DefaultSigningKey.SigningMethod().Alg() + // FIXME: no need to use a Golang template to render JSON, just build the JSON response directly in the future ctx.JSONTemplate("user/auth/oidc_wellknown") } diff --git a/routers/web/swagger_json.go b/routers/web/swagger_json.go index 201249cd92..d00eefbc74 100644 --- a/routers/web/swagger_json.go +++ b/routers/web/swagger_json.go @@ -5,21 +5,33 @@ package web import ( "html/template" + "net/http" + "strings" "gitea.dev/modules/setting" + "gitea.dev/modules/templates" + "gitea.dev/modules/util" "gitea.dev/services/context" ) -// SwaggerV1Json render swagger v1 json -func SwaggerV1Json(ctx *context.Context) { - ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer)) - ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe - ctx.JSONTemplate("swagger/v1_json") +func swaggerJsonServe(ctx *context.Context, file string) { + buf, err := templates.AssetFS().ReadFile(file) + if err != nil { + ctx.HTTPError(http.StatusInternalServerError, "unable to read api json file: "+file) + return + } + r := strings.NewReplacer( + "0.0.0+GITEA-API-APP-VERSION", template.JSEscapeString(setting.AppVer), + "/GITEA-API-APP-SUBURL/", template.JSEscapeString(setting.AppSubURL)+"/", + ) + ctx.Resp.Header().Set("Content-Type", "application/json") + _, _ = r.WriteString(ctx.Resp, util.UnsafeBytesToString(buf)) } -// OpenAPI3Json render OpenAPI 3.0 json (auto-converted from Swagger 2.0) -func OpenAPI3Json(ctx *context.Context) { - ctx.Data["SwaggerAppVer"] = template.HTML(template.JSEscapeString(setting.AppVer)) - ctx.Data["SwaggerAppSubUrl"] = setting.AppSubURL // it is JS-safe - ctx.JSONTemplate("swagger/v1_openapi3_json") +func SwaggerV1Json(ctx *context.Context) { + swaggerJsonServe(ctx, "swagger/v1-swagger.generated.json") +} + +func OpenAPI3Json(ctx *context.Context) { + swaggerJsonServe(ctx, "swagger/v1-openapi3.generated.json") } diff --git a/tailwind.config.ts b/tailwind.config.ts index f597121743..ef1112d3d9 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -26,7 +26,6 @@ export default { prefix: 'tw-', important: true, // the frameworks are mixed together, so tailwind needs to override other framework's styles content: [ - '!./templates/swagger/v1_json.tmpl', '!./templates/user/auth/oidc_wellknown.tmpl', '!**/*_test.go', './{build,models,modules,routers,services}/**/*.go', diff --git a/templates/swagger/v1-input.json b/templates/swagger/v1-input.json new file mode 100644 index 0000000000..d85fab11db --- /dev/null +++ b/templates/swagger/v1-input.json @@ -0,0 +1,17 @@ +{ + "info": { + "version": "0.0.0+GITEA-API-APP-VERSION" + }, + "basePath": "/GITEA-API-APP-SUBURL/api/v1", + "tags": [ + {"name": "admin", "description": "Site administration"}, + {"name": "issue", "description": "Issues, pull requests, comments, labels and milestones"}, + {"name": "miscellaneous", "description": "Miscellaneous endpoints"}, + {"name": "notification", "description": "User notifications"}, + {"name": "organization", "description": "Organizations and teams"}, + {"name": "package", "description": "Package registry"}, + {"name": "repository", "description": "Repositories and their contents"}, + {"name": "settings", "description": "Server settings"}, + {"name": "user", "description": "The authenticated user"} + ] +} diff --git a/templates/swagger/v1_openapi3_json.tmpl b/templates/swagger/v1-openapi3.generated.json similarity index 99% rename from templates/swagger/v1_openapi3_json.tmpl rename to templates/swagger/v1-openapi3.generated.json index bb448b7218..1b586abc0d 100644 --- a/templates/swagger/v1_openapi3_json.tmpl +++ b/templates/swagger/v1-openapi3.generated.json @@ -10733,7 +10733,7 @@ "url": "http://opensource.org/licenses/MIT" }, "title": "Gitea API", - "version": "{{.SwaggerAppVer}}" + "version": "0.0.0+GITEA-API-APP-VERSION" }, "openapi": "3.0.3", "paths": { @@ -33073,7 +33073,6 @@ }, "summary": "Create a repository", "tags": [ - "repository", "user" ] } @@ -34201,7 +34200,45 @@ ], "servers": [ { - "url": "{{.SwaggerAppSubUrl}}/api/v1" + "url": "/GITEA-API-APP-SUBURL/api/v1" + } + ], + "tags": [ + { + "description": "Site administration", + "name": "admin" + }, + { + "description": "Issues, pull requests, comments, labels and milestones", + "name": "issue" + }, + { + "description": "Miscellaneous endpoints", + "name": "miscellaneous" + }, + { + "description": "User notifications", + "name": "notification" + }, + { + "description": "Organizations and teams", + "name": "organization" + }, + { + "description": "Package registry", + "name": "package" + }, + { + "description": "Repositories and their contents", + "name": "repository" + }, + { + "description": "Server settings", + "name": "settings" + }, + { + "description": "The authenticated user", + "name": "user" } ] } \ No newline at end of file diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1-swagger.generated.json similarity index 99% rename from templates/swagger/v1_json.tmpl rename to templates/swagger/v1-swagger.generated.json index 4d7421531b..fb8409a7c5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1-swagger.generated.json @@ -17,9 +17,9 @@ "name": "MIT", "url": "http://opensource.org/licenses/MIT" }, - "version": "{{.SwaggerAppVer}}" + "version": "0.0.0+GITEA-API-APP-VERSION" }, - "basePath": "{{.SwaggerAppSubUrl}}/api/v1", + "basePath": "/GITEA-API-APP-SUBURL/api/v1", "paths": { "/admin/actions/jobs": { "get": { @@ -20912,7 +20912,6 @@ "application/json" ], "tags": [ - "repository", "user" ], "summary": "Create a repository", @@ -32087,5 +32086,43 @@ { "TOTPHeader": [] } + ], + "tags": [ + { + "description": "Site administration", + "name": "admin" + }, + { + "description": "Issues, pull requests, comments, labels and milestones", + "name": "issue" + }, + { + "description": "Miscellaneous endpoints", + "name": "miscellaneous" + }, + { + "description": "User notifications", + "name": "notification" + }, + { + "description": "Organizations and teams", + "name": "organization" + }, + { + "description": "Package registry", + "name": "package" + }, + { + "description": "Repositories and their contents", + "name": "repository" + }, + { + "description": "Server settings", + "name": "settings" + }, + { + "description": "The authenticated user", + "name": "user" + } ] } \ No newline at end of file diff --git a/templates/swagger/v1_input.json b/templates/swagger/v1_input.json deleted file mode 100644 index e74c8fc9c4..0000000000 --- a/templates/swagger/v1_input.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info": { - "version": "{{.SwaggerAppVer}}" - }, - "basePath": "{{.SwaggerAppSubUrl}}/api/v1" -} diff --git a/tests/integration/links_test.go b/tests/integration/links_test.go index a3198a6759..d81a6d5ad5 100644 --- a/tests/integration/links_test.go +++ b/tests/integration/links_test.go @@ -31,6 +31,42 @@ func TestLinks(t *testing.T) { t.Run("NoLoginNotExist", testLinksNoLoginNotExist) t.Run("AsUser", testLinksAsUser) t.Run("RepoCommon", testLinksRepoCommon) + t.Run("ApiJson", testLinksApiJson) +} + +func testLinksApiJson(t *testing.T) { + defer test.MockVariableValue(&setting.AppVer, "1.2.3")() + defer test.MockVariableValue(&setting.AppSubURL)() + t.Run("Swagger", func(t *testing.T) { + for _, subURL := range []string{"", "/sub"} { + setting.AppSubURL = subURL + resp := MakeRequest(t, NewRequest(t, "GET", "/swagger.v1.json"), http.StatusOK) + decoded := DecodeJSON(t, resp, &struct { + BasePath string `json:"basePath"` + Info struct { + Version string `json:"version"` + } + }{}) + assert.Equal(t, subURL+"/api/v1", decoded.BasePath) + assert.Equal(t, "1.2.3", decoded.Info.Version) + } + }) + t.Run("OpenAPI3", func(t *testing.T) { + for _, subURL := range []string{"", "/sub"} { + setting.AppSubURL = subURL + resp := MakeRequest(t, NewRequest(t, "GET", "/openapi3.v1.json"), http.StatusOK) + decoded := DecodeJSON(t, resp, &struct { + Servers []struct { + URL string `json:"url"` + } `json:"servers"` + Info struct { + Version string `json:"version"` + } + }{}) + assert.Equal(t, subURL+"/api/v1", decoded.Servers[0].URL) + assert.Equal(t, "1.2.3", decoded.Info.Version) + } + }) } func testLinksNoLogin(t *testing.T) {