Documentation Hosting Setup
This page describes the deployment setup for ClearlyOps documentation.
Overview
ClearlyOps documentation is split into two sites:
| Site | URL | Access | Content |
|---|---|---|---|
| Public | docs.clearlyops.com |
Public | API docs, SDK reference, portfolio-safe content |
| Internal | docs-internal.clearlyops.com |
Protected | Deployment guides, runbooks, internal ops |
Both sites are:
- Built: GitHub Actions generates from code and docstrings
- Hosted: Cloudflare Pages serves the static sites
- Secured: Cloudflare Access protects internal docs; secrets fetched from GCP Secret Manager
Architecture
┌─────────────────┐ ┌──────────────────────────────┐
│ GitHub Actions │ │ GCP Secret Manager │
│ (build/deploy) │ │ (cloudflare-pages-api-token)│
└────────┬────────┘ └──────────────┬───────────────┘
│ │
│ OIDC │ gcloud secrets
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ Deploy Job │
│ - Authenticate to GCP via Workload Identity (OIDC) │
│ - Fetch Cloudflare token from Secret Manager │
│ - Deploy public site to clearlyops-docs-public │
│ - Deploy internal site to clearlyops-docs-internal │
└─────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────────────────┐
│ Public Docs │ │ Internal Docs │
│ docs.clearlyops│ │ docs-internal.clearlyops │
│ (no Access) │ │ + Cloudflare Access │
│ │ │ + pages.dev protected │
│ │ │ + preview deploys disabled │
└─────────────────┘ └──────────────────────────────┘
Agent-Managed Infrastructure
Infrastructure can be managed programmatically using scripts/setup_docs_infra.py:
# Plan changes (dry-run, no mutations)
python scripts/setup_docs_infra.py plan --config deploy/infra/docs.toml
# Apply changes (requires confirmation)
python scripts/setup_docs_infra.py apply --config deploy/infra/docs.toml --confirm docs-internal.clearlyops.com
# Check current status
python scripts/setup_docs_infra.py status --format json
The script:
- Fetches Cloudflare API token from GCP Secret Manager (no local secrets)
- Ensures Pages projects exist
- Ensures custom domains are attached
- Ensures DNS records are correct and proxied
- Ensures Cloudflare Access protects internal docs (both custom domain AND *.pages.dev)
- Outputs a JSON receipt for auditing
Configuration
Infrastructure configuration lives in deploy/infra/docs.toml:
[gcp]
project_id = "your-gcp-project-id"
secret_cloudflare_api_token = "cloudflare-pages-api-token"
workload_identity_provider = "projects/.../providers/..."
service_account_email = "[email protected]"
[cloudflare]
account_id = "your-cloudflare-account-id"
zone_id = "your-cloudflare-zone-id"
[docs.public]
pages_project_name = "clearlyops-docs-public"
custom_domain = "docs.clearlyops.com"
[docs.internal]
pages_project_name = "clearlyops-docs-internal"
custom_domain = "docs-internal.clearlyops.com"
preview_deployments = "disabled"
[access]
allowed_emails = ["[email protected]"]
See deploy/infra/docs.example.toml for a complete example.
GitHub Actions Workflow
The workflow (.github/workflows/docs.yml) uses GitHub OIDC → GCP authentication:
- Build job:
- Generate docs:
python scripts/generate_docs.py - Build public site:
mkdocs build --config-file mkdocs.public.yml --site-dir site-public - Build internal site:
mkdocs build --config-file mkdocs.internal.yml --site-dir site-internal -
Upload artifacts
-
Deploy job (push only):
- Authenticate to GCP using Workload Identity Federation
- Fetch Cloudflare token from GCP Secret Manager
- Deploy public site to
clearlyops-docs-public - Deploy internal site to
clearlyops-docs-internal
Required GitHub Variables (not secrets)
These are stored as repository variables (Settings → Secrets and variables → Variables):
| Variable | Description |
|---|---|
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account ID |
GCP_PROJECT_ID |
GCP project ID |
GCP_WORKLOAD_IDENTITY_PROVIDER |
Workload Identity provider resource name |
GCP_SERVICE_ACCOUNT |
Service account email for deployments |
No Cloudflare API token in GitHub — it's fetched from GCP Secret Manager at runtime.
One-Time Bootstrap
1. Create Cloudflare API Token
- Cloudflare Dashboard → Profile → API Tokens
- Create Token with permissions:
Account→Cloudflare Pages→EditAccount→Access: Apps and Policies→EditZone→DNS→Edit(for your zone)- Copy the token
2. Store Token in GCP Secret Manager
export GOOGLE_CLOUD_PROJECT=your-project-id
# Create the secret
gcloud secrets create cloudflare-pages-api-token \
--replication-policy="automatic" \
--project=$GOOGLE_CLOUD_PROJECT
# Add the token value
echo "YOUR_CLOUDFLARE_TOKEN" | gcloud secrets versions add cloudflare-pages-api-token \
--data-file=- \
--project=$GOOGLE_CLOUD_PROJECT
3. Set Up Workload Identity Federation
# Create Workload Identity Pool
gcloud iam workload-identity-pools create github-pool \
--location="global" \
--display-name="GitHub Actions Pool" \
--project=$GOOGLE_CLOUD_PROJECT
# Create Provider
gcloud iam workload-identity-pools providers create-oidc github-provider \
--location="global" \
--workload-identity-pool="github-pool" \
--display-name="GitHub Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--project=$GOOGLE_CLOUD_PROJECT
# Create Service Account
gcloud iam service-accounts create docs-deployer \
--display-name="Docs Deployer" \
--project=$GOOGLE_CLOUD_PROJECT
# Grant secret access
gcloud secrets add-iam-policy-binding cloudflare-pages-api-token \
--member="serviceAccount:docs-deployer@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor" \
--project=$GOOGLE_CLOUD_PROJECT
# Allow GitHub to impersonate
gcloud iam service-accounts add-iam-policy-binding \
docs-deployer@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/github-pool/attribute.repository/YOUR_ORG/clearlyops" \
--project=$GOOGLE_CLOUD_PROJECT
4. Configure GitHub Repository Variables
Add these as repository variables (not secrets):
CLOUDFLARE_ACCOUNT_IDGCP_PROJECT_IDGCP_WORKLOAD_IDENTITY_PROVIDER(full resource name)GCP_SERVICE_ACCOUNT(email)
5. Run Infrastructure Setup
python scripts/setup_docs_infra.py apply --config deploy/infra/docs.toml --confirm docs-internal.clearlyops.com
Bypass Prevention
The internal docs are protected against bypass:
- Custom domain protected:
docs-internal.clearlyops.comrequires Cloudflare Access login - Pages.dev protected:
clearlyops-docs-internal.pages.devalso requires login - Preview deployments disabled: No PR preview URLs are generated for internal docs
Local Development
# Install dependencies
pip install -e ".[docs]"
# Generate dynamic content
python scripts/generate_docs.py
# Build public site
mkdocs build --strict --config-file mkdocs.public.yml --site-dir site-public
# Build internal site
mkdocs build --strict --config-file mkdocs.internal.yml --site-dir site-internal
# Serve public site locally
mkdocs serve --config-file mkdocs.public.yml
# Visit http://127.0.0.1:8000
# Serve internal site locally
mkdocs serve --config-file mkdocs.internal.yml
# Visit http://127.0.0.1:8000
Troubleshooting
Build fails
- Check
[docs]dependencies:pip install -e ".[docs]" - Run
mkdocs build --strictlocally to see errors - Check for broken links in markdown files
Deploy fails with auth error
- Verify GCP Workload Identity is configured correctly
- Check service account has
secretmanager.secretAccessorrole - Verify repository variable names match workflow
Access control not working
- Use
scripts/setup_docs_infra.py statusto verify Access app exists - Check both custom domain and
*.pages.devare protected - Verify allowed emails in
deploy/infra/docs.toml
Secret not found
- Verify secret exists:
gcloud secrets describe cloudflare-pages-api-token - Check service account permissions
- Ensure
GCP_PROJECT_IDis correct
Security Checklist
- [ ] Cloudflare API token stored in GCP Secret Manager (not GitHub)
- [ ] GitHub uses OIDC → GCP (no long-lived credentials)
- [ ] Internal docs protected by Cloudflare Access
- [ ] Both
docs-internal.clearlyops.comAND*.pages.devprotected - [ ] Preview deployments disabled for internal project
- [ ] Service account has minimal permissions (only secret access)
- [ ] Regular rotation of Cloudflare API token
Reference
- Infra config:
deploy/infra/docs.toml - Setup script:
scripts/setup_docs_infra.py - CI workflow:
.github/workflows/docs.yml - Internal runbook:
deploy/docs-hosting-internal.md - Cloudflare Pages: https://developers.cloudflare.com/pages/
- Workload Identity: https://cloud.google.com/iam/docs/workload-identity-federation