Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ gradlew text eol=lf
services/frontend/src/services/api/blueshell/** linguist-generated=true
services/frontend/src/services/api/discord/** linguist-generated=true
*.gen.ts linguist-generated=true
services/api/openapi.json linguist-generated=true
services/api/openapi.yaml linguist-generated=true

# Yarn PnP runtime + lockfiles are tooling output, not source.
services/frontend/.pnp.cjs linguist-generated=true
Expand Down
2 changes: 1 addition & 1 deletion .github/diff-stats.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
service: frontend
category: generated

- glob: "services/api/openapi.json"
- glob: "services/api/openapi.yaml"
service: api
category: generated

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ jobs:
- name: Assert Blueshell spec and client are in sync
run: |
git diff --exit-code -- \
services/api/openapi.json \
services/api/openapi.yaml \
services/frontend/src/services/api/blueshell \
services/frontend/src/services/api/blueshell.runtime.ts \
services/frontend/src/services/api/index.ts
Expand All @@ -534,6 +534,6 @@ jobs:
with:
name: openapi-sync-logs
path: |
services/api/build/openapi.raw.json
services/api/build/openapi.raw.yaml
services/api/build/*.log
if-no-files-found: ignore
2 changes: 1 addition & 1 deletion scripts/generate_openapi.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ normalize_specs
echo "Generating frontend TypeScript clients..."
# `up -d frontend` is a no-op when the compose config is unchanged,
# but forces a recreate when mounts or env have drifted (e.g. after
# the openapi.yaml → openapi.json rename). Without this, `exec` runs
# the spec path changing). Without this, `exec` runs
# against a stale container that still has the old bind-mounts.
docker compose up -d frontend
docker compose exec frontend sh -c \
Expand Down
28 changes: 8 additions & 20 deletions scripts/openapi-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ DISCORD_OPENAPI_URL="${DISCORD_OPENAPI_URL:-https://raw.githubusercontent.com/di
BREVO_OPENAPI_URL="${BREVO_OPENAPI_URL:-https://api.brevo.com/v3/swagger_definition_v3.yml}"

SHARED_OPENAPI_DIR="${SHARED_OPENAPI_DIR:-libs/openapi-specs}"
# The spec is minified JSON produced by `jq -c` below. We keep the
# extension as `.json` so downstream tools that pick their parser by
# extension (notably @hey-api/openapi-ts, which otherwise parses
# `.yaml` as YAML and errors on single-line flow-style documents)
# read it correctly.
API_OPENAPI_SPEC="${API_OPENAPI_SPEC:-services/api/openapi.json}"
# Block-style YAML with its keys sorted, written by the generator test. One line per
# value, so two branches that each add an endpoint conflict only where they disagree
# rather than on the single line a minified document is.
API_OPENAPI_SPEC="${API_OPENAPI_SPEC:-services/api/openapi.yaml}"

check_common_prerequisites() {
for cmd in curl jq; do
Expand Down Expand Up @@ -40,21 +38,11 @@ regen_brevo_client() {
./gradlew --no-daemon --build-cache :libs:clients:brevo:generate
}

# Normalizes the Blueshell API spec in-place. Caller must have written
# either the normalized spec or its `.raw.json` upstream.
# The Blueshell spec needs no normalising: the generator test sorts its keys and writes
# block YAML. Kept as a no-op so the call sites read the same for every spec.
normalize_api_spec() {
echo "Normalizing Blueshell OpenAPI spec..."
local tmp
tmp="$(mktemp)"

if [ -f "${API_OPENAPI_SPEC%.json}.raw.json" ]; then
jq -S -c . "${API_OPENAPI_SPEC%.json}.raw.json" > "$tmp" && mv "$tmp" "$API_OPENAPI_SPEC"
rm -f "${API_OPENAPI_SPEC%.json}.raw.json"
elif [ -f "$API_OPENAPI_SPEC" ]; then
jq -S -c . "$API_OPENAPI_SPEC" > "$tmp" && mv "$tmp" "$API_OPENAPI_SPEC"
else
echo "$API_OPENAPI_SPEC (or .raw.json) not found" >&2
rm -f "$tmp"
if [ ! -f "$API_OPENAPI_SPEC" ]; then
echo "$API_OPENAPI_SPEC not found" >&2
exit 1
fi
}
Expand Down
28 changes: 11 additions & 17 deletions services/api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -272,15 +272,15 @@ val discordLiveTest by tasks.registering(Test::class) {
}

// Generate OpenAPI spec via in-memory H2 (no MariaDB required).
// Runs the openapi-gen-tagged test, extracts the spec from build/openapi.raw.json,
// and normalizes it (sorts keys, minifies) into services/api/openapi.json.
// Runs the openapi-gen-tagged test, which writes sorted block YAML to
// build/openapi.raw.yaml, and copies that to services/api/openapi.yaml.
val openApiGenTest by tasks.registering(Test::class) {
description = "Runs the OpenAPI spec generation test tagged with @Tag(\"openapi-gen\")."
group = "verification"
testClassesDirs = sourceSets["test"].output.classesDirs
classpath = sourceSets["test"].runtimeClasspath
useJUnitPlatform { includeTags("openapi-gen") }
// Disable caching so the test always runs and recreates openapi.raw.json.
// Disable caching so the test always runs and recreates openapi.raw.yaml.
// The raw file is not declared as a cacheable output; it's a side effect
// of the test used by dumpOpenApiSpec. Always running is fine since this
// task only runs when dumpOpenApiSpec is invoked (openapi-sync CI + local regen).
Expand All @@ -289,31 +289,25 @@ val openApiGenTest by tasks.registering(Test::class) {
}

val dumpOpenApiSpec by tasks.registering {
description = "Generates OpenAPI spec via in-memory H2 without database. Normalizes and writes to services/api/openapi.json."
description = "Generates the OpenAPI spec via in-memory H2 without a database, into services/api/openapi.yaml."
group = "verification"

dependsOn(openApiGenTest)

doLast {
val rawFile = File(buildDir, "openapi.raw.json")
val outputFile = File(projectDir, "openapi.json")
// The generator test already sorts the keys and writes block YAML, so there is
// nothing to normalise here and no external tool to depend on.
val rawFile = File(buildDir, "openapi.raw.yaml")
val outputFile = File(projectDir, "openapi.yaml")

if (!rawFile.exists()) {
throw RuntimeException("OpenAPI raw spec not found at ${rawFile.absolutePath}")
}

// Normalize using jq: sort keys (-S) and compact output (-c)
val process = ProcessBuilder("jq", "-S", "-c", ".", rawFile.absolutePath)
.redirectOutput(outputFile)
.redirectError(ProcessBuilder.Redirect.INHERIT)
.start()
val exitCode = process.waitFor()
rawFile.copyTo(outputFile, overwrite = true)
rawFile.delete()

if (exitCode != 0) {
throw RuntimeException("jq normalization failed with exit code $exitCode")
}

println("OpenAPI spec normalized and written to ${outputFile.absolutePath}")
println("OpenAPI spec written to ${outputFile.absolutePath}")
}
}

Expand Down
1 change: 0 additions & 1 deletion services/api/openapi.json

This file was deleted.

Loading
Loading