Upload API
Shelf provides an HTTP API for deploying documentation versions. This is designed for CI/CD pipelines — analogous to nuget push or docker push.
Prerequisites
- The product must be registered via a config file
- An API key must be configured (
Shelf__ApiKey)
If no API key is configured, the upload endpoint returns 503 Service Unavailable.
Uploading a Version
curl -X POST \
-H "Authorization: Bearer $SHELF_API_KEY" \
-H "Content-Type: application/zip" \
--data-binary @docs.zip \
https://docs.cocoar.dev/api/products/configuration/versions/v6The ZIP file should contain the VitePress build output with index.html at the root:
docs.zip
├── index.html
├── assets/
│ ├── style.a1b2c3.css
│ └── app.d4e5f6.js
├── guide/
│ └── getting-started.html
├── llms.txt
└── llms-full.txtWhat Happens
- API key is validated
- Product registration is checked (must exist in config)
- Version format is validated against the version pattern (supports SemVer:
v5,v5.2,v5.2.0,v5.2.0-beta.1) - ZIP is extracted to a temporary directory
- Validation:
index.htmlmust exist at the root - Atomic move to
/data/docs/{product}/{version}/ - ManifestService detects the new version automatically
- Response:
201 Created
Re-uploading an existing version replaces it atomically.
CI/CD Integration
Setup
- Add
SHELF_API_KEYas a repository secret in GitHub - Ensure the product is registered on the server
- Add a deploy job to your workflow
GitHub Actions with GitVersion
name: Deploy Docs
on:
push:
branches: [main]
jobs:
deploy-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: GitVersion
id: version
uses: gittools/actions/gitversion/execute@v3
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Build
working-directory: website
run: npm ci && npx vitepress build
- name: Package
run: cd website/.vitepress/dist && zip -r $GITHUB_WORKSPACE/docs.zip .
- name: Deploy to Shelf
env:
KEY: ${{ secrets.SHELF_API_KEY }}
VER: ${{ steps.version.outputs.major }}.${{ steps.version.outputs.minor }}
run: |
curl -f -X POST \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/zip" \
--data-binary @$GITHUB_WORKSPACE/docs.zip \
https://docs.cocoar.dev/api/products/configuration/versions/v$VERVersion Format
Choose what makes sense for your docs. Use the GitVersion output variables in the upload URL:
- Major only →
v5(useoutputs.major) - Major.Minor →
v5.2(recommended, useoutputs.major+outputs.minor) - Full SemVer →
v5.2.0(useoutputs.majorMinorPatch)
Manual Trigger Variant
Replace the on: section to allow manual deploys with a version input:
on:
workflow_dispatch:
inputs:
version:
description: 'Version (e.g. v5.2)'
required: trueThen use the inputs.version variable instead of the GitVersion output in the env section.
Key Points
- ZIP from inside the dist directory —
cd .vitepress/dist && zip -r ... .soindex.htmlis at the root - Use
curl -f— fails the step on HTTP errors (4xx/5xx) - Re-upload replaces atomically — safe to re-run the pipeline
- Pre-release versions are never "latest" — visitors are redirected to the highest stable version
API Endpoints
List Products
GET /api/productsReturns all registered products with their version information. No authentication required.
[
{
"name": "configuration",
"displayName": "Cocoar.Configuration",
"description": "Reactive configuration for .NET",
"source": "upload",
"latest": "v5.2.0",
"versions": ["v5.2.0", "v5.1.0", "v5.0.0"]
}
]List Versions
GET /api/products/{product}/versionsReturns version information for a specific product. No authentication required.
{
"name": "configuration",
"latest": "v5.2.0",
"versions": ["v5.2.0", "v5.1.0", "v5.0.0"]
}Upload Version
POST /api/products/{product}/versions/{version}Uploads a ZIP file as a new documentation version. Requires Authorization: Bearer {key} header.
Error Responses
| Status | When |
|---|---|
201 | Successfully deployed |
400 | Invalid version format, corrupt ZIP, or missing index.html |
401 | Missing or invalid API key |
404 | Product not registered |
409 | Concurrent upload for the same product/version |
413 | ZIP exceeds maximum upload size |
503 | No API key configured (upload disabled) |
Security
- Authentication: Bearer token checked against the configured API key
- ZIP-Slip protection: All extracted paths are validated to stay within the target directory
- Atomic deployment: Files are extracted to a temp directory, validated, then moved — Shelf never serves a half-extracted state
- Concurrent upload protection: Simultaneous uploads for the same product/version return
409 Conflict - Size limit: Configurable via
MaxUploadSizeBytes(default: 100 MB)