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 Admin UI, API, or seed file)
- An API key — either the product's own key, or a master key. See Authentication
Per-product keys
Give each pipeline the per-product key from its product's Tags & API tab instead of the master key — a leaked key can then only deploy that one product.
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 (Bearer token or cookie session)
- Product registration is checked (must exist)
- Version format is validated against the version pattern (supports SemVer:
v5,v5.2,v5.2.0,v5.2.0-beta.1,v1.0.0-vue-ui.10) - 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.
Deleting a Version
curl -X DELETE \
-H "Authorization: Bearer $SHELF_API_KEY" \
https://docs.cocoar.dev/_api/products/configuration/versions/v6The version directory is removed from disk. ManifestService detects the change automatically.
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) - Pre-release --
v5.2.0-beta.1(use full version for pre-release builds)
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
- Node.js required -- CI workflows need a Node.js setup step before building (for the Vue client)
API Reference
All API routes use the /_api/ prefix. See Authentication for details on auth methods.
Products
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /_api/products | No | List all registered products with version info |
GET | /_api/products/{product} | No | Get a single product with version info |
POST | /_api/products | Yes | Create a new product |
PUT | /_api/products/{product} | Yes | Update a product |
DELETE | /_api/products/{product} | Yes | Delete product config. Pass ?deleteData=true to also delete docs from disk |
Versions
| Method | Path | Auth | Description |
|---|---|---|---|
GET | /_api/products/{product}/versions | No | List versions of a product |
POST | /_api/products/{product}/versions/{version} | Yes | Upload a ZIP as a new version |
DELETE | /_api/products/{product}/versions/{version} | Yes | Delete a version |
Authentication
See Authentication — the Admin UI signs in via Modgud (email code); API access uses Bearer keys.
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",
"visibility": "public",
"tags": ["C#", ".NET"],
"showWhenEmpty": false,
"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 authentication.
Delete Version
DELETE /_api/products/{product}/versions/{version}Deletes a documentation version. Requires authentication.
Error Responses
| Status | When |
|---|---|
201 | Successfully deployed |
400 | Invalid version format, corrupt ZIP, or missing index.html |
401 | Missing or invalid authentication |
404 | Product not registered |
409 | Concurrent upload for the same product/version |
413 | ZIP exceeds maximum upload size |
Security
- Authentication: Bearer token or cookie session checked against the configured API key. See Authentication
- 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)