feat: add Slate documentation templates to skeleton
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Adds complete Slate documentation infrastructure to generated projects:
- docs/ directory with Gemfile, config.rb, and source templates
- Dockerfile for building docs site
- Dockerfile.nginx for serving static docs
- generate-docs.sh script for CI integration
- Claude command for AI-assisted docs generation
- OpenAPI → Slate markdown conversion via widdershins
Also includes:
- --export-openapi flag for service binaries
- DNS provisioning for docs.{domain} subdomain
- Updated project_infra for docs DNS records
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f64377116a
commit
af91bad0ff
@ -7,13 +7,13 @@
|
|||||||
Push to main branch triggers Woodpecker CI to build and deploy automatically:
|
Push to main branch triggers Woodpecker CI to build and deploy automatically:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Just push - Woodpecker handles the rest
|
# Push to Gitea - Woodpecker handles the rest
|
||||||
|
# origin is now Gitea (CI) - push triggers Woodpecker automatically
|
||||||
git push origin main
|
git push origin main
|
||||||
|
|
||||||
# Or push to both remotes
|
# If you need to push to GitHub backup:
|
||||||
GITEA_TOKEN=$(kubectl get secret rdev-credentials -n rdev -o jsonpath='{.data.GITEA_TOKEN}' | base64 -d)
|
GITEA_TOKEN=$(kubectl get secret rdev-credentials -n rdev -o jsonpath='{.data.GITEA_TOKEN}' | base64 -d)
|
||||||
git push https://jordan:${GITEA_TOKEN}@git.threesix.ai/jordan/rdev.git main
|
git push https://jordan:${GITEA_TOKEN}@git.threesix.ai/jordan/rdev.git main
|
||||||
git push origin main
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Images are built via kaniko and pushed to `registry.threesix.ai/rdev/*`.
|
Images are built via kaniko and pushed to `registry.threesix.ai/rdev/*`.
|
||||||
|
|||||||
@ -2,6 +2,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"{{GO_MODULE}}/pkg/app"
|
"{{GO_MODULE}}/pkg/app"
|
||||||
"{{GO_MODULE}}/pkg/logging"
|
"{{GO_MODULE}}/pkg/logging"
|
||||||
"{{GO_MODULE}}/services/{{COMPONENT_NAME}}/internal/adapter/memory"
|
"{{GO_MODULE}}/services/{{COMPONENT_NAME}}/internal/adapter/memory"
|
||||||
@ -10,6 +14,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Parse flags
|
||||||
|
exportOpenAPI := flag.Bool("export-openapi", false, "Export OpenAPI spec to stdout and exit")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// If exporting OpenAPI, generate spec and exit (used by CI for docs generation)
|
||||||
|
if *exportOpenAPI {
|
||||||
|
spec := api.NewServiceSpec()
|
||||||
|
jsonBytes, err := spec.JSON()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to generate OpenAPI spec: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(string(jsonBytes))
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// Create logger
|
// Create logger
|
||||||
logger := logging.Default()
|
logger := logging.Default()
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
# Generate API Documentation
|
||||||
|
|
||||||
|
Regenerate the Slate API documentation from OpenAPI specs.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Run this command when you need to preview documentation changes locally or after modifying OpenAPI specs.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
1. **Ensure services are running locally**
|
||||||
|
```bash
|
||||||
|
./scripts/dev.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Generate markdown from OpenAPI specs**
|
||||||
|
```bash
|
||||||
|
chmod +x docs/scripts/generate-docs.sh
|
||||||
|
./docs/scripts/generate-docs.sh http://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Build Slate documentation locally** (optional)
|
||||||
|
```bash
|
||||||
|
cd docs
|
||||||
|
bundle install
|
||||||
|
bundle exec middleman serve
|
||||||
|
```
|
||||||
|
Then open http://localhost:4567 to preview.
|
||||||
|
|
||||||
|
## What This Does
|
||||||
|
|
||||||
|
1. Discovers all services with OpenAPI specs
|
||||||
|
2. Fetches `/openapi.json` from each running service
|
||||||
|
3. Converts specs to Slate markdown using Widdershins
|
||||||
|
4. Updates `docs/source/includes/` with service documentation
|
||||||
|
|
||||||
|
## CI Pipeline
|
||||||
|
|
||||||
|
In production, this process runs automatically:
|
||||||
|
- CI fetches OpenAPI specs from built service binaries
|
||||||
|
- Widdershins converts to Slate markdown
|
||||||
|
- Middleman builds static HTML
|
||||||
|
- Static files deployed to `docs.{domain}`
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **"Connection refused"**: Ensure services are running on expected ports
|
||||||
|
- **"No OpenAPI spec found"**: Check that services expose `/openapi.json`
|
||||||
|
- **"widdershins not found"**: Run `npm install -g widdershins`
|
||||||
|
- **"bundle not found"**: Install Ruby and run `gem install bundler`
|
||||||
@ -38,6 +38,28 @@ cd {{PROJECT_NAME}}
|
|||||||
| `./scripts/quality.sh` | Run quality checks on all components |
|
| `./scripts/quality.sh` | Run quality checks on all components |
|
||||||
| `./scripts/discover.sh` | List all components in the monorepo |
|
| `./scripts/discover.sh` | List all components in the monorepo |
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
API documentation is automatically generated from OpenAPI specs and deployed to:
|
||||||
|
|
||||||
|
- **Docs**: https://docs.{{DOMAIN}}
|
||||||
|
- **OpenAPI Spec**: Each service exposes `/openapi.json`
|
||||||
|
|
||||||
|
To regenerate docs locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start services locally
|
||||||
|
./scripts/dev.sh
|
||||||
|
|
||||||
|
# Generate Slate markdown from OpenAPI specs
|
||||||
|
./docs/scripts/generate-docs.sh http://localhost
|
||||||
|
|
||||||
|
# Preview docs (optional - requires Ruby)
|
||||||
|
cd docs && bundle install && bundle exec middleman serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Documentation is automatically rebuilt on every push to `main`.
|
||||||
|
|
||||||
## Adding Components
|
## Adding Components
|
||||||
|
|
||||||
Components are added via the rdev API:
|
Components are added via the rdev API:
|
||||||
|
|||||||
15
internal/adapter/templates/templates/skeleton/docs/.gitignore
vendored
Normal file
15
internal/adapter/templates/templates/skeleton/docs/.gitignore
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Slate build output
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Ruby/Bundler
|
||||||
|
.bundle/
|
||||||
|
vendor/bundle/
|
||||||
|
Gemfile.lock
|
||||||
|
|
||||||
|
# Temp files
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Generated includes (except _errors.md)
|
||||||
|
source/includes/_*_spec.json
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
# Slate documentation builder
|
||||||
|
# Used by CI to generate static HTML from OpenAPI specs
|
||||||
|
|
||||||
|
FROM ruby:3.2-slim
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
build-essential \
|
||||||
|
nodejs \
|
||||||
|
npm \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install widdershins globally for OpenAPI to Slate markdown conversion
|
||||||
|
RUN npm install -g widdershins
|
||||||
|
|
||||||
|
WORKDIR /docs
|
||||||
|
|
||||||
|
# Copy Gemfile first for layer caching
|
||||||
|
COPY Gemfile Gemfile.lock* ./
|
||||||
|
RUN bundle install
|
||||||
|
|
||||||
|
# Copy the rest of the docs source
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build static site
|
||||||
|
CMD ["bundle", "exec", "middleman", "build", "--clean"]
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
# Production nginx image for serving Slate documentation
|
||||||
|
# Built by CI after Slate generates static HTML
|
||||||
|
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Remove default nginx content
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
|
||||||
|
# Copy built static files from Slate
|
||||||
|
COPY build/ /usr/share/nginx/html/
|
||||||
|
|
||||||
|
# Custom nginx config for SPA-style routing
|
||||||
|
RUN cat > /etc/nginx/conf.d/default.conf << 'EOF'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Enable gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
|
||||||
|
# Cache static assets
|
||||||
|
location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Serve index.html for all routes (SPA fallback)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "OK";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
14
internal/adapter/templates/templates/skeleton/docs/Gemfile
Normal file
14
internal/adapter/templates/templates/skeleton/docs/Gemfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Slate documentation dependencies
|
||||||
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
|
# Slate uses Middleman for static site generation
|
||||||
|
gem 'middleman', '~> 4.4'
|
||||||
|
gem 'middleman-autoprefixer', '~> 3.0'
|
||||||
|
gem 'middleman-syntax', '~> 3.2'
|
||||||
|
gem 'middleman-sprockets', '~> 4.1'
|
||||||
|
|
||||||
|
# Slate-specific dependencies
|
||||||
|
gem 'rouge', '~> 4.0'
|
||||||
|
gem 'redcarpet', '~> 3.5'
|
||||||
|
gem 'nokogiri', '~> 1.15'
|
||||||
|
gem 'activesupport', '~> 7.0'
|
||||||
56
internal/adapter/templates/templates/skeleton/docs/config.rb
Normal file
56
internal/adapter/templates/templates/skeleton/docs/config.rb
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# Slate/Middleman configuration
|
||||||
|
# Based on slatedocs/slate with customizations for monorepo API docs
|
||||||
|
|
||||||
|
# Markdown rendering
|
||||||
|
set :markdown_engine, :redcarpet
|
||||||
|
set :markdown,
|
||||||
|
fenced_code_blocks: true,
|
||||||
|
smartypants: true,
|
||||||
|
disable_indented_code_blocks: true,
|
||||||
|
prettify: true,
|
||||||
|
strikethrough: true,
|
||||||
|
tables: true,
|
||||||
|
with_toc_data: true,
|
||||||
|
no_intra_emphasis: true
|
||||||
|
|
||||||
|
# Syntax highlighting
|
||||||
|
activate :syntax
|
||||||
|
set :haml, { ugly: true }
|
||||||
|
|
||||||
|
# Assets
|
||||||
|
set :css_dir, 'stylesheets'
|
||||||
|
set :js_dir, 'javascripts'
|
||||||
|
set :images_dir, 'images'
|
||||||
|
set :fonts_dir, 'fonts'
|
||||||
|
|
||||||
|
# Build directory
|
||||||
|
set :build_dir, 'build'
|
||||||
|
|
||||||
|
# Relative assets for static hosting
|
||||||
|
activate :relative_assets
|
||||||
|
set :relative_links, true
|
||||||
|
|
||||||
|
# Build-specific configuration
|
||||||
|
configure :build do
|
||||||
|
activate :minify_css
|
||||||
|
activate :minify_javascript, ignore: [/all_.*\.js/]
|
||||||
|
activate :asset_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
# Development helpers
|
||||||
|
helpers do
|
||||||
|
def toc_data(page_content)
|
||||||
|
content = page_content.dup
|
||||||
|
toc = []
|
||||||
|
|
||||||
|
content.scan(/<h([12]).*?id="([^"]+)".*?>(.+?)<\/h\1>/m) do |level, id, text|
|
||||||
|
toc << {
|
||||||
|
level: level.to_i,
|
||||||
|
id: id,
|
||||||
|
text: text.gsub(/<[^>]+>/, '')
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
toc
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Generate Slate documentation from OpenAPI specs
|
||||||
|
#
|
||||||
|
# This script:
|
||||||
|
# 1. Discovers all services with OpenAPI specs
|
||||||
|
# 2. Uses widdershins to convert OpenAPI JSON to Slate markdown
|
||||||
|
# 3. Injects the generated includes into the main index
|
||||||
|
#
|
||||||
|
# Usage: ./docs/scripts/generate-docs.sh [base_url]
|
||||||
|
# base_url: Optional base URL for fetching specs (default: http://localhost)
|
||||||
|
#
|
||||||
|
# The script expects services to be running locally during CI/CD build.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
DOCS_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
PROJECT_ROOT="$(dirname "$DOCS_DIR")"
|
||||||
|
INCLUDES_DIR="$DOCS_DIR/source/includes"
|
||||||
|
|
||||||
|
BASE_URL="${1:-http://localhost}"
|
||||||
|
|
||||||
|
echo "==> Generating API documentation from OpenAPI specs"
|
||||||
|
echo " Base URL: $BASE_URL"
|
||||||
|
echo " Docs dir: $DOCS_DIR"
|
||||||
|
|
||||||
|
# Ensure includes directory exists
|
||||||
|
mkdir -p "$INCLUDES_DIR"
|
||||||
|
|
||||||
|
# Clean old service includes (keep _errors.md)
|
||||||
|
find "$INCLUDES_DIR" -name '_*.md' ! -name '_errors.md' -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
# Track which services we generate docs for
|
||||||
|
SERVICES=()
|
||||||
|
|
||||||
|
# Discover services by looking for OpenAPI spec files or main.go
|
||||||
|
for service_dir in "$PROJECT_ROOT"/services/*/; do
|
||||||
|
[ -d "$service_dir" ] || continue
|
||||||
|
|
||||||
|
service_name=$(basename "$service_dir")
|
||||||
|
[ "$service_name" = ".gitkeep" ] && continue
|
||||||
|
|
||||||
|
echo "==> Processing service: $service_name"
|
||||||
|
|
||||||
|
# Try to fetch OpenAPI spec from running service
|
||||||
|
spec_url="$BASE_URL/api/$service_name/openapi.json"
|
||||||
|
spec_file="$INCLUDES_DIR/_${service_name}_spec.json"
|
||||||
|
|
||||||
|
if curl -sf --connect-timeout 5 "$spec_url" > "$spec_file" 2>/dev/null; then
|
||||||
|
echo " Fetched spec from $spec_url"
|
||||||
|
else
|
||||||
|
# Fallback: check for static spec file
|
||||||
|
if [ -f "$service_dir/openapi.json" ]; then
|
||||||
|
cp "$service_dir/openapi.json" "$spec_file"
|
||||||
|
echo " Using static spec from $service_dir/openapi.json"
|
||||||
|
else
|
||||||
|
echo " WARNING: No OpenAPI spec found for $service_name, skipping"
|
||||||
|
rm -f "$spec_file"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Convert OpenAPI to Slate markdown using widdershins
|
||||||
|
output_file="$INCLUDES_DIR/_${service_name}.md"
|
||||||
|
|
||||||
|
echo " Converting to markdown..."
|
||||||
|
widdershins \
|
||||||
|
--language_tabs 'shell:curl' 'go:Go' \
|
||||||
|
--summary \
|
||||||
|
--omitHeader \
|
||||||
|
--resolve \
|
||||||
|
--shallowSchemas \
|
||||||
|
"$spec_file" \
|
||||||
|
-o "$output_file"
|
||||||
|
|
||||||
|
# Clean up temp spec file
|
||||||
|
rm -f "$spec_file"
|
||||||
|
|
||||||
|
SERVICES+=("$service_name")
|
||||||
|
echo " Generated: $output_file"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Update index.html.md with service includes
|
||||||
|
INDEX_FILE="$DOCS_DIR/source/index.html.md"
|
||||||
|
if [ -f "$INDEX_FILE" ]; then
|
||||||
|
echo "==> Updating index with service includes"
|
||||||
|
|
||||||
|
# Build includes list
|
||||||
|
INCLUDES=""
|
||||||
|
for svc in "${SERVICES[@]}"; do
|
||||||
|
INCLUDES="$INCLUDES - $svc"$'\n'
|
||||||
|
done
|
||||||
|
|
||||||
|
# Update the includes section in frontmatter
|
||||||
|
# This is a simple approach - insert after 'includes:' line
|
||||||
|
if grep -q "^includes:" "$INDEX_FILE"; then
|
||||||
|
# Create temp file with updated includes
|
||||||
|
awk -v services="$INCLUDES" '
|
||||||
|
/^includes:/ {
|
||||||
|
print $0
|
||||||
|
print " - errors"
|
||||||
|
# Add service includes
|
||||||
|
n = split(services, arr, "\n")
|
||||||
|
for (i = 1; i <= n; i++) {
|
||||||
|
if (arr[i] != "") print arr[i]
|
||||||
|
}
|
||||||
|
# Skip existing includes until next frontmatter key or ---
|
||||||
|
while ((getline line) > 0) {
|
||||||
|
if (line ~ /^[a-z_]+:/ || line == "---") {
|
||||||
|
print line
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
' "$INDEX_FILE" > "$INDEX_FILE.tmp" && mv "$INDEX_FILE.tmp" "$INDEX_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " Updated includes: ${SERVICES[*]:-none}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Documentation generation complete"
|
||||||
|
echo " Services documented: ${#SERVICES[@]}"
|
||||||
|
echo ""
|
||||||
|
echo "To build the static site:"
|
||||||
|
echo " cd $DOCS_DIR && bundle exec middleman build"
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
# Errors
|
||||||
|
|
||||||
|
The API uses standard HTTP status codes to indicate success or failure.
|
||||||
|
|
||||||
|
## Error Response Format
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "ERROR_CODE",
|
||||||
|
"message": "Human-readable error message"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"request_id": "abc123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Error Codes
|
||||||
|
|
||||||
|
| HTTP Status | Error Code | Description |
|
||||||
|
|-------------|------------|-------------|
|
||||||
|
| 400 | `BAD_REQUEST` | The request body is malformed or missing required fields |
|
||||||
|
| 401 | `UNAUTHORIZED` | Authentication is required but was not provided |
|
||||||
|
| 403 | `FORBIDDEN` | The authenticated user lacks permission for this action |
|
||||||
|
| 404 | `NOT_FOUND` | The requested resource does not exist |
|
||||||
|
| 409 | `CONFLICT` | The request conflicts with existing data (e.g., duplicate) |
|
||||||
|
| 422 | `VALIDATION_ERROR` | The request data failed validation rules |
|
||||||
|
| 429 | `RATE_LIMITED` | Too many requests; slow down |
|
||||||
|
| 500 | `INTERNAL_ERROR` | An unexpected server error occurred |
|
||||||
|
| 503 | `SERVICE_UNAVAILABLE` | The service is temporarily unavailable |
|
||||||
|
|
||||||
|
## Validation Errors
|
||||||
|
|
||||||
|
Validation errors (422) include field-specific details:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_ERROR",
|
||||||
|
"message": "Validation failed",
|
||||||
|
"details": {
|
||||||
|
"name": "name is required",
|
||||||
|
"email": "invalid email format"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
When rate limited (429), the response includes headers indicating when you can retry:
|
||||||
|
|
||||||
|
| Header | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `X-RateLimit-Limit` | Maximum requests per window |
|
||||||
|
| `X-RateLimit-Remaining` | Requests remaining in current window |
|
||||||
|
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
|
||||||
|
| `Retry-After` | Seconds to wait before retrying |
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
title: {{PROJECT_NAME}} API Documentation
|
||||||
|
|
||||||
|
language_tabs:
|
||||||
|
- shell: curl
|
||||||
|
- go: Go
|
||||||
|
|
||||||
|
toc_footers:
|
||||||
|
- <a href='https://{{DOMAIN}}'>{{PROJECT_NAME}}</a>
|
||||||
|
- <a href='https://github.com/slatedocs/slate'>Documentation Powered by Slate</a>
|
||||||
|
|
||||||
|
includes:
|
||||||
|
- errors
|
||||||
|
|
||||||
|
search: true
|
||||||
|
|
||||||
|
code_clipboard: true
|
||||||
|
|
||||||
|
meta:
|
||||||
|
- name: description
|
||||||
|
content: API documentation for {{PROJECT_NAME}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
Welcome to the **{{PROJECT_NAME}}** API documentation.
|
||||||
|
|
||||||
|
This documentation covers all services in the {{PROJECT_NAME}} monorepo. Each service provides a REST API with JSON responses.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
Most endpoints require authentication via Bearer token:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -X GET "https://{{DOMAIN}}/api/service-name/endpoint" \
|
||||||
|
-H "Authorization: Bearer YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
req, _ := http.NewRequest("GET", "https://{{DOMAIN}}/api/service-name/endpoint", nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer YOUR_TOKEN")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Base URLs
|
||||||
|
|
||||||
|
| Environment | URL |
|
||||||
|
|-------------|-----|
|
||||||
|
| Production | `https://{{DOMAIN}}` |
|
||||||
|
| Staging | `https://staging.{{DOMAIN}}` |
|
||||||
|
|
||||||
|
## Response Format
|
||||||
|
|
||||||
|
All API responses use a consistent envelope:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": { ... },
|
||||||
|
"meta": {
|
||||||
|
"request_id": "abc123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Error responses include an error object:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "NOT_FOUND",
|
||||||
|
"message": "Resource not found"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"request_id": "abc123"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# Services
|
||||||
|
|
||||||
|
The following services are available in this project. Click on a service name to jump to its API documentation.
|
||||||
|
|
||||||
|
<!-- SERVICE_INCLUDES_BELOW -->
|
||||||
|
<!-- Auto-generated includes will be inserted here by generate-docs.sh -->
|
||||||
|
|
||||||
@ -0,0 +1,103 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
<title><%= current_page.data.title || "API Documentation" %></title>
|
||||||
|
|
||||||
|
<style media="screen">
|
||||||
|
<%= Rouge::Themes::Base16::Monokai.render(:scope => '.highlight') %>
|
||||||
|
</style>
|
||||||
|
<style media="print">
|
||||||
|
* {
|
||||||
|
-webkit-transition: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<%= stylesheet_link_tag :screen, media: :screen %>
|
||||||
|
<%= stylesheet_link_tag :print, media: :print %>
|
||||||
|
|
||||||
|
<% if current_page.data.search %>
|
||||||
|
<%= javascript_include_tag "all" %>
|
||||||
|
<% else %>
|
||||||
|
<%= javascript_include_tag "all_nosearch" %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if current_page.data.code_clipboard %>
|
||||||
|
<script>
|
||||||
|
$(function() { setupCodeCopy(); });
|
||||||
|
</script>
|
||||||
|
<% end %>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="<%= page_classes %>" data-languages="<%=h language_tabs.map{ |lang| lang.is_a?(Hash) ? lang.keys.first : lang }.to_json %>">
|
||||||
|
<a href="#" id="nav-button">
|
||||||
|
<span>
|
||||||
|
NAV
|
||||||
|
<%= image_tag('navbar.png') %>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="toc-wrapper">
|
||||||
|
<% if language_tabs.any? %>
|
||||||
|
<div class="lang-selector">
|
||||||
|
<% language_tabs.each do |lang| %>
|
||||||
|
<% if lang.is_a? Hash %>
|
||||||
|
<a href="#" data-language-name="<%= lang.keys.first %>"><%= lang.values.first %></a>
|
||||||
|
<% else %>
|
||||||
|
<a href="#" data-language-name="<%= lang %>"><%= lang %></a>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% if current_page.data.search %>
|
||||||
|
<div class="search">
|
||||||
|
<input type="text" class="search" id="input-search" placeholder="Search">
|
||||||
|
</div>
|
||||||
|
<ul class="search-results"></ul>
|
||||||
|
<% end %>
|
||||||
|
<ul id="toc" class="toc-list-h1">
|
||||||
|
<% toc_data(page_content).each do |h1| %>
|
||||||
|
<li>
|
||||||
|
<a href="#<%= h1[:id] %>" class="toc-h1 toc-link" data-title="<%= h1[:content] %>"><%= h1[:content] %></a>
|
||||||
|
<% if h1[:children].length > 0 %>
|
||||||
|
<ul class="toc-list-h2">
|
||||||
|
<% h1[:children].each do |h2| %>
|
||||||
|
<li>
|
||||||
|
<a href="#<%= h2[:id] %>" class="toc-h2 toc-link" data-title="<%= h2[:content] %>"><%= h2[:content] %></a>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% if current_page.data.toc_footers %>
|
||||||
|
<ul class="toc-footer">
|
||||||
|
<% current_page.data.toc_footers.each do |footer| %>
|
||||||
|
<li><%= footer %></li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<div class="dark-box"></div>
|
||||||
|
<div class="content">
|
||||||
|
<%= page_content %>
|
||||||
|
</div>
|
||||||
|
<div class="dark-box">
|
||||||
|
<% if language_tabs.any? %>
|
||||||
|
<div class="lang-selector">
|
||||||
|
<% language_tabs.each do |lang| %>
|
||||||
|
<% if lang.is_a? Hash %>
|
||||||
|
<a href="#" data-language-name="<%= lang.keys.first %>"><%= lang.values.first %></a>
|
||||||
|
<% else %>
|
||||||
|
<a href="#" data-language-name="<%= lang %>"><%= lang %></a>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
// Custom variables for Slate theming
|
||||||
|
// Override Slate defaults to match project branding
|
||||||
|
|
||||||
|
// Colors - dark theme
|
||||||
|
$nav-bg: #1e1e2e !default;
|
||||||
|
$nav-text: #cdd6f4 !default;
|
||||||
|
$nav-active-bg: #313244 !default;
|
||||||
|
$nav-active-text: #f5c2e7 !default;
|
||||||
|
$nav-hover-bg: #45475a !default;
|
||||||
|
|
||||||
|
$content-bg: #1e1e2e !default;
|
||||||
|
$content-text: #cdd6f4 !default;
|
||||||
|
$code-bg: #181825 !default;
|
||||||
|
$code-text: #cdd6f4 !default;
|
||||||
|
|
||||||
|
$border-color: #45475a !default;
|
||||||
|
|
||||||
|
// Code highlighting (catppuccin-mocha inspired)
|
||||||
|
$code-annotation-bg: #313244 !default;
|
||||||
|
$code-annotation-text: #a6adc8 !default;
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
$font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif !default;
|
||||||
|
$code-font-family: "SF Mono", Consolas, Monaco, "Andale Mono", monospace !default;
|
||||||
|
|
||||||
|
// Sizes
|
||||||
|
$nav-width: 230px !default;
|
||||||
|
$max-content-width: 800px !default;
|
||||||
|
|
||||||
|
// Language tabs
|
||||||
|
$lang-select-bg: #11111b !default;
|
||||||
|
$lang-select-text: #cdd6f4 !default;
|
||||||
|
$lang-select-active-bg: #313244 !default;
|
||||||
|
$lang-select-active-text: #f5c2e7 !default;
|
||||||
@ -0,0 +1,158 @@
|
|||||||
|
// Slate documentation styles
|
||||||
|
// Imports Slate defaults and applies custom variables
|
||||||
|
|
||||||
|
@import 'variables';
|
||||||
|
|
||||||
|
// Base Slate styles
|
||||||
|
@import 'normalize';
|
||||||
|
@import 'icon-font';
|
||||||
|
|
||||||
|
// Custom overrides
|
||||||
|
body {
|
||||||
|
background-color: $content-bg;
|
||||||
|
color: $content-text;
|
||||||
|
font-family: $font-family;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
.toc-wrapper {
|
||||||
|
background-color: $nav-bg;
|
||||||
|
width: $nav-width;
|
||||||
|
|
||||||
|
.toc-link {
|
||||||
|
color: $nav-text;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $nav-active-bg;
|
||||||
|
color: $nav-active-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $nav-hover-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-h2 {
|
||||||
|
padding-left: 25px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code blocks
|
||||||
|
pre {
|
||||||
|
background-color: $code-bg;
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: $code-text;
|
||||||
|
font-family: $code-font-family;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline code
|
||||||
|
code {
|
||||||
|
background-color: $code-bg;
|
||||||
|
color: $code-text;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: $code-font-family;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Language tabs
|
||||||
|
.lang-selector {
|
||||||
|
background-color: $lang-select-bg;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $lang-select-text;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: $lang-select-active-bg;
|
||||||
|
color: $lang-select-active-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tables
|
||||||
|
table {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: $code-bg;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: $content-text;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Links
|
||||||
|
a {
|
||||||
|
color: $nav-active-text;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content area
|
||||||
|
.page-wrapper {
|
||||||
|
margin-left: $nav-width;
|
||||||
|
max-width: $max-content-width;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code annotations
|
||||||
|
.code-annotation {
|
||||||
|
background-color: $code-annotation-bg;
|
||||||
|
color: $code-annotation-text;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
.search {
|
||||||
|
input {
|
||||||
|
background-color: $code-bg;
|
||||||
|
border: 1px solid $border-color;
|
||||||
|
color: $content-text;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: $code-annotation-text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Responsive
|
||||||
|
@media (max-width: 930px) {
|
||||||
|
.toc-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-wrapper {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -292,8 +292,13 @@ func (a *App) ListenAddr() string {
|
|||||||
return a.serverConfig.Addr()
|
return a.serverConfig.Addr()
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnableDocs adds /docs and /openapi.json endpoints to the application.
|
// EnableDocs adds OpenAPI endpoints to the application.
|
||||||
// It mounts the Scalar UI at /docs and the OpenAPI JSON spec at /openapi.json.
|
// It mounts:
|
||||||
|
// - /openapi.json: The raw OpenAPI spec (consumed by CI for Slate doc generation)
|
||||||
|
// - /docs: Redirects to the Slate documentation site at docs.{domain}
|
||||||
|
//
|
||||||
|
// During CI builds, Widdershins converts /openapi.json to Slate markdown,
|
||||||
|
// which is then built into static HTML and deployed to docs.{domain}.
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
//
|
//
|
||||||
@ -302,7 +307,7 @@ func (a *App) ListenAddr() string {
|
|||||||
// application.EnableDocs(spec)
|
// application.EnableDocs(spec)
|
||||||
func (a *App) EnableDocs(spec *openapi.OpenAPISpec) {
|
func (a *App) EnableDocs(spec *openapi.OpenAPISpec) {
|
||||||
openapi.Mount(a.router, spec)
|
openapi.Mount(a.router, spec)
|
||||||
a.logger.Info("API documentation enabled", "docs", "/docs", "spec", "/openapi.json")
|
a.logger.Info("API documentation enabled", "spec", "/openapi.json", "docs_redirect", "/docs")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements http.Handler, allowing App to be used in tests.
|
// ServeHTTP implements http.Handler, allowing App to be used in tests.
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
package openapi
|
package openapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
scalargo "github.com/bdpiprava/scalar-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Mount registers /docs and /openapi.json endpoints on the router.
|
// Mount registers the /openapi.json endpoint on the router.
|
||||||
|
// The OpenAPI spec is consumed by CI to generate Slate documentation
|
||||||
|
// which is served at docs.{domain}. No interactive UI is mounted here.
|
||||||
func Mount(r chi.Router, spec *OpenAPISpec) {
|
func Mount(r chi.Router, spec *OpenAPISpec) {
|
||||||
// Serve OpenAPI JSON
|
// Serve OpenAPI JSON spec (consumed by Widdershins during doc generation)
|
||||||
r.Get("/openapi.json", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/openapi.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
@ -24,26 +24,19 @@ func Mount(r chi.Router, spec *OpenAPISpec) {
|
|||||||
_, _ = w.Write(specBytes)
|
_, _ = w.Write(specBytes)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Serve Scalar docs UI
|
// /docs redirects to the Slate documentation site
|
||||||
r.Get("/docs", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/docs", func(w http.ResponseWriter, r *http.Request) {
|
||||||
scheme := "http"
|
// Construct docs URL from current host
|
||||||
|
// e.g., api.slug.threesix.ai -> docs.slug.threesix.ai
|
||||||
|
scheme := "https"
|
||||||
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
|
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
|
||||||
scheme = proto
|
scheme = proto
|
||||||
} else if r.TLS != nil {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
specURL := fmt.Sprintf("%s://%s/openapi.json", scheme, r.Host)
|
|
||||||
|
|
||||||
html, err := scalargo.NewV2(
|
|
||||||
scalargo.WithSpecURL(specURL),
|
|
||||||
scalargo.WithDarkMode(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
// Extract the base domain (remove service prefix if present)
|
||||||
_, _ = fmt.Fprint(w, html)
|
host := r.Host
|
||||||
|
docsURL := scheme + "://docs." + host
|
||||||
|
|
||||||
|
http.Redirect(w, r, docsURL, http.StatusTemporaryRedirect)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,6 +127,9 @@ type CreateProjectResult struct {
|
|||||||
Domain string
|
Domain string
|
||||||
URL string
|
URL string
|
||||||
|
|
||||||
|
// API documentation URL (docs.{slug}.{domain})
|
||||||
|
DocsURL string
|
||||||
|
|
||||||
// All domains associated with the project
|
// All domains associated with the project
|
||||||
Domains []*domain.ProjectDomain
|
Domains []*domain.ProjectDomain
|
||||||
|
|
||||||
|
|||||||
@ -64,22 +64,25 @@ func (s *ProjectInfraService) CreateProject(ctx context.Context, req CreateProje
|
|||||||
// 5. Create custom subdomain if requested
|
// 5. Create custom subdomain if requested
|
||||||
s.createCustomDNS(ctx, req, projectID, result)
|
s.createCustomDNS(ctx, req, projectID, result)
|
||||||
|
|
||||||
// 6. Activate CI (Woodpecker) - Before seeding so the webhook is installed
|
// 6. Create docs subdomain for API documentation (docs.{slug}.{domain})
|
||||||
|
s.createDocsDNS(ctx, slug, projectID, result)
|
||||||
|
|
||||||
|
// 7. Activate CI (Woodpecker) - Before seeding so the webhook is installed
|
||||||
ciActivated := s.activateCI(ctx, result)
|
ciActivated := s.activateCI(ctx, result)
|
||||||
|
|
||||||
// 7. Seed repository with template
|
// 8. Seed repository with template
|
||||||
templateSeeded := s.seedTemplate(ctx, req, result)
|
templateSeeded := s.seedTemplate(ctx, req, result)
|
||||||
|
|
||||||
// 8. Provision database and cache
|
// 9. Provision database and cache
|
||||||
s.provisionResources(ctx, result)
|
s.provisionResources(ctx, result)
|
||||||
|
|
||||||
// 9. Create initial K8s deployment (before triggering CI build)
|
// 10. Create initial K8s deployment (before triggering CI build)
|
||||||
// This ensures the deployment exists for `kubectl set image` in CI pipeline
|
// This ensures the deployment exists for `kubectl set image` in CI pipeline
|
||||||
if templateSeeded {
|
if templateSeeded {
|
||||||
s.createInitialDeployment(ctx, req, result)
|
s.createInitialDeployment(ctx, req, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 10. Trigger initial CI build if both CI and template are ready
|
// 11. Trigger initial CI build if both CI and template are ready
|
||||||
if ciActivated && templateSeeded && s.ciProvider != nil {
|
if ciActivated && templateSeeded && s.ciProvider != nil {
|
||||||
pipelineNum, err := s.ciProvider.TriggerBuild(ctx, result.GitRepoOwner, result.GitRepoName, "main")
|
pipelineNum, err := s.ciProvider.TriggerBuild(ctx, result.GitRepoOwner, result.GitRepoName, "main")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -266,6 +269,51 @@ func (s *ProjectInfraService) createCustomDNS(ctx context.Context, req CreatePro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// createDocsDNS creates DNS record for docs.{slug}.{defaultDomain}.
|
||||||
|
// This enables Slate API documentation to be served at docs.{domain}.
|
||||||
|
func (s *ProjectInfraService) createDocsDNS(ctx context.Context, slug, projectID string, result *CreateProjectResult) {
|
||||||
|
if s.dns == nil {
|
||||||
|
return // DNS not configured, skip silently (not critical)
|
||||||
|
}
|
||||||
|
|
||||||
|
log := logging.FromContext(ctx).WithService("project_infra")
|
||||||
|
docsSubdomain := "docs." + slug
|
||||||
|
docsDomain := docsSubdomain + "." + s.defaultDomain
|
||||||
|
|
||||||
|
dnsRecord, err := s.dns.CreateRecord(ctx, domain.DNSRecord{
|
||||||
|
Type: "A",
|
||||||
|
Name: docsSubdomain,
|
||||||
|
Content: s.clusterIP,
|
||||||
|
TTL: 1,
|
||||||
|
Proxied: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Warn("failed to create docs DNS record", logging.FieldError, err, "domain", docsDomain)
|
||||||
|
result.NextSteps = append(result.NextSteps, "Create docs DNS manually: "+docsDomain+" → "+s.clusterIP)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in project_domains table
|
||||||
|
if s.domainRepo != nil {
|
||||||
|
pd := &domain.ProjectDomain{
|
||||||
|
ProjectID: projectID,
|
||||||
|
Domain: docsDomain,
|
||||||
|
Type: domain.DomainTypeAlias, // docs subdomain is an alias
|
||||||
|
DNSRecordID: dnsRecord.ID,
|
||||||
|
DNSRecordType: "A",
|
||||||
|
Verified: true,
|
||||||
|
}
|
||||||
|
if err := s.domainRepo.Create(ctx, pd); err != nil {
|
||||||
|
log.Warn("failed to store docs domain", logging.FieldError, err)
|
||||||
|
} else {
|
||||||
|
result.Domains = append(result.Domains, pd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.DocsURL = "https://" + docsDomain
|
||||||
|
log.Info("docs DNS created", "domain", docsDomain)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ProjectInfraService) activateCI(ctx context.Context, result *CreateProjectResult) bool {
|
func (s *ProjectInfraService) activateCI(ctx context.Context, result *CreateProjectResult) bool {
|
||||||
log := logging.FromContext(ctx).WithService("project_infra")
|
log := logging.FromContext(ctx).WithService("project_infra")
|
||||||
if s.ciProvider == nil {
|
if s.ciProvider == nil {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user