Skip to content

openapi for v1 #9389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
9 changes: 9 additions & 0 deletions scripts/build_api_v1_spec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
echo "Building API v1.1 documentation with Redocly CLI"
cd src-api/api-spec-v1;
echo "Bundle api docs and remove unused components"
npx redocly bundle openapi.json --remove-unused-components --output openapi-final.json
echo "Lint API docs"
npx redocly lint openapi-final.json
echo "Build docs with redocly cli."
npx redocly build-docs openapi-final.json
101 changes: 101 additions & 0 deletions scripts/convert_examples_to_x_codeSamples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import os
import json
from glob import glob

API_SPEC_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../src-api/api-spec-v1"))
json_files = [f for f in glob(os.path.join(API_SPEC_DIR, "*.json"))]

CURL_PREFIX = "curl "

changed_files = []

def add_code_sample(op_obj, curl_str):
code_sample = {
"lang": "Shell",
"label": "cURL",
"source": curl_str
}
if "x-codeSamples" not in op_obj:
op_obj["x-codeSamples"] = []
# Avoid duplicates
if not any(cs.get("source") == curl_str for cs in op_obj["x-codeSamples"]):
op_obj["x-codeSamples"].append(code_sample)

def process_operation(op_obj):
# Check for x-curl-example in responses/requestBody/content
# 1. Responses
for resp in op_obj.get("responses", {}).values():
for content in resp.get("content", {}).values():
curl_str = content.pop("x-curl-example", None)
if isinstance(curl_str, str) and curl_str.strip().startswith(CURL_PREFIX):
add_code_sample(op_obj, curl_str)
# 2. RequestBody
if "requestBody" in op_obj:
for content in op_obj["requestBody"].get("content", {}).values():
curl_str = content.pop("x-curl-example", None)
if isinstance(curl_str, str) and curl_str.strip().startswith(CURL_PREFIX):
add_code_sample(op_obj, curl_str)
# 3. Legacy: scan for examples with curl string (shouldn't be present, but just in case)
for resp in op_obj.get("responses", {}).values():
for content in resp.get("content", {}).values():
examples = content.get("examples", {})
to_remove = []
for k, v in examples.items():
if (
isinstance(v, dict)
and "value" in v
and isinstance(v["value"], str)
and v["value"].strip().startswith(CURL_PREFIX)
):
add_code_sample(op_obj, v["value"])
to_remove.append(k)
for k in to_remove:
del examples[k]
if not examples:
content.pop("examples", None)
if "requestBody" in op_obj:
for content in op_obj["requestBody"].get("content", {}).values():
examples = content.get("examples", {})
to_remove = []
for k, v in examples.items():
if (
isinstance(v, dict)
and "value" in v
and isinstance(v["value"], str)
and v["value"].strip().startswith(CURL_PREFIX)
):
add_code_sample(op_obj, v["value"])
to_remove.append(k)
for k in to_remove:
del examples[k]
if not examples:
content.pop("examples", None)

def process_paths(paths):
for path_item in paths.values():
for method in ["get", "post", "put", "delete", "patch", "options", "head", "trace"]:
if method in path_item:
process_operation(path_item[method])

for file in json_files:
with open(file, "r") as f:
try:
data = json.load(f)
except Exception as e:
print(f"Could not parse {file}: {e}")
continue
before = json.dumps(data, sort_keys=True)
if "paths" in data:
process_paths(data["paths"])
after = json.dumps(data, sort_keys=True)
if before != after:
with open(file, "w") as f:
json.dump(data, f, indent=2)
changed_files.append(file)

if changed_files:
print("Updated files:")
for f in changed_files:
print(f" {f}")
else:
print("No files needed updating.")
59 changes: 59 additions & 0 deletions scripts/convert_examples_to_x_curl_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import os
import json
from glob import glob

API_SPEC_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../src-api/api-spec-v1"))
json_files = [f for f in glob(os.path.join(API_SPEC_DIR, "*.json"))]

CURL_PREFIX = "curl "

changed_files = []

def process_examples(obj):
if isinstance(obj, dict):
# Look for 'examples' at this level
if "examples" in obj and isinstance(obj["examples"], dict):
to_remove = []
for k, v in obj["examples"].items():
if (
isinstance(v, dict)
and "value" in v
and isinstance(v["value"], str)
and v["value"].strip().startswith(CURL_PREFIX)
):
# Move to x-curl-example
obj["x-curl-example"] = v["value"]
to_remove.append(k)
for k in to_remove:
del obj["examples"][k]
# Remove 'examples' if now empty
if not obj["examples"]:
del obj["examples"]
# Recurse
for v in obj.values():
process_examples(v)
elif isinstance(obj, list):
for item in obj:
process_examples(item)

for file in json_files:
with open(file, "r") as f:
try:
data = json.load(f)
except Exception as e:
print(f"Could not parse {file}: {e}")
continue
before = json.dumps(data, sort_keys=True)
process_examples(data)
after = json.dumps(data, sort_keys=True)
if before != after:
with open(file, "w") as f:
json.dump(data, f, indent=2)
changed_files.append(file)

if changed_files:
print("Updated files:")
for f in changed_files:
print(f" {f}")
else:
print("No files needed updating.")
72 changes: 72 additions & 0 deletions scripts/merge_openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import os
import json
from glob import glob

# Directory containing the OpenAPI JSON files
DIR = os.path.join(os.path.dirname(__file__), "../src-api/api-spec-v1")
OUTPUT_FILE = os.path.join(DIR, "openapi.json")

# List all .json files except the output file itself
json_files = [
f for f in glob(os.path.join(DIR, "*.json"))
if not f.endswith("openapi.json")
]

merged = {
"openapi": "3.0.3",
"info": {
"title": "CircleCI API v1 - Combined",
"version": "1.0.0",
"description": "Combined OpenAPI spec for CircleCI API v1. Contains all endpoints from artifacts, jobs, envvars, keys, projects, and user."
},
"servers": [
{ "url": "https://circleci.com/api/v1.1" }
],
"tags": [],
"paths": {},
"components": {
"securitySchemes": {
"circleToken": {
"type": "apiKey",
"in": "header",
"name": "Circle-Token"
}
},
"schemas": {}
}
}

tags_seen = set()

for file in json_files:
with open(file, "r") as f:
data = json.load(f)
# Merge tags
for tag in data.get("tags", []):
tag_tuple = (tag["name"], tag.get("description", ""))
if tag_tuple not in tags_seen:
merged["tags"].append(tag)
tags_seen.add(tag_tuple)
# Merge paths
for path, path_item in data.get("paths", {}).items():
if path in merged["paths"]:
raise ValueError(f"Duplicate path found: {path}")
merged["paths"][path] = path_item
# Merge schemas
schemas = data.get("components", {}).get("schemas", {})
for schema_name, schema_def in schemas.items():
if schema_name in merged["components"]["schemas"]:
# Check if the schema is identical
if merged["components"]["schemas"][schema_name] != schema_def:
print(f"Conflict for schema '{schema_name}':")
print("Existing definition:", merged["components"]["schemas"][schema_name])
print("New definition:", schema_def)
raise ValueError(f"Conflicting schema found: {schema_name}")
# If identical, skip
continue
merged["components"]["schemas"][schema_name] = schema_def

# Write the merged OpenAPI spec
with open(OUTPUT_FILE, "w") as f:
json.dump(merged, f, indent=2)
print(f"Merged OpenAPI spec written to {OUTPUT_FILE}")
Loading