# Phase 5: Publish & Deploy - Research

**Researched:** 2026-02-27
**Domain:** PyPI Trusted Publisher (GitHub Actions OIDC), GitHub Pages (MkDocs + custom domain), CI/CD branching workflows
**Confidence:** HIGH

---

## Summary

This phase has two distinct pipelines: (1) publish `meshulash-guard` to PyPI via GitHub Actions Trusted Publisher (OIDC), and (2) deploy the MkDocs docs site to `docs.guard.meshulash.ai` via GitHub Pages from the private `meshulash-guard-docs` repo.

The PyPI publishing approach uses PyPA's official `pypa/gh-action-pypi-publish` action (v1.13.0, latest), which implements the Trusted Publisher / OIDC flow without any API tokens. The workflow must be structured in two jobs: a build job (produces dist/ artifacts) and a separate publish job (with `id-token: write` permission). The first-time setup requires creating a **Pending Trusted Publisher** on PyPI (account-level, not project-level) before any code is pushed, since the project doesn't exist on PyPI yet.

For docs deployment, MkDocs's built-in `mkdocs gh-deploy` command handles everything: builds the site, commits to `gh-pages` branch, and pushes. The critical detail is that the CNAME file must live in `docs/` (the `docs_dir`) — not just on the `gh-pages` branch — or it gets overwritten on each deploy. The current CNAME file contains `docs.meshulash.ai` and must be updated to `docs.guard.meshulash.ai`. The Cloudflare DNS record is already set to gray cloud (DNS only), which is the correct setting for GitHub Pages SSL provisioning.

**Primary recommendation:** Set up the PyPI Pending Trusted Publisher on pypi.org before writing any workflow code. That registration step is the gate; everything else is mechanical.

---

## Standard Stack

### Core

| Tool / Action | Version | Purpose | Why Standard |
|---------------|---------|---------|--------------|
| `pypa/gh-action-pypi-publish` | v1.13.0 (`release/v1` branch) | Uploads dist/ to PyPI via OIDC | Official PyPA action; only action that natively supports Trusted Publishers |
| `actions/setup-python` | v5 | Install Python in CI | Official GitHub action |
| `python -m build` | (via `build` package) | Creates wheel + sdist from pyproject.toml | PEP 517 standard build frontend; works with hatchling |
| `actions/upload-artifact` / `actions/download-artifact` | v4 | Pass dist/ between jobs | Required when build and publish are separate jobs |
| `maybe-hello-world/pyproject-check-version` | v4 | Compares pyproject.toml version to live PyPI version | Marketplace action; used in PR gate workflow |
| `mkdocs gh-deploy` | (via mkdocs-material) | Builds and deploys docs to gh-pages branch | Built-in MkDocs command; no extra action needed |

### Supporting

| Tool | Version | Purpose | When to Use |
|------|---------|---------|-------------|
| `actions/checkout` | v4 | Checkout repo | Every workflow |
| `actions/cache` | v4 | Cache pip/mkdocs dependencies | Speed up docs deploy workflow |
| GitHub Environments | N/A | Scoped permissions + optional manual approval | Strongly recommended for pypi job |

### Alternatives Considered

| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| `pypa/gh-action-pypi-publish` | `twine upload` | Twine requires API token management; not recommended |
| `python -m build` | `hatch build` | Both work; `python -m build` is the neutral PEP 517 frontend; hatch build is hatch-specific but equivalent |
| `mkdocs gh-deploy` | `peaceiris/actions-gh-pages` + build step | More complex; mkdocs gh-deploy is self-contained |

### Installation (in CI)

```bash
# Build step
pip install build
python -m build  # produces dist/

# Docs step
pip install mkdocs-material>=9.0,<10.0
mkdocs gh-deploy --force
```

---

## Architecture Patterns

### Recommended Workflow Structure: Two Workflows, Two Repos

**Repo 1 (meshulash-guard SDK repo):** Two workflow files.

```
.github/
  workflows/
    ci.yml          # Runs on PR to main: test + version bump check
    publish.yml     # Runs on push to main: build + publish to PyPI
```

**Repo 2 (meshulash-guard-docs repo):** One workflow file.

```
.github/
  workflows/
    deploy.yml      # Runs on push to main: mkdocs gh-deploy
```

---

### Pattern 1: PyPI Publish — Two-Job Build + Publish

**What:** Separate jobs for build and publish. Build produces dist/ artifact; publish downloads it and uploads to PyPI. This is the official PyPA recommended structure.

**Why two jobs:** The `id-token: write` permission should be scoped only to the publishing job, not the build job. Narrowing permissions reduces blast radius if the build step is compromised.

**Example publish.yml (on merge to main):**

```yaml
# Source: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-pypi
name: Publish to PyPI

on:
  push:
    branches:
      - main

jobs:
  build:
    name: Build distribution
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.x"
      - name: Install build
        run: pip install build
      - name: Build wheel and sdist
        run: python -m build
      - name: Upload dist artifacts
        uses: actions/upload-artifact@v4
        with:
          name: python-package-distributions
          path: dist/

  publish:
    name: Publish to PyPI
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/meshulash-guard
    permissions:
      id-token: write  # REQUIRED for Trusted Publisher OIDC
    steps:
      - name: Download dist artifacts
        uses: actions/download-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
      - name: Publish to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
```

---

### Pattern 2: PR Gate — Version Bump Check

**What:** On PR to main, verify that pyproject.toml version is higher than the currently published PyPI version. Fails the PR if no version bump.

**Example ci.yml (PR check portion):**

```yaml
# Source: https://github.com/marketplace/actions/python-project-version-check
name: CI

on:
  pull_request:
    branches:
      - main

jobs:
  version-check:
    name: Version bump required
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check version is higher than PyPI
        uses: maybe-hello-world/pyproject-check-version@v4
        id: versioncheck
        with:
          pyproject-path: "./pyproject.toml"
      - name: Fail if version not bumped
        if: steps.versioncheck.outputs.local_version_is_higher == 'false'
        run: |
          echo "Version in pyproject.toml (${{ steps.versioncheck.outputs.local_version }}) must be higher than PyPI (${{ steps.versioncheck.outputs.public_version }})"
          exit 1
```

---

### Pattern 3: Docs Auto-Deploy

**What:** On push to main in docs repo, run mkdocs gh-deploy. Requires `contents: write` permission to push to gh-pages branch.

**Example deploy.yml:**

```yaml
# Source: https://squidfunk.github.io/mkdocs-material/publishing-your-site/
name: Deploy Docs

on:
  push:
    branches:
      - main

permissions:
  contents: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Configure Git
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
      - uses: actions/setup-python@v5
        with:
          python-version: 3.x
      - uses: actions/cache@v4
        with:
          key: mkdocs-material-${{ hashFiles('requirements.txt') }}
          path: ~/.cache/pip
          restore-keys: mkdocs-material-
      - run: pip install -r requirements.txt
      - run: mkdocs gh-deploy --force
```

---

### Pattern 4: PyPI Pending Trusted Publisher Setup (One-Time Manual Step)

**What:** Before any GitHub Actions workflow runs, register a Pending Trusted Publisher on PyPI. This creates the project-less publisher that mints a real publisher on first push.

**Where:** pypi.org → Account settings → "Your projects" sidebar → "Publishing" (account level, not project level — project doesn't exist yet).

**Information required:**
- Owner (GitHub org or username): `meshulash` (or whatever the org is)
- Repository name: `meshulash-guard`
- Workflow filename: `publish.yml` (must match exactly, including `.yml` vs `.yaml`)
- Environment name: `pypi` (optional but strongly recommended)
- PyPI project name: `meshulash-guard`

**Key caveat:** Registering a Pending Trusted Publisher does NOT reserve the PyPI name. If someone registers `meshulash-guard` on PyPI before your first publish, the pending publisher is invalidated. Publish promptly after setup.

---

### Anti-Patterns to Avoid

- **Reusable workflows for publishing:** Trusted Publishers cannot be used from within a reusable workflow. The `publish.yml` must be a standalone, non-reusable workflow file.
- **`id-token: write` on the whole workflow:** Set this permission only on the publishing job. Avoids unnecessary credential exposure during the build step.
- **Storing dist/ across unrelated runs:** Upload artifacts from the build job in the same workflow run, download in the publish job. Don't reuse artifacts from previous runs.
- **CNAME on gh-pages branch only:** If CNAME exists only on the `gh-pages` branch (e.g., set via GitHub UI), `mkdocs gh-deploy` overwrites it on next deploy. CNAME must be in `docs/` (the `docs_dir`) so MkDocs copies it into site/ on every build.
- **Orange-clouding Cloudflare before GitHub provisions SSL:** GitHub Pages can't issue a Let's Encrypt cert while DNS is proxied through Cloudflare. Must stay gray cloud until "Enforce HTTPS" becomes available in GitHub repo settings, then can optionally be orange-clouded.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| OIDC token exchange with PyPI | Custom curl/requests script | `pypa/gh-action-pypi-publish@release/v1` | Token exchange, validation, retry, and attestation built in |
| PyPI version comparison | Parse PyPI JSON API manually | `maybe-hello-world/pyproject-check-version@v4` | Handles pre-release semantics, network errors, package not found (first publish edge case) |
| Pushing built docs to gh-pages | Manual git operations | `mkdocs gh-deploy --force` | Uses `ghp-import` internally; handles orphan branch, commit history, force push correctly |
| SSL certificate provisioning | Nothing | GitHub Pages + Let's Encrypt (automatic) | Fully automatic; no cert management needed |

**Key insight:** All four "hard problems" in this phase have dedicated, maintained solutions. No custom scripting needed.

---

## Common Pitfalls

### Pitfall 1: Workflow Filename Mismatch on PyPI

**What goes wrong:** Trusted Publisher registration on PyPI uses `release.yml`, but the actual workflow file is named `publish.yml`. Every publish attempt fails with `invalid-pending-publisher` error.

**Why it happens:** The workflow filename in PyPI's Trusted Publisher config must match the file in `.github/workflows/` exactly, including the extension (`.yml` vs `.yaml` matters).

**How to avoid:** Decide the workflow filename before registering the publisher. Use it exactly. Double-check both after creation.

**Warning signs:** `invalid-pending-publisher` error in GitHub Actions logs during publish job.

---

### Pitfall 2: CNAME Gets Wiped on Every Docs Deploy

**What goes wrong:** Custom domain `docs.guard.meshulash.ai` works after initial GitHub Pages setup, but after the next `mkdocs gh-deploy` run, GitHub Pages reverts to `meshulash.github.io` because the CNAME file is gone from gh-pages.

**Why it happens:** `mkdocs gh-deploy` force-pushes a fresh build to the `gh-pages` branch. If CNAME is only on the branch (set via GitHub UI), it gets overwritten. MkDocs only preserves files it knows about from the build.

**How to avoid:** Place the CNAME file inside the `docs/` directory (the `docs_dir`). MkDocs copies everything in `docs_dir` into `site/`, which then gets pushed to `gh-pages`.

**Current state:** The CNAME file already exists at `docs/docs/CNAME` but contains `docs.meshulash.ai`. It must be updated to `docs.guard.meshulash.ai`. The `mkdocs.yml` `site_url` also needs updating from `https://docs.meshulash.ai` to `https://docs.guard.meshulash.ai`.

**Warning signs:** GitHub Pages settings show no custom domain after a docs deploy.

---

### Pitfall 3: GitHub Pages SSL Blocked by Cloudflare Orange Cloud

**What goes wrong:** "Enforce HTTPS" checkbox is grayed out / unavailable in GitHub repo settings even though the CNAME record is live.

**Why it happens:** When Cloudflare DNS proxy (orange cloud) is enabled, GitHub's Let's Encrypt certificate provisioning cannot see the DNS records it needs to verify domain ownership.

**How to avoid:** Keep the Cloudflare CNAME record as "DNS only" (gray cloud) — which the context confirms is already set and VERIFIED LIVE. Do not toggle it to proxied until after "Enforce HTTPS" is successfully enabled in GitHub settings.

**Warning signs:** "Enforce HTTPS" option missing or grayed out in GitHub Pages settings → DNS & Custom domain section.

---

### Pitfall 4: Pending Publisher Doesn't Reserve the Name

**What goes wrong:** Another PyPI user registers `meshulash-guard` after you create the Pending Trusted Publisher but before your first publish. Your publisher is invalidated.

**Why it happens:** "Pending" publishers exist at account level and don't create the project or reserve the name until first publish.

**How to avoid:** After creating the Pending Trusted Publisher, immediately run the publish workflow to create the project. Don't leave it pending for days.

**Warning signs:** First publish fails with a name conflict error.

---

### Pitfall 5: `id-token: write` on Wrong Scope

**What goes wrong:** OIDC token minting fails, or token is available in the build job where it isn't needed (security concern).

**Why it happens:** Setting `permissions: id-token: write` at the workflow level instead of the job level exposes the token to all jobs.

**How to avoid:** Only set `id-token: write` on the `publish` job, not globally on the workflow.

---

### Pitfall 6: `pyproject-check-version` Fails on First Publish (Package Not Yet on PyPI)

**What goes wrong:** The version check action runs on the very first PR before the package is on PyPI. The action may error out or return unexpected output when `public_version` is "not found."

**Why it happens:** The action queries PyPI's API for the current published version. If the package doesn't exist, the API returns 404.

**How to avoid:** Check the action's behavior on first publish (v4 handles this case — returns `local_version_is_higher: true` when package doesn't exist). Alternatively, skip the version check on the initial PR by using a conditional or a branch protection exception. Confirm this behavior in the action's README before relying on it.

**Warning signs:** Version check step fails on first PR with an HTTP 404 or similar error.

---

## Code Examples

Verified patterns from official sources:

### PyPI Pending Trusted Publisher: Account-Level Registration (Manual)

Navigate to: `https://pypi.org/manage/account/publishing/`

Required form fields:
- PyPI Project Name: `meshulash-guard`
- Owner: `<github-org-or-username>`
- Repository name: `meshulash-guard`
- Workflow name: `publish.yml`
- Environment name: `pypi` (optional, strongly recommended)

---

### Full publish.yml (two-job pattern)

```yaml
# Source: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-pypi
name: Publish to PyPI

on:
  push:
    branches:
      - main

jobs:
  build:
    name: Build distribution
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.x"
      - run: pip install build
      - run: python -m build
      - uses: actions/upload-artifact@v4
        with:
          name: python-package-distributions
          path: dist/

  publish:
    name: Publish to PyPI
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/meshulash-guard
    permissions:
      id-token: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: python-package-distributions
          path: dist/
      - uses: pypa/gh-action-pypi-publish@release/v1
```

---

### Full ci.yml (PR to main: version check + tests)

```yaml
name: CI

on:
  pull_request:
    branches:
      - main

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12", "3.13"]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pip install -e ".[dev]"
      - run: pytest

  version-check:
    name: Version bump required
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: maybe-hello-world/pyproject-check-version@v4
        id: versioncheck
        with:
          pyproject-path: "./pyproject.toml"
      - name: Fail if version not bumped
        if: steps.versioncheck.outputs.local_version_is_higher == 'false'
        run: |
          echo "ERROR: pyproject.toml version (${{ steps.versioncheck.outputs.local_version }}) must be > PyPI version (${{ steps.versioncheck.outputs.public_version }})"
          exit 1
```

---

### Full deploy.yml (docs repo, push to main)

```yaml
# Source: https://squidfunk.github.io/mkdocs-material/publishing-your-site/
name: Deploy Docs

on:
  push:
    branches:
      - main

permissions:
  contents: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Configure Git
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
      - uses: actions/setup-python@v5
        with:
          python-version: "3.x"
      - uses: actions/cache@v4
        with:
          key: mkdocs-material-${{ hashFiles('requirements.txt') }}
          path: ~/.cache/pip
          restore-keys: mkdocs-material-
      - run: pip install -r requirements.txt
      - run: mkdocs gh-deploy --force
```

---

### CNAME File Content (docs/docs/CNAME)

```
docs.guard.meshulash.ai
```

Single line, no trailing slash, no `https://`.

---

### mkdocs.yml site_url update

```yaml
site_url: https://docs.guard.meshulash.ai
```

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| PyPI API tokens in GitHub Secrets | Trusted Publisher (OIDC) — no tokens | 2023 (GA), now strongly recommended | Zero token management; tokens expire automatically; no rotation needed |
| Twine upload in CI | `pypa/gh-action-pypi-publish` | 2022+ | Native Trusted Publisher support; easier |
| Manual first-upload to create project | Pending Trusted Publisher | 2023 | Can set up full automation before any code lands on PyPI |
| Upload by git tag | Upload on merge to main | User decision | Simpler; no tag management |

**Deprecated/outdated:**
- PyPI API tokens: Still supported but not recommended when Trusted Publishers are available
- `twine`: Still works but adds unnecessary friction; `pypa/gh-action-pypi-publish` is the standard

---

## Claude's Discretion Items — Recommendations

### Python Version Support Range

**Current pyproject.toml:** `requires-python = ">=3.9"`

**EOL Status (as of 2026-02):**
- Python 3.9: EOL October 2025 (already EOL)
- Python 3.10: EOL October 2026 (security-only mode)
- Python 3.11: EOL October 2028 (security-only mode)
- Python 3.12: EOL October 2029 (bugfix mode — current recommended)
- Python 3.13: EOL October 2030 (bugfix mode — latest)

**Recommendation:** Change `requires-python` to `>=3.10`. Python 3.9 reached EOL in October 2025. Supporting it in a brand-new SDK has no practical benefit and adds CI matrix burden. This also avoids publishing a package that officially supports an EOL Python version at launch.

**CI matrix recommendation:** Test against `["3.10", "3.11", "3.12", "3.13"]` — covers current security + bugfix releases without testing EOL Python.

---

### TestPyPI One-Time Verification (Optional)

**Recommendation:** Skip TestPyPI for this phase. The Trusted Publisher flow is well-established, and the primary risk (wrong package name, wrong metadata) is easily caught by inspecting `python -m build` output locally before the first merge. TestPyPI adds complexity (requires a second Trusted Publisher registration for test.pypi.org) without significant benefit for a straightforward package.

If desired, can add a separate `testpypi-publish.yml` triggered manually (`workflow_dispatch`) for one-off verification. Not recommended as ongoing CI step.

---

### Exact GitHub Actions Workflow Structure

**Recommendation:** Two files in SDK repo (`ci.yml` for PR checks, `publish.yml` for release), one file in docs repo (`deploy.yml`). See Code Examples section for complete YAML.

The `ci.yml` PR check combines tests (matrix) + version-check in parallel jobs. If either fails, the PR is blocked.

---

### Build System Details

**Recommendation:** No changes needed. `hatchling>=1.26` is already in `pyproject.toml`. `python -m build` will use hatchling as the build backend automatically. No additional configuration required.

The `[tool.hatch.build.targets.wheel]` section already correctly specifies `packages = ["src/meshulash_guard"]`, which is correct for the src layout.

---

## Pre-Conditions / Manual Steps Before Code

These steps must happen BEFORE any workflow is pushed to the repos:

1. **PyPI: Create Pending Trusted Publisher** — `https://pypi.org/manage/account/publishing/` — register with exact repo/workflow details before pushing `publish.yml`

2. **GitHub: Create `pypi` Environment in SDK repo** — repo Settings → Environments → New environment named `pypi`

3. **GitHub: Create `dev` branch in SDK repo** — `git checkout -b dev && git push -u origin dev`

4. **GitHub Pages: Configure in docs repo** — Repo Settings → Pages → Source: "Deploy from a branch" → Branch: `gh-pages` → Folder: `/root` (this must be done after the first `mkdocs gh-deploy` run creates the branch, OR set to gh-pages branch before first deploy)

5. **GitHub Pages: Set custom domain** — After gh-pages branch exists, go to Pages settings → Custom domain → `docs.guard.meshulash.ai` → Save → Enable "Enforce HTTPS" (may take up to 24h)

6. **CNAME file update** — Update `docs/docs/CNAME` from `docs.meshulash.ai` → `docs.guard.meshulash.ai`

7. **mkdocs.yml site_url update** — Update from `https://docs.meshulash.ai` → `https://docs.guard.meshulash.ai`

---

## Open Questions

1. **GitHub org vs. personal account for PyPI Trusted Publisher**
   - What we know: PyPI Trusted Publisher requires exact repo owner
   - What's unclear: Is the SDK repo under the `meshulash` GitHub org or a personal account? The Pending Trusted Publisher form needs the exact owner value.
   - Recommendation: Confirm GitHub org/username before registering publisher

2. **`pyproject-check-version@v4` behavior when package is not yet on PyPI**
   - What we know: The action returns `local_version_is_higher: true` or `false`
   - What's unclear: Whether it errors or gracefully returns `true` on a 404 (package not found) — the first PR before any PyPI publish
   - Recommendation: Check the action's README/changelog for v4 behavior on new packages, or add `continue-on-error: true` for the version check step on the very first PR

3. **GitHub Team plan verification**
   - What we know: GitHub Pages on private repos requires GitHub Pro, Team, or Enterprise
   - What's unclear: The context states "believed to be active" — should be verified in account settings before work begins
   - Recommendation: Confirm GitHub Team plan is active before building the docs deploy workflow

4. **Does GitHub create `gh-pages` branch automatically on first `mkdocs gh-deploy`?**
   - What we know: `mkdocs gh-deploy` uses `ghp-import` which pushes to `gh-pages` branch
   - What's unclear: Whether the branch must exist first or is auto-created
   - Recommendation: It is auto-created by ghp-import. No manual branch creation needed. But GitHub Pages source must be configured afterward.

---

## Sources

### Primary (HIGH confidence)

- PyPI Docs — Trusted Publishers: https://docs.pypi.org/trusted-publishers/
- PyPI Docs — Adding a Trusted Publisher: https://docs.pypi.org/trusted-publishers/adding-a-publisher/
- PyPI Docs — Creating Project via Pending Publisher: https://docs.pypi.org/trusted-publishers/creating-a-project-through-oidc/
- PyPI Docs — Using a Publisher (workflow example): https://docs.pypi.org/trusted-publishers/using-a-publisher/
- PyPI Docs — Troubleshooting: https://docs.pypi.org/trusted-publishers/troubleshooting/
- GitHub Docs — Configuring OIDC in PyPI: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-pypi
- GitHub Docs — Custom Domains for GitHub Pages: https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site
- GitHub Docs — GitHub Pages Limits: https://docs.github.com/en/pages/getting-started-with-github-pages/github-pages-limits
- MkDocs — Deploying Your Docs: https://www.mkdocs.org/user-guide/deploying-your-docs/
- MkDocs Material — Publishing Your Site: https://squidfunk.github.io/mkdocs-material/publishing-your-site/
- Python Devguide — Version Status: https://devguide.python.org/versions/

### Secondary (MEDIUM confidence)

- `pypa/gh-action-pypi-publish` GitHub Marketplace — v1.13.0 verified current: https://github.com/marketplace/actions/pypi-publish
- `maybe-hello-world/pyproject-check-version@v4` — inputs/outputs verified: https://github.com/marketplace/actions/python-project-version-check
- Python Packaging User Guide — CI/CD Workflows: https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/

### Tertiary (LOW confidence — needs validation)

- Cloudflare orange cloud behavior after SSL provisioned: Multiple community sources agree but not verified against Cloudflare official docs
- `pyproject-check-version@v4` behavior on 404/new packages: Not confirmed from official README; flagged as open question

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — official PyPA action, official GitHub docs, official MkDocs docs
- Architecture (publish workflow): HIGH — matches official PyPA recommendations exactly
- Architecture (docs workflow): HIGH — from MkDocs Material official publishing docs
- Pitfalls: HIGH for CNAME/Cloudflare (multiple confirmed sources), MEDIUM for version check edge case
- Python version support: HIGH — from official Python devguide with specific EOL dates

**Research date:** 2026-02-27
**Valid until:** 2026-08-27 (stable tooling; PyPA action and MkDocs deploy approach are stable)
