# Docs Portal Static Cloudflare Deployment v1

## Purpose

Define the safe deployment path for the Docusaurus documentation portal as a
static artifact. The portal must be built, publication-checked, and uploaded to
static hosting. It must not expose a developer workstation, local Docusaurus
server, or dev-control host as the public documentation endpoint.

## Selected Model

Use Cloudflare Pages, or an equivalent static host, for the first publication
path.

```mermaid
flowchart LR
  repo["GPUaaS repo"] --> check["docs portal check"]
  check --> build["static Docusaurus build"]
  build --> evidence["deployment preflight evidence"]
  evidence --> upload["Cloudflare Pages upload"]
  upload --> dns["docs hostname"]
  dns --> review["post-deploy smoke and rollback evidence"]
```

The deployment source is `packages/docs/build` after `make docs-portal-check`
passes.

## Required Inputs

Do not commit these values. Supply them through CI variables, a secret manager,
or an operator-local environment file ignored by git.

Example local operator template:

```text
doc/operations/local-dev/docs-portal-cloudflare.env.example
```

| Input | Purpose | Notes |
| --- | --- | --- |
| `DOCS_PORTAL_PUBLICATION_TRACK` | target publication track | `internal`, `customer`, `partner`, `public`, `ops`, or `governance` |
| `DOCS_PORTAL_HOSTNAME` | target docs hostname | current choice: `docs.aicloud.core42.dev` |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account | secret or protected CI variable |
| `CLOUDFLARE_PAGES_PROJECT` | Pages project name | non-secret but environment-specific |
| `CLOUDFLARE_API_TOKEN` | upload credential | secret; never print |

Current first live project:

```text
CLOUDFLARE_PAGES_PROJECT=aicloud-docs
```

The publish wrapper also accepts an operator env file through:

```text
DOCS_PORTAL_CLOUDFLARE_CREDS_FILE=/absolute/path/to/cloudflare.env
```

That env file may use either the docs-publish variable names
(`CLOUDFLARE_ACCOUNT_ID`, `CLOUDFLARE_API_TOKEN`) or the older operator
credential names already used elsewhere in GPUaaS (`AccountID`, `APIToken`).

## Minimum Cloudflare Token Scope

For Cloudflare Pages deployment, create a purpose-specific token. Do not reuse
global API keys.

| Scope | Why |
| --- | --- |
| Account:Cloudflare Pages:Edit | upload Pages deployment |
| Account:Cloudflare Pages:Read | read deployment/project status |
| Zone:DNS:Read | verify hostname/DNS posture |
| Zone:Zone:Read | resolve zone metadata during setup |

If DNS records are managed by the same automation, add `Zone:DNS:Edit` only for
the deployment identity that owns DNS changes. Prefer separating Pages upload
from DNS mutation.

## Current Credential Split

The current working setup is intentionally split:

- Pages deploy uses a Pages-capable token/account;
- `core42.dev` DNS and Cloudflare Access use the zone-side token/account.

This is acceptable for the first internal publication as long as the split is
documented and repeatable:

1. create or manage the Pages project in the Pages-capable account;
2. attach the custom hostname from the Pages side;
3. create or update the `docs.aicloud.core42.dev` CNAME in the `core42.dev`
   zone;
4. create or update the Cloudflare Access app/policy in the `core42.dev` zone;
5. use the Pages-capable token for subsequent content publishes.

## Build And Preflight

Run the deterministic preflight before any upload:

```bash
bash scripts/ci/docs_portal_static_deploy_preflight.sh
```

The preflight:

1. runs `make docs-portal-check`;
2. verifies `packages/docs/build/index.html` exists;
3. records publication track, hostname presence, Pages project presence, build
   path, git SHA, source-doc inventory path, and contract artifact paths;
4. writes evidence to `dist/docs-portal-static-deploy-preflight/` by default;
5. redacts token presence and never prints token bytes.

Use a custom evidence path when running under CI:

```bash
DOCS_PORTAL_DEPLOY_EVIDENCE_DIR=dist/docs-portal-static-deploy-preflight/${CI_PIPELINE_ID} \
  bash scripts/ci/docs_portal_static_deploy_preflight.sh
```

## Upload Command

The repository does not require `wrangler` as a dependency. Install or provide
it in the CI runner image used for the actual upload step.

Preferred wrapper:

```bash
bash scripts/ops/docs_portal_publish_cloudflare_pages.sh
```

The wrapper reruns the deploy preflight, validates required environment
variables, executes `wrangler pages deploy`, and writes sanitized durable
evidence under `dist/docs-portal-cloudflare-publish/`.

GitLab CI mirrors the same path with:

- `docs_portal_quality_gate`
- `docs_portal_publish_internal`

Expected upload shape:

```bash
wrangler pages deploy packages/docs/build \
  --project-name "$CLOUDFLARE_PAGES_PROJECT" \
  --branch "$DOCS_PORTAL_PUBLICATION_TRACK" \
  --commit-dirty=false
```

Run the upload only after preflight evidence is attached to the deploy-run task.

## DNS And Hostname

Use a dedicated docs hostname. Do not reuse API, app, auth, registry, or
operator hostnames.

Recommended posture:

| Setting | Baseline |
| --- | --- |
| DNS | CNAME or Cloudflare Pages custom domain |
| TLS | Cloudflare managed certificate |
| HSTS | enable after hostname and rollback path are validated |
| Always Use HTTPS | enabled |
| Cache | static assets long-lived; HTML short-lived or revalidated |
| Access | internal/ops/governance tracks require Cloudflare Access or equivalent |

## First Access Policy

For the first protected internal publication, use Cloudflare Access with
one-time email passcode login. Keep the portal static; do not add portal-local
authentication in this iteration.

Allow:

- `@core42.ai`
- `@g42.ai`
- `subahsram@gmail.com`

This is a short-term access model chosen for ease of administration while the
audience remains small and manually shared. It is intentionally coarse-grained:
the URL is not meant for the full domains even though the policy technically
permits those addresses.

Follow-up direction:

- move to IdP-backed groups when the audience grows;
- split tighter publication tracks if some portal sections are too sensitive
  for domain-wide access;
- keep any future role-aware filtering in the publication-track model, not as
  ad hoc page exceptions.

Current first-iteration hostname decision:

```text
docs.aicloud.core42.dev
```

Reasoning:

- keeps the portal human-friendly;
- stays shallow enough for the wildcard/TLS model that already proved stable;
- does not force a rename of the existing `aicloud-<env>-<service>.core42.dev`
  runtime fleet.

## First Protected Internal Publication Readback

The first live protected publication path was proven with:

- Pages project: `aicloud-docs`
- custom domain: `docs.aicloud.core42.dev`
- DNS: proxied CNAME to `aicloud-docs.pages.dev`
- Access app: `AI Cloud Docs Portal`
- initial allow policy:
  - `@core42.ai`
  - `@g42.ai`
  - `subashram@gmail.com`

Expected unauthenticated readback:

- `curl -I https://docs.aicloud.core42.dev/` returns a Cloudflare Access
  redirect/challenge (`302`, `401`, or `403` depending on policy/session),
  not a public `200`.

## Security Posture

| Control | Requirement |
| --- | --- |
| Bot/WAF | enable managed challenge/rate controls appropriate to the track |
| Secrets | no tokens, private URLs, tenant names, raw keys, local file paths, or unreviewed evidence in public/customer/partner builds |
| API playground | mock-only for public until live playground approval exists |
| Headers | set `X-Content-Type-Options`, `Referrer-Policy`, and conservative `Permissions-Policy` |
| CSP | start report-only if embedded renderer tooling requires inline/script allowances; tighten before public launch |
| Logs | do not log bearer tokens or query-string secrets; playground must not put tokens in URLs |

## Rollback

Cloudflare Pages keeps deployment history. Rollback is a promotion operation:

1. identify the last known-good Pages deployment id;
2. promote or roll back through the Cloudflare Pages UI/API;
3. record actor, old deployment id, new deployment id, hostname, reason, and
   post-rollback smoke result;
4. create a `CD-FIX-*` or `DOC-FIX-*` task for the failed deployment.

Do not roll back by pointing DNS at a local server or dev host.

## Post-Deploy Smoke

At minimum:

| Check | Expected |
| --- | --- |
| `GET /` | `200`, static HTML, correct portal title |
| `GET /api/openapi.draft.yaml` | `200`, committed OpenAPI artifact |
| `GET /api/asyncapi.draft.yaml` | `200`, committed AsyncAPI artifact |
| Security headers | configured according to track |
| No index leak | no source maps or build artifacts that expose secrets |

## Related Docs

- `doc/PORTAL_SYNC.md`
- `doc/product/GPUaaS_Documentation_and_Developer_Portal_Docusaurus_v1.md`
- `packages/docs/docs/portal-roadmap/publication-tracks/index.mdx`
- `packages/docs/docs/portal-roadmap/publication-filtering/index.mdx`
- `packages/docs/docs/portal-roadmap/external-readiness/index.mdx`
