diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 59439aa5..f3624322 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -159,3 +159,19 @@ jobs: echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**PyPI:** https://pypi.org/project/signalwire-sdk/${{ steps.tag_version.outputs.tag_version }}/" >> $GITHUB_STEP_SUMMARY + + # Republish the API reference for the version that just shipped. + # + # Called here rather than triggered by `release: published` inside + # reference-docs.yml: the Create GitHub Release step above uses + # secrets.GITHUB_TOKEN, and GitHub deliberately does not raise + # workflow-triggering events for that token, so a release trigger would never + # fire on this path. `needs: publish-release` also gives the ordering we + # actually want, with the docs going out only after the PyPI upload succeeded. + reference-docs: + needs: publish-release + permissions: + contents: write # the called workflow force-pushes the built site to gh-pages + uses: ./.github/workflows/reference-docs.yml + with: + tag: ${{ github.ref_name }} # the `v*` tag that triggered this run diff --git a/.github/workflows/reference-check.yml b/.github/workflows/reference-check.yml new file mode 100644 index 00000000..fef3058c --- /dev/null +++ b/.github/workflows/reference-check.yml @@ -0,0 +1,48 @@ +# API reference docs, PR check. +# +# Build-only counterpart to reference-docs.yml: no deploy, no write permission. +# Catches a crashing generator, a root `signalwire` package that will not import +# (gen.sh imports it to enumerate subpackages), and unresolved cross-references, +# on the PR rather than on release day against the live site. +# +# It does NOT catch a submodule whose runtime dependency is missing: mkdocstrings +# analyses statically through griffe, so such a module still renders a clean page. +# Nothing here lints workflow YAML either, so a mistake in reference-docs.yml +# still surfaces at release time. porting-sdk ships scripts/actionlint_gate.py, +# which run-ci.sh does not currently call; wiring it up would close that gap. +name: reference-check + +on: + pull_request: + paths: + - "signalwire/**" + - "reference/**" + - "pyproject.toml" + - ".github/workflows/reference-check.yml" + # Changing the publish workflow at least re-runs this build. It is not + # validation OF that workflow; see the note above. + - ".github/workflows/reference-docs.yml" + +permissions: + contents: read # never deploys + +concurrency: + group: reference-check-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Install SDK (editable) + doc toolchain + run: | + python -m pip install --upgrade pip + pip install -r reference/requirements.txt + pip install -e . + # gen.sh without --no-build runs the strict `mkdocs build`. + - name: Generate + build (strict) + run: bash reference/gen.sh --no-install diff --git a/.github/workflows/reference-docs.yml b/.github/workflows/reference-docs.yml index 3180fa52..66acfb0e 100644 --- a/.github/workflows/reference-docs.yml +++ b/.github/workflows/reference-docs.yml @@ -1,40 +1,190 @@ -# API reference docs (pilot). +# API reference docs. # # Builds the SignalWire Python SDK API reference (MkDocs Material + mkdocstrings) -# and publishes it to this repo's OWN GitHub Pages via mike (versioned): +# and publishes a single, unversioned site to this repo's OWN GitHub Pages: # https://signalwire.github.io/signalwire-python/ # -# PILOT triggers: workflow_dispatch + push to the pilot branch. This is NOT -# gated on releases yet. -# PRODUCTION (later): switch the trigger to `v*` tags so each release version -# is deployed and `latest` is re-aliased. See the commented block below. +# Publishing is deliberately narrow. `gh-deploy` force-pushes, but it does NOT +# discard history: mkdocs passes no_history=False, so ghp-import parents each +# deploy onto origin/gh-pages and --force forces only the push. A bad publish is +# therefore recoverable, by hand, with: +# git push --force origin gh-pages~1:gh-pages +# That recovery depends on somebody noticing, so the gate below exists to keep an +# older or invalid version from becoming the public site in the first place: +# * called by publish-release.yml only after the full CI gate set and the PyPI +# upload have succeeded, so a rejected tag can never become the public site. +# * stable semver tags only, and only when the tag is at least as new as the +# version the last publish recorded at the site root. +# * workflow_dispatch builds a preview by default. It publishes only when the +# `deploy` input is ticked, only from the default branch, and never from a +# fork. +# * anything the gate cannot positively determine fails CLOSED. +# +# There is deliberately NO `release: published` trigger. publish-release.yml +# creates the GitHub Release with `secrets.GITHUB_TOKEN`, and GitHub does not +# raise workflow-triggering events for actions taken with that token +# (workflow_dispatch and repository_dispatch excepted). A `release` trigger would +# therefore sit here looking correct and silently never fire on the normal tag +# path. The reusable `workflow_call` below is the wire that actually connects. # # MANUAL REPO SETTING REQUIRED to go live (one-time, in the GitHub UI): -# Settings -> Pages -> Build and deployment -> Source = "Deploy from a branch" -# -> Branch = `gh-pages` / `(root)`. mike pushes the built site there. +# Settings > Pages > Build and deployment > Source = "Deploy from a branch", +# Branch = `gh-pages` / `(root)`. `mkdocs gh-deploy` pushes the built site there. name: reference-docs on: + workflow_call: + inputs: + tag: + description: "Release tag being published (vX.Y.Z). Its presence is what marks a run as a release publish." + type: string + required: true workflow_dispatch: - push: - branches: - - docs/api-reference-pilot - - # --- PRODUCTION trigger (enable when promoting out of the pilot) ------------ - # push: - # tags: - # - "v*" + inputs: + deploy: + description: "Publish to gh-pages (leave false for a build-only preview)" + type: boolean + default: false + version_marker: + description: "Recovery only: overwrite the site's version.txt with this (vX.Y.Z). Leave blank to preserve the current marker." + type: string + required: false + default: "" +# Applies to the workflow_dispatch path. On the workflow_call path the caller's +# job grants the token permissions, so publish-release.yml sets contents: write +# on the job that calls this workflow. permissions: - contents: write # mike pushes the built site to the gh-pages branch + contents: write # gh-deploy pushes the built site to the gh-pages branch jobs: build-deploy: runs-on: ubuntu-latest + # Job level, NOT workflow level. A called workflow's jobs run inside the + # CALLER's run, so a top-level `concurrency:` here would be ignored on the + # publish-release.yml path and two releases could race the force-push. + # + # Publishing runs share one serialized group; previews get a per-ref group of + # their own, so a newly queued preview can never displace a pending release + # publish (which would drop it as "cancelled" rather than failing red). + concurrency: + group: ${{ (inputs.tag != '' || inputs.deploy) && 'reference-docs-production' || format('reference-docs-preview-{0}', github.ref) }} + cancel-in-progress: false # never interrupt a push mid-flight steps: - uses: actions/checkout@v7 with: - fetch-depth: 0 # mike needs full history + the gh-pages branch + # Required, and do NOT "optimize" it away: this is what fetches + # origin/gh-pages. Without that ref present, the force-push lands a + # parentless orphan commit and the deploy history really is destroyed. + fetch-depth: 0 + ref: ${{ inputs.tag || github.ref }} + + # Runs BEFORE the toolchain install, so a run that cannot publish does not + # first pay several minutes of pip. It needs nothing but git. + # + # Single decision point for whether this run may force-push the public + # site, and the single place that decides what the version marker becomes. + # `inputs.tag` is only defined for workflow_call and `inputs.deploy` only + # for workflow_dispatch; the one belonging to the other trigger evaluates + # to an empty string, which is what separates the two paths below. + - name: Decide whether to publish + id: gate + env: + TAG: ${{ inputs.tag }} + DISPATCH_DEPLOY: ${{ inputs.deploy }} + DISPATCH_MARKER: ${{ inputs.version_marker }} + REF_NAME: ${{ github.ref_name }} + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + IS_UPSTREAM: ${{ github.repository == 'signalwire/signalwire-python' }} + run: | + set -euo pipefail + SEMVER='^v[0-9]+\.[0-9]+\.[0-9]+$' + publish=false + marker="" + + # Probe gh-pages rather than trusting checkout's refspec, and keep the + # three states distinct. `ls-remote --exit-code` returns 2 for "no such + # ref" and something else for "could not ask", which is the difference + # between a legitimate bootstrap and a failure we must not read as one. + rc=0 + git ls-remote --exit-code --heads origin gh-pages >/dev/null 2>&1 || rc=$? + case "$rc" in + 0) branch=present ;; + 2) branch=absent ;; + *) branch=unknown ;; + esac + + prev="" + if [ "$branch" = "present" ]; then + # No `|| true` here: an unreadable gh-pages must stop the run rather + # than fall through as "no marker, go ahead". + git fetch --no-tags origin +refs/heads/gh-pages:refs/remotes/origin/gh-pages + if git cat-file -e refs/remotes/origin/gh-pages:version.txt 2>/dev/null; then + prev="$(git show refs/remotes/origin/gh-pages:version.txt | tr -d '[:space:]')" + fi + fi + echo "gh-pages=$branch marker-on-site='${prev}'" + + if [ "$IS_UPSTREAM" != "true" ]; then + echo "::notice::fork build; not publishing" + elif [ "$branch" = "unknown" ]; then + echo "::error::could not determine the state of gh-pages; refusing to publish" + elif [ -n "$TAG" ]; then + # Release publish, called from publish-release.yml after its gates. + # publish-release.yml triggers on the `v*` glob, so an rc/beta tag + # does reach here; the shape check is what stops it overwriting the + # stable site. Actions expressions have no regex, hence a step. + if [[ ! "$TAG" =~ $SEMVER ]]; then + echo "::warning::$TAG is not stable semver; not republishing" + elif [ "$branch" = "absent" ]; then + echo "::notice::gh-pages does not exist yet; treating $TAG as the first publish" + publish=true + marker="$TAG" + elif [ -z "$prev" ]; then + # Every publish stamps a marker, so a site without one has been + # hand-edited or predates this workflow. Fail closed and make the + # operator repair it deliberately (dispatch with version_marker). + echo "::error::gh-pages carries no readable version.txt; refusing to publish $TAG. Repair with a manual dispatch using the version_marker input." + elif [ "$(printf '%s\n%s\n' "${prev#v}" "${TAG#v}" | sort -V | tail -1)" = "${TAG#v}" ]; then + # Equal versions pass, so re-running a release can republish it. + publish=true + marker="$TAG" + else + echo "::warning::live site is $prev, which is newer than $TAG; not republishing" + fi + else + # Manual dispatch. + if [ "${DISPATCH_DEPLOY:-false}" != "true" ]; then + echo "::notice::preview build; the deploy input was not ticked" + elif [ -z "$DEFAULT_BRANCH" ] || [ "$REF_NAME" != "$DEFAULT_BRANCH" ]; then + # Otherwise any branch in the dropdown could publish itself as the + # official public API reference, running its own code under a + # contents: write token. + echo "::error::refusing to publish from '$REF_NAME'; dispatch a publish from the default branch ('${DEFAULT_BRANCH:-unknown}')" + else + publish=true + # gh-deploy replaces the whole site tree (ghp-import emits + # deleteall), so NOT re-stamping here would erase the marker and + # silently disarm the monotonicity guard for the next release. + if [ -n "$DISPATCH_MARKER" ]; then + if [[ ! "$DISPATCH_MARKER" =~ $SEMVER ]]; then + echo "::error::version_marker '$DISPATCH_MARKER' is not vX.Y.Z" + exit 1 + fi + marker="$DISPATCH_MARKER" + elif [ -n "$prev" ]; then + marker="$prev" + else + # Bootstrap floor, so the site always carries a readable marker + # and "missing" stays meaningful as an anomaly. + marker="v0.0.0" + fi + fi + fi + + echo "publish=$publish" >> "$GITHUB_OUTPUT" + echo "marker=$marker" >> "$GITHUB_OUTPUT" + echo "publish=$publish marker=$marker" - uses: actions/setup-python@v6 with: @@ -46,24 +196,31 @@ jobs: pip install -r reference/requirements.txt pip install -e . - - name: Configure git for mike - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - name: Generate API pages - run: bash reference/gen.sh --no-build + run: bash reference/gen.sh --no-build --no-install - - name: Resolve version - id: ver - run: | - # Pilot: use the package version. Production: derive from the v* tag, - # e.g. VERSION="${GITHUB_REF_NAME#v}". - VERSION=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") - echo "version=$VERSION" >> "$GITHUB_OUTPUT" + # Must run after gen.sh, which does `rm -rf _docs`. The value goes through + # `env:` rather than inline `${{ }}`: git permits shell metacharacters in + # tag names, and this job holds contents: write. + - name: Stamp the version marker + if: steps.gate.outputs.marker != '' + env: + MARKER: ${{ steps.gate.outputs.marker }} + run: printf '%s\n' "$MARKER" > reference/_docs/version.txt - - name: Deploy with mike (versioned) to gh-pages + # Validates preview runs, and fails a publish before it reaches gh-deploy. + # gh-deploy builds again internally (its --strict is honoured there too), + # so publishing runs do build twice; that is deliberate insurance, not an + # oversight. + - name: Build (strict), the gate before any push + run: python3 -m mkdocs build --strict --config-file reference/mkdocs.yml + + - name: Configure git + if: steps.gate.outputs.publish == 'true' run: | - mike deploy --config-file reference/mkdocs.yml --push --update-aliases \ - "${{ steps.ver.outputs.version }}" latest - mike set-default --config-file reference/mkdocs.yml --push latest + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Deploy to gh-pages + if: steps.gate.outputs.publish == 'true' + run: python3 -m mkdocs gh-deploy --strict --config-file reference/mkdocs.yml --force diff --git a/reference/README.md b/reference/README.md index e39a0b2f..f42326ac 100644 --- a/reference/README.md +++ b/reference/README.md @@ -1,8 +1,8 @@ -# API reference (pilot) +# API reference Language-native API reference for the SignalWire Python SDK, generated from docstrings with **MkDocs Material + mkdocstrings**, wrapped in the SignalWire -**Fern navbar**, and versioned with **mike**. Published to this repo's own +**Fern navbar**, and published as a single unversioned site to this repo's own GitHub Pages: > https://signalwire.github.io/signalwire-python/ @@ -15,7 +15,9 @@ existing top-level `docs/` directory (owned by `doc-audit.yml`). ``` reference/ mkdocs.yml MkDocs Material + mkdocstrings config (docs_dir = _docs) + requirements.txt pinned doc toolchain (MkDocs Material, mkdocstrings) gen.sh installs the SDK editable, generates the API pages, builds + dev-server.sh generates the pages, then serves them with live reload overrides/main.html extends Material's base.html; injects the Fern navbar assets/ Fern tokens/CSS/JS + logo/favicon (committed; no shared host yet) _docs/ GENERATED docs tree (one page per signalwire.) [gitignored] @@ -32,65 +34,103 @@ first — keep that. From the repo root, in a virtualenv: ```bash +pip install -r reference/requirements.txt pip install -e . -pip install mkdocs-material "mkdocstrings[python]" mike # Generate API pages + build the static site into reference/_site bash reference/gen.sh -# Live preview (regenerates pages first, then serves with autoreload) -bash reference/gen.sh --no-build -mkdocs serve --config-file reference/mkdocs.yml +# Live preview: regenerates the pages, then serves with autoreload, bound to +# 0.0.0.0 so a forwarded port reaches it. PORT defaults to 3001. +bash reference/dev-server.sh # or: bash reference/dev-server.sh 3005 ``` +`gen.sh` also takes `--no-build` (generate the pages only) and `--no-install` +(skip the pip installs). Both CI workflows pass `--no-install`, since they install +the pinned toolchain themselves; the publishing one adds `--no-build`, since it +builds in a separate step. + `use_directory_urls: false`, so every page is a real `.html` file and the site works under the `/signalwire-python/` base path. -## Versioning (mike) - -`extra.version.provider: mike` in `mkdocs.yml` turns on Material's version -selector; it reads `versions.json` from the site root at runtime. mike commits -each deployed version to a local `gh-pages` branch. - -```bash -bash reference/gen.sh --no-build # generate pages first - -# Deploy a version and (re)point the `latest` alias at it; set it as default. -mike deploy --config-file reference/mkdocs.yml --update-aliases 3.0.2 latest -mike set-default --config-file reference/mkdocs.yml latest -mike list --config-file reference/mkdocs.yml # -> 3.0.2 [latest], 3.0.1 - -mike serve --config-file reference/mkdocs.yml # preview all versions + selector -``` +## Deploy -Add `--push` to push the `gh-pages` branch to the remote (CI does this; do not -locally). +The site is unversioned: `mkdocs gh-deploy` publishes it to the `gh-pages` branch, +where the current release's docs sit at the site root. There is no per-version +path and no `latest` alias to navigate to. A version selector (via `mike`) can be +added later without touching the markup. CI does the publishing, see below. ## CI -`.github/workflows/reference-docs.yml` (plain `actions/setup-python`, no Docker): - -- **Pilot triggers:** `workflow_dispatch` + push to `docs/api-reference-pilot`. -- Installs the SDK editable + `mkdocs-material`, `mkdocstrings[python]`, `mike`, - runs `gen.sh --no-build`, then `mike deploy --push --update-aliases latest`. -- **Production trigger (later):** `v*` tags (commented in the workflow), so each - release deploys its version and re-aliases `latest`. - -## Manual repo setting required to go live - -One-time, in the GitHub UI: - -> **Settings → Pages → Build and deployment → Source = "Deploy from a branch" → -> Branch = `gh-pages` / `(root)`.** - -mike pushes the built site to `gh-pages`; Pages then serves it at -`https://signalwire.github.io/signalwire-python/`. - -## Pilot simplifications - -- **Language switcher:** cross-site links with Python marked active; other - languages point at the POC demo (no per-language hosted site exists yet). -- **Theme toggle:** drives Material's own light/dark color scheme. No - cross-origin `localStorage` theme sync with signalwire.com/docs (different - origin — out of scope). -- Out of scope: llms.txt/markdown emission, Docker, other languages, custom domain. +Two workflows, both plain `actions/setup-python`, no Docker. + +`.github/workflows/reference-check.yml` builds on every PR touching `signalwire/`, +`reference/`, or `pyproject.toml`. It runs `gen.sh --no-install`, whose strict +`mkdocs build` fails the check on a generator error, a root `signalwire` package +that will not import, or a new unresolved cross-reference. It does **not** catch a +submodule with a missing runtime dependency: mkdocstrings analyses statically +through griffe, so that module still renders a clean page. Build only: +`contents: read`, never deploys. + +`.github/workflows/reference-docs.yml` builds and publishes. It installs from +`reference/requirements.txt` plus the SDK editable, runs `gen.sh --no-build +--no-install`, builds with `--strict`, and only then runs `mkdocs gh-deploy --force`. + +- **Triggers:** a reusable `workflow_call` from `publish-release.yml`, which + invokes it after the CI gates and the PyPI upload have succeeded, plus + `workflow_dispatch` for manual and fork-preview runs. There is deliberately no + `release: published` trigger: that release is created with `GITHUB_TOKEN`, and + GitHub raises no workflow-triggering events for that token, so the trigger + would look right and never fire on the normal tag path. +- **Publishes only when** the run is on `signalwire/signalwire-python` (not a + fork), the tag is stable semver (`vX.Y.Z`) so an rc tag cannot overwrite the + stable site, and the tag is at least as new as the version recorded in + `version.txt` at the root of the live site. Anything the gate cannot positively + determine, such as an unreadable `gh-pages` or a site with no marker, fails + closed rather than publishing. +- A `workflow_dispatch` run builds a preview, and publishes only if you tick the + `deploy` input **and** dispatch from the default branch. Without that second + condition any branch in the dropdown could publish itself as the official + public reference, running its own code under a `contents: write` token. +- **On rollback:** `gh-deploy` force-pushes but does not discard history. mkdocs + passes `no_history=False`, so ghp-import parents each deploy onto the previous + one and `--force` forces only the push. A bad publish is recoverable by hand + with `git push --force origin gh-pages~1:gh-pages`. The gate exists because + that recovery depends on somebody noticing, not because rollback is impossible. + This is also why `fetch-depth: 0` must stay: it is what fetches + `origin/gh-pages`, and without it the force-push lands a parentless orphan and + the history really is gone. + +## Going live + +Two one-time steps, in this order. The Pages dropdown cannot offer `gh-pages` +until the branch exists, so the dispatch has to come first. + +1. Run `reference-docs` manually from the default branch with the `deploy` input + ticked. That creates `gh-pages` and stamps `version.txt` with the `v0.0.0` + bootstrap floor. +2. In the GitHub UI: **Settings > Pages > Build and deployment > + Source = "Deploy from a branch", Branch = `gh-pages` / `(root)`.** + +Pages then serves the site at `https://signalwire.github.io/signalwire-python/`. + +### The version marker + +`version.txt` at the site root records the version currently published, and the +monotonicity check reads it. **Every** publish stamps it, dispatches included: +`gh-deploy` replaces the whole site tree, so a dispatch that did not re-stamp +would erase the marker and silently disarm the guard for the next release. A +dispatch preserves whatever is already there. + +A site with no readable marker is therefore an anomaly, and the gate refuses to +publish over it. If the marker ever needs repairing, for instance after a typo +tag like `v31.0.0` sorts newest and starts blocking real releases, dispatch a +publish with the `version_marker` input set to the correct `vX.Y.Z`. + +## Scope + +- **Theme toggle:** drives Material's own light/dark color scheme. There is no + cross-origin `localStorage` theme sync with signalwire.com/docs, which is a + different origin. +- **Not included:** versioning (mike), llms.txt/markdown emission, Docker, other + languages, custom domain. diff --git a/reference/assets/css/navbar.css b/reference/assets/css/navbar.css index cf008327..9551cf90 100644 --- a/reference/assets/css/navbar.css +++ b/reference/assets/css/navbar.css @@ -111,30 +111,8 @@ html { scroll-padding-top: calc(var(--fern-header-height) + var(--md-header-heig #fern-header [data-fern-logo] img { height: 28px; width: auto; display: block; } #fern-header .fern-logo-text { font-weight: 600; font-size: var(--text-sm); white-space: nowrap; } +/* The center slot is an empty spacer; the language switcher it once held is gone. */ #fern-header .fern-header-center { flex: 1; display: flex; justify-content: center; min-width: 0; } -#fern-header .fern-header-center .lang-trigger { max-width: 360px; } - -/* --- Language switcher trigger (search-bar slot) --- */ -.lang-trigger { - appearance: none; background: transparent; border: 0; font: inherit; - flex: 1; display: inline-flex; align-items: center; - gap: calc(var(--spacing) * 2); - height: calc(var(--spacing) * 9); - padding: calc(var(--spacing) * 2); - border-radius: calc(var(--radius) * 2); - box-shadow: inset 0 0 0 1px var(--grayscale-a5); - color: var(--grayscale-a11); font-size: var(--text-sm); font-weight: 500; - cursor: pointer; overflow: hidden; - transition: background-color 0.15s, box-shadow 0.15s; -} -.lang-trigger:hover, .lang-trigger[data-state="open"] { background-color: var(--grayscale-a3); } -.lang-trigger:focus-visible { outline: none; box-shadow: inset 0 0 0 1px var(--accent); } -.lang-trigger svg { width: calc(var(--spacing) * 4); height: calc(var(--spacing) * 4); flex-shrink: 0; } -.lang-trigger .lang-trigger-label { color: var(--grayscale-12); white-space: nowrap; } -.lang-trigger .lang-trigger-meta { - margin-left: auto; color: var(--grayscale-a9); font-weight: 400; - white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -} /* --- Right-side button group --- */ .fern-button-group { display: inline-flex; align-items: center; gap: calc(var(--spacing) * 2); } @@ -213,18 +191,10 @@ html { scroll-padding-top: calc(var(--fern-header-height) + var(--md-header-heig #product-panel .fern-product-selector-radio-group > a[href*="server-sdks"]::before, #product-panel .fern-product-selector-radio-group > a[href$="/docs/apis"]::before { top: -28px; font-size: 11px; } -/* --- Language dropdown items --- */ -#lang-panel { min-width: 320px; } -#lang-panel .fern-dropdown-item { gap: calc(var(--spacing) * 2); } -#lang-panel .item-meta { margin-left: auto; padding-left: calc(var(--spacing) * 4); font-size: var(--text-xs); color: var(--grayscale-a9); } -.lang-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--lang-color, var(--grayscale-8)); display: inline-block; flex-shrink: 0; } -#lang-panel .fern-dropdown-item[aria-current="true"] { background: var(--accent-a3); } - /* --- Responsive: collapse like the Fern header --- */ @media (max-width: 1023px) { #fern-header .fern-header-navbar-links .fern-button-text { display: none; } #fern-header .fern-logo-text { display: none; } - .lang-trigger .lang-trigger-meta { display: none; } } @media (max-width: 600px) { #product-panel { width: 96vw; } diff --git a/reference/assets/img/signalwire-favicon.png b/reference/assets/img/signalwire-favicon.png index 4b8ff992..ac64199f 100644 Binary files a/reference/assets/img/signalwire-favicon.png and b/reference/assets/img/signalwire-favicon.png differ diff --git a/reference/assets/js/navbar.js b/reference/assets/js/navbar.js index 4c345717..38b55c25 100644 --- a/reference/assets/js/navbar.js +++ b/reference/assets/js/navbar.js @@ -1,9 +1,9 @@ /* ============================================================================= Fern navbar behaviour for the injected header (no iframe). Ported and trimmed from the SDK-docs POC (assets/js/app.js): - - dropdown open/close + anchored positioning (product / language / support / theme) - - theme toggle drives Material's own color scheme (default <-> slate) - Out of scope for the pilot (different origin from signalwire.com): + - dropdown open/close + anchored positioning (product / support / theme) + - theme toggle drives Material's own color scheme (default and slate) + Not implemented (different origin from signalwire.com): - cross-origin localStorage["theme"] sync with the Fern docs site - iframe theme bridging to other generators ============================================================================= */ @@ -102,7 +102,6 @@ /* --- Init ----------------------------------------------------------------- */ function init() { registerDropdown("product-trigger", "product-panel", { align: "start" }); - registerDropdown("lang-trigger", "lang-panel", { align: "center" }); registerDropdown("support-trigger", "support-panel", { align: "end" }); registerDropdown("theme-trigger", "theme-panel", { align: "end" }); @@ -111,7 +110,7 @@ themeItems[i].addEventListener("click", function () { closeAll(); var pref = this.getAttribute("data-theme-pref"); - // System resolves to the OS preference; pilot maps it to light/dark now. + // System resolves to the OS preference, mapped onto light/dark here. if (pref === "system") { pref = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } @@ -119,8 +118,6 @@ setTimeout(syncThemeUI, 0); }); } - // Header sun/moon button = quick toggle. - var themeTrigger = document.getElementById("theme-trigger"); syncThemeUI(); // Material re-applies the scheme asynchronously; observe to keep icons in sync. var obs = new MutationObserver(syncThemeUI); diff --git a/reference/gen.sh b/reference/gen.sh index 1b393129..f3be6fdf 100755 --- a/reference/gen.sh +++ b/reference/gen.sh @@ -10,10 +10,26 @@ # The package is nested (signalwire/signalwire/) and mkdocstrings needs the # editable install to resolve imports — that is preserved here. # -# Usage: reference/gen.sh # generate pages + `mkdocs build` -# reference/gen.sh --no-build # generate pages only (mike runs the build) +# Usage: reference/gen.sh # generate pages + strict `mkdocs build` +# reference/gen.sh --no-build # generate pages only (the caller builds/deploys) +# reference/gen.sh --no-install # skip the pip installs (the caller installed already) +# Both flags are accepted together, in either order. set -euo pipefail +NO_BUILD=0 +NO_INSTALL=0 +usage() { + sed -n '13,16p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' +} +for arg in "$@"; do + case "$arg" in + --no-build) NO_BUILD=1 ;; + --no-install) NO_INSTALL=1 ;; + -h|--help) usage; exit 0 ;; + *) echo "gen.sh: unknown option: $arg" >&2; usage >&2; exit 2 ;; + esac +done + HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # .../reference REPO="$(cd "$HERE/.." && pwd)" # repo root DOCS="$HERE/_docs" @@ -21,8 +37,12 @@ CFG="$HERE/mkdocs.yml" # Install the doc toolchain + the SDK (editable, so mkdocstrings can import it). # Use `python3 -m pip` so this works whether or not a bare `pip` is on PATH. -python3 -m pip install -q -r "$HERE/requirements.txt" -python3 -m pip install -q -e "$REPO" +# CI installs both itself and passes --no-install, so the pinned environment is +# not re-resolved underneath the build. +if [ "$NO_INSTALL" -eq 0 ]; then + python3 -m pip install -q -r "$HERE/requirements.txt" + python3 -m pip install -q -e "$REPO" +fi # Build the docs tree: generated landing page + one API page per subpackage. rm -rf "$DOCS" @@ -47,6 +67,13 @@ for name in names: fh.write(f"# signalwire.{name}\n\n::: signalwire.{name}\n") written.append(name) +# The loop above writes nothing when `signalwire` exposes no subpackages, and +# every command below still succeeds, so `set -e` would let a gutted site sail +# through to deploy. (This is NOT an install check: PYTHONPATH points at the +# source tree, so the import resolves with or without the editable install.) +if not written: + sys.exit("gen.sh: no API pages generated (is the signalwire package importable?)") + # API-section landing entry. with open(os.path.join(api_out, "index.md"), "w") as fh: fh.write("# API reference\n\n") @@ -77,10 +104,11 @@ with open(os.path.join(docs_root, "index.md"), "w") as fh: print("api pages:", ", ".join(written)) PY -if [ "${1:-}" = "--no-build" ]; then +if [ "$NO_BUILD" -eq 1 ]; then echo "pages generated under $DOCS (skipping mkdocs build)" exit 0 fi -python3 -m mkdocs build --config-file "$CFG" +# --strict so a local run fails on the same warnings CI fails on. +python3 -m mkdocs build --strict --config-file "$CFG" echo "python -> $HERE/_site ($(find "$HERE/_site" -name '*.html' | wc -l) pages)" diff --git a/reference/mkdocs.yml b/reference/mkdocs.yml index e8d754a9..48d2b1d3 100644 --- a/reference/mkdocs.yml +++ b/reference/mkdocs.yml @@ -3,16 +3,17 @@ site_description: API reference for the SignalWire Python SDK, generated from do site_url: https://signalwire.github.io/signalwire-python/ # Generated per-run by gen.sh (one API page per importable subpackage of -# `signalwire`, plus the SDK README as the landing page). This is NOT the repo's +# `signalwire`, plus a generated landing page). This is NOT the repo's # existing top-level docs/ dir (owned by doc-audit.yml) — keep them separate. docs_dir: _docs site_dir: _site use_directory_urls: false -# Mike serves each version under //, so links must be relative. +# Served under the project-pages subpath (/signalwire-python/); links stay relative. theme: name: material custom_dir: overrides + favicon: assets/img/signalwire-favicon.png features: - navigation.sections - navigation.indexes @@ -33,11 +34,6 @@ theme: icon: material/weather-sunny name: Switch to light mode -extra: - version: - provider: mike - default: latest - extra_css: - assets/fern/tokens.css - assets/fern/fern.css diff --git a/reference/overrides/main.html b/reference/overrides/main.html index 6e92558c..fe0cc2bf 100644 --- a/reference/overrides/main.html +++ b/reference/overrides/main.html @@ -7,9 +7,9 @@ mkdocs.yml (extra_css / extra_javascript); navbar.css pushes Material's layout down by --header-height so this fixed bar doesn't overlap content. - Pilot simplifications (see reference/README.md): - - Language switcher = cross-site links, Python marked active; the other - languages point at the POC demo (no per-language hosted site exists yet). + Scope (see reference/README.md): + - No language switcher. The header center is an empty spacer until the other + SDK languages have hosted reference sites of their own. - Theme toggle drives Material's own light/dark scheme. No cross-origin localStorage theme sync with signalwire.com/docs (different origin). --> diff --git a/reference/requirements.txt b/reference/requirements.txt index 6b9325ba..137a880e 100644 --- a/reference/requirements.txt +++ b/reference/requirements.txt @@ -4,5 +4,4 @@ mkdocs==1.6.1 mkdocs-material==9.7.6 mkdocstrings==1.0.4 mkdocstrings-python==2.0.4 -mike==2.2.0 pymdown-extensions==10.21.3 diff --git a/signalwire/signalwire/pom/pom_tool.py b/signalwire/signalwire/pom/pom_tool.py index 4dbc30da..3b0b1545 100644 --- a/signalwire/signalwire/pom/pom_tool.py +++ b/signalwire/signalwire/pom/pom_tool.py @@ -3,14 +3,20 @@ POM Tool - Command line utility for working with Prompt Object Model files Usage: - pom_tool [--output=] [--outfile=] [--merge_pom="
:"] - pom_tool (-h | --help) + +```text +pom-tool [--output=] [--outfile=] [--merge_pom="
:"] +pom-tool (-h | --help) +``` Options: - -h --help Show this help message - --output= Output format: md, xml, json, yaml [default: md] - --outfile= Output file (if not specified, prints to stdout) - --merge_pom= Merge another POM into a section: "
:" + +```text +-h --help Show this help message +--output= Output format: md, xml, json, yaml [default: md] +--outfile= Output file (if not specified, prints to stdout) +--merge_pom= Merge another POM into a section: "
:" +``` """ import argparse @@ -85,7 +91,7 @@ def main() -> None: # §6.2-python: argparse (stdlib) replaced the unmaintained docopt — identical CLI # surface (same flags/defaults/usage), one fewer dependency. parser = argparse.ArgumentParser( - prog="pom_tool", + prog="pom-tool", description="POM Tool - work with Prompt Object Model files", ) parser.add_argument("input_file", help="POM file to load (JSON or YAML)") diff --git a/signalwire/signalwire/relay/client.py b/signalwire/signalwire/relay/client.py index 6dc4252e..dc77e77d 100644 --- a/signalwire/signalwire/relay/client.py +++ b/signalwire/signalwire/relay/client.py @@ -10,7 +10,7 @@ - Each Action registers with a ``control_id`` and listens for its own event_type (e.g. ``calling.call.play``). Actions filter events by ``control_id`` so multiple concurrent actions on the same call work. -- Result code checking accepts any 2xx (matching the JS SDK regex /^2[0-9][0-9]$/). +- Result code checking accepts any 2xx (matching the JS SDK regex ``/^2[0-9][0-9]$/``). ``signalwire.connect`` responses skip code checking entirely. - Execute has a configurable timeout (default 10s) to detect half-open connections. - Requests made while disconnected are queued and flushed after re-auth.