From 436b039b1a6c6572206df3118869294c3b1877e6 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:42:50 +0000 Subject: [PATCH 01/12] feat: apply One Identity to Python portal chrome and landing --- docs/assets/favicon.svg | 14 +- docs/assets/logo.svg | 9 ++ docs/index.md | 77 +++++---- docs/javascripts/identity.js | 19 +++ docs/stylesheets/identity-tokens.css | 36 +++++ docs/stylesheets/layout.css | 229 ++++++++++++++++++++++++--- mkdocs.yml | 17 +- overrides/partials/footer.html | 17 ++ overrides/partials/header.html | 49 ++++++ 9 files changed, 400 insertions(+), 67 deletions(-) create mode 100644 docs/assets/logo.svg create mode 100644 docs/javascripts/identity.js create mode 100644 docs/stylesheets/identity-tokens.css create mode 100644 overrides/partials/footer.html create mode 100644 overrides/partials/header.html diff --git a/docs/assets/favicon.svg b/docs/assets/favicon.svg index 3141555e..02879965 100644 --- a/docs/assets/favicon.svg +++ b/docs/assets/favicon.svg @@ -1,5 +1,9 @@ - - - - - + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 00000000..02879965 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 704a2561..6bd4794b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ hide:
-

First-party Python SDK · 2.0 stable

+

Python SDK · 2.14.1 published

# Durable workflows in Python. @@ -62,33 +62,6 @@ dispatches them to your workflow and activity code. -
- -## Choose who runs the runtime - -The workflow types, activity types, and task queue stay the same. Only the -endpoint, credentials, and operating boundary change. - -
-
- Available without an account -

Self-hosted Server

-

Run the published Server image locally, then execute the complete Python journey below.

- Start locally -
-
- Managed runtime · limited access -

Durable Workflow Cloud

-

Connect the same program to a provisioned namespace with separate client and worker credentials.

-

- Connect a namespace - Request early access → -

-
-
- -
-
## Run your first local workflow @@ -96,7 +69,7 @@ endpoint, credentials, and operating boundary change. This source-free path runs one Python file containing an activity, workflow, worker, and client against the stable Server channel. -### 1. Start Server +### 1. Start Server { #start-server } Docker keeps this first run local. Select the stable 2.x Server channel: @@ -122,13 +95,24 @@ docker run -d --name durable-workflow-python-server -p 8080:8080 \ until curl -sf http://127.0.0.1:8080/api/ready >/dev/null; do sleep 1; done ``` -### 2. Save `greeter.py` +
+
+ +

Step 2 of 3

+ +### Save `greeter.py` { #save-greeter } The named constants make the authoring contract visible: the decorator and start call share a workflow type, the workflow and decorator share an activity type, and the client and worker share one task queue. Values cross those boundaries with the supported Avro authoring codec. +First [start Server](#start-server), then save this complete program and +[run it in step 3](#run-greeter). Copy preserves the whole file and its indentation. + +
+
+ ```python import asyncio import logging @@ -181,7 +165,11 @@ async def main() -> None: asyncio.run(main()) ``` -### 3. Run it +
+
+ + +### 3. Run it { #run-greeter } ```bash python greeter.py @@ -201,6 +189,33 @@ Hello, world!
+
+ +## Choose who runs the runtime + +The workflow types, activity types, and task queue stay the same. Only the +endpoint, credentials, and operating boundary change. + +
+
+ Available without an account +

Self-hosted Server

+

Run the published Server image locally, then execute the complete Python journey below.

+ Start locally +
+
+ Managed runtime · limited access +

Durable Workflow Cloud

+

Connect the same program to a provisioned namespace with separate client and worker credentials.

+

+ Connect a namespace + Request early access → +

+
+
+ +
+
## Connect a managed Cloud namespace diff --git a/docs/javascripts/identity.js b/docs/javascripts/identity.js new file mode 100644 index 00000000..e04c6c46 --- /dev/null +++ b/docs/javascripts/identity.js @@ -0,0 +1,19 @@ +/* Native disclosure semantics retain links and keyboard navigation without JS. */ +(() => { + const close = (switcher, restoreFocus) => { + if (!switcher.open) return; + switcher.open = false; + if (restoreFocus) switcher.querySelector("summary").focus(); + }; + document.querySelectorAll("[data-sdk-switcher]").forEach((switcher) => { + switcher.addEventListener("keydown", (event) => { + if (event.key === "Escape" && switcher.open) { + event.preventDefault(); + close(switcher, true); + } + }); + document.addEventListener("click", (event) => { + if (!switcher.contains(event.target)) close(switcher, false); + }); + }); +})(); diff --git a/docs/stylesheets/identity-tokens.css b/docs/stylesheets/identity-tokens.css new file mode 100644 index 00000000..ab57ae7a --- /dev/null +++ b/docs/stylesheets/identity-tokens.css @@ -0,0 +1,36 @@ +/* One Durable Workflow. Shared role tokens; no framework or remote imports. */ +:root, +[data-theme='light'] { + color-scheme: light; + --dw-paper: #f6f7f5; + --dw-surface: #ffffff; + --dw-ink: #0f1513; + --dw-ink-2: #3b4541; + --dw-muted: #5e6964; + --dw-line: #dde2df; + --dw-action: #0f7a52; + --dw-tint: #e3f4ec; + --dw-on-action: #ffffff; + --dw-font-display: 'Space Grotesk', Inter, system-ui, sans-serif; + --dw-font-body: Inter, system-ui, sans-serif; + --dw-font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Consolas, monospace; + --dw-code-background: #0f1513; + --dw-code-foreground: #e8edea; + --dw-code-muted: #9aa6a1; + --dw-logo-paper: #f6f7f5; + --dw-radius-action: 6px; + --dw-radius-card: 8px; +} + +[data-theme='dark'] { + color-scheme: dark; + --dw-paper: #0f1513; + --dw-surface: #172019; + --dw-ink: #e8edea; + --dw-ink-2: #b7c2bd; + --dw-muted: #9aa6a1; + --dw-line: #2e3934; + --dw-action: #5fe0a6; + --dw-tint: #123b2a; + --dw-on-action: #0f1513; +} diff --git a/docs/stylesheets/layout.css b/docs/stylesheets/layout.css index 3101e89b..3abf7472 100644 --- a/docs/stylesheets/layout.css +++ b/docs/stylesheets/layout.css @@ -1,30 +1,24 @@ -:root, -[data-md-color-scheme="default"] { - --dw-blue: #3156d3; - --dw-blue-deep: #1f3ca5; - --dw-yellow: #ffd43b; - --dw-ink: #172033; - --dw-muted: #566078; - --dw-panel: #f5f7ff; - --dw-panel-raised: #ffffff; - --dw-line: #dce2f2; - --dw-code: #111827; - --dw-code-ink: #edf2ff; +/* Material maps its persisted scheme onto the canonical shared role tokens. */ +:root, [data-md-color-scheme="default"] { + --dw-blue: var(--dw-action); + --dw-blue-deep: var(--dw-action); + --dw-panel: var(--dw-paper); + --dw-panel-raised: var(--dw-surface); + --dw-code: var(--dw-code-background); + --dw-code-ink: var(--dw-code-foreground); } - [data-md-color-scheme="slate"] { - --dw-blue: #91a7ff; - --dw-blue-deep: #b6c4ff; - --dw-yellow: #ffe278; - --dw-ink: #f2f5ff; - --dw-muted: #bac3d9; - --dw-panel: #17202f; - --dw-panel-raised: #1d2839; - --dw-line: #344159; - --dw-code: #0a101b; - --dw-code-ink: #edf2ff; + color-scheme: dark; + --dw-paper: #0f1513; + --dw-surface: #172019; + --dw-ink: #e8edea; + --dw-ink-2: #b7c2bd; + --dw-muted: #9aa6a1; + --dw-line: #2e3934; + --dw-action: #5fe0a6; + --dw-tint: #123b2a; + --dw-on-action: #0f1513; } - @media screen and (max-width: 44.984375em) { .md-header__topic { font-size: 0.7rem; @@ -781,3 +775,190 @@ #__search:checked ~ header [inert] { visibility: hidden; } + +/* One Identity: shared chrome and Python documentation components. */ +:root { + --md-text-font: var(--dw-font-body); + --md-code-font: var(--dw-font-mono); + --md-default-fg-color: var(--dw-ink); + --md-default-fg-color--light: var(--dw-muted); + --md-default-bg-color: var(--dw-paper); + --md-primary-fg-color: var(--dw-action); + --md-primary-bg-color: var(--dw-on-action); + --md-accent-fg-color: var(--dw-action); + --md-code-bg-color: var(--dw-code-background); + --md-code-fg-color: var(--dw-code-foreground); + --md-code-hl-color: #123b2a; + --md-code-hl-number-color: #e8edea; + --md-code-hl-special-color: #e8edea; + --md-code-hl-function-color: #e8edea; + --md-code-hl-constant-color: #e8edea; + --md-code-hl-keyword-color: #5fe0a6; + --md-code-hl-string-color: #5fe0a6; + --md-code-hl-name-color: #e8edea; + --md-code-hl-operator-color: #e8edea; + --md-code-hl-punctuation-color: #e8edea; + --md-code-hl-comment-color: #9aa6a1; + --md-code-hl-generic-color: #9aa6a1; + --md-code-hl-variable-color: #e8edea; +} +body, .md-main { background: var(--dw-paper); color: var(--dw-ink); } +body { font-family: var(--dw-font-body); } +.md-grid { max-width: 1200px; } +.md-typeset { font-size: 16px; line-height: 1.5; } +.md-typeset h1, .md-typeset h2, .md-typeset h3 { + color: var(--dw-ink); font-family: var(--dw-font-display); font-weight: 600; + letter-spacing: -.03em; scroll-margin-top: 150px; +} +.md-typeset a { color: var(--dw-action); } +a, button, summary, input { -webkit-tap-highlight-color: transparent; } +a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible { + outline: 2px solid var(--dw-action); outline-offset: 4px; +} +.dw-header { color: var(--dw-ink); background: var(--dw-surface); box-shadow: none; } +.dw-global-bar { display: flex; align-items: center; gap: 32px; min-height: 76px; padding: 12px 24px; } +.dw-brand { display: inline-flex; align-items: center; gap: 10px; flex-shrink: 0; color: var(--dw-ink); text-decoration: none; } +.dw-brand__mark { display: block; object-fit: contain; width: 30px; height: 30px; } +[data-md-color-scheme="slate"] .dw-brand__mark { box-sizing: content-box; padding: 4px; border-radius: 8px; background: var(--dw-logo-paper); } +.dw-brand__name { font-family: var(--dw-font-display); font-weight: 600; font-size: 17px; white-space: nowrap; } +.dw-brand__slash { color: var(--dw-muted); font-size: 16px; } +.dw-brand__surface { color: var(--dw-action); font: 500 13px var(--dw-font-mono); } +.dw-global-nav { display: flex; align-items: center; flex: 1; gap: 24px; font-size: 14px; } +.dw-global-nav > a, .dw-sdk-switcher summary { color: var(--dw-ink-2); text-decoration: none; min-height: 44px; display: flex; align-items: center; } +.dw-sdk-switcher { position: relative; } +.dw-sdk-switcher summary { cursor: pointer; gap: 6px; font-weight: 600; color: var(--dw-ink); list-style: none; } +.dw-sdk-switcher summary::-webkit-details-marker { display: none; } +.dw-sdk-switcher ul { position: absolute; z-index: 20; min-width: 160px; top: 44px; left: -16px; list-style: none; margin: 0; padding: 8px; border: 1px solid var(--dw-line); border-radius: 8px; background: var(--dw-surface); } +.dw-sdk-switcher a { display: flex; justify-content: space-between; padding: 12px; border-radius: 4px; color: var(--dw-ink-2); } +.dw-sdk-switcher a:hover, .dw-sdk-switcher a[aria-current] { background: var(--dw-tint); color: var(--dw-action); } +.dw-global-actions { display: flex; align-items: center; gap: 20px; font-size: 14px; } +.dw-github-link { color: var(--dw-ink-2); } +.dw-docs-toolbar { padding: 0 20px; min-height: 48px; border-block: 1px solid var(--dw-line); gap: 12px; } +.dw-docs-toolbar__title { font-size: 13px; color: var(--dw-ink-2); } +.dw-channel-label { font: 400 11px var(--dw-font-mono); color: var(--dw-muted); } +.dw-docs-toolbar__controls { margin-left: auto; display: flex; align-items: center; } +.dw-docs-toolbar .md-header__button { width: 40px; height: 44px; padding: 10px; margin: 0; color: var(--dw-ink-2); } +.dw-docs-toolbar .md-search__form { background: var(--dw-paper); border: 1px solid var(--dw-line); color: var(--dw-ink); } +.dw-docs-toolbar .md-search__input { color: var(--dw-ink); } +.dw-docs-toolbar .md-search__input::placeholder { color: var(--dw-muted); } +.md-sidebar { color: var(--dw-ink-2); } +.md-nav__link--active { color: var(--dw-action); } +@media (min-width: 1220px) { + body:has(.dw-landing) .md-sidebar--primary { display: none; } +} +body:has(.dw-landing) .md-main__inner { margin-top: 0; } +body:has(.dw-landing) .md-content__inner { margin: 0 24px; padding: 0; } +.dw-landing { display: flex; flex-direction: column; gap: 88px; padding: 72px 0 96px; margin: 0; } +.dw-hero { padding: 0; gap: 48px; align-items: center; grid-template-columns: minmax(0, 1.6fr) minmax(320px, 1fr); } +.dw-eyebrow, .dw-tag { color: var(--dw-action); font: 500 12px var(--dw-font-mono); letter-spacing: .08em; line-height: 1.5; margin-bottom: 22px; } +.dw-hero h1 { font-size: 64px; line-height: 1; letter-spacing: -.03em; max-width: 12ch; } +.dw-hero__copy > p:not(.dw-eyebrow, .dw-hero__facts) { color: var(--dw-ink-2); font-size: 19px; line-height: 1.55; margin: 22px 0 0; max-width: 580px; } +.dw-hero__actions { gap: 12px; margin-top: 22px; } +.dw-button, .dw-text-link { min-height: 44px; font-size: 15px; font-weight: 600; } +.dw-button { border-radius: 6px; padding: 0 20px; border-color: var(--dw-line); } +.dw-button--primary { background: var(--dw-action); border-color: var(--dw-action); color: var(--dw-on-action) !important; } +[data-md-color-scheme="slate"] .dw-button--primary { color: var(--dw-on-action) !important; } +.dw-button--secondary { color: var(--dw-ink) !important; background: var(--dw-surface); } +.dw-button:hover, .dw-role-card:hover, .dw-link-card:hover { box-shadow: none; border-color: var(--dw-action); } +.dw-text-link { padding: 10px 8px; } +.dw-hero__facts { font: 400 13px var(--dw-font-mono); color: var(--dw-muted); margin-top: 22px; gap: 8px 20px; } +.dw-hero__facts span::before { display: none; } +.dw-install-card { border: none; border-radius: 8px; box-shadow: none; padding: 0; overflow: hidden; } +.dw-install-card h2 { display: none; } +.dw-install-card > p:not(.dw-install-card__links) { padding: 0 16px 18px; font-size: 13px; line-height: 1.55; color: var(--dw-code-muted); } +.dw-install-card__links { padding: 14px 16px; margin: 0; border-top: 1px solid #26302c; font-size: 13px; gap: 20px; } +.dw-install-card__links a { color: #5fe0a6 !important; } +.dw-section { margin: 0; padding: 0; border: 0; } +.dw-section > h2 { margin: 0 0 8px; font-size: 36px; } +.dw-section > p { color: var(--dw-ink-2); margin: 0 0 24px; line-height: 1.5; } +.dw-role-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; } +.dw-role-card, .dw-link-card, .dw-runtime-card, .dw-credential-grid li { + background: var(--dw-surface); border: 1px solid var(--dw-line); border-radius: 8px; box-shadow: none; padding: 20px; +} +.dw-role-card { min-height: 190px; gap: 8px; } +.dw-role-card__step { color: var(--dw-action); background: none; padding: 0; font: 400 12px var(--dw-font-mono); } +.dw-role-card strong { color: var(--dw-ink); font: 600 20px var(--dw-font-display); } +.dw-role-card > span:not(.dw-role-card__step, .dw-role-card__link) { color: var(--dw-ink-2); font-size: 14px; line-height: 1.5; } +.dw-role-card__link { font-size: 13px; font-weight: 600; color: var(--dw-action); } +.dw-quickstart-grid { display: grid; grid-template-columns: minmax(0, .7fr) minmax(0, 1.3fr); gap: 48px; align-items: start; margin-block: 64px; } +.dw-quickstart-copy h3 { font-size: 32px; margin: 0 0 16px; } +.dw-quickstart-copy p { color: var(--dw-ink-2); font-size: 16px; } +.dw-quickstart-copy .dw-eyebrow { color: var(--dw-action); font-size: 12px; } +.dw-quickstart-code { min-width: 0; } +.dw-runtime-grid { gap: 16px; } +.dw-runtime-card { border-top: 1px solid var(--dw-line); min-height: 220px; } +.dw-runtime-card h3 { font: 600 20px var(--dw-font-display); margin: 0; } +.dw-runtime-card p { color: var(--dw-ink-2); font-size: 14px; line-height: 1.5; } +.dw-runtime-card .dw-tag { font-size: 11px; margin-bottom: 12px; } +.dw-runtime-card--secondary .dw-tag { color: var(--dw-muted); } +.dw-checkpoint, .dw-version { background: var(--dw-surface); border: 1px solid var(--dw-line); border-radius: 8px; } +.dw-checkpoint { padding: 16px; } +.dw-version { margin: 0; padding: 20px; } +.md-typeset pre, .md-typeset code { font-family: var(--dw-font-mono); } +.md-typeset pre { background: var(--dw-code-background); color: var(--dw-code-foreground); } +.dw-landing .highlight { position: relative; background: var(--dw-code-background); border: 1px solid #26302c; border-radius: 8px; overflow: hidden; } +.dw-landing .highlight::before { content: "terminal"; display: block; border-bottom: 1px solid #26302c; padding: 12px 16px; color: var(--dw-code-muted); font: 400 12px var(--dw-font-mono); } +.dw-install-card .highlight::before { content: "INSTALL"; letter-spacing: .08em; } +.dw-quickstart-code .highlight::before { content: "greeter.py"; } +.dw-checkpoint .highlight::before { content: "output"; } +.dw-landing .highlight pre { margin: 0; max-width: 100%; } +.dw-landing .highlight pre code { padding: 18px 16px; background: var(--dw-code-background); color: var(--dw-code-foreground); font-size: 13px; line-height: 1.75; white-space: pre; overflow-x: auto; } +.dw-quickstart-code .highlight pre code { max-height: 640px; overflow: auto; } +.dw-landing .md-clipboard { top: 8px; right: 8px; color: var(--dw-code-muted); border: 1px solid #2e3934; border-radius: 4px; opacity: 1; } +.dw-footer { background: var(--dw-surface); border-top: 1px solid var(--dw-line); } +.dw-footer__inner { padding: 48px 24px 28px; } +.dw-footer__columns { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 48px; } +.dw-footer__brand p { max-width: 270px; color: var(--dw-muted); font-size: 13px; line-height: 1.5; } +.dw-footer nav { display: flex; flex-direction: column; gap: 12px; font-size: 13px; } +.dw-footer h2 { margin: 0 0 4px; font: 400 11px var(--dw-font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--dw-muted); } +.dw-footer nav a { color: var(--dw-ink-2); } +.dw-footer__bottom { border-top: 1px solid var(--dw-line); margin-top: 40px; padding-top: 24px; display: flex; justify-content: space-between; gap: 16px; color: var(--dw-muted); font-size: 12px; } +.dw-status-pill { display: inline-flex; align-items: center; gap: 6px; background: var(--dw-paper); color: var(--dw-muted); padding: 5px 9px; border-radius: 20px; font: 400 11px var(--dw-font-mono); } +.dw-status-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; } +@media (max-width: 1100px) { + .dw-global-bar { flex-wrap: wrap; gap: 8px 24px; } + .dw-global-nav { order: 3; flex-basis: 100%; } + .dw-global-actions { margin-left: auto; } + .dw-hero { grid-template-columns: minmax(0, 1fr) minmax(300px, .8fr); gap: 32px; } + .dw-hero h1 { font-size: 56px; } +} +@media (max-width: 760px) { + .dw-global-bar { padding: 12px 20px 0; gap: 4px 12px; } + .dw-brand { gap: 7px; } + .dw-brand__name { font-size: 14px; } + .dw-brand__surface { font-size: 11px; } + .dw-brand__mark { width: 26px; height: 26px; } + .dw-global-actions { gap: 0; } + .dw-global-actions .dw-button { display: none; } + .dw-github-link { font-size: 12px; } + .dw-global-nav { gap: 22px; font-size: 13px; justify-content: space-between; } + .dw-docs-toolbar { padding: 0 12px; gap: 8px; } + .dw-docs-toolbar__title { font-size: 12px; } + .dw-channel-label { font-size: 10px; } + body:has(.dw-landing) .md-content__inner { margin: 0 20px; } + .dw-landing { padding: 48px 0 64px; gap: 56px; } + .dw-hero { grid-template-columns: 1fr; gap: 32px; } + .dw-hero h1 { font-size: clamp(40px, 10.7vw, 56px); max-width: 12ch; } + .dw-hero__copy > p:not(.dw-eyebrow, .dw-hero__facts) { font-size: 18px; } + .dw-hero__actions { align-items: stretch; } + .dw-hero__actions .dw-button, .dw-hero__actions .dw-text-link { width: auto; } + .dw-install-card { width: 100%; } + .dw-role-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .dw-section > h2 { font-size: 32px; } + .dw-role-card { padding: 16px; min-height: 205px; } + .dw-quickstart-grid { grid-template-columns: 1fr; gap: 24px; margin-block: 48px; } + .dw-runtime-grid, .dw-link-grid, .dw-credential-grid { grid-template-columns: 1fr; } + .dw-runtime-card, .dw-role-card, .dw-link-card { box-shadow: none; } + .dw-footer__columns { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; } + .dw-footer__brand { grid-column: 1 / -1; } + .dw-footer__bottom { flex-wrap: wrap; } +} +@media (max-width: 370px) { + .dw-global-bar { padding-inline: 16px; } + .dw-brand__name { font-size: 13px; } + .dw-brand { gap: 5px; } + .dw-github-link { font-size: 11px; } + .dw-docs-toolbar__title { font-size: 11px; } + .dw-channel-label { display: none; } + .dw-role-grid { grid-template-columns: 1fr; } +} diff --git a/mkdocs.yml b/mkdocs.yml index e4c03548..288328d9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,7 +9,7 @@ theme: name: material custom_dir: overrides font: false - logo: assets/favicon.svg + logo: assets/logo.svg favicon: assets/favicon.svg features: - navigation.sections @@ -18,23 +18,23 @@ theme: - content.code.annotate - toc.follow palette: - - media: "(prefers-color-scheme: light)" - scheme: default - primary: indigo + - scheme: default + primary: custom toggle: icon: material/weather-night name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: indigo + - scheme: slate + primary: custom toggle: icon: material/weather-sunny name: Switch to light mode extra_javascript: + - javascripts/identity.js - path: javascripts/analytics.js type: module extra_css: + - stylesheets/identity-tokens.css - stylesheets/layout.css plugins: @@ -63,6 +63,7 @@ plugins: - "!^to_dict$" markdown_extensions: + - attr_list - admonition - md_in_html - pymdownx.details @@ -101,6 +102,8 @@ nav: - Testing: reference/testing.md extra: + published_sdk_version: "2.14.1" + docs_channel: source-preview social: - icon: fontawesome/brands/github link: https://github.com/durable-workflow/sdk-python diff --git a/overrides/partials/footer.html b/overrides/partials/footer.html new file mode 100644 index 00000000..35c950a7 --- /dev/null +++ b/overrides/partials/footer.html @@ -0,0 +1,17 @@ + diff --git a/overrides/partials/header.html b/overrides/partials/header.html new file mode 100644 index 00000000..436c354c --- /dev/null +++ b/overrides/partials/header.html @@ -0,0 +1,49 @@ +
+
+ + + Durable Workflowpython + + + +
+ +
From b48d8c2fe1049aa197c9cc2a981fc3d97e72e5b2 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:52:48 +0000 Subject: [PATCH 02/12] feat: share local identity fonts and fresh public status summary --- docs/javascripts/identity.js | 48 +++++++++ docs/javascripts/status.mjs | 97 ++++++++++++++++++ docs/stylesheets/fonts/inter-OFL.txt | 93 +++++++++++++++++ docs/stylesheets/fonts/inter-latin.woff2 | Bin 0 -> 48256 bytes .../fonts/jetbrains-mono-latin.woff2 | Bin 0 -> 31432 bytes docs/stylesheets/fonts/jetbrainsmono-OFL.txt | 93 +++++++++++++++++ docs/stylesheets/fonts/manifest.json | 38 +++++++ .../fonts/space-grotesk-latin.woff2 | Bin 0 -> 13284 bytes docs/stylesheets/fonts/spacegrotesk-OFL.txt | 93 +++++++++++++++++ docs/stylesheets/identity-fonts.css | 25 +++++ docs/stylesheets/layout.css | 9 +- mkdocs.yml | 6 +- overrides/partials/footer.html | 2 +- 13 files changed, 501 insertions(+), 3 deletions(-) create mode 100644 docs/javascripts/status.mjs create mode 100644 docs/stylesheets/fonts/inter-OFL.txt create mode 100644 docs/stylesheets/fonts/inter-latin.woff2 create mode 100644 docs/stylesheets/fonts/jetbrains-mono-latin.woff2 create mode 100644 docs/stylesheets/fonts/jetbrainsmono-OFL.txt create mode 100644 docs/stylesheets/fonts/manifest.json create mode 100644 docs/stylesheets/fonts/space-grotesk-latin.woff2 create mode 100644 docs/stylesheets/fonts/spacegrotesk-OFL.txt create mode 100644 docs/stylesheets/identity-fonts.css diff --git a/docs/javascripts/identity.js b/docs/javascripts/identity.js index e04c6c46..3a405dcb 100644 --- a/docs/javascripts/identity.js +++ b/docs/javascripts/identity.js @@ -1,3 +1,5 @@ +import { watchPublicStatus } from "./status.mjs"; + /* Native disclosure semantics retain links and keyboard navigation without JS. */ (() => { const close = (switcher, restoreFocus) => { @@ -16,4 +18,50 @@ if (!switcher.contains(event.target)) close(switcher, false); }); }); + + document.querySelectorAll('.md-header__option label[for^="__palette_"]').forEach((control) => { + control.setAttribute("role", "button"); + control.setAttribute("aria-label", control.title); + control.tabIndex = 0; + control.addEventListener("keydown", (event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + control.click(); + } + }); + }); + + const pill = document.querySelector("[data-public-status]"); + if (!pill) return; + const labels = { + operational: "All systems operational", + degraded: "Degraded performance", + incident: "Service incident", + maintenance: "Maintenance", + stale: "Status stale", + unverified: "Status unverified", + unavailable: "Status unavailable", + unknown: "Status unknown", + }; + const renderStatus = (state) => { + const safeState = Object.hasOwn(labels, state) ? state : "unknown"; + if (pill.dataset.publicStatus === safeState) return; + pill.dataset.publicStatus = safeState; + pill.querySelector("[data-status-label]").textContent = labels[safeState]; + }; + let stopStatus; + const startStatus = () => { + stopStatus?.(); + renderStatus("unknown"); + stopStatus = watchPublicStatus(renderStatus); + }; + startStatus(); + window.addEventListener("pagehide", () => { + stopStatus?.(); + stopStatus = undefined; + renderStatus("unknown"); + }); + window.addEventListener("pageshow", (event) => { + if (event.persisted) startStatus(); + }); })(); diff --git a/docs/javascripts/status.mjs b/docs/javascripts/status.mjs new file mode 100644 index 00000000..0240ec43 --- /dev/null +++ b/docs/javascripts/status.mjs @@ -0,0 +1,97 @@ +// Shared public footer summary. No component rows, credentials or telemetry. +export const STATUS_API = 'https://status.durable-workflow.com/api/current.json'; +const statuses = new Set(['ok', 'warning', 'failed', 'unknown']); +const levels = new Set(['ok', 'warning', 'critical', 'unknown']); +const unknown = () => ({state: 'unknown', validUntil: 0}); +const timestamp = value => typeof value === 'string' && /^\d{4}-\d\d-\d\dT.+(?:Z|[+-]\d\d:\d\d)$/.test(value) ? Date.parse(value) : NaN; + +function evidenceWindow(snapshot, now) { + if (!snapshot || snapshot.schema !== 'durable_workflow.status_page_snapshot.v1' + || snapshot.product !== 'cloud' || snapshot.environment !== 'prod' || !Number.isFinite(now)) return unknown(); + const checked = timestamp(snapshot.checked_at); + const generated = timestamp(snapshot.generated_at); + const freshness = snapshot.freshness; + if (!Number.isFinite(checked) || !Number.isFinite(generated) || checked > now + || generated > now || generated < checked || !freshness + || !Number.isFinite(freshness.stale_after_seconds) || freshness.stale_after_seconds <= 0 + || freshness.stale_after_seconds > 900 || !Number.isFinite(freshness.age_seconds) + || freshness.age_seconds < 0) return unknown(); + const validUntil = checked + freshness.stale_after_seconds * 1000; + if (freshness.status === 'stale' || now >= validUntil || freshness.age_seconds >= freshness.stale_after_seconds) return {state: 'stale', validUntil: 0}; + if (freshness.status !== 'fresh') return unknown(); + return {validUntil}; +} + +function aggregateState(snapshot, validUntil) { + const impact = snapshot.customer_impact; + if (!statuses.has(snapshot.status) || !levels.has(snapshot.alert_level) + || !impact || !statuses.has(impact.status) || !levels.has(impact.alert_level)) return unknown(); + const reason = snapshot.meta?.public_status_reason; + if (reason === 'cloud_unreachable') return {state: 'unavailable', validUntil}; + if (reason === 'monitor_stale' || reason === 'probe_unverified') return {state: 'unverified', validUntil}; + if (reason === 'publisher_stale') return {state: 'stale', validUntil: 0}; + if (reason !== undefined && reason !== null && reason !== '') return unknown(); + const stateTokens = [snapshot.status, impact.status]; + const levelTokens = [snapshot.alert_level, impact.alert_level]; + if (stateTokens.includes('failed') || levelTokens.includes('critical')) return {state: 'incident', validUntil}; + if (stateTokens.includes('warning') || levelTokens.includes('warning')) { + const families = impact.affected_families; + const maintenanceOnly = Array.isArray(families) && families.length === 1 && families[0] === 'maintenance'; + return {state: maintenanceOnly ? 'maintenance' : 'degraded', validUntil}; + } + if (stateTokens.includes('unknown') || levelTokens.includes('unknown')) return {state: 'unknown', validUntil}; + return {state: 'operational', validUntil}; +} + +export function publicStatus(snapshot, now = Date.now()) { + const evidence = evidenceWindow(snapshot, now); + return evidence.state ? evidence : aggregateState(snapshot, evidence.validUntil); +} + +export async function fetchPublicStatus({fetchImpl = globalThis.fetch, now = Date.now, signal} = {}) { + const response = await fetchImpl(STATUS_API, { + method: 'GET', mode: 'cors', credentials: 'omit', cache: 'no-store', + redirect: 'error', referrerPolicy: 'no-referrer', signal, + }); + if (!response.ok) throw new Error('Public status unavailable'); + const body = await response.text(); + if (body.length > 65536) throw new Error('Public status response too large'); + return publicStatus(JSON.parse(body), now()); +} + +// At most one request at a time, five-second deadline, sixty-second cadence. +// Invalidate a previously successful result at its evidence cutoff, even if the +// next request hangs or the tab has been backgrounded. The caller owns cleanup. +export function watchPublicStatus(onChange, {fetchImpl = globalThis.fetch, now = Date.now} = {}) { + let stopped = false; + let controller; + let timer; + let expiry; + const publish = summary => { + clearTimeout(expiry); + onChange(summary.state); + if (summary.validUntil > now()) { + expiry = setTimeout(() => onChange('stale'), summary.validUntil - now()); + } + }; + const refresh = async () => { + controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 5000); + try { + const summary = await fetchPublicStatus({fetchImpl, now, signal: controller.signal}); + if (!stopped) publish(summary); + } catch { + if (!stopped) publish(unknown()); + } finally { + clearTimeout(timeout); + if (!stopped) timer = setTimeout(refresh, 60000); + } + }; + refresh(); + return () => { + stopped = true; + controller?.abort(); + clearTimeout(timer); + clearTimeout(expiry); + }; +} diff --git a/docs/stylesheets/fonts/inter-OFL.txt b/docs/stylesheets/fonts/inter-OFL.txt new file mode 100644 index 00000000..21f6aff9 --- /dev/null +++ b/docs/stylesheets/fonts/inter-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/stylesheets/fonts/inter-latin.woff2 b/docs/stylesheets/fonts/inter-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d15208de03cd1ad7c5199f0a0ce915fe841e4722 GIT binary patch literal 48256 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∾Na#*TBTU(!jF?9zmR-!C*+RdvGrd0&{`| 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 literal 0 HcmV?d00001 diff --git a/docs/stylesheets/fonts/jetbrainsmono-OFL.txt b/docs/stylesheets/fonts/jetbrainsmono-OFL.txt new file mode 100644 index 00000000..821a3dac --- /dev/null +++ b/docs/stylesheets/fonts/jetbrainsmono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/docs/stylesheets/fonts/manifest.json b/docs/stylesheets/fonts/manifest.json new file mode 100644 index 00000000..7eff1ef3 --- /dev/null +++ b/docs/stylesheets/fonts/manifest.json @@ -0,0 +1,38 @@ +{ + "css_source": "https://fonts.googleapis.com/css2?family=Inter:wght@400..600&family=JetBrains+Mono:wght@400..600&family=Space+Grotesk:wght@600&display=swap", + "fonts": [ + { + "bytes": 48256, + "family": "Inter", + "file": "inter-latin.woff2", + "license_file": "inter-OFL.txt", + "license_sha256": "5b9321a4298cfeb6b34354164a1c3afc3db114569984c502b9b35d988fd58c57", + "license_source": "https://raw.githubusercontent.com/google/fonts/24ddc1f324cac5d9f5aedca126d298f56e3f1640/ofl/inter/OFL.txt", + "sha256": "3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62", + "source": "https://fonts.gstatic.com/s/inter/v20/UcC73FwrK3iLTeHuS_nVMrMxCp50SjIa1ZL7.woff2" + }, + { + "bytes": 31432, + "family": "JetBrains Mono", + "file": "jetbrains-mono-latin.woff2", + "license_file": "jetbrainsmono-OFL.txt", + "license_sha256": "b2fe5e8987594e9ffd1d2ca52a2f5d73eb8335243893c5d6254b5ad69269591d", + "license_source": "https://raw.githubusercontent.com/google/fonts/24ddc1f324cac5d9f5aedca126d298f56e3f1640/ofl/jetbrainsmono/OFL.txt", + "sha256": "83c005d49d8a6a50474c73a5a36ac0468076e9c4a29da7bdb14995d80560a5be", + "source": "https://fonts.gstatic.com/s/jetbrainsmono/v24/tDbv2o-flEEny0FZhsfKu5WU4zr3E_BX0PnT8RD8yKwBNntkaToggR7BYRbKPxDcwg.woff2" + }, + { + "bytes": 13284, + "family": "Space Grotesk", + "file": "space-grotesk-latin.woff2", + "license_file": "spacegrotesk-OFL.txt", + "license_sha256": "564ce565c371c5e5bbf286006565a7c9aa55a9f56e7ca58d56e05d649dd61a72", + "license_source": "https://raw.githubusercontent.com/google/fonts/24ddc1f324cac5d9f5aedca126d298f56e3f1640/ofl/spacegrotesk/OFL.txt", + "sha256": "685bbbf69fa616df1ef81847c85fc76be097ddfb3468ff2257be54511ab3130f", + "source": "https://fonts.gstatic.com/s/spacegrotesk/v22/V8mQoQDjQSkFtoMM3T6r8E7mF71Q-gOoraIAEj42VnskPMA.woff2" + } + ], + "license_repository_commit": "24ddc1f324cac5d9f5aedca126d298f56e3f1640", + "runtime_cdn": false, + "self_hosted": true +} diff --git a/docs/stylesheets/fonts/space-grotesk-latin.woff2 b/docs/stylesheets/fonts/space-grotesk-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a1db41a0ac0cbe2edd0d0a0d07d90fb8fa366d14 GIT binary patch literal 13284 zcmVB*58CeiBKqM(v&pO z&<$5_-o1)?M^m!HOdO-9lxfc~4vb%RF;HN!5pEL0mr%KiL9M*s8u(#8ERUGDsa+9p z3ftc@_1_lU)>~cx zU`xWr*~nCcM1lu`1)Ald#XjCYy_dWccL7y-6{e38<5JHu{6}Z{KZxT0q&r+nAC}Z* zh$Er%;a=7x6RfbNl(NjBU~RO~5N!%-6Kodw59Qm{B7CT2uGaSAJGB3&6b;RN@32H= zx`uX8MWkAYgu>98V|A2FBeXczcR71E1h;))%eEkxA51SEEc*YymfF1BOo4C9GOEDB zkzCkyMaWY~*^zdKIVJi_G+Sa^jYSxY#%bW`_x`MxAz;&I7!f@QA{~+M{aXJU z>iRxj;@IhmNRc9k6fxo##~SzhrB%1OR}Xs{pEVYhkcp0nI3XjFeC_Y1l)&@{Oa&Q% z07C&Q9f?>D1?UilMok`i@p}GabZQYY*cihODpIQ?wBt~FBSon}u zL=XuHL`Dq>r-c|;Aa*W@hX)cV3gQ*DK(%Tpl^Tq1Bkr;>*xw{ADZ_8+=%ZfM33N=Y>b^xWNy2 z)JA6WkJ2il{(wgLBobX}Rr8lnnj z(Rfn;`Ax{yY(hJ(J`>FaZtE|1modrMJSYrgiIJYB^CAQ z+!t;!BBxmop) z=DdPj6u5{vn2#;k!I6O8*U+NFfFVWHX~O$n2(gEJW&x7_Dq9a|)_~Y#X>o;+?R;i**c*?kEt@QK^4flh8+&GB4FhTV z`@(??IF8~pF0eo;5!jBuP1g7_Y(d7LU6WVSUmv4l;sca6G`~b#_KU9WhTdf@IC;Y5 zUWTL>R5wXlHnbOMyyLp7Q*ouKM9Dv}Nbm{_@ ziS}c|wdL^aZ^N|h#w9spVtA(CwS)C>N{K$tto$9gyn`5yritc*`!09>IBA#R+F9Q< ziIS^BQ_tf#8XtjWf@9A@B+JP*uA&11OBwcU+jO+lDAcKmF37Mx6v8?I&Z~)V}hm)l&2pzv!O_j#fbwFjgP(@HlKG~VKKOMdc~pr`h}3(e$zW#x$4n%gP_^6acqo=1IoI@&qpV-LA?ev0;RsHq?%Bk79I#QYO+0OeW7*6+)l6%#T{GrN;~#*8yJ|mrZ(RqzVLl#0t<3 z{`r_S%b39mqxLTkB708Wx?HP~jA)g$^x>%n&SDNW%P5YdDo{dfHopcgw~M>SMPI0} za;P~x|8ik8Eb7Yr$?*Rgoi7pu4X}ollAV*0TLgk*}829~$WS_!)#G znGIRvH`A8t<)pPHcv+K)?(&=LiYunL;fAU1x@($;o+|dqCl$W6-}iW>cr9{ zjz;kemq>#o>Ls&S3QMJtD&xXb`CPJfP^*v^6iQwo3Z;ltSZJgaoH3TeOg0UnSg~NG zs?pS#37KU!wmIhFnP(n``IbVKTLb45Ot2sXAAHDaSYE;~pUC!^3}49hl?;IvD}t4Q zoNFl1NHIFu09APh)g(uvi(2pb29q8OJtK@yzFL*f$&p^Jb>EAj}*5=sdXF=Dz& z7)DAD8M(sT(9)oa$`pkf%K;3bA8ruN2vY(G0b6D4g9C*|@D71$7>5J)fLVeBF$5c| zpx0DL-AI$3NCQY9Nl_qT5ieYhD^jaE?L>)^)cZNrpd)DRs) zgLkl&)adtRgKH2C{DC09^>6xny}ybB{kDFCZTufw)z9e%^__i7U)SRtiHuuJq;|wk zxLRLmnn@ZvtUUA9y?T%zqdj}v{!Ojv?~V4iZ0N44$=FY@R-I#d`fsw8q0~d?A{JRq ziiXXCQPc0vx79vZC+#;NV=Y3Q(|wMK5`-^=;4z>gt{Zw3lr#f6`Xwv4|CeZqlBsFv z=oy$;xOvp5)xf8XUr1QD9=#%B2BnM`HD=s|Nh?e-)ilLQlqyrMLZvFzYSgMT-3&8} zW3p<(-_EzdLW?Z6#8S(wwOgbA{O=OKdyjtq>Qm3X^2wI~>JS@o5kH}kMG4vc^=!80au&mu9#SmTU0 z!9@ z`n5V%XOWss%+bxx*X}5P{Y2^ur|m6ndnFsjqEhV#J_K(U4lEs~oJ9q7YHH!3XJSbk z)12ZM#QzZ{dQ{B*iCrvy-z2(Bc2a5 zRjH1eS!&gpZibmLN3NZ@8w+wF7FleGrLla>3M;L$+L~x=TvPS8A0C!`gvXwEI{66P zC8gY>r;V&p*tsuztbi5_e|}{O=i0oiUh$dFec?-A<)Tr%L0&;@lqi-WkYt^gI+CwW zoWqyTyw-*Ho5l#xDrW; ztAc}lg#0Ax;d%QKGEfD}=b@kh%P%>&=!LkX3NU&-_4j@fA!v66m^XoV|GH!$*@*N> zI_#g0sz4`yM3+gr>K1*6_0!(~0~PXu1dB()x6rI$qD4{vYmhbaGyB7$J$Elqg4A#M z=}KVv-XTv3grGI!fr2B@F)*{rYr+&__{^fzW=3mU-%a4=)@7&d;S5kUvq^IzR{z0uD&9K$li-_m3Z@DwI|| zAUtKg+PJOeTzyIidt5KEFMPONAM%Y84r4nhRtOm!3`EUF-qKQK{E@kGHLJ`!IWP#Vwg3%zb5gYCWpdisw0J&SSVEn#7FckfRWk?humPvI8^|3r7TA)vwZLZZFtBgvt65I+G zf!cEOhzS}b6p0FA6Jfk|e@HeO2fjXaxB7&1S&O$m`W{JdmQ2Fk8;I-1!3+0{9SKIE zic3Q}!o0Q5zP8vmUia_Q?2Qk;MT`VSx-${tQpD=*mNPFH2ApS#@HyYVN$H2LO3%m;Uc?aMX z)XF*dwf|ed=$4c$2XqG@-&Z}b91cMk5HK5ny7t_@!w>*m0b&hr??+VT+y5vkO9)Wx zVkB$QX2gW68Z^1>vDX6GoQ^eLr|U+wbO;Pa2C5Kokn)!`79l9^Eo%Zvp;OB^eeH_yN!_J>Rol%QGW~4u+Vy1PuHX~(|g7*e|gvr+w7FwcH9Tt+Lu$YfQ4#4OhJ3i`X~KK4`M)EF~OM@ zYFW{I$Yf+%`zuTAgkdR3&8dDD^m3>b!p@nPrnEN0mxhcNaZBsg`>huDK5dh(Es<(< zPOp#dBzG8%;c!rR7SjEZc)S=!Ip$}owl&|AAGWT}hPVUX#L?y}t;~lb+7Q(GPOlNo znP0Cq_vll%3TGwUtGSw&uxbTGA4jbBm8M#yQ?Xv@hbF>D8Ui$gvI&*vg!c=nh+yJ8 zPBjY^BBew+Mn*W@I;NFdzkn6|u2(IE-^>2d0_sJR|D5 zpUWUKz$!?M3uBIS%knYXf=wrYbq*x_5?Kgqdj(Vqhhg*B24EN4l51N)SHK8`&7b(M z2)%~sS#%5azmSQuRE(<_DvUj8fam;SEMh>O~b@*hGQO zrqPuSI9(KhPg%2}BQoUb)5^#t1nbNyz=96 zuu(}OV6Jye!fHB_0e?j^g++P7^sj}i47jUbu$6GDQ7azEQ#FxeNo?Qs1a36AJ|IUAZ*yvN&y9Jr^xAVwbM{7rhGK@iC4S|44qfvGT|A8 z{fU|^b>*7753E1{5;{OA9R)ak)>t&7az*+AuWWjEV?|AHP`eqes*pszv<5k(?s=Uj zZ|}T5(BQ$mj~LIF+k}V;r_5W48fVk#41Np~T*VrRb%Ty!b(6?=8OvIhIEONt*O24y z)GVraJ=;KB)*fun_DJ!`HCO30h*B<;0&74JaR78xn}|gK0doyVbgFw`sEFgAqEi?f z4tf^`@Wkmzix9z<>if^&DL&!>C|cHi$o!+1tg23&R?hu1Rkm5I)ty2`!AFw$%BxyF z=~%@*V;x^E4$(hI`NHf591IwtmAnvL*w4F+?8srww!{i&Hw_$O0AK0B7Jmq`9Nn zPzaUtcE#bN!`bct@2F9rX-Cy)AWfbJzCFCr8o`J!Iu;H{Fw z9|f))36Gu~^8VAfoUGed}oRsQuw!-Wr%0 zh>wOiSe@_*!*i>s&xKeCHcHWg{YZ|4#lAz{mq8fptpP;qDz$|vJn23!KYE*x+Mva! zf7Bmi;ofg1{-{#oNj>R@69PyevHT0fj_~s&K}C#bY4prCbTE?)Azbp3MGL|J8+x18 z4bcZD$WU&hQ$*6i=DLU>0@%5v?ufWa=2!7TD%g5c56#NVf<4 zA??w==MVXS_X!Hx^rC>x72eJhdbO!B9n;Hf|F+3kIT=`6&Y9I)>|uVeD&`+hvU}or zuT^R5rhwPWhg7K(e3mqDVP|{n^bVp9KT{?6E^56GQkYX@0wj?+cQVJpedK$O^@+Ls+)u@ znabv=w0{`pFtk;BVyAQ2jx1+eX;O%N1)J5=c-45zV9AnF-s8+(#HM$bHM=+M-qzIO# z+2T&Jp|?>k3U5Wrd)|`qDz~oyEC_6+l zK#jDKH7eaaoDKHb+#(m-G%R;YjsFFd=(m@Imu#X~3{*hdQZ2q2Iu)IGks#fh^FEE; zpONvW!-ZZB3NFHN`HQR7Ege_QxmLdnI|0J!p|n6Kxbr>rm>z|A>Fng~y=<(pGtdsV z)6{4zI`ivGWI~l3$dP3@M1<_k^#r&mG!Tlmu3-;>a_-+}UlNI=pGwR4rP-dmDwfJt z%w}7t^W}@nZofbG?8D3PxghuIm0NG_F~-3q&jmf^rGMKY!0PsX>Fw@RddvBCQ3hKM zbwWXvotr5Zy1h)rmfkh&4a^MY1ZPcTHETU1ld+z)8d#|1J2%aom|;_wFP~dGf*Q=w zm};y@O_TGCqox_LnrbxAAa-PKEpYy_bz9eOSiOOLuQT+TYF+i7wezg2HGz!!CnEZb zj0od#Byw2z!x8BvYLrR`A`FsL>BgGDXEi&MTs)Xg|6d*ECi8B!TJK_1ICg)%9N5nD&R8tHcs&8HA=gZlo|2e9-ChW8L_kq501)#ZV7uN3Gm5Zt|I2 zxoYuU15maIa{|ing=MBLQ^}p$1G;4-! zNb*TUc`L}MXKMVFLboJKEw?>@_diwsMe6m!3#4n(F81%Aqq3+jdI2R6|!qHr(QSWde}0D zN~!WC1>kGPtq37<%K_R@H7S`Y;k=o8(O|Qf%vOux#nerwGBGRzFJHPcwm*tJ5FNXG z1+3_LWylhO{GnTE=^-SP@?W{to`eEPAiCZ5O4IDNCD7I-bCP$fZFWZ%#J)kuocq%K zd`O<6#%EBqr{g5Hx%Z9qwHodPcQGDM30u$OvbD8T(MA&O2OgJ|3{xL<)R(FJ(YBVS zNyKuWtK<%OA=LWDi*T#hs1jw)lzu=a{Q`O^1H#5Y>i+oz8anF#tV$k~$orjmr@X&Y z(i%{>oR&Ffyi zRK*PWz5Zz7L-N`VF385nx{mmX$yvYpk3#79d`t~3Gqa1~UkfG-5>qJRHnd&d-={E} zdKA)rDgfDQe4kvH!JB0vh0jdu6J7gO6D^QJ#cyFy?ej?H*oLxa@Mkr~WYBuE{56#N97uWd1OL0Tf~RB-Q#NFW!}HtIn;!-SwzO|JTg zP;~RnPZ=I+MU|=BR`2OC{~IO`yJ_^Z1i~f#XW<62lxK^&0gXMwDP7@PaWHXkgKtAN zi5Tz!T3Sp!x2i=!kCSDmg>r+#@>l#`ObXC`8yeX?a;pT?pITEK}$F-0QhpvRG(VC=@o08cO1<+HgW-GR1`|za~>FX!c4Ty#11+ zft4AL=`st=+X8KR@?0{zN}3%$Wo|{Mk}JRLnw2ZPSMFz z1`KF}Iy4}*c4=$|X*z?Uf-9J6n88Sw+6XkDtEHdZcA3G+#)pdn@^^pDXo%&mTQ}as~lZsr-+lzc2pyXqne%0 zaf(bT9l~LyS6Vq5jg?cG&O$hViLuA{a&!Y#YQui=jb#RT7f;dGsO}nXwF|jeY}YDDXD0@9R&Z>K33+&+D|a`hOTD< zNaoC9Dy28tOQDKo=R~0HFUI1r#rvrQB`WZgVDOj{s!n4}byDrY@^`&k5$nET>WFpk zb=iB{W0CQxf;Ewe7(hR@$9gB$6ioGw$J(D5(Xw}hI&9fj2-w&8fz@%H2he>aCWt^+ zcrkG?9#|rv;Cn|RyOAoR&^~_+Wn~bJh6lE4RVkAR<4cLfrKHlb|7qUtFeqsLaNtAn zS29!AG`utLj`*1}{DIsfc-zC^jdx&f^e+9gY$p8Aw3c>LLn90p=r!#Lp2-;DDVk)` za*MW#UA^>;d}8%irQ=>Bw zn>dQ!?%^I@=w*>ZMuvPF&l(2f70X9T1%M=s=+>K;Xjih$vK7rpDl z`6T+`HG6!Z>=QeUCjW#+zb&UTY##&d+|Z{XKwYnXu_t;&1)lypV_nRR_v>SRKk1F|2D-Y<_%~(2dUE6KW zT`_#WXZLIGB(qW`@U)am`+cqK zpLoUJmXdmWai7=c<$mbxyVJ-1g)h|bK*eRf!!}&w{vmbB`lEu@`fZVA7Z*epJYF0DAI}D<@Dq`Z(T79_!E`$JJ%=ASvPZM` zfy0M8lfgc}-S0}1;80a`2mcH97AY1v5}WyMCsFv|4E8mN=seTX|eTMYB$1VE*`h1;;5x9fPAu zbdskkD(KHs^KCUV8xF?E#YMkvs-eI^CRInNuxlTQKhUTdA5h!mK866PVtz|_VB6j_ z{OQXFG>j;RQ)Q`V0#n24I2wA$_#zK24sXrY(H@~^)faq`llKRB%j4;2YiQ{3*}n)u-``KL z&8@FQszpd;MU$wy3B1p3kM%U+J20HaYSvvU<-xfcY^%jkaPg4862+;~J4<+VJ@vMN z0B$LN6}j{(spOSE>TzzXn#()5CNCXnZ$-(nQ7GAwVX9<_2m_!0LwgN?z*1!!PtYLE zl8GAIIm+_+fY;@nm>_;Y_tm+lhwtWEwptQ@7I=k3UZmoOd#N;3|U?t$2C*|Ey8dyUIqB~*DX8qV&e6#CK;9%a8 z@4p2$aSWnjqZh^ZT54U!G~=nog479n!~&aO9K{I3MhY&5y;ifoAe%qoC-{Wg_q!S4SBl!J%rrzcQ$C_@|g1PqT?Er`W2ecIM^+oi< zZ?~`erZ$Fs;hTTzr@E1ivw6(Rvfv1s9?K_v?0P`Ej3l^CaY5D(zSF+n80Qs_F}EOb zJ)jBsT6m0YaH*YjOZctJ$n3(x^1^~mUG1-w7+$tucWFvU;hZwD!wBvmFS*x0wf$w4 zL4`cGF}4NkUjyPbuIV37_^%~Gi4FC?eliZa)eB~rx;CvsHi3p;$M_6c4co+u2bpaEQf{daYhxuaiX;4JPd~ zR9iuTjY?@PC};)KLNNJGN7de4UZ4v?`i}-Ws^6P$jQkw|o&Sd~QK%PT$|)-4G_c5( z+-52BF=kqdij&OB*XU~e0vs0$#!|63)nnK5W1grWY;W+ZO*% ze(c+s{PwBGo*rHbj!DY4xfh?s9_e1T4!ZXG4XfA_?1WL{ifC-oraBbsB1{eti!eh0 zs?gr0xK4jiV9*JS{L4zpOBj7V=t);GhD`b(K*94^JU6}v^!fUf`AKrA3fvxl0qZ_cRDLo1F$W^rTH%kih!2h!V8i)E5|M>mO zNJ9P(*jXhOi~51yFCPB~HC$BiJ$AhVi&Kr1Kk@f(+xwDsseUo)IY3}YR9Y?e>w^XP z)x@GvO6?L`tx~P=D1}0^;NQ~`H9!2?KJfqfifNqNbneJ&-VyCHQ?kw@=H<6)UO?)ex@t`eFCzyZZK6O}P9ijW(WmnSw_Id02ONj|;?22OANZ^_ zUS8W067epNns;{Ygbt;q%rIsz)r!0^d>9scy!zAblA900XL<7p@y!6a_8@TcBP(-C z(a~m!g-K+=F>g6Mh$P};_i3(#x5->#Fp*q1>Rq1>1UDc3HPYcA()gdi+TZ}z+WiJF zc$r=FPNX^SLsx%YRv+`A8PIr|v|=PdPh5Rg1Hu4uN+BtPcV#H9Gcn)xkiWcWS-JZ|Oe^Ug=)kpkzz1NuPwMsxAT82KcB>G_)qZvPV=FdQj&$yH$PBq_lf2KDAt* z5q3OJ@`GCD-oURb@tk%%BjtiRyF+M@hH3Vv2YFUkzt{6@vU6e0%eWtpk3?uhyu1P4 zvl8NTM#t?Dx3hab+n$k2@3Gx5{=9mX0)ta>vu>fB1u64?N0mZ@@mWkzXV#8!Z?1ZT z4$w9_(tD3{b*0OXK6BBGQs_YZNQ;1Xt?hV_3CHaTI>9^R_wtsM$N_3qZFAK;A+X0N zB!Q3MvqGDU@ydnKp&h_Sv?kTrbW+~vnd&sK6btddk2bO9G}R3To{lDQ#gROzctX-S zgXJZi2^%}$!fR%*1xNT|VoFiR65%Ztj>0LY&GKT3!b=64%&{rvVnJ=se$^?6x31CS zjEzA+feU`b9`DVU{Wc!Qp8&pmP(6RN($$CXxU6npcO2+p00A%i3Uz-O!k6sIko6i_ z)lJp84M4LD8g;$g&SmRVp3W~hi;OOXZ?8RdGO{S=MXlG#WRwHnr~HVe{Y`k(wro?{ ze1%;qrQLFG9Jo`6S}3EERW${p6PatnDMV04Cc8;x?LN#|0+FEyP;HVp;p)MpH;Cf{ zE<#ERiUCW`vYK#~HSDJhh*IcBCMqv*$(%3B;8N5nh8%)))0E(0rkQ8N^gJr7cHt&g z*loO&i0MGO`%^~U;D)ktQ3=9D8Tc{4okal7<|r6B?y-R!T ziE)H`Vp08O^*$NDU&$^O@1lkDalRIWMTeABY;*`pGEJ3mX2KWo4!loWprb6kOGyz1 z)WU(6LWFqbpDqBvKa6~hK^>c!4a+Vah>}rAB$A+aEyKT&-UAv#&j1AMuLB8FDg^wb z5QzpJ7f4s(v;(IZIQ>8)xy-AG-bb7Q$q&v-o^!)jAk3 zNd+7lF#)`h4ss|G9m!A(H=LokOxMhWeJG8@rlBN{QA4>Rr49|IBjdcv8}1oL0F!}Z z1%-79N%9Da2-E1(tAj_3UqBEnOI)9%poAW&jGkU9HAL5lW+AE;f!0seRImlgUCF8oQqyT?X{>i+9`_4#2hL zjxodH_LPgGNhlRGIN0OBYP)6Jtp+?(DF$+^_H~?ywIScBKEl=o4`|2IL!XE$z4fk? zhWSPH@cTjxHOi4MI86Lh60<8#z5)qS-k_jWHproRv5l$OlRCM$VF(tvo%)-jX&cVsW z%_BmjDA8iXiW4tEq9n=GG_-W|42(?75EfQ8b`DN1Zk}P^R_j9T%J83oWH8VdX-^xt zVK!SM9=W)UysXoz`D`AWgLQP@edjL-3+d%;>*RMS9K(#S2$3@rlyVEpm3w97#((bOkkLfzN%kzO!aovqNR$&B!P z|CN>+w7f z9@7G!H%HUB1zL4Y(K4GgBTb!-Q|(9nw>k79+-MZG#WQhf6$Lqqrun~n-zevH9A?n9gNUc0x_e%&jZFR i1rY!(rsO~@

Run

Self-hosted ServerCloudPricingStatus
- + From ff43362876a0434f1d088da7d4e2f2a82af4f89f Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:46:28 +0000 Subject: [PATCH 03/12] fix: bind redesigned stable docs to the published SDK source --- .github/workflows/docs.yml | 102 +- .gitignore | 8 + docs/index.md | 16 +- docs/javascripts/status.mjs | 28 +- docs/stylesheets/layout.css | 1644 ++++++++++++++++------------ overrides/partials/footer.html | 2 +- overrides/partials/header.html | 2 +- scripts/check-docs-analytics.py | 3 - scripts/check-docs-layout.py | 109 +- scripts/prepare_portal.py | 132 +++ tests/test_docs_release_channel.py | 48 +- tests/test_portal_source_split.py | 136 +++ 12 files changed, 1452 insertions(+), 778 deletions(-) create mode 100644 scripts/prepare_portal.py create mode 100644 tests/test_portal_source_split.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4232aff6..3aa88830 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -11,6 +11,7 @@ on: - 'pyproject.toml' - 'scripts/check-docs-analytics.py' - 'scripts/check-docs-layout.py' + - 'scripts/prepare_portal.py' - 'scripts/docstring_cross_references.py' - '.github/workflows/docs.yml' workflow_dispatch: @@ -45,8 +46,9 @@ jobs: fi - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: - ref: ${{ (inputs.stable_tag != '' && format('refs/tags/{0}', inputs.stable_tag)) || github.sha }} + ref: ${{ github.sha }} fetch-depth: 0 + persist-credentials: false - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" @@ -54,27 +56,49 @@ jobs: uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust with: clients: pip,python + - name: Check out the selected published SDK source + if: inputs.stable_tag != '' + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + ref: ${{ format('refs/tags/{0}', inputs.stable_tag) }} + path: .docs-sdk + persist-credentials: false - name: Resolve the public documentation release channel id: release-channel env: STABLE_TAG: ${{ inputs.stable_tag }} GH_TOKEN: ${{ github.token }} run: | - version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])') + docs_revision=$(git rev-parse HEAD) + test "$docs_revision" = "$GITHUB_SHA" + sdk_revision=$docs_revision + project=pyproject.toml + if [ -n "$STABLE_TAG" ]; then + project=.docs-sdk/pyproject.toml + fi + version=$(python - "$project" <<'PY' + import sys + import tomllib + print(tomllib.load(open(sys.argv[1], "rb"))["project"]["version"]) + PY + ) if [ -n "$STABLE_TAG" ]; then test "$GITHUB_EVENT_NAME" = workflow_dispatch test "$GITHUB_REF" = refs/heads/main test "$version" = "$STABLE_TAG" released=$(gh release view "$STABLE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,isPrerelease --jq '(.isDraft == false and .isPrerelease == false)') test "$released" = true - source_revision=$(git rev-parse HEAD) + sdk_revision=$(git -C .docs-sdk rev-parse HEAD) tag_revision=$(git rev-parse "refs/tags/$STABLE_TAG^{commit}") - test "$source_revision" = "$tag_revision" + test "$sdk_revision" = "$tag_revision" python - "$version" <<'PY' import json import re import sys import urllib.request + from urllib.parse import urlsplit + from pathlib import Path + import os version = sys.argv[1] with urllib.request.urlopen( @@ -87,6 +111,7 @@ jobs: if package.get("info", {}).get("name") != "durable-workflow" or package["info"].get("version") != version: raise SystemExit("PyPI package identity differs from selected tag") kinds = set() + wheel = None for artifact in package.get("urls", []): if artifact.get("yanked") is not False: continue @@ -96,16 +121,50 @@ jobs: if not filename.startswith(f"durable_workflow-{version}-") and filename != f"durable_workflow-{version}.tar.gz": raise SystemExit("PyPI artifact filename differs from selected package") kinds.add(artifact.get("packagetype")) + if filename == f"durable_workflow-{version}-py3-none-any.whl": + if wheel is not None: + raise SystemExit("Selected package has duplicate universal wheels") + wheel = artifact if not {"bdist_wheel", "sdist"} <= kinds: raise SystemExit("Selected stable package has no unyanked wheel and sdist") + if wheel is None: + raise SystemExit("Selected package has no universal Python wheel") + url = urlsplit(wheel.get("url", "")) + if (url.scheme != "https" or url.netloc != "files.pythonhosted.org" + or url.query or url.fragment or not url.path.startswith("/packages/") + or not url.path.endswith("/" + wheel["filename"]) + or not re.fullmatch(r"[A-Za-z0-9/_.-]+", url.path)): + raise SystemExit("Published wheel URL is outside the package authority") + with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output: + output.write(f"wheel_url={wheel['url']}\nwheel_sha256={wheel['digests']['sha256']}\n") PY echo stable=true >> "$GITHUB_OUTPUT" else echo stable=false >> "$GITHUB_OUTPUT" fi echo "version=$version" >> "$GITHUB_OUTPUT" + echo "docs_revision=$docs_revision" >> "$GITHUB_OUTPUT" + echo "sdk_revision=$sdk_revision" >> "$GITHUB_OUTPUT" - name: Install package and documentation dependencies - run: pip install -e '.[docs]' + env: + STABLE_TAG: ${{ inputs.stable_tag }} + PUBLISHED_WHEEL_URL: ${{ steps.release-channel.outputs.wheel_url }} + PUBLISHED_WHEEL_SHA256: ${{ steps.release-channel.outputs.wheel_sha256 }} + run: | + if [ -n "$STABLE_TAG" ]; then + python - <<'PY' + import tomllib + from pathlib import Path + dependencies = tomllib.loads(Path("pyproject.toml").read_text())["project"]["optional-dependencies"]["docs"] + Path(".docs-requirements.txt").write_text("\n".join(dependencies) + "\n") + PY + pip install --only-binary=:all: -r .docs-requirements.txt + pip install --only-binary=:all: "$PUBLISHED_WHEEL_URL#sha256=$PUBLISHED_WHEEL_SHA256" + else + pip install -e '.[docs]' + fi + - name: Require the accepted central APT floor before browser system dependencies + uses: durable-workflow/.github/.github/actions/require-fixed-apt@18783be6bef737cc778e4c7282a8797e39daa61a - name: Install Chromium env: NODE_OPTIONS: --use-openssl-ca @@ -116,6 +175,9 @@ jobs: - name: Build and check the portal env: CLOUDFLARE_WEB_ANALYTICS_TOKEN: ${{ vars.CLOUDFLARE_WEB_ANALYTICS_TOKEN }} + STABLE_TAG: ${{ inputs.stable_tag }} + DOCS_WHEEL_SHA256: ${{ steps.release-channel.outputs.wheel_sha256 }} + PYTHONDONTWRITEBYTECODE: '1' run: | if ! printf '%s' "$CLOUDFLARE_WEB_ANALYTICS_TOKEN" | grep -Eq '^[a-f0-9]{32}$'; then echo 'CLOUDFLARE_WEB_ANALYTICS_TOKEN must be the canonical 32-character site token' >&2 @@ -123,29 +185,23 @@ jobs: fi sed -i "s/__CLOUDFLARE_WEB_ANALYTICS_TOKEN__/$CLOUDFLARE_WEB_ANALYTICS_TOKEN/" \ docs/javascripts/analytics.js - mkdocs build --strict + python scripts/prepare_portal.py + mkdocs build --strict --config-file .docs-build.yml python scripts/docstring_cross_references.py --site site python scripts/check-docs-analytics.py site --require-token - python scripts/check-docs-layout.py site + python scripts/check-docs-layout.py site --screenshots docs-review test -s site/index.html test "$(cat site/CNAME)" = "python.durable-workflow.com" - - name: Bind Pages artifact to the exact source revision - env: - SDK_VERSION: ${{ steps.release-channel.outputs.version }} + - name: Bind the artifact to both documentation and SDK sources run: | - SOURCE_REVISION=$(git rev-parse HEAD) - export SOURCE_REVISION - python - <<'PY' - import json - import os - from pathlib import Path - - Path("site/deployment-audit.json").write_text(json.dumps({ - "schema": "durable-workflow.sdk-python.docs-deployment/v1", - "source_revision": os.environ["SOURCE_REVISION"], - "sdk_version": os.environ["SDK_VERSION"], - }) + "\n") - PY + cp .docs-provenance.json site/deployment-audit.json + cp .docs-api-inventory.json site/api-source-inventory.json + - name: Retain rendered review screenshots + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: python-portal-review-${{ github.sha }} + path: docs-review + retention-days: 7 - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 if: github.server_url == 'https://github.com' with: diff --git a/.gitignore b/.gitignore index d8b04350..ab8ce130 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,11 @@ dist/ build/ site/ *.egg-info/ + +# Generated documentation source split and provenance +.docs-sdk/ +.docs-stage-*/ +.docs-build.yml +.docs-api-inventory.json +.docs-provenance.json +.docs-requirements.txt diff --git a/docs/index.md b/docs/index.md index 6bd4794b..facbabb9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,12 +53,12 @@ of your application dependencies. A client asks the runtime to do durable work. A worker receives tasks and dispatches them to your workflow and activity code. -
- 01ClientStarts and controls workflow runs from your app.Client API → - 02WorkerPolls a task queue and dispatches registered handlers.Worker API → - 03WorkflowDescribes replay-safe orchestration and durable decisions.Workflow API → - 04ActivityPerforms side effects such as API calls and database work.Activity API → -
+ @@ -201,14 +201,14 @@ endpoint, credentials, and operating boundary change. Available without an account

Self-hosted Server

Run the published Server image locally, then execute the complete Python journey below.

- Start locally + Start locally
Managed runtime · limited access

Durable Workflow Cloud

Connect the same program to a provisioned namespace with separate client and worker credentials.

- Connect a namespace + Connect a namespace Request early access →

diff --git a/docs/javascripts/status.mjs b/docs/javascripts/status.mjs index 0240ec43..2659eae6 100644 --- a/docs/javascripts/status.mjs +++ b/docs/javascripts/status.mjs @@ -3,10 +3,22 @@ export const STATUS_API = 'https://status.durable-workflow.com/api/current.json' const statuses = new Set(['ok', 'warning', 'failed', 'unknown']); const levels = new Set(['ok', 'warning', 'critical', 'unknown']); const unknown = () => ({state: 'unknown', validUntil: 0}); -const timestamp = value => typeof value === 'string' && /^\d{4}-\d\d-\d\dT.+(?:Z|[+-]\d\d:\d\d)$/.test(value) ? Date.parse(value) : NaN; +function timestamp(value) { + if (typeof value !== 'string') return Number.NaN; + const match = /^(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d)(?:\.\d{1,9})?(Z|([+-])(\d\d):(\d\d))$/.exec(value); + if (!match) return Number.NaN; + const parsed = Date.parse(value); + if (!Number.isFinite(parsed)) return Number.NaN; + const offsetMinutes = match[2] === 'Z' ? 0 : Number(match[4]) * 60 + Number(match[5]); + const offsetSign = match[3] === '-' ? -1 : 1; + // Date.parse normalizes impossible days and 24:00. Round-trip the supplied + // calendar fields in their original offset before accepting the evidence. + const suppliedCalendar = new Date(parsed + offsetMinutes * offsetSign * 60000).toISOString().slice(0, 19); + return suppliedCalendar === match[1] ? parsed : Number.NaN; +} function evidenceWindow(snapshot, now) { - if (!snapshot || snapshot.schema !== 'durable_workflow.status_page_snapshot.v1' + if (snapshot?.schema !== 'durable_workflow.status_page_snapshot.v1' || snapshot.product !== 'cloud' || snapshot.environment !== 'prod' || !Number.isFinite(now)) return unknown(); const checked = timestamp(snapshot.checked_at); const generated = timestamp(snapshot.generated_at); @@ -31,15 +43,15 @@ function aggregateState(snapshot, validUntil) { if (reason === 'monitor_stale' || reason === 'probe_unverified') return {state: 'unverified', validUntil}; if (reason === 'publisher_stale') return {state: 'stale', validUntil: 0}; if (reason !== undefined && reason !== null && reason !== '') return unknown(); - const stateTokens = [snapshot.status, impact.status]; - const levelTokens = [snapshot.alert_level, impact.alert_level]; - if (stateTokens.includes('failed') || levelTokens.includes('critical')) return {state: 'incident', validUntil}; - if (stateTokens.includes('warning') || levelTokens.includes('warning')) { + const stateTokens = new Set([snapshot.status, impact.status]); + const levelTokens = new Set([snapshot.alert_level, impact.alert_level]); + if (stateTokens.has('failed') || levelTokens.has('critical')) return {state: 'incident', validUntil}; + if (stateTokens.has('warning') || levelTokens.has('warning')) { const families = impact.affected_families; const maintenanceOnly = Array.isArray(families) && families.length === 1 && families[0] === 'maintenance'; return {state: maintenanceOnly ? 'maintenance' : 'degraded', validUntil}; } - if (stateTokens.includes('unknown') || levelTokens.includes('unknown')) return {state: 'unknown', validUntil}; + if (stateTokens.has('unknown') || levelTokens.has('unknown')) return {state: 'unknown', validUntil}; return {state: 'operational', validUntil}; } @@ -87,7 +99,7 @@ export function watchPublicStatus(onChange, {fetchImpl = globalThis.fetch, now = if (!stopped) timer = setTimeout(refresh, 60000); } }; - refresh(); + void refresh(); return () => { stopped = true; controller?.abort(); diff --git a/docs/stylesheets/layout.css b/docs/stylesheets/layout.css index d3812496..f776cdf4 100644 --- a/docs/stylesheets/layout.css +++ b/docs/stylesheets/layout.css @@ -7,6 +7,7 @@ --dw-code: var(--dw-code-background); --dw-code-ink: var(--dw-code-foreground); } + [data-md-color-scheme="slate"] { color-scheme: dark; --dw-paper: #0f1513; @@ -19,15 +20,10 @@ --dw-tint: #123b2a; --dw-on-action: #0f1513; } -@media screen and (max-width: 44.984375em) { - .md-header__topic { - font-size: 0.7rem; - } -} +/* Structural primitives; visual values come from the shared role tokens. */ .dw-landing { color: var(--dw-ink); - margin-top: 0.75rem; } .dw-landing * { @@ -35,937 +31,1161 @@ } .dw-landing a { - text-underline-offset: 0.18em; + text-underline-offset: .18em; } .dw-hero { display: grid; - gap: 1.5rem; - grid-template-columns: minmax(0, 1.3fr) minmax(18rem, 0.7fr); - padding: 1.5rem 0 1.75rem; -} - -.dw-eyebrow, -.dw-tag { - color: var(--dw-blue); - display: block; - font-size: 0.68rem; - font-weight: 750; - letter-spacing: 0; - margin: 0 0 0.8rem; - text-transform: uppercase; -} - -.dw-hero h1 { - color: var(--dw-ink); - font-size: 3.2rem; - letter-spacing: 0; - line-height: 0.98; - margin: 0; - max-width: 13ch; -} - -.dw-hero__copy > p:not(.dw-eyebrow, .dw-hero__facts) { - color: var(--dw-muted); - font-size: 1.02rem; - line-height: 1.65; - margin: 1.25rem 0 0; - max-width: 35rem; } -.dw-hero__actions { - align-items: center; +.dw-hero__actions, .dw-inline-actions, .dw-runtime-card__actions { display: flex; + align-items: center; flex-wrap: wrap; - gap: 0.65rem; - margin-top: 1.55rem; } .dw-button { - align-items: center; - border: 1px solid var(--dw-blue); - border-radius: 0.5rem; display: inline-flex; - font-size: 0.74rem; - font-weight: 720; + align-items: center; justify-content: center; - min-height: 2.55rem; - padding: 0.58rem 0.7rem; + border: 1px solid var(--dw-line); text-decoration: none; } -.dw-button--primary { - background: var(--dw-blue); - color: #fff !important; +.dw-text-link { + display: inline-flex; + align-items: center; + color: var(--dw-action); + text-decoration: none; } -[data-md-color-scheme="slate"] .dw-button--primary { - color: #111827 !important; +.dw-hero__facts { + display: flex; + flex-wrap: wrap; } -.dw-button--secondary { - background: var(--dw-panel-raised); - color: var(--dw-blue-deep) !important; +.dw-install-card { + align-self: center; + min-width: 0; + background: var(--dw-code-background); + color: var(--dw-code-foreground); } -.dw-text-link { - color: var(--dw-blue-deep) !important; - font-size: 0.72rem; - font-weight: 700; - padding: 0.4rem 0.2rem; +.dw-install-card__links { + display: flex; + justify-content: space-between; } -.dw-button:hover, -.dw-button:focus-visible, -.dw-role-card:hover, -.dw-role-card:focus-visible, -.dw-link-card:hover, -.dw-link-card:focus-visible { - border-color: var(--dw-blue-deep); - box-shadow: 0 0 0 0.12rem var(--md-default-bg-color), 0 0 0 0.25rem var(--dw-blue); +.dw-role-grid, .dw-link-grid, .dw-runtime-grid { + display: grid; + gap: 16px; } -.dw-button:focus-visible, -.dw-text-link:focus-visible, -.dw-role-card:focus-visible, -.dw-link-card:focus-visible, -.dw-cloud-promotion__action:focus-visible { - outline: none; +.dw-link-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); } -.dw-hero__facts { - color: var(--dw-muted); - display: flex; - flex-wrap: wrap; - font-size: 0.68rem; - font-weight: 650; - gap: 0.45rem 1rem; - margin: 1.3rem 0 0; +.dw-runtime-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); } -.dw-hero__facts span::before { - background: var(--dw-blue); - border-radius: 50%; - content: ""; - display: inline-block; - height: 0.28rem; - margin-right: 0.35rem; - vertical-align: 0.08rem; - width: 0.28rem; +.dw-role-card, .dw-link-card, .dw-runtime-card { + display: flex; + flex-direction: column; + min-width: 0; + text-decoration: none; } -.dw-install-card { - align-self: center; - background: var(--dw-code); - border: 1px solid rgb(255 255 255 / 12%); - border-radius: 0.4rem; - box-shadow: 0 1.2rem 2.8rem rgb(20 31 60 / 15%); - color: var(--dw-code-ink); - min-width: 0; - overflow: hidden; - padding: 1.1rem; +.dw-role-card__link, .dw-link-card > span:last-child { + margin-top: auto; + padding-top: 12px; + color: var(--dw-action); + font-size: 13px; + font-weight: 600; } -.dw-install-card h2 { - color: var(--dw-code-ink); - font-size: 0.72rem; - letter-spacing: 0; - margin: 0 0 0.7rem; - text-transform: uppercase; +.dw-link-card { + gap: 8px; } -.dw-install-card .highlight { - margin: 0; +.dw-link-card strong { + font-size: 16px; + color: var(--dw-ink); } -.dw-install-card .highlight pre { - background: rgb(255 255 255 / 6%); - border: 1px solid rgb(255 255 255 / 9%); - border-radius: 0.4rem; - color: var(--dw-code-ink); - font-size: 0.68rem; - max-width: 100%; - overflow-x: auto; +.dw-link-card > span:nth-child(2) { + font-size: 14px; + color: var(--dw-ink-2); } -.dw-install-card > p:not(.dw-install-card__links) { - color: #aebad0; - font-size: 0.7rem; - line-height: 1.55; - margin: 0.85rem 0 0; +.dw-first-run > h3 { + font-size: 24px; + margin: 32px 0 12px; } -.dw-install-card__links { - display: flex; - flex-wrap: wrap; - gap: 0.8rem; - margin: 0.8rem 0 0; +.dw-first-run h3 code { + background: transparent; + color: inherit; + font: inherit; } -.dw-install-card__links a { - color: var(--dw-yellow) !important; - font-size: 0.68rem; - font-weight: 700; +.dw-runtime-card { + gap: 12px; } -.dw-section { - border-top: 1px solid var(--dw-line); - margin-top: 2.5rem; - padding-top: 2rem; +.dw-runtime-card .dw-text-link { + align-self: flex-start; } -.dw-section > h2 { - color: var(--dw-ink); - font-size: 2.2rem; - letter-spacing: 0; - line-height: 1.08; - margin: 0; +.dw-runtime-card__actions { + gap: 12px; + margin-top: auto; } -.dw-section > h2 + p { - color: var(--dw-muted); - line-height: 1.7; - margin: 0.75rem 0 1.5rem; - max-width: 42rem; +.dw-cloud-promotion__action { + color: var(--dw-action); + font-size: 13px; + font-weight: 600; + min-height: 44px; + display: inline-flex; + align-items: center; } -.dw-role-grid, -.dw-link-grid { +.dw-credential-grid { display: grid; - gap: 0.75rem; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + list-style: none; + padding: 0; + margin: 0 0 24px; } -.dw-role-card, -.dw-link-card { - background: var(--dw-panel-raised); - border: 1px solid var(--dw-line); - border-radius: 0.4rem; - color: var(--dw-ink) !important; +.dw-credential-grid li { display: flex; flex-direction: column; + gap: 8px; min-width: 0; - padding: 1rem; - text-decoration: none; } -.dw-role-card { - min-height: 10rem; +.dw-credential-grid strong { + font-size: 16px; } -.dw-role-card__step { - color: var(--dw-blue); - font-family: var(--md-code-font-family); - font-size: 0.62rem; - font-weight: 700; +.dw-credential-grid span { + font-size: 14px; + color: var(--dw-ink-2); } -.dw-role-card strong, -.dw-link-card strong { - font-size: 0.9rem; - margin: 0.65rem 0 0.35rem; +.dw-credential-grid code { + overflow-wrap: anywhere; } -.dw-role-card > span:not(.dw-role-card__step, .dw-role-card__link), -.dw-link-card > span:nth-child(2) { - color: var(--dw-muted); - font-size: 0.7rem; - line-height: 1.5; +.dw-inline-actions { + gap: 12px; + margin-top: 20px; } -.dw-role-card__link, -.dw-link-card > span:last-child { - color: var(--dw-blue-deep); - font-size: 0.68rem; - font-weight: 700; - margin-top: auto; - padding-top: 0.9rem; +.dw-checkpoint > p { + color: var(--dw-ink-2); + font-size: 14px; } -.dw-first-run { - counter-reset: local-step; +@media (prefers-reduced-motion: reduce) { + .dw-landing *, + .dw-landing *::before, + .dw-landing *::after { + scroll-behavior: auto !important; + transition: none !important; + } } -.dw-first-run > h3 { - color: var(--dw-ink); - counter-increment: local-step; - font-size: 1.05rem; - margin: 2.2rem 0 0.7rem; +@media screen and (max-width: 59.984375em) { + .md-sidebar--primary .md-nav__list.dw-navigation__ancestor-list { + overflow: visible; + } + .md-nav--primary > .dw-navigation__close { + display: flex; + } + .md-sidebar--primary .md-source__repository { + font-size: 0.6rem; + } + #__search:checked ~ header .md-search__overlay { + border-radius: 0; + height: 100dvh; + inset: 0; + position: fixed; + transform: none; + width: 100vw; + } + #__search:checked ~ .md-container { + visibility: hidden; + } +} + +.md-header__button[role="button"]:focus-visible, +.dw-navigation__close:focus-visible, +.md-search__icon[role="button"]:focus-visible { + border-radius: 0.1rem; + outline: 0.15rem solid currentColor; + outline-offset: 0.1rem; } -.dw-first-run > h3::before { +.dw-navigation__close { align-items: center; - background: var(--dw-blue); + background-color: var(--md-primary-bg-color); + border: 0; border-radius: 50%; - color: #fff; - content: counter(local-step); - display: inline-flex; - font-size: 0.65rem; - height: 1.55rem; + color: var(--md-primary-fg-color); + cursor: pointer; + display: none; + height: 2rem; justify-content: center; - margin-right: 0.55rem; - vertical-align: 0.1rem; - width: 1.55rem; + position: absolute; + right: 0.4rem; + top: 0.4rem; + width: 2rem; + z-index: 2; } -.dw-first-run > p, -.dw-version > p { - color: var(--dw-muted); - line-height: 1.7; - max-width: 45rem; +.dw-navigation__close svg { + height: 1.2rem; + width: 1.2rem; } -.dw-first-run .highlight { - border: 1px solid var(--dw-line); - border-radius: 0.4rem; - max-width: 100%; - overflow: hidden; +.dw-navigation__close:focus-visible { + border-radius: 50%; } -.dw-first-run .highlight pre { - max-width: 100%; - overflow-x: auto; +#__search:checked ~ header .md-search__icon[for="__search"][role="button"] { + pointer-events: auto; } -.dw-runtime-grid { - display: grid; - gap: 0.9rem; - grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); +#__search:checked ~ header .md-search__icon[for="__search"][role="button"] svg:first-child { + display: none; } -.dw-runtime-card { - border: 1px solid var(--dw-line); - border-radius: 0.4rem; - display: flex; - flex-direction: column; - min-width: 0; - padding: 1.25rem; +#__search:checked ~ header .md-search__icon[for="__search"][role="button"] svg:last-child { + display: block; } -.dw-runtime-card--primary { - background: var(--dw-panel); - border-left: 0.25rem solid var(--dw-blue); +/* Keep result-group expanders in flow inside the modal instead of floating + above unrelated inert document content at the overlay boundary. */ +.md-search-result__more > summary { + position: static; } -.dw-runtime-card--secondary { - background: transparent; - border-style: dashed; +#__search:checked ~ header [inert] { + visibility: hidden; } -.dw-runtime-card h3, -.dw-runtime-card p { - margin: 0; +/* One Identity: shared chrome and Python documentation components. */ +:root, [data-md-color-scheme="default"], [data-md-color-scheme="slate"] { + --md-text-font: var(--dw-font-body); + --md-code-font: var(--dw-font-mono); + --md-default-fg-color: var(--dw-ink); + --md-default-fg-color--light: var(--dw-muted); + --md-default-bg-color: var(--dw-paper); + --md-primary-fg-color: var(--dw-action); + --md-primary-bg-color: var(--dw-on-action); + --md-accent-fg-color: var(--dw-action); + --md-code-bg-color: var(--dw-code-background); + --md-code-fg-color: var(--dw-code-foreground); + --md-code-hl-color: #123b2a; + --md-code-hl-number-color: #e8edea; + --md-code-hl-special-color: #e8edea; + --md-code-hl-function-color: #e8edea; + --md-code-hl-constant-color: #e8edea; + --md-code-hl-keyword-color: #5fe0a6; + --md-code-hl-string-color: #5fe0a6; + --md-code-hl-name-color: #e8edea; + --md-code-hl-operator-color: #e8edea; + --md-code-hl-punctuation-color: #e8edea; + --md-code-hl-comment-color: #9aa6a1; + --md-code-hl-generic-color: #9aa6a1; + --md-code-hl-variable-color: #e8edea; } -.dw-runtime-card h3 { +body, .md-main { + background: var(--dw-paper); color: var(--dw-ink); - font-size: 1.1rem; } -.dw-runtime-card p { - color: var(--dw-muted); - font-size: 0.74rem; - line-height: 1.6; - margin: 0.55rem 0 1rem; +body { + font-family: var(--dw-font-body); } -.dw-runtime-card .dw-button { - align-self: flex-start; - margin-top: auto; +.md-grid { + max-width: 1200px; } -.dw-runtime-card__actions { - align-items: center; - display: flex; - flex-wrap: wrap; - gap: 0.55rem 0.85rem; - margin-top: auto; +.md-typeset { + font-size: 16px; + line-height: 1.5; } -.dw-runtime-card__actions .dw-button, -.dw-runtime-card__actions .dw-cloud-promotion__action { - margin-top: 0; +.md-typeset h1, .md-typeset h2, .md-typeset h3 { + color: var(--dw-ink); + font-family: var(--dw-font-display); + font-weight: 600; + letter-spacing: -.03em; + scroll-margin-top: 150px; } -.dw-cloud-promotion__eyebrow { - letter-spacing: 0; +.md-typeset a { + color: var(--dw-action); } -.dw-cloud-promotion__action { - align-self: flex-start; - color: var(--dw-blue-deep) !important; - font-size: 0.72rem; - font-weight: 700; - margin-top: auto; - padding: 0.3rem 0; +a, button, summary, input { + -webkit-tap-highlight-color: transparent; } -.dw-cloud-promotion__action:hover, -.dw-cloud-promotion__action:focus-visible { - box-shadow: 0 0.15rem 0 var(--dw-blue); +a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible { + outline: 2px solid var(--dw-action); + outline-offset: 4px; } -.dw-checkpoint { - background: var(--dw-panel); - border: 1px solid var(--dw-line); - border-left: 0.25rem solid var(--dw-blue); - border-radius: 0.4rem; - margin-top: 1.25rem; - padding: 0.9rem 1rem; +.dw-header { + color: var(--dw-ink); + background: var(--dw-surface); + box-shadow: none; } -.dw-checkpoint > p { - color: var(--dw-muted); - font-size: 0.74rem; - line-height: 1.6; - margin: 0 0 0.7rem; +.dw-global-bar { + display: flex; + align-items: center; + gap: 32px; + min-height: 76px; + padding: 12px 24px; } -.dw-checkpoint .highlight { - margin: 0; +.dw-brand { + display: inline-flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + color: var(--dw-ink); + text-decoration: none; } -.dw-cloud-path > p { - color: var(--dw-muted); - line-height: 1.7; - max-width: 48rem; +.dw-brand__mark { + display: block; + object-fit: contain; + width: 30px; + height: 30px; } -.dw-cloud-path > h3 { - color: var(--dw-ink); - font-size: 1.05rem; - margin: 1.8rem 0 0.8rem; +[data-md-color-scheme="slate"] .dw-brand__mark { + box-sizing: content-box; + padding: 4px; + border-radius: 8px; + background: var(--dw-logo-paper); } -.dw-cloud-path .highlight { - border: 1px solid var(--dw-line); - border-radius: 0.4rem; - max-width: 100%; - overflow: hidden; +.dw-brand__name { + font-family: var(--dw-font-display); + font-weight: 600; + font-size: 17px; + white-space: nowrap; } -.dw-cloud-path .highlight pre { - max-width: 100%; - overflow-x: auto; +.dw-brand__slash { + color: var(--dw-muted); + font-size: 16px; } -.dw-credential-grid { - display: grid; - gap: 0.75rem; - grid-template-columns: repeat(3, minmax(0, 1fr)); +.dw-brand__surface { + color: var(--dw-action); + font: 500 13px var(--dw-font-mono); +} + +.dw-global-nav { + display: flex; + align-items: center; + flex: 1; + gap: 24px; + font-size: 14px; +} + +.dw-global-nav > a, .dw-sdk-switcher summary { + color: var(--dw-ink-2); + text-decoration: none; + min-height: 44px; + display: flex; + align-items: center; +} + +.dw-sdk-switcher { + position: relative; +} + +.dw-sdk-switcher summary { + cursor: pointer; + gap: 6px; + font-weight: 600; + color: var(--dw-ink); list-style: none; - margin: 0 0 1.25rem; - padding: 0; } -.dw-credential-grid li { - background: var(--dw-panel-raised); +.dw-sdk-switcher summary::-webkit-details-marker { + display: none; +} + +.dw-sdk-switcher ul { + position: absolute; + z-index: 20; + min-width: 160px; + top: 44px; + left: -16px; + list-style: none; + margin: 0; + padding: 8px; border: 1px solid var(--dw-line); - border-radius: 0.4rem; + border-radius: 8px; + background: var(--dw-surface); +} + +.dw-sdk-switcher a { display: flex; - flex-direction: column; - gap: 0.5rem; - min-width: 0; - padding: 1rem; + justify-content: space-between; + padding: 12px; + border-radius: 4px; + color: var(--dw-ink-2); } -.dw-credential-grid strong { +.dw-sdk-switcher a:hover, .dw-sdk-switcher a[aria-current] { + background: var(--dw-tint); + color: var(--dw-action); +} + +.dw-global-actions { + display: flex; + align-items: center; + gap: 20px; + font-size: 14px; +} + +.dw-github-link { + color: var(--dw-ink-2); +} + +.dw-docs-toolbar { + padding: 0 20px; + min-height: 48px; + border-block: 1px solid var(--dw-line); + gap: 12px; +} + +.dw-docs-toolbar__title { + font-size: 13px; + color: var(--dw-ink-2); +} + +.dw-channel-label { + font: 400 11px var(--dw-font-mono); + color: var(--dw-muted); +} + +.dw-docs-toolbar__controls { + margin-left: auto; + display: flex; + align-items: center; +} + +.dw-docs-toolbar .md-header__button { + width: 40px; + height: 44px; + padding: 10px; + margin: 0; + color: var(--dw-ink-2); +} + +.dw-docs-toolbar .md-search__form { + background: var(--dw-paper); + border: 1px solid var(--dw-line); color: var(--dw-ink); - font-size: 0.82rem; } -.dw-credential-grid span { +.dw-docs-toolbar .md-search__input { + color: var(--dw-ink); +} + +.dw-docs-toolbar .md-search__input::placeholder { color: var(--dw-muted); - font-size: 0.7rem; - line-height: 1.55; } -.dw-credential-grid code { - overflow-wrap: anywhere; +.md-sidebar { + color: var(--dw-ink-2); } -.dw-inline-actions { - align-items: center; +.md-nav__link--active { + color: var(--dw-action); +} + +@media (min-width: 1220px) { + body:has(.dw-landing) .md-sidebar--primary { + display: none; + } +} + +body:has(.dw-landing) .md-main__inner { + margin-top: 0; +} + +body:has(.dw-landing) .md-content__inner { + margin: 0 24px; + padding: 0; +} + +.dw-landing { display: flex; - flex-wrap: wrap; - gap: 0.65rem 1rem; - margin-top: 1.2rem; + flex-direction: column; + gap: 88px; + padding: 72px 0 96px; + margin: 0; } -.dw-inline-actions .dw-button, -.dw-inline-actions .dw-text-link { - width: auto; +.dw-hero { + padding: 0; + gap: 48px; + align-items: center; + grid-template-columns: minmax(0, 1fr) 340px; } -.dw-link-card { - min-height: 9rem; +.dw-eyebrow, .dw-tag { + color: var(--dw-action); + font: 500 12px var(--dw-font-mono); + letter-spacing: .08em; + line-height: 1.5; + margin-bottom: 22px; } -.dw-version { - background: var(--dw-panel); +.dw-hero h1 { + font-size: 64px; + line-height: 1; + letter-spacing: -.03em; + max-width: none; +} + +.dw-hero__copy > p:not(.dw-eyebrow, .dw-hero__facts) { + color: var(--dw-ink-2); + font-size: 19px; + line-height: 1.55; + margin: 22px 0 0; + max-width: 580px; +} + +.dw-hero__actions { + gap: 12px; + margin-top: 22px; +} + +.dw-button, .dw-text-link { + min-height: 44px; + font-size: 15px; + font-weight: 600; +} + +.dw-button { + border-radius: 6px; + padding: 0 20px; + border-color: var(--dw-line); +} + +.dw-button--primary { + background: var(--dw-action); + border-color: var(--dw-action); + color: var(--dw-on-action) !important; +} + +[data-md-color-scheme="slate"] .dw-button--primary { + color: var(--dw-on-action) !important; +} + +.dw-button--secondary { + color: var(--dw-ink) !important; + background: var(--dw-surface); +} + +.dw-button:hover, .dw-role-card:hover, .dw-link-card:hover { + box-shadow: none; + border-color: var(--dw-action); +} + +.dw-text-link { + padding: 10px 8px; +} + +.dw-hero__facts { + font: 400 13px var(--dw-font-mono); + color: var(--dw-muted); + margin-top: 22px; + gap: 8px 20px; +} + +.dw-hero__facts span::before { + display: none; +} + +.dw-install-card { + border: none; + border-radius: 8px; + box-shadow: none; + padding: 0; + overflow: hidden; +} + +.dw-install-card h2 { + display: none; +} + +.dw-install-card > p:not(.dw-install-card__links) { + padding: 0 16px 18px; + font-size: 13px; + line-height: 1.55; + color: var(--dw-code-muted); +} + +.dw-install-card__links { + padding: 14px 16px; + margin: 0; + border-top: 1px solid #26302c; + font-size: 13px; + gap: 20px; +} + +.dw-install-card__links a { + color: #5fe0a6 !important; +} + +.dw-section { + margin: 0; + padding: 0; + border: 0; +} + +.dw-section > h2 { + margin: 0 0 8px; + font-size: 36px; +} + +.dw-section > p { + color: var(--dw-ink-2); + margin: 0 0 24px; + line-height: 1.5; +} + +.dw-role-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 16px; +} + +.dw-role-card, .dw-link-card, .dw-runtime-card, .dw-credential-grid li { + background: var(--dw-surface); border: 1px solid var(--dw-line); - border-radius: 0.4rem; - margin-bottom: 2rem; - padding: 1.2rem; + border-radius: 8px; + box-shadow: none; + padding: 20px; } -.dw-version > h2 { - font-size: 1rem; +.dw-role-card { + min-height: 190px; + gap: 8px; } -.dw-version > p { - font-size: 0.74rem; - margin-bottom: 0; +.dw-role-card__step { + color: var(--dw-action); + background: none; + padding: 0; + font: 400 12px var(--dw-font-mono); } -@media screen and (max-width: 76rem) { - .dw-role-grid, - .dw-link-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } +.dw-role-card strong { + color: var(--dw-ink); + font: 600 20px var(--dw-font-display); +} - .dw-credential-grid { - grid-template-columns: 1fr; - } +.dw-role-card > span:not(.dw-role-card__step, .dw-role-card__link) { + color: var(--dw-ink-2); + font-size: 14px; + line-height: 1.5; } -@media screen and (max-width: 64rem) { - .dw-hero { - gap: 1.75rem; - grid-template-columns: 1fr; - padding-block: 1.75rem 2.25rem; - } +.dw-role-card__link { + font-size: 13px; + font-weight: 600; + color: var(--dw-action); +} - .dw-hero h1 { - font-size: 2.75rem; - } +.dw-quickstart-grid { + display: grid; + grid-template-columns: minmax(0, .7fr) minmax(0, 1.3fr); + gap: 48px; + align-items: start; + margin-block: 64px; +} - .dw-install-card { - width: 100%; - } +.dw-quickstart-copy h3 { + font-size: 32px; + margin: 0 0 16px; } -@media screen and (max-width: 44.984375em) { - body:has(.dw-landing) .md-main__inner, - body:has(.dw-landing) .md-content__inner { - margin-top: 0; - } +.dw-quickstart-copy p { + color: var(--dw-ink-2); + font-size: 16px; +} - .dw-landing { - margin-top: 0; - } +.dw-quickstart-copy .dw-eyebrow { + color: var(--dw-action); + font-size: 12px; +} + +.dw-quickstart-code { + min-width: 0; +} + +.dw-runtime-grid { + gap: 16px; +} + +.dw-runtime-card { + border-top: 1px solid var(--dw-line); + min-height: 220px; +} + +.dw-runtime-card h3 { + font: 600 20px var(--dw-font-display); + margin: 0; +} + +.dw-runtime-card p { + color: var(--dw-ink-2); + font-size: 14px; + line-height: 1.5; +} + +.dw-runtime-card .dw-tag { + font-size: 11px; + margin-bottom: 12px; +} + +.dw-runtime-card--secondary .dw-tag { + color: var(--dw-muted); +} + +.dw-checkpoint, .dw-version { + background: var(--dw-surface); + border: 1px solid var(--dw-line); + border-radius: 8px; +} + +.dw-checkpoint { + padding: 16px; +} + +.dw-version { + margin: 0; + padding: 20px; +} + +.md-typeset pre, .md-typeset code { + font-family: var(--dw-font-mono); +} + +.md-typeset pre { + background: var(--dw-code-background); + color: var(--dw-code-foreground); +} + +.dw-landing .highlight { + position: relative; + background: var(--dw-code-background); + border: 1px solid #26302c; + border-radius: 8px; + overflow: hidden; +} + +.dw-landing .highlight::before { + content: "terminal"; + display: block; + border-bottom: 1px solid #26302c; + padding: 12px 16px; + color: var(--dw-code-muted); + font: 400 12px var(--dw-font-mono); +} + +.dw-install-card .highlight::before { + content: "INSTALL"; + letter-spacing: .08em; +} + +.dw-quickstart-code .highlight::before { + content: "greeter.py"; +} + +.dw-checkpoint .highlight::before { + content: "output"; +} + +.dw-landing .highlight pre { + margin: 0; + max-width: 100%; +} + +.dw-landing .highlight pre code { + padding: 18px 16px; + background: var(--dw-code-background); + color: var(--dw-code-foreground); + font-size: 13px; + line-height: 1.75; + white-space: pre; + overflow-x: auto; +} + +.dw-quickstart-code .highlight pre code { + max-height: 640px; + overflow: auto; +} + +.dw-landing button[data-clipboard-target] { + top: 8px; + right: 8px; + color: var(--dw-code-muted); + border: 1px solid #2e3934; + border-radius: 4px; + opacity: 1; +} + +.dw-footer { + background: var(--dw-surface); + border-top: 1px solid var(--dw-line); +} + +.dw-footer__inner { + padding: 48px 24px 28px; +} + +.dw-footer__columns { + display: grid; + grid-template-columns: 1.5fr 1fr 1fr 1fr; + gap: 48px; +} + +.dw-footer__brand p { + max-width: 270px; + color: var(--dw-muted); + font-size: 13px; + line-height: 1.5; +} + +.dw-footer nav { + display: flex; + flex-direction: column; + gap: 12px; + font-size: 13px; +} + +.dw-footer h2 { + margin: 0 0 4px; + font: 400 11px var(--dw-font-mono); + letter-spacing: .08em; + text-transform: uppercase; + color: var(--dw-muted); +} + +.dw-footer nav a { + color: var(--dw-ink-2); +} + +.dw-footer__bottom { + border-top: 1px solid var(--dw-line); + margin-top: 40px; + padding-top: 24px; + display: flex; + justify-content: space-between; + gap: 16px; + color: var(--dw-muted); + font-size: 12px; +} + +.dw-status-pill { + display: inline-flex; + align-items: center; + gap: 6px; + background: var(--dw-paper); + color: var(--dw-muted); + padding: 5px 9px; + border-radius: 20px; + font: 400 11px var(--dw-font-mono); +} + +.dw-status-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background: currentColor; +} +@media (max-width: 1100px) { + .dw-global-bar { + flex-wrap: wrap; + gap: 8px 24px; + } + .dw-global-nav { + order: 3; + flex-basis: 100%; + } + .dw-global-actions { + margin-left: auto; + } .dw-hero { - gap: 0.75rem; - padding-block: 0.75rem 0.5rem; + grid-template-columns: minmax(0, 1fr) minmax(300px, .8fr); + gap: 32px; } - .dw-hero h1 { - font-size: 1.65rem; - line-height: 1.05; - max-width: none; - } - - .dw-eyebrow { - margin-bottom: 0.45rem; + font-size: 56px; } +} - .dw-hero__copy > p:not(.dw-eyebrow, .dw-hero__facts) { - font-size: 0.82rem; - line-height: 1.45; - margin-top: 0.65rem; +@media (max-width: 760px) { + .dw-global-bar { + padding: 12px 20px 0; + gap: 4px 12px; } - - .dw-hero__actions { - display: grid; - gap: 0.4rem; - grid-template-columns: repeat(2, minmax(0, 1fr)); - margin-top: 0.8rem; + .dw-brand { + gap: 7px; } - - .dw-hero__actions .dw-button--primary { - grid-column: 1 / -1; + .dw-brand__name { + font-size: 14px; } - - .dw-hero__actions .dw-button, - .dw-hero__actions .dw-text-link { - font-size: 0.66rem; - min-height: 2.15rem; - padding: 0.4rem; + .dw-brand__surface { + font-size: 11px; } - - .dw-hero__facts { - gap: 0.35rem 0.7rem; - margin-top: 0.65rem; + .dw-brand__mark { + width: 26px; + height: 26px; } - - .dw-install-card { - padding: 0.75rem; + .dw-global-actions { + gap: 0; } - - .dw-install-card h2 { - margin-bottom: 0.4rem; + .dw-global-actions .dw-button { + display: inline-flex; + font-size: 13px; + padding: 0 12px; } - - .dw-install-card > p:not(.dw-install-card__links) { - font-size: 0.65rem; - line-height: 1.4; - margin-top: 0.55rem; + .dw-github-link { + display: none; } - - .dw-install-card__links { - flex-direction: row; + .dw-global-nav { + gap: 22px; + font-size: 13px; justify-content: space-between; - margin-top: 0.55rem; } - - .dw-section { - margin-top: 2.75rem; - padding-top: 2rem; + .dw-docs-toolbar { + padding: 0 12px; + gap: 8px; } - - .dw-model { - margin-top: 0.75rem; - padding-top: 1rem; + .dw-docs-toolbar__title { + font-size: 12px; } - - .dw-section > h2 { - font-size: 1.65rem; + .dw-channel-label { + font-size: 10px; } - - .dw-install-card__links { - align-items: stretch; + body:has(.dw-landing) .md-content__inner { + margin: 0 20px; } - - .dw-button, - .dw-text-link { - text-align: center; - width: 100%; + .dw-landing { + padding: 48px 0 64px; + gap: 56px; } - - .dw-role-grid, - .dw-link-grid, - .dw-runtime-grid { + .dw-hero { grid-template-columns: 1fr; + gap: 32px; } - - .dw-runtime-card__actions, - .dw-inline-actions { + .dw-hero h1 { + font-size: clamp(40px, 10.7vw, 56px); + max-width: 12ch; + } + .dw-hero__copy > p:not(.dw-eyebrow, .dw-hero__facts) { + font-size: 18px; + } + .dw-hero__actions { align-items: stretch; - flex-direction: column; } - - .dw-runtime-card__actions .dw-button, - .dw-runtime-card__actions .dw-cloud-promotion__action, - .dw-inline-actions .dw-button, - .dw-inline-actions .dw-text-link { - text-align: center; + .dw-hero__actions .dw-button, .dw-hero__actions .dw-text-link { + width: auto; + } + .dw-install-card { width: 100%; } - - .dw-role-card, - .dw-link-card { - min-height: 0; + .dw-role-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); } - - .dw-first-run .highlight pre, - .dw-install-card .highlight pre { - font-size: 0.66rem; + .dw-section > h2 { + font-size: 32px; + } + .dw-role-card { + padding: 16px; + min-height: 205px; + } + .dw-quickstart-grid { + grid-template-columns: 1fr; + gap: 24px; + margin-block: 48px; + } + .dw-runtime-grid, .dw-link-grid, .dw-credential-grid { + grid-template-columns: 1fr; + } + .dw-runtime-card, .dw-role-card, .dw-link-card { + box-shadow: none; + } + .dw-footer__columns { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 32px; + } + .dw-footer__brand { + grid-column: 1 / -1; + } + .dw-footer__bottom { + flex-wrap: wrap; } } -@media screen and (min-width: 32rem) and (max-width: 44.984375em) and (max-height: 24rem) { - .dw-hero__actions { - grid-template-columns: repeat(3, minmax(0, 1fr)); +@media (max-width: 370px) { + .dw-global-bar { + padding-inline: 16px; } - - .dw-hero__actions .dw-button--primary { - grid-column: auto; + .dw-brand__name { + font-size: 13px; + } + .dw-brand { + gap: 5px; + } + .dw-github-link { + font-size: 11px; + } + .dw-docs-toolbar__title { + font-size: 11px; + } + .dw-channel-label { + display: none; + } + .dw-role-grid { + grid-template-columns: 1fr; } } -@media (prefers-reduced-motion: reduce) { - .dw-landing *, - .dw-landing *::before, - .dw-landing *::after { - scroll-behavior: auto !important; - transition: none !important; - } +.dw-status-pill[data-public-status="operational"] { + color: var(--dw-action); + background: var(--dw-tint); } -@media screen and (max-width: 59.984375em) { - .md-sidebar--primary .md-nav__list.dw-navigation__ancestor-list { - overflow: visible; - } +.dw-status-pill[data-public-status="degraded"] { + color: #8c5600; + background: #fff0d6; +} - .md-nav--primary > .dw-navigation__close { - display: flex; - } +.dw-status-pill[data-public-status="incident"] { + color: #b3261e; + background: #fce8e8; +} - .md-sidebar--primary .md-source__repository { - font-size: 0.6rem; - } +.dw-status-pill[data-public-status="maintenance"] { + color: #3f51b5; + background: #e7edff; +} - #__search:checked ~ header .md-search__overlay { - border-radius: 0; - height: 100dvh; - inset: 0; - position: fixed; - transform: none; - width: 100vw; - } +[data-md-color-scheme="slate"] .dw-status-pill[data-public-status="degraded"] { + color: #f2c46d; + background: #3b2d17; +} - #__search:checked ~ .md-container { - visibility: hidden; - } +[data-md-color-scheme="slate"] .dw-status-pill[data-public-status="incident"] { + color: #ffaaa5; + background: #3c211f; } -.md-header__button[role="button"]:focus-visible, -.dw-navigation__close:focus-visible, -.md-search__icon[role="button"]:focus-visible { - border-radius: 0.1rem; - outline: 0.15rem solid currentColor; - outline-offset: 0.1rem; +[data-md-color-scheme="slate"] .dw-status-pill[data-public-status="maintenance"] { + color: #b6c4ff; + background: #242a43; } -.dw-navigation__close { - align-items: center; - background-color: var(--md-primary-bg-color); +.dw-install-card .highlight { border: 0; - border-radius: 50%; - color: var(--md-primary-fg-color); - cursor: pointer; - display: none; - height: 2rem; - justify-content: center; - position: absolute; - right: 0.4rem; - top: 0.4rem; - width: 2rem; - z-index: 2; + border-radius: 0; + margin: 0; } -.dw-navigation__close svg { - height: 1.2rem; - width: 1.2rem; +.dw-install-card .highlight pre code { + font-size: 15px; } -.dw-navigation__close:focus-visible { - border-radius: 50%; +.dw-landing button[data-clipboard-target] { + width: auto; + height: 28px; + padding: 4px 8px; + background: transparent; } -#__search:checked ~ header .md-search__icon[for="__search"][role="button"] { - pointer-events: auto; +.dw-landing button[data-clipboard-target]::after { + content: "Copy"; + display: block; + width: auto; + height: auto; + margin: 0; + font: 400 12px var(--dw-font-mono); + background: none; + mask: none; + -webkit-mask: none; } -#__search:checked ~ header .md-search__icon[for="__search"][role="button"] svg:first-child { - display: none; +.dw-global-nav a:hover, .dw-github-link:hover, .dw-footer nav a:hover { + color: var(--dw-action); + text-decoration: underline; + text-underline-offset: 3px; } -#__search:checked ~ header .md-search__icon[for="__search"][role="button"] svg:last-child { - display: block; +.md-nav--primary .md-nav__title { + background: var(--dw-surface); + color: var(--dw-ink); + box-shadow: none; +} +[data-md-color-scheme="slate"] .md-logo img { + background: var(--dw-logo-paper); + padding: 4px; + border-radius: 8px; } -/* Keep result-group expanders in flow inside the modal instead of floating - above unrelated inert document content at the overlay boundary. */ -.md-search-result__more > summary { +/* Keep the maintained theme's copy action in the code header in both theme versions. */ +.dw-landing .highlight pre { position: static; } - -#__search:checked ~ header [inert] { - visibility: hidden; +.dw-landing .md-code__nav { + position: absolute; + top: 8px; + right: 8px; } - -/* One Identity: shared chrome and Python documentation components. */ -:root, [data-md-color-scheme="default"], [data-md-color-scheme="slate"] { - --md-text-font: var(--dw-font-body); - --md-code-font: var(--dw-font-mono); - --md-default-fg-color: var(--dw-ink); - --md-default-fg-color--light: var(--dw-muted); - --md-default-bg-color: var(--dw-paper); - --md-primary-fg-color: var(--dw-action); - --md-primary-bg-color: var(--dw-on-action); - --md-accent-fg-color: var(--dw-action); - --md-code-bg-color: var(--dw-code-background); - --md-code-fg-color: var(--dw-code-foreground); - --md-code-hl-color: #123b2a; - --md-code-hl-number-color: #e8edea; - --md-code-hl-special-color: #e8edea; - --md-code-hl-function-color: #e8edea; - --md-code-hl-constant-color: #e8edea; - --md-code-hl-keyword-color: #5fe0a6; - --md-code-hl-string-color: #5fe0a6; - --md-code-hl-name-color: #e8edea; - --md-code-hl-operator-color: #e8edea; - --md-code-hl-punctuation-color: #e8edea; - --md-code-hl-comment-color: #9aa6a1; - --md-code-hl-generic-color: #9aa6a1; - --md-code-hl-variable-color: #e8edea; +.dw-landing button[data-clipboard-target] { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 44px; } -body, .md-main { background: var(--dw-paper); color: var(--dw-ink); } -body { font-family: var(--dw-font-body); } -.md-grid { max-width: 1200px; } -.md-typeset { font-size: 16px; line-height: 1.5; } -.md-typeset h1, .md-typeset h2, .md-typeset h3 { - color: var(--dw-ink); font-family: var(--dw-font-display); font-weight: 600; - letter-spacing: -.03em; scroll-margin-top: 150px; +@media print { + button[data-clipboard-target] { + display: none; + } } -.md-typeset a { color: var(--dw-action); } -a, button, summary, input { -webkit-tap-highlight-color: transparent; } -a:focus-visible, button:focus-visible, summary:focus-visible, input:focus-visible { - outline: 2px solid var(--dw-action); outline-offset: 4px; -} -.dw-header { color: var(--dw-ink); background: var(--dw-surface); box-shadow: none; } -.dw-global-bar { display: flex; align-items: center; gap: 32px; min-height: 76px; padding: 12px 24px; } -.dw-brand { display: inline-flex; align-items: center; gap: 10px; flex-shrink: 0; color: var(--dw-ink); text-decoration: none; } -.dw-brand__mark { display: block; object-fit: contain; width: 30px; height: 30px; } -[data-md-color-scheme="slate"] .dw-brand__mark { box-sizing: content-box; padding: 4px; border-radius: 8px; background: var(--dw-logo-paper); } -.dw-brand__name { font-family: var(--dw-font-display); font-weight: 600; font-size: 17px; white-space: nowrap; } -.dw-brand__slash { color: var(--dw-muted); font-size: 16px; } -.dw-brand__surface { color: var(--dw-action); font: 500 13px var(--dw-font-mono); } -.dw-global-nav { display: flex; align-items: center; flex: 1; gap: 24px; font-size: 14px; } -.dw-global-nav > a, .dw-sdk-switcher summary { color: var(--dw-ink-2); text-decoration: none; min-height: 44px; display: flex; align-items: center; } -.dw-sdk-switcher { position: relative; } -.dw-sdk-switcher summary { cursor: pointer; gap: 6px; font-weight: 600; color: var(--dw-ink); list-style: none; } -.dw-sdk-switcher summary::-webkit-details-marker { display: none; } -.dw-sdk-switcher ul { position: absolute; z-index: 20; min-width: 160px; top: 44px; left: -16px; list-style: none; margin: 0; padding: 8px; border: 1px solid var(--dw-line); border-radius: 8px; background: var(--dw-surface); } -.dw-sdk-switcher a { display: flex; justify-content: space-between; padding: 12px; border-radius: 4px; color: var(--dw-ink-2); } -.dw-sdk-switcher a:hover, .dw-sdk-switcher a[aria-current] { background: var(--dw-tint); color: var(--dw-action); } -.dw-global-actions { display: flex; align-items: center; gap: 20px; font-size: 14px; } -.dw-github-link { color: var(--dw-ink-2); } -.dw-docs-toolbar { padding: 0 20px; min-height: 48px; border-block: 1px solid var(--dw-line); gap: 12px; } -.dw-docs-toolbar__title { font-size: 13px; color: var(--dw-ink-2); } -.dw-channel-label { font: 400 11px var(--dw-font-mono); color: var(--dw-muted); } -.dw-docs-toolbar__controls { margin-left: auto; display: flex; align-items: center; } -.dw-docs-toolbar .md-header__button { width: 40px; height: 44px; padding: 10px; margin: 0; color: var(--dw-ink-2); } -.dw-docs-toolbar .md-search__form { background: var(--dw-paper); border: 1px solid var(--dw-line); color: var(--dw-ink); } -.dw-docs-toolbar .md-search__input { color: var(--dw-ink); } -.dw-docs-toolbar .md-search__input::placeholder { color: var(--dw-muted); } -.md-sidebar { color: var(--dw-ink-2); } -.md-nav__link--active { color: var(--dw-action); } -@media (min-width: 1220px) { - body:has(.dw-landing) .md-sidebar--primary { display: none; } -} -body:has(.dw-landing) .md-main__inner { margin-top: 0; } -body:has(.dw-landing) .md-content__inner { margin: 0 24px; padding: 0; } -.dw-landing { display: flex; flex-direction: column; gap: 88px; padding: 72px 0 96px; margin: 0; } -.dw-hero { padding: 0; gap: 48px; align-items: center; grid-template-columns: minmax(0, 1.6fr) minmax(320px, 1fr); } -.dw-eyebrow, .dw-tag { color: var(--dw-action); font: 500 12px var(--dw-font-mono); letter-spacing: .08em; line-height: 1.5; margin-bottom: 22px; } -.dw-hero h1 { font-size: 64px; line-height: 1; letter-spacing: -.03em; max-width: 12ch; } -.dw-hero__copy > p:not(.dw-eyebrow, .dw-hero__facts) { color: var(--dw-ink-2); font-size: 19px; line-height: 1.55; margin: 22px 0 0; max-width: 580px; } -.dw-hero__actions { gap: 12px; margin-top: 22px; } -.dw-button, .dw-text-link { min-height: 44px; font-size: 15px; font-weight: 600; } -.dw-button { border-radius: 6px; padding: 0 20px; border-color: var(--dw-line); } -.dw-button--primary { background: var(--dw-action); border-color: var(--dw-action); color: var(--dw-on-action) !important; } -[data-md-color-scheme="slate"] .dw-button--primary { color: var(--dw-on-action) !important; } -.dw-button--secondary { color: var(--dw-ink) !important; background: var(--dw-surface); } -.dw-button:hover, .dw-role-card:hover, .dw-link-card:hover { box-shadow: none; border-color: var(--dw-action); } -.dw-text-link { padding: 10px 8px; } -.dw-hero__facts { font: 400 13px var(--dw-font-mono); color: var(--dw-muted); margin-top: 22px; gap: 8px 20px; } -.dw-hero__facts span::before { display: none; } -.dw-install-card { border: none; border-radius: 8px; box-shadow: none; padding: 0; overflow: hidden; } -.dw-install-card h2 { display: none; } -.dw-install-card > p:not(.dw-install-card__links) { padding: 0 16px 18px; font-size: 13px; line-height: 1.55; color: var(--dw-code-muted); } -.dw-install-card__links { padding: 14px 16px; margin: 0; border-top: 1px solid #26302c; font-size: 13px; gap: 20px; } -.dw-install-card__links a { color: #5fe0a6 !important; } -.dw-section { margin: 0; padding: 0; border: 0; } -.dw-section > h2 { margin: 0 0 8px; font-size: 36px; } -.dw-section > p { color: var(--dw-ink-2); margin: 0 0 24px; line-height: 1.5; } -.dw-role-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; } -.dw-role-card, .dw-link-card, .dw-runtime-card, .dw-credential-grid li { - background: var(--dw-surface); border: 1px solid var(--dw-line); border-radius: 8px; box-shadow: none; padding: 20px; -} -.dw-role-card { min-height: 190px; gap: 8px; } -.dw-role-card__step { color: var(--dw-action); background: none; padding: 0; font: 400 12px var(--dw-font-mono); } -.dw-role-card strong { color: var(--dw-ink); font: 600 20px var(--dw-font-display); } -.dw-role-card > span:not(.dw-role-card__step, .dw-role-card__link) { color: var(--dw-ink-2); font-size: 14px; line-height: 1.5; } -.dw-role-card__link { font-size: 13px; font-weight: 600; color: var(--dw-action); } -.dw-quickstart-grid { display: grid; grid-template-columns: minmax(0, .7fr) minmax(0, 1.3fr); gap: 48px; align-items: start; margin-block: 64px; } -.dw-quickstart-copy h3 { font-size: 32px; margin: 0 0 16px; } -.dw-quickstart-copy p { color: var(--dw-ink-2); font-size: 16px; } -.dw-quickstart-copy .dw-eyebrow { color: var(--dw-action); font-size: 12px; } -.dw-quickstart-code { min-width: 0; } -.dw-runtime-grid { gap: 16px; } -.dw-runtime-card { border-top: 1px solid var(--dw-line); min-height: 220px; } -.dw-runtime-card h3 { font: 600 20px var(--dw-font-display); margin: 0; } -.dw-runtime-card p { color: var(--dw-ink-2); font-size: 14px; line-height: 1.5; } -.dw-runtime-card .dw-tag { font-size: 11px; margin-bottom: 12px; } -.dw-runtime-card--secondary .dw-tag { color: var(--dw-muted); } -.dw-checkpoint, .dw-version { background: var(--dw-surface); border: 1px solid var(--dw-line); border-radius: 8px; } -.dw-checkpoint { padding: 16px; } -.dw-version { margin: 0; padding: 20px; } -.md-typeset pre, .md-typeset code { font-family: var(--dw-font-mono); } -.md-typeset pre { background: var(--dw-code-background); color: var(--dw-code-foreground); } -.dw-landing .highlight { position: relative; background: var(--dw-code-background); border: 1px solid #26302c; border-radius: 8px; overflow: hidden; } -.dw-landing .highlight::before { content: "terminal"; display: block; border-bottom: 1px solid #26302c; padding: 12px 16px; color: var(--dw-code-muted); font: 400 12px var(--dw-font-mono); } -.dw-install-card .highlight::before { content: "INSTALL"; letter-spacing: .08em; } -.dw-quickstart-code .highlight::before { content: "greeter.py"; } -.dw-checkpoint .highlight::before { content: "output"; } -.dw-landing .highlight pre { margin: 0; max-width: 100%; } -.dw-landing .highlight pre code { padding: 18px 16px; background: var(--dw-code-background); color: var(--dw-code-foreground); font-size: 13px; line-height: 1.75; white-space: pre; overflow-x: auto; } -.dw-quickstart-code .highlight pre code { max-height: 640px; overflow: auto; } -.dw-landing .md-clipboard { top: 8px; right: 8px; color: var(--dw-code-muted); border: 1px solid #2e3934; border-radius: 4px; opacity: 1; } -.dw-footer { background: var(--dw-surface); border-top: 1px solid var(--dw-line); } -.dw-footer__inner { padding: 48px 24px 28px; } -.dw-footer__columns { display: grid; grid-template-columns: 1.5fr 1fr 1fr 1fr; gap: 48px; } -.dw-footer__brand p { max-width: 270px; color: var(--dw-muted); font-size: 13px; line-height: 1.5; } -.dw-footer nav { display: flex; flex-direction: column; gap: 12px; font-size: 13px; } -.dw-footer h2 { margin: 0 0 4px; font: 400 11px var(--dw-font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--dw-muted); } -.dw-footer nav a { color: var(--dw-ink-2); } -.dw-footer__bottom { border-top: 1px solid var(--dw-line); margin-top: 40px; padding-top: 24px; display: flex; justify-content: space-between; gap: 16px; color: var(--dw-muted); font-size: 12px; } -.dw-status-pill { display: inline-flex; align-items: center; gap: 6px; background: var(--dw-paper); color: var(--dw-muted); padding: 5px 9px; border-radius: 20px; font: 400 11px var(--dw-font-mono); } -.dw-status-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; } -@media (max-width: 1100px) { - .dw-global-bar { flex-wrap: wrap; gap: 8px 24px; } - .dw-global-nav { order: 3; flex-basis: 100%; } - .dw-global-actions { margin-left: auto; } - .dw-hero { grid-template-columns: minmax(0, 1fr) minmax(300px, .8fr); gap: 32px; } - .dw-hero h1 { font-size: 56px; } + +.md-typeset .dw-role-grid { + margin: 0; + padding: 0; + list-style: none; } -@media (max-width: 760px) { - .dw-global-bar { padding: 12px 20px 0; gap: 4px 12px; } - .dw-brand { gap: 7px; } - .dw-brand__name { font-size: 14px; } - .dw-brand__surface { font-size: 11px; } - .dw-brand__mark { width: 26px; height: 26px; } - .dw-global-actions { gap: 0; } - .dw-global-actions .dw-button { display: none; } - .dw-github-link { font-size: 12px; } - .dw-global-nav { gap: 22px; font-size: 13px; justify-content: space-between; } - .dw-docs-toolbar { padding: 0 12px; gap: 8px; } - .dw-docs-toolbar__title { font-size: 12px; } - .dw-channel-label { font-size: 10px; } - body:has(.dw-landing) .md-content__inner { margin: 0 20px; } - .dw-landing { padding: 48px 0 64px; gap: 56px; } - .dw-hero { grid-template-columns: 1fr; gap: 32px; } - .dw-hero h1 { font-size: clamp(40px, 10.7vw, 56px); max-width: 12ch; } - .dw-hero__copy > p:not(.dw-eyebrow, .dw-hero__facts) { font-size: 18px; } - .dw-hero__actions { align-items: stretch; } - .dw-hero__actions .dw-button, .dw-hero__actions .dw-text-link { width: auto; } - .dw-install-card { width: 100%; } - .dw-role-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .dw-section > h2 { font-size: 32px; } - .dw-role-card { padding: 16px; min-height: 205px; } - .dw-quickstart-grid { grid-template-columns: 1fr; gap: 24px; margin-block: 48px; } - .dw-runtime-grid, .dw-link-grid, .dw-credential-grid { grid-template-columns: 1fr; } - .dw-runtime-card, .dw-role-card, .dw-link-card { box-shadow: none; } - .dw-footer__columns { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 32px; } - .dw-footer__brand { grid-column: 1 / -1; } - .dw-footer__bottom { flex-wrap: wrap; } +.md-typeset .dw-role-grid > li { + display: flex; + min-width: 0; + margin: 0; +} +.dw-role-card { + width: 100%; } -@media (max-width: 370px) { - .dw-global-bar { padding-inline: 16px; } - .dw-brand__name { font-size: 13px; } - .dw-brand { gap: 5px; } - .dw-github-link { font-size: 11px; } - .dw-docs-toolbar__title { font-size: 11px; } - .dw-channel-label { display: none; } - .dw-role-grid { grid-template-columns: 1fr; } -} -.dw-status-pill[data-public-status="operational"] { color: var(--dw-action); background: var(--dw-tint); } -.dw-status-pill[data-public-status="degraded"] { color: #8c5600; background: #fff0d6; } -.dw-status-pill[data-public-status="incident"] { color: #b3261e; background: #fce8e8; } -.dw-status-pill[data-public-status="maintenance"] { color: #3f51b5; background: #e7edff; } -[data-md-color-scheme="slate"] .dw-status-pill[data-public-status="degraded"] { color: #f2c46d; background: #3b2d17; } -[data-md-color-scheme="slate"] .dw-status-pill[data-public-status="incident"] { color: #ffaaa5; background: #3c211f; } -[data-md-color-scheme="slate"] .dw-status-pill[data-public-status="maintenance"] { color: #b6c4ff; background: #242a43; } diff --git a/overrides/partials/footer.html b/overrides/partials/footer.html index a8494269..0c148a94 100644 --- a/overrides/partials/footer.html +++ b/overrides/partials/footer.html @@ -10,7 +10,7 @@ - + diff --git a/overrides/partials/header.html b/overrides/partials/header.html index 436c354c..31b66e16 100644 --- a/overrides/partials/header.html +++ b/overrides/partials/header.html @@ -9,7 +9,7 @@
SDKs diff --git a/scripts/check-docs-analytics.py b/scripts/check-docs-analytics.py index c4b2e634..fa953c41 100644 --- a/scripts/check-docs-analytics.py +++ b/scripts/check-docs-analytics.py @@ -13,12 +13,9 @@ build_directory = args.build_directory runtime = Path("docs/javascripts/analytics.js").read_text(encoding="utf-8") -styles = Path("docs/stylesheets/layout.css").read_text(encoding="utf-8") if (build_directory / "javascripts/analytics.js").read_text(encoding="utf-8") != runtime: raise SystemExit("Rendered MkDocs analytics runtime is stale") -if not re.search(r"\.dw-cloud-promotion__eyebrow\s*\{[^}]*letter-spacing:\s*0;", styles, re.DOTALL): - raise SystemExit("Promotion eyebrow letter spacing must remain zero") for required in ( "https://static.cloudflareinsights.com/beacon.min.js", diff --git a/scripts/check-docs-layout.py b/scripts/check-docs-layout.py index 60a44e17..aa6766e7 100644 --- a/scripts/check-docs-layout.py +++ b/scripts/check-docs-layout.py @@ -74,7 +74,8 @@ def assert_rendered_health(page: Page, label: str, errors: list[str]) -> None: const style = getComputedStyle(element); const scrollable = ['auto', 'scroll'].includes(style.overflowX); return { - label: (element.getAttribute('aria-label') || element.textContent || element.tagName).trim().slice(0, 80), + label: (element.getAttribute('aria-label') || element.textContent || element.tagName) + .trim().slice(0, 80), left: box.left, right: box.right, clippedText: ['A', 'BUTTON', 'SUMMARY'].includes(element.tagName) @@ -136,7 +137,13 @@ def assert_landing_contract(page: Page) -> None: def open_page(browser: Browser, base_url: str, path: str, viewport: dict[str, int]) -> tuple[Page, list[str]]: - page = browser.new_page(viewport=viewport) + page = browser.new_page( + viewport=viewport, color_scheme="dark", permissions=["clipboard-read", "clipboard-write"], + ) + page.route( + "https://status.durable-workflow.com/api/current.json", + lambda route: route.fulfill(status=200, content_type="application/json", body="{}"), + ) errors = runtime_errors(page) response = page.goto(f"{base_url}{path}", wait_until="networkidle") assert response is not None and response.ok, f"{path} returned a non-success response" @@ -151,7 +158,8 @@ def exercise_mobile_controls(page: Page) -> None: drawer = page.locator(".md-sidebar--primary") assert drawer.is_visible(), "mobile navigation did not open" drawer_box = drawer.bounding_box() - assert drawer_box is not None and drawer_box["x"] >= -1 and drawer_box["width"] <= 390 + width = page.viewport_size["width"] if page.viewport_size else 390 + assert drawer_box is not None and drawer_box["x"] >= -1 and drawer_box["width"] <= width close_navigation = drawer.locator(".dw-navigation__close") close_navigation.click() page.wait_for_function("!document.querySelector('#__drawer').checked") @@ -166,15 +174,70 @@ def exercise_mobile_controls(page: Page) -> None: page.wait_for_timeout(250) assert search_input.is_visible(), "mobile search did not open" search_box = page.locator(".md-search").bounding_box() - assert search_box is not None and search_box["x"] >= -1 and search_box["width"] <= 391 + assert search_box is not None and search_box["x"] >= -1 and search_box["width"] <= width + 1 page.locator(".md-search__icon[for='__search']").click() page.wait_for_function("!document.querySelector('#__search').checked") assert search.evaluate("element => element === document.activeElement"), "search focus was not restored" +def exercise_identity(page: Page, theme: str) -> None: + page.wait_for_function("document.body.dataset.mdColorScheme === 'default'") + if theme == "dark": + toggle = page.get_by_role("button", name="Switch to dark mode", exact=True) + toggle.focus() + toggle.press("Enter") + page.wait_for_function("document.body.dataset.mdColorScheme === 'slate'") + page.reload(wait_until="networkidle") + page.wait_for_function("document.body.dataset.mdColorScheme === 'slate'") + page.evaluate("document.fonts.ready") + fonts = page.evaluate("[...document.fonts].filter(font => font.status === 'loaded').map(font => font.family)") + assert {"Inter", "Space Grotesk", "JetBrains Mono"} <= set(fonts), fonts + assert page.locator(".highlight").first.evaluate( + "element => getComputedStyle(element).backgroundColor", + ) == "rgb(15, 21, 19)" + switcher = page.locator("[data-sdk-switcher]") + trigger = switcher.locator("summary") + trigger.focus() + trigger.press("Enter") + page.wait_for_function("document.querySelector('[data-sdk-switcher]').open") + assert switcher.get_by_role("link", name="Rust", exact=True).get_attribute("href") == "https://rust.durable-workflow.com/" + trigger.press("Escape") + assert not switcher.evaluate("element => element.open") + assert trigger.evaluate("element => element === document.activeElement") + assert page.locator("[data-status-label]").text_content() == "Status unknown" + assert page.locator(".dw-global-actions .dw-button").is_visible() + + +def exercise_copy_and_steps(page: Page) -> None: + spacing = page.locator(".dw-cloud-promotion__eyebrow").evaluate( + """element => { + const style = getComputedStyle(element); + return parseFloat(style.letterSpacing) / parseFloat(style.fontSize); + }""", + ) + assert abs(spacing - 0.08) < 0.001, "Eyebrow tracking differs from the canonical design" + for selector in (".dw-install-card", ".dw-quickstart-code"): + block = page.locator(selector) + expected = block.locator("pre code").text_content() + # Never inspect a shared host's previous clipboard contents. + page.evaluate("navigator.clipboard.writeText('sdk-python-public-copy-control')") + block.locator("button[data-clipboard-target]").click() + copied = page.evaluate("navigator.clipboard.readText()") + assert copied.rstrip("\n") == expected.rstrip("\n"), "Copy changed or truncated the code" + assert page.locator("#start-server").count() == 1 + assert page.locator("#save-greeter").count() == 1 + assert page.locator("#run-greeter").count() == 1 + assert page.locator(".dw-quickstart-copy a[href='#start-server']").count() == 1 + assert page.locator(".dw-quickstart-copy a[href='#run-greeter']").count() == 1 + code = page.locator(".dw-quickstart-code pre code").text_content() or "" + assert "asyncio.run(main())" in code and "workflows=[GreeterWorkflow]" in code + assert "activities=[greet]" in code and "task_queue=TASK_QUEUE" in code + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("site", nargs="?", type=Path, default=Path("site")) + parser.add_argument("--screenshots", type=Path) args = parser.parse_args() site = args.site.resolve() if not (site / "index.html").is_file(): @@ -183,22 +246,32 @@ def main() -> int: with serve(site) as base_url, sync_playwright() as playwright: browser = playwright.chromium.launch() try: - desktop, desktop_errors = open_page(browser, base_url, "/", {"width": 1440, "height": 920}) - assert_landing_contract(desktop) - assert_rendered_health(desktop, "desktop landing", desktop_errors) - desktop.close() + if args.screenshots: + args.screenshots.mkdir(parents=True, exist_ok=True) + for width in (1440, 1024, 390, 320): + for theme in ("light", "dark"): + page, errors = open_page(browser, base_url, "/", {"width": width, "height": 920}) + exercise_identity(page, theme) + assert_landing_contract(page) + assert_rendered_health(page, f"{width} {theme} landing", errors) + exercise_copy_and_steps(page) + if width < 960: + exercise_mobile_controls(page) + assert_rendered_health(page, f"{width} {theme} controls", errors) + if args.screenshots: + page.screenshot(path=str(args.screenshots / f"python-{width}-{theme}.png"), full_page=True) + page.close() for path in ("/sdk-reference/", "/reference/client/"): - page, errors = open_page(browser, base_url, path, {"width": 1440, "height": 920}) - assert_rendered_health(page, f"desktop {path}", errors) - page.close() - - mobile, mobile_errors = open_page(browser, base_url, "/", {"width": 390, "height": 844}) - assert_landing_contract(mobile) - assert_rendered_health(mobile, "mobile landing", mobile_errors) - exercise_mobile_controls(mobile) - assert_rendered_health(mobile, "mobile controls", mobile_errors) - mobile.close() + for width in (1440, 390): + for theme in ("light", "dark"): + page, errors = open_page(browser, base_url, path, {"width": width, "height": 920}) + exercise_identity(page, theme) + assert_rendered_health(page, f"{width} {theme} {path}", errors) + if width < 960: + exercise_mobile_controls(page) + page.close() + finally: browser.close() diff --git a/scripts/prepare_portal.py b/scripts/prepare_portal.py new file mode 100644 index 00000000..6aafb366 --- /dev/null +++ b/scripts/prepare_portal.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +"""Bind current presentation to an explicitly selected published SDK source.""" + +from __future__ import annotations + +import hashlib +import importlib.metadata +import json +import os +import re +import shutil +import subprocess +import tempfile +from pathlib import Path +from typing import Any + +import tomllib +import yaml + +PRESENTATION_DIRECTORIES = ("assets", "stylesheets", "javascripts") +CODE_BLOCKS = re.compile(r"```([^\n]*)\n(.*?)```", re.DOTALL) + + +def git(root: Path, *arguments: str) -> str: + return subprocess.check_output(["git", "-C", str(root), *arguments], text=True).strip() + + +def source_files(root: Path, directory: str) -> list[Path]: + names = subprocess.check_output(["git", "-C", str(root), "ls-files", "-z", directory]).decode().split("\0") + files = [] + for name in filter(None, names): + source = root / name + if source.is_symlink() or not source.resolve().is_relative_to(root.resolve()) or not source.is_file(): + raise ValueError("Selected SDK source contains an unsupported file") + files.append(source) + return sorted(files) + + +def sdk_inventory(sdk: Path, *, verify_installed: bool) -> list[dict[str, Any]]: + distribution = importlib.metadata.distribution("durable-workflow") if verify_installed else None + rows = [] + for source in source_files(sdk, "src/durable_workflow"): + data = source.read_bytes() + relative = source.relative_to(sdk / "src").as_posix() + if distribution is not None and distribution.locate_file(relative).read_bytes() != data: + raise ValueError("Installed published SDK differs from selected API source") + rows.append({"path": relative, "bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}) + if not rows: + raise ValueError("Selected SDK has no API source") + return rows + + +def prepare(root: Path, environment: dict[str, str]) -> dict[str, Any]: + for name in (".docs-build.yml", ".docs-api-inventory.json", ".docs-provenance.json"): + (root / name).unlink(missing_ok=True) + config = yaml.safe_load((root / "mkdocs.yml").read_text()) + docs_revision = git(root, "rev-parse", "HEAD") + if environment.get("GITHUB_SHA", docs_revision) != docs_revision: + raise ValueError("Documentation checkout differs from the selected workflow revision") + sdk = root + channel = "source-preview" + tag = environment.get("STABLE_TAG", "") + if tag: + if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", tag): + raise ValueError("Stable SDK requires an exact stable tag") + sdk = root / ".docs-sdk" + revision = git(sdk, "rev-parse", "HEAD") + if revision != git(root, "rev-parse", f"refs/tags/{tag}^{{commit}}"): + raise ValueError("API checkout differs from the published tag") + if git(sdk, "status", "--porcelain", "--untracked-files=all"): + raise ValueError("Selected SDK source is modified") + project = tomllib.loads((sdk / "pyproject.toml").read_text())["project"] + version = project["version"] + if version != tag or importlib.metadata.version("durable-workflow") != tag: + raise ValueError("Published SDK source/package version differs from the selected tag") + if not re.fullmatch(r"[0-9a-f]{64}", environment.get("DOCS_WHEEL_SHA256", "")): + raise ValueError("Published wheel digest is required") + stage = Path(tempfile.mkdtemp(prefix=".docs-stage-", dir=root)) + # The complete SDK documentation content comes from tracked tag files. + # Only this closed set of presentation assets and the reviewed landing + # page come from the current documentation revision. + for source in source_files(sdk, "docs"): + target = stage / source.relative_to(sdk / "docs") + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + for directory in PRESENTATION_DIRECTORIES: + shutil.copytree(root / "docs" / directory, stage / directory, dirs_exist_ok=True) + landing = (root / "docs/index.md").read_text() + if CODE_BLOCKS.findall(landing) != CODE_BLOCKS.findall((sdk / "docs/index.md").read_text()): + raise ValueError("Landing examples differ from the selected published SDK documentation") + floors = re.findall(r"Python ([0-9]+\.[0-9]+)\+", landing) + if len(floors) != 1 or project.get("requires-python") != f">={floors[0]}": + raise ValueError("Landing Python support floor differs from the published SDK") + installed_python = importlib.metadata.distribution("durable-workflow").metadata.get("Requires-Python") + if installed_python != project["requires-python"]: + raise ValueError("Installed Python support metadata differs from the selected SDK source") + landing, matches = re.subn( + r"Python SDK · [0-9]+\.[0-9]+\.[0-9]+ published", f"Python SDK · {tag} published", landing, + ) + if matches != 1: + raise ValueError("Published package label is missing or ambiguous") + (stage / "index.md").write_text(landing) + config["docs_dir"] = str(stage) + config["nav"] = yaml.safe_load((sdk / "mkdocs.yml").read_text())["nav"] + documentation = next(plugin["mkdocstrings"] for plugin in config["plugins"] + if isinstance(plugin, dict) and "mkdocstrings" in plugin) + documentation["handlers"]["python"]["paths"] = [str(sdk / "src")] + config["extra"]["published_sdk_version"] = tag + channel = "stable" + else: + revision = docs_revision + version = tomllib.loads((root / "pyproject.toml").read_text())["project"]["version"] + inventory = sdk_inventory(sdk, verify_installed=bool(tag)) + config["extra"]["docs_channel"] = channel + (root / ".docs-build.yml").write_text(yaml.safe_dump(config, sort_keys=False)) + inventory_bytes = (json.dumps(inventory, sort_keys=True, separators=(",", ":")) + "\n").encode() + (root / ".docs-api-inventory.json").write_bytes(inventory_bytes) + audit = { + "schema": "durable-workflow.sdk-python.docs-deployment/v2", + "channel": channel, + "docs_source_revision": docs_revision, + "sdk_source_revision": revision, + "sdk_version": version, + "sdk_source_inventory_sha256": hashlib.sha256(inventory_bytes).hexdigest(), + "published_wheel_sha256": environment.get("DOCS_WHEEL_SHA256") if tag else None, + } + (root / ".docs-provenance.json").write_text(json.dumps(audit, sort_keys=True) + "\n") + return audit + + +if __name__ == "__main__": + prepare(Path.cwd(), dict(os.environ)) diff --git a/tests/test_docs_release_channel.py b/tests/test_docs_release_channel.py index ee57694f..d3f72e69 100644 --- a/tests/test_docs_release_channel.py +++ b/tests/test_docs_release_channel.py @@ -30,6 +30,7 @@ def select_channel( root: Path, *, version: str = "2.14.1", tag: str = "", event: str = "workflow_dispatch", ref: str = "refs/heads/main", published: bool = True, lookup_exit: int = 0, matching_source: bool = True, registry: dict[str, Any] | None = None, + docs_version: str | None = None, matching_docs: bool = True, ) -> tuple[subprocess.CompletedProcess[str], dict[str, str], str]: steps = yaml.safe_load(WORKFLOW.read_text())["jobs"]["build"]["steps"] validation = next(step["run"] for step in steps if step.get("name") == "Validate the selected stable tag") @@ -46,7 +47,8 @@ def select_channel( if registry is None: registry = {"info": {"name": "durable-workflow", "version": "2.14.1"}, "urls": [ {"filename": "durable_workflow-2.14.1-py3-none-any.whl", "packagetype": "bdist_wheel", - "yanked": False, "digests": {"sha256": "1" * 64}}, + "yanked": False, "digests": {"sha256": "1" * 64}, + "url": "https://files.pythonhosted.org/packages/fixture/durable_workflow-2.14.1-py3-none-any.whl"}, {"filename": "durable_workflow-2.14.1.tar.gz", "packagetype": "sdist", "yanked": False, "digests": {"sha256": "2" * 64}}, ]} @@ -81,8 +83,16 @@ def git(*arguments: str) -> None: git("add", "pyproject.toml") git("commit", "-m", "Fixture source") git("tag", "2.14.1") + git("worktree", "add", "--detach", ".docs-sdk", "2.14.1") + if docs_version is not None: + (root / "pyproject.toml").write_text(f'[project]\nversion = "{docs_version}"\n') + git("add", "pyproject.toml") + git("commit", "-m", "Documentation source successor") if not matching_source: - git("commit", "--allow-empty", "-m", "Unpublished successor") + git("-C", str(root / ".docs-sdk"), "commit", "--allow-empty", "-m", "Wrong API source") + environment["GITHUB_SHA"] = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=root, text=True).strip() + if not matching_docs: + environment["GITHUB_SHA"] = "0" * 40 script = (validation + "\n" if tag else "") + selector result = subprocess.run( ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", script], @@ -96,14 +106,19 @@ def git(*arguments: str) -> None: def test_main_source_is_preview_even_with_a_numeric_version(tmp_path: Path, version: str) -> None: result, values, calls = select_channel(tmp_path, version=version, event="push") assert result.returncode == 0, result.stderr - assert values == {"stable": "false", "version": version} + assert values["stable"] == "false" and values["version"] == version + assert values["docs_revision"] == values["sdk_revision"] + assert len(values["docs_revision"]) == 40 assert calls == "" def test_explicit_published_tag_with_exact_source_can_select_stable(tmp_path: Path) -> None: result, values, calls = select_channel(tmp_path, tag="2.14.1") assert result.returncode == 0, result.stderr - assert values == {"stable": "true", "version": "2.14.1"} + assert values["stable"] == "true" and values["version"] == "2.14.1" + assert values["docs_revision"] == values["sdk_revision"] + assert values["wheel_sha256"] == "1" * 64 + assert values["wheel_url"].startswith("https://files.pythonhosted.org/packages/") assert "release view 2.14.1 --repo durable-workflow/sdk-python" in calls @@ -134,3 +149,28 @@ def test_registry_identity_or_missing_artifacts_refuse_stable(tmp_path: Path, re result, values, _ = select_channel(tmp_path, tag="2.14.1", registry=registry) assert result.returncode != 0 assert values.get("stable") != "true" + + +def test_current_docs_can_select_the_separate_published_sdk(tmp_path: Path) -> None: + result, values, _ = select_channel(tmp_path, tag="2.14.1", docs_version="2.15.0") + assert result.returncode == 0, result.stderr + assert values["stable"] == "true" and values["version"] == "2.14.1" + assert values["docs_revision"] != values["sdk_revision"] + + +def test_wrong_documentation_revision_refuses_before_publication_lookup(tmp_path: Path) -> None: + result, values, calls = select_channel(tmp_path, tag="2.14.1", matching_docs=False) + assert result.returncode != 0 and values.get("stable") != "true" + assert calls == "" + + +def test_current_workflow_owns_docs_and_the_central_apt_floor() -> None: + steps = yaml.safe_load(WORKFLOW.read_text())["jobs"]["build"]["steps"] + checkouts = [step for step in steps if step.get("uses", "").startswith("actions/checkout@")] + assert checkouts[0]["with"]["ref"] == "${{ github.sha }}" + assert checkouts[1]["with"]["path"] == ".docs-sdk" + assert all(step["with"]["persist-credentials"] is False for step in checkouts) + apt = next(i for i, step in enumerate(steps) if step.get("uses", "").startswith( + "durable-workflow/.github/.github/actions/require-fixed-apt@")) + chromium = next(i for i, step in enumerate(steps) if step.get("name") == "Install Chromium") + assert apt < chromium diff --git a/tests/test_portal_source_split.py b/tests/test_portal_source_split.py new file mode 100644 index 00000000..0d96384e --- /dev/null +++ b/tests/test_portal_source_split.py @@ -0,0 +1,136 @@ +"""Exercise source/package/content separation with local inert Git fixtures.""" + +from __future__ import annotations + +import importlib.util +import json +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import pytest +import yaml + +if sys.version_info < (3, 11): + pytest.skip("The portal build uses Python 3.12 tomllib", allow_module_level=True) + +SCRIPT = Path(__file__).parents[1] / "scripts/prepare_portal.py" +SPEC = importlib.util.spec_from_file_location("prepare_portal", SCRIPT) +assert SPEC is not None and SPEC.loader is not None +portal = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(portal) + + +def git(root: Path, *args: str) -> str: + return subprocess.check_output( + ["git", "-c", "core.hooksPath=/dev/null", "-c", "user.name=Portal fixture", + "-c", "user.email=portal@example.test", "-C", str(root), *args], text=True, + stderr=subprocess.DEVNULL, + ).strip() + + +@pytest.fixture +def sources(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[Path, dict[str, str], Path]: + root = tmp_path / "repo" + root.mkdir() + files = { + "pyproject.toml": '[project]\nversion = "2.14.1"\nrequires-python = ">=3.10"\n', + ".gitignore": ".docs-sdk/\n.docs-stage-*/\n.docs-*.json\n.docs-build.yml\n", + "src/durable_workflow/__init__.py": 'PUBLIC = "published"\n', + "docs/index.md": '# Published\n```python\nprint("complete example")\n```\n', + "docs/reference/metrics.md": "Published metric contract\n", + "docs/assets/logo.svg": "published-logo", + "docs/stylesheets/layout.css": "published-style", + "docs/javascripts/identity.js": "published-script", + } + config: dict[str, Any] = { + "docs_dir": "docs", "plugins": ["search", {"mkdocstrings": {"handlers": {"python": {"paths": ["src"]}}}}], + "extra": {"published_sdk_version": "2.14.1"}, "nav": [{"Published metrics": "reference/metrics.md"}], + } + for name, body in files.items(): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(body) + (root / "mkdocs.yml").write_text(yaml.safe_dump(config)) + git(root, "init") + git(root, "add", ".") + git(root, "commit", "-m", "Published fixture") + git(root, "tag", "2.14.1") + git(root, "worktree", "add", "--detach", ".docs-sdk", "2.14.1") + (root / "pyproject.toml").write_text('[project]\nversion = "2.15.0"\n') + (root / "src/durable_workflow/__init__.py").write_text('PUBLIC = "unreleased"\n') + (root / "docs/reference/metrics.md").write_text("Unreleased metric contract\n") + (root / "docs/index.md").write_text( + '# Redesigned\nPython SDK · 2.14.1 published\nPython 3.10+\n' + '```python\nprint("complete example")\n```\n', + ) + (root / "docs/stylesheets/layout.css").write_text("redesigned-style") + config["nav"] = [{"Unreleased page": "future.md"}] + (root / "mkdocs.yml").write_text(yaml.safe_dump(config)) + git(root, "add", ".") + git(root, "commit", "-m", "Current docs source") + installed = tmp_path / "installed/durable_workflow" + installed.mkdir(parents=True) + (installed / "__init__.py").write_text('PUBLIC = "published"\n') + monkeypatch.setattr(portal.importlib.metadata, "version", lambda _name: "2.14.1") + monkeypatch.setattr(portal.importlib.metadata, "distribution", lambda _name: SimpleNamespace( + locate_file=lambda path: installed.parent / path, metadata={"Requires-Python": ">=3.10"}, + )) + env = {"STABLE_TAG": "2.14.1", "DOCS_WHEEL_SHA256": "1" * 64, "GITHUB_SHA": git(root, "rev-parse", "HEAD")} + return root, env, installed + + +def test_stable_build_combines_current_presentation_and_published_content(sources: tuple) -> None: + root, env, _installed = sources + audit = portal.prepare(root, env) + config = yaml.safe_load((root / ".docs-build.yml").read_text()) + stage = Path(config["docs_dir"]) + assert (stage / "reference/metrics.md").read_text() == "Published metric contract\n" + assert (stage / "stylesheets/layout.css").read_text() == "redesigned-style" + assert (stage / "index.md").read_text().startswith("# Redesigned") + assert config["nav"] == [{"Published metrics": "reference/metrics.md"}] + assert config["plugins"][1]["mkdocstrings"]["handlers"]["python"]["paths"] == [str(root / ".docs-sdk/src")] + assert audit["docs_source_revision"] != audit["sdk_source_revision"] + assert audit["sdk_version"] == "2.14.1" and audit["channel"] == "stable" + inventory = json.loads((root / ".docs-api-inventory.json").read_text()) + assert [row["path"] for row in inventory] == ["durable_workflow/__init__.py"] + assert str(root) not in (root / ".docs-provenance.json").read_text() + + +@pytest.mark.parametrize("fault", [ + "sdk-source", "installed-code", "installed-version", "example", "wheel", "docs-revision", "support-floor", +]) +def test_changed_identity_or_examples_refuse_and_remove_stale_audit( + sources: tuple, monkeypatch: pytest.MonkeyPatch, fault: str, +) -> None: + root, env, installed = sources + portal.prepare(root, env) + if fault == "sdk-source": + (root / ".docs-sdk/src/durable_workflow/__init__.py").write_text("tampered") + elif fault == "installed-code": + (installed / "__init__.py").write_text("different wheel bytes") + elif fault == "installed-version": + monkeypatch.setattr(portal.importlib.metadata, "version", lambda _name: "2.15.0") + elif fault == "example": + (root / "docs/index.md").write_text('```python\nprint("new API")\n```\n') + elif fault == "wheel": + env["DOCS_WHEEL_SHA256"] = "" + elif fault == "support-floor": + page = root / "docs/index.md" + page.write_text(page.read_text().replace("Python 3.10+", "Python 3.11+")) + else: + env["GITHUB_SHA"] = "0" * 40 + with pytest.raises(ValueError): + portal.prepare(root, env) + assert not (root / ".docs-provenance.json").exists() + + +def test_source_build_stays_preview_with_current_api_and_no_package_claim(sources: tuple) -> None: + root, env, _installed = sources + env["STABLE_TAG"] = "" + audit = portal.prepare(root, env) + assert audit["channel"] == "source-preview" and audit["sdk_version"] == "2.15.0" + assert audit["docs_source_revision"] == audit["sdk_source_revision"] + assert audit["published_wheel_sha256"] is None From 789a9838295b7dfb9f497edb0226a9a0163152aa Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:01:59 +0000 Subject: [PATCH 04/12] fix: preserve full quickstart copying and portal qualification --- .github/workflows/docs.yml | 15 +-- docs/index.md | 28 ++++++ docs/stylesheets/layout.css | 143 ++++++++++++----------------- requirements-docs.txt | 43 +++++++++ scripts/check-docs-layout.py | 74 +++++++++------ scripts/prepare_portal.py | 100 +++++++++++--------- tests/test_docs_release_channel.py | 12 ++- tests/test_portal_source_split.py | 9 +- 8 files changed, 252 insertions(+), 172 deletions(-) create mode 100644 requirements-docs.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3aa88830..2bc1b249 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,6 +9,7 @@ on: - 'overrides/**' - 'mkdocs.yml' - 'pyproject.toml' + - 'requirements-docs.txt' - 'scripts/check-docs-analytics.py' - 'scripts/check-docs-layout.py' - 'scripts/prepare_portal.py' @@ -151,20 +152,14 @@ jobs: PUBLISHED_WHEEL_URL: ${{ steps.release-channel.outputs.wheel_url }} PUBLISHED_WHEEL_SHA256: ${{ steps.release-channel.outputs.wheel_sha256 }} run: | + pip install --only-binary=:all: -r requirements-docs.txt if [ -n "$STABLE_TAG" ]; then - python - <<'PY' - import tomllib - from pathlib import Path - dependencies = tomllib.loads(Path("pyproject.toml").read_text())["project"]["optional-dependencies"]["docs"] - Path(".docs-requirements.txt").write_text("\n".join(dependencies) + "\n") - PY - pip install --only-binary=:all: -r .docs-requirements.txt - pip install --only-binary=:all: "$PUBLISHED_WHEEL_URL#sha256=$PUBLISHED_WHEEL_SHA256" + pip install --no-deps --only-binary=:all: "$PUBLISHED_WHEEL_URL#sha256=$PUBLISHED_WHEEL_SHA256" else - pip install -e '.[docs]' + pip install --no-deps --only-binary=:all: -e . fi - name: Require the accepted central APT floor before browser system dependencies - uses: durable-workflow/.github/.github/actions/require-fixed-apt@18783be6bef737cc778e4c7282a8797e39daa61a + uses: durable-workflow/.github/.github/actions/require-fixed-apt@18783be6bef737cc778e4c7282a8797e39daa61a # fixed-apt - name: Install Chromium env: NODE_OPTIONS: --use-openssl-ca diff --git a/docs/index.md b/docs/index.md index facbabb9..8689adc3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -33,10 +33,14 @@ namespace. ## Install +
+ ```bash pip install durable-workflow ``` +
+ Use a virtual environment and lock the resolved package version with the rest of your application dependencies. @@ -73,12 +77,18 @@ worker, and client against the stable Server channel. Docker keeps this first run local. Select the stable 2.x Server channel: +
+ ```bash export DW_SERVER_IMAGE='durableworkflow/server:2' ``` +
+ Then bootstrap and start that qualified image: +
+ ```bash export DURABLE_WORKFLOW_RUNTIME_URL='http://127.0.0.1:8080' export DURABLE_WORKFLOW_RUNTIME_NAMESPACE='default' @@ -95,6 +105,8 @@ docker run -d --name durable-workflow-python-server -p 8080:8080 \ until curl -sf http://127.0.0.1:8080/api/ready >/dev/null; do sleep 1; done ``` +
+
@@ -113,6 +125,8 @@ First [start Server](#start-server), then save this complete program and
+
+ ```python import asyncio import logging @@ -165,21 +179,29 @@ async def main() -> None: asyncio.run(main()) ``` +
+
### 3. Run it { #run-greeter } +
+ ```bash python greeter.py ``` +
+
The SDK reports registration before it handles the workflow, followed by the completed result: +
+ ```text worker py-worker-… registered on python-workers Hello, world! @@ -187,6 +209,8 @@ Hello, world!
+
+
@@ -236,6 +260,8 @@ Replace the placeholders with values returned for your namespace, then run the same `greeter.py`. Keep client and worker tokens in their respective processes when you split the example for production. +
+ ```bash export DURABLE_WORKFLOW_RUNTIME_URL='' export DURABLE_WORKFLOW_RUNTIME_NAMESPACE='' @@ -245,6 +271,8 @@ unset DURABLE_WORKFLOW_TOKEN python greeter.py ``` +
+

Cloud runtime guideRun the Python playground →

diff --git a/docs/stylesheets/layout.css b/docs/stylesheets/layout.css index f776cdf4..8d98db04 100644 --- a/docs/stylesheets/layout.css +++ b/docs/stylesheets/layout.css @@ -22,9 +22,6 @@ } /* Structural primitives; visual values come from the shared role tokens. */ -.dw-landing { - color: var(--dw-ink); -} .dw-landing * { box-sizing: border-box; @@ -34,48 +31,12 @@ text-underline-offset: .18em; } -.dw-hero { - display: grid; -} - .dw-hero__actions, .dw-inline-actions, .dw-runtime-card__actions { display: flex; align-items: center; flex-wrap: wrap; } -.dw-button { - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid var(--dw-line); - text-decoration: none; -} - -.dw-text-link { - display: inline-flex; - align-items: center; - color: var(--dw-action); - text-decoration: none; -} - -.dw-hero__facts { - display: flex; - flex-wrap: wrap; -} - -.dw-install-card { - align-self: center; - min-width: 0; - background: var(--dw-code-background); - color: var(--dw-code-foreground); -} - -.dw-install-card__links { - display: flex; - justify-content: space-between; -} - .dw-role-grid, .dw-link-grid, .dw-runtime-grid { display: grid; gap: 16px; @@ -85,10 +46,6 @@ grid-template-columns: repeat(4, minmax(0, 1fr)); } -.dw-runtime-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - .dw-role-card, .dw-link-card, .dw-runtime-card { display: flex; flex-direction: column; @@ -129,10 +86,6 @@ font: inherit; } -.dw-runtime-card { - gap: 12px; -} - .dw-runtime-card .dw-text-link { align-self: flex-start; } @@ -194,8 +147,8 @@ .dw-landing *, .dw-landing *::before, .dw-landing *::after { - scroll-behavior: auto !important; - transition: none !important; + scroll-behavior: auto; + transition: none; } } @@ -537,6 +490,8 @@ body:has(.dw-landing) .md-content__inner { } .dw-landing { + color: var(--dw-ink); + display: flex; flex-direction: column; gap: 88px; @@ -545,6 +500,8 @@ body:has(.dw-landing) .md-content__inner { } .dw-hero { + display: grid; + padding: 0; gap: 48px; align-items: center; @@ -586,23 +543,25 @@ body:has(.dw-landing) .md-content__inner { } .dw-button { + display: inline-flex; + align-items: center; + justify-content: center; + border: 1px solid var(--dw-line); + text-decoration: none; + border-radius: 6px; padding: 0 20px; border-color: var(--dw-line); } -.dw-button--primary { +.dw-button.dw-button--primary { background: var(--dw-action); border-color: var(--dw-action); - color: var(--dw-on-action) !important; -} - -[data-md-color-scheme="slate"] .dw-button--primary { - color: var(--dw-on-action) !important; + color: var(--dw-on-action); } -.dw-button--secondary { - color: var(--dw-ink) !important; +.dw-button.dw-button--secondary { + color: var(--dw-ink); background: var(--dw-surface); } @@ -612,10 +571,18 @@ body:has(.dw-landing) .md-content__inner { } .dw-text-link { + display: inline-flex; + align-items: center; + color: var(--dw-action); + text-decoration: none; + padding: 10px 8px; } .dw-hero__facts { + display: flex; + flex-wrap: wrap; + font: 400 13px var(--dw-font-mono); color: var(--dw-muted); margin-top: 22px; @@ -627,6 +594,11 @@ body:has(.dw-landing) .md-content__inner { } .dw-install-card { + align-self: center; + min-width: 0; + background: var(--dw-code-background); + color: var(--dw-code-foreground); + border: none; border-radius: 8px; box-shadow: none; @@ -646,6 +618,9 @@ body:has(.dw-landing) .md-content__inner { } .dw-install-card__links { + display: flex; + justify-content: space-between; + padding: 14px 16px; margin: 0; border-top: 1px solid #26302c; @@ -654,7 +629,7 @@ body:has(.dw-landing) .md-content__inner { } .dw-install-card__links a { - color: #5fe0a6 !important; + color: #5fe0a6; } .dw-section { @@ -687,11 +662,6 @@ body:has(.dw-landing) .md-content__inner { padding: 20px; } -.dw-role-card { - min-height: 190px; - gap: 8px; -} - .dw-role-card__step { color: var(--dw-action); background: none; @@ -744,10 +714,14 @@ body:has(.dw-landing) .md-content__inner { } .dw-runtime-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; } .dw-runtime-card { + gap: 12px; + border-top: 1px solid var(--dw-line); min-height: 220px; } @@ -826,11 +800,6 @@ body:has(.dw-landing) .md-content__inner { content: "output"; } -.dw-landing .highlight pre { - margin: 0; - max-width: 100%; -} - .dw-landing .highlight pre code { padding: 18px 16px; background: var(--dw-code-background); @@ -846,15 +815,6 @@ body:has(.dw-landing) .md-content__inner { overflow: auto; } -.dw-landing button[data-clipboard-target] { - top: 8px; - right: 8px; - color: var(--dw-code-muted); - border: 1px solid #2e3934; - border-radius: 4px; - opacity: 1; -} - .dw-footer { background: var(--dw-surface); border-top: 1px solid var(--dw-line); @@ -1119,13 +1079,6 @@ body:has(.dw-landing) .md-content__inner { font-size: 15px; } -.dw-landing button[data-clipboard-target] { - width: auto; - height: 28px; - padding: 4px 8px; - background: transparent; -} - .dw-landing button[data-clipboard-target]::after { content: "Copy"; display: block; @@ -1157,6 +1110,9 @@ body:has(.dw-landing) .md-content__inner { /* Keep the maintained theme's copy action in the code header in both theme versions. */ .dw-landing .highlight pre { + margin: 0; + max-width: 100%; + position: static; } .dw-landing .md-code__nav { @@ -1165,6 +1121,18 @@ body:has(.dw-landing) .md-content__inner { right: 8px; } .dw-landing button[data-clipboard-target] { + top: 8px; + right: 8px; + color: var(--dw-code-muted); + border: 1px solid #2e3934; + border-radius: 4px; + opacity: 1; + + width: auto; + height: 28px; + padding: 4px 8px; + background: transparent; + display: inline-flex; align-items: center; justify-content: center; @@ -1187,5 +1155,12 @@ body:has(.dw-landing) .md-content__inner { margin: 0; } .dw-role-card { + min-height: 190px; + gap: 8px; + width: 100%; } + +button[data-clipboard-target] { + color: var(--dw-code-muted); +} diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 00000000..f3884860 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,43 @@ +# Resolved documentation and SDK runtime dependencies for the Python 3.12 portal builder. +anyio==4.16.0 +babel==2.18.0 +backrefs==8.2 +certifi==2026.10.23 +charset-normalizer==3.5.2 +click==8.5.0 +colorama==0.4.6 +fastavro==1.13.1 +ghp-import==2.1.0 +greenlet==3.5.6 +griffelib==2.4.1 +h11==0.16.0 +httpcore==1.0.9 +httpx==0.28.1 +idna==3.20 +Jinja2==3.1.6 +Markdown==3.11.3 +MarkupSafe==3.0.4 +mergedeep==1.3.4 +mkdocs==1.6.1 +mkdocs-autorefs==1.4.5 +mkdocs-get-deps==0.2.2 +mkdocs-material==9.8.0 +mkdocs-material-extensions==1.3.1 +mkdocstrings==1.0.8 +mkdocstrings-python==2.0.10 +packaging==26.3 +paginate==0.5.7 +pathspec==1.1.1 +platformdirs==4.14.5 +playwright==1.64.1 +pyee==14.0.0 +Pygments==2.21.0 +pymdown-extensions==12.3 +python-dateutil==4.0.0 +PyYAML==6.0.3 +pyyaml_env_tag==1.1 +requests==2.34.2 +typing_extensions==4.16.0 +tzdata==2026.6 +urllib3==2.8.0 +watchdog==7.1.0 diff --git a/scripts/check-docs-layout.py b/scripts/check-docs-layout.py index aa6766e7..f6302b37 100644 --- a/scripts/check-docs-layout.py +++ b/scripts/check-docs-layout.py @@ -13,6 +13,8 @@ from playwright.sync_api import Browser, Page, sync_playwright +FOCUSED = "element => element === document.activeElement" + class SiteHandler(http.server.SimpleHTTPRequestHandler): def log_message(self, _format: str, *_args: object) -> None: @@ -163,7 +165,7 @@ def exercise_mobile_controls(page: Page) -> None: close_navigation = drawer.locator(".dw-navigation__close") close_navigation.click() page.wait_for_function("!document.querySelector('#__drawer').checked") - assert navigation.evaluate("element => element === document.activeElement"), "navigation focus was not restored" + assert navigation.evaluate(FOCUSED), "navigation focus was not restored" search = page.locator(".md-header__button[for='__search']") search.click() @@ -177,7 +179,7 @@ def exercise_mobile_controls(page: Page) -> None: assert search_box is not None and search_box["x"] >= -1 and search_box["width"] <= width + 1 page.locator(".md-search__icon[for='__search']").click() page.wait_for_function("!document.querySelector('#__search').checked") - assert search.evaluate("element => element === document.activeElement"), "search focus was not restored" + assert search.evaluate(FOCUSED), "search focus was not restored" def exercise_identity(page: Page, theme: str) -> None: @@ -203,7 +205,7 @@ def exercise_identity(page: Page, theme: str) -> None: assert switcher.get_by_role("link", name="Rust", exact=True).get_attribute("href") == "https://rust.durable-workflow.com/" trigger.press("Escape") assert not switcher.evaluate("element => element.open") - assert trigger.evaluate("element => element === document.activeElement") + assert trigger.evaluate(FOCUSED) assert page.locator("[data-status-label]").text_content() == "Status unknown" assert page.locator(".dw-global-actions .dw-button").is_visible() @@ -216,12 +218,19 @@ def exercise_copy_and_steps(page: Page) -> None: }""", ) assert abs(spacing - 0.08) < 0.001, "Eyebrow tracking differs from the canonical design" + targets = page.locator("button[data-clipboard-target]").evaluate_all( + "buttons => buttons.map(button => button.dataset.clipboardTarget)", + ) + assert len(set(targets)) == len(targets), "Code blocks share a copy target" for selector in (".dw-install-card", ".dw-quickstart-code"): block = page.locator(selector) expected = block.locator("pre code").text_content() # Never inspect a shared host's previous clipboard contents. page.evaluate("navigator.clipboard.writeText('sdk-python-public-copy-control')") block.locator("button[data-clipboard-target]").click() + page.wait_for_function( + "async () => (await navigator.clipboard.readText()) !== 'sdk-python-public-copy-control'", + ) copied = page.evaluate("navigator.clipboard.readText()") assert copied.rstrip("\n") == expected.rstrip("\n"), "Copy changed or truncated the code" assert page.locator("#start-server").count() == 1 @@ -230,8 +239,38 @@ def exercise_copy_and_steps(page: Page) -> None: assert page.locator(".dw-quickstart-copy a[href='#start-server']").count() == 1 assert page.locator(".dw-quickstart-copy a[href='#run-greeter']").count() == 1 code = page.locator(".dw-quickstart-code pre code").text_content() or "" - assert "asyncio.run(main())" in code and "workflows=[GreeterWorkflow]" in code - assert "activities=[greet]" in code and "task_queue=TASK_QUEUE" in code + assert "asyncio.run(main())" in code + assert "workflows=[GreeterWorkflow]" in code + assert "activities=[greet]" in code + assert "task_queue=TASK_QUEUE" in code + + +def check_landing_views(browser: Browser, base_url: str, screenshots: Path | None) -> None: + for width in (1440, 1024, 390, 320): + for theme in ("light", "dark"): + page, errors = open_page(browser, base_url, "/", {"width": width, "height": 920}) + exercise_identity(page, theme) + assert_landing_contract(page) + assert_rendered_health(page, f"{width} {theme} landing", errors) + exercise_copy_and_steps(page) + if width < 960: + exercise_mobile_controls(page) + assert_rendered_health(page, f"{width} {theme} controls", errors) + if screenshots: + page.screenshot(path=str(screenshots / f"python-{width}-{theme}.png"), full_page=True) + page.close() + + +def check_api_views(browser: Browser, base_url: str) -> None: + for path in ("/sdk-reference/", "/reference/client/"): + for width in (1440, 390): + for theme in ("light", "dark"): + page, errors = open_page(browser, base_url, path, {"width": width, "height": 920}) + exercise_identity(page, theme) + assert_rendered_health(page, f"{width} {theme} {path}", errors) + if width < 960: + exercise_mobile_controls(page) + page.close() def main() -> int: @@ -248,29 +287,8 @@ def main() -> int: try: if args.screenshots: args.screenshots.mkdir(parents=True, exist_ok=True) - for width in (1440, 1024, 390, 320): - for theme in ("light", "dark"): - page, errors = open_page(browser, base_url, "/", {"width": width, "height": 920}) - exercise_identity(page, theme) - assert_landing_contract(page) - assert_rendered_health(page, f"{width} {theme} landing", errors) - exercise_copy_and_steps(page) - if width < 960: - exercise_mobile_controls(page) - assert_rendered_health(page, f"{width} {theme} controls", errors) - if args.screenshots: - page.screenshot(path=str(args.screenshots / f"python-{width}-{theme}.png"), full_page=True) - page.close() - - for path in ("/sdk-reference/", "/reference/client/"): - for width in (1440, 390): - for theme in ("light", "dark"): - page, errors = open_page(browser, base_url, path, {"width": width, "height": 920}) - exercise_identity(page, theme) - assert_rendered_health(page, f"{width} {theme} {path}", errors) - if width < 960: - exercise_mobile_controls(page) - page.close() + check_landing_views(browser, base_url, args.screenshots) + check_api_views(browser, base_url) finally: browser.close() diff --git a/scripts/prepare_portal.py b/scripts/prepare_portal.py index 6aafb366..608fc083 100644 --- a/scripts/prepare_portal.py +++ b/scripts/prepare_portal.py @@ -50,6 +50,61 @@ def sdk_inventory(sdk: Path, *, verify_installed: bool) -> list[dict[str, Any]]: return rows +def published_identity(root: Path, sdk: Path, tag: str, environment: dict[str, str]) -> tuple[str, dict[str, Any]]: + if not re.fullmatch(r"\d+\.\d+\.\d+", tag): + raise ValueError("Stable SDK requires an exact stable tag") + revision = git(sdk, "rev-parse", "HEAD") + if revision != git(root, "rev-parse", f"refs/tags/{tag}^{{commit}}"): + raise ValueError("API checkout differs from the published tag") + if git(sdk, "status", "--porcelain", "--untracked-files=all"): + raise ValueError("Selected SDK source is modified") + project = tomllib.loads((sdk / "pyproject.toml").read_text())["project"] + if project["version"] != tag or importlib.metadata.version("durable-workflow") != tag: + raise ValueError("Published SDK source/package version differs from the selected tag") + if not re.fullmatch(r"[0-9a-f]{64}", environment.get("DOCS_WHEEL_SHA256", "")): + raise ValueError("Published wheel digest is required") + return revision, project + + +def published_landing(root: Path, sdk: Path, project: dict[str, Any], tag: str) -> str: + landing = (root / "docs/index.md").read_text() + if CODE_BLOCKS.findall(landing) != CODE_BLOCKS.findall((sdk / "docs/index.md").read_text()): + raise ValueError("Landing examples differ from the selected published SDK documentation") + floors = re.findall(r"Python (\d+\.\d+)\+", landing) + if len(floors) != 1 or project.get("requires-python") != f">={floors[0]}": + raise ValueError("Landing Python support floor differs from the published SDK") + installed_python = importlib.metadata.distribution("durable-workflow").metadata.get("Requires-Python") + if installed_python != project["requires-python"]: + raise ValueError("Installed Python support metadata differs from the selected SDK source") + landing, matches = re.subn( + r"Python SDK · \d+\.\d+\.\d+ published", f"Python SDK · {tag} published", landing, + ) + if matches != 1: + raise ValueError("Published package label is missing or ambiguous") + return landing + + +def stage_published_docs(root: Path, sdk: Path, config: dict[str, Any], project: dict[str, Any], tag: str) -> None: + stage = Path(tempfile.mkdtemp(prefix=".docs-stage-", dir=root)) + # The complete SDK documentation content comes from tracked tag files. + # Only this closed set of presentation assets and the reviewed landing + # page come from the current documentation revision. + for source in source_files(sdk, "docs"): + target = stage / source.relative_to(sdk / "docs") + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + for directory in PRESENTATION_DIRECTORIES: + shutil.copytree(root / "docs" / directory, stage / directory, dirs_exist_ok=True) + landing = published_landing(root, sdk, project, tag) + (stage / "index.md").write_text(landing) + config["docs_dir"] = str(stage) + config["nav"] = yaml.safe_load((sdk / "mkdocs.yml").read_text())["nav"] + documentation = next(plugin["mkdocstrings"] for plugin in config["plugins"] + if isinstance(plugin, dict) and "mkdocstrings" in plugin) + documentation["handlers"]["python"]["paths"] = [str(sdk / "src")] + config["extra"]["published_sdk_version"] = tag + + def prepare(root: Path, environment: dict[str, str]) -> dict[str, Any]: for name in (".docs-build.yml", ".docs-api-inventory.json", ".docs-provenance.json"): (root / name).unlink(missing_ok=True) @@ -61,51 +116,10 @@ def prepare(root: Path, environment: dict[str, str]) -> dict[str, Any]: channel = "source-preview" tag = environment.get("STABLE_TAG", "") if tag: - if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", tag): - raise ValueError("Stable SDK requires an exact stable tag") sdk = root / ".docs-sdk" - revision = git(sdk, "rev-parse", "HEAD") - if revision != git(root, "rev-parse", f"refs/tags/{tag}^{{commit}}"): - raise ValueError("API checkout differs from the published tag") - if git(sdk, "status", "--porcelain", "--untracked-files=all"): - raise ValueError("Selected SDK source is modified") - project = tomllib.loads((sdk / "pyproject.toml").read_text())["project"] + revision, project = published_identity(root, sdk, tag, environment) version = project["version"] - if version != tag or importlib.metadata.version("durable-workflow") != tag: - raise ValueError("Published SDK source/package version differs from the selected tag") - if not re.fullmatch(r"[0-9a-f]{64}", environment.get("DOCS_WHEEL_SHA256", "")): - raise ValueError("Published wheel digest is required") - stage = Path(tempfile.mkdtemp(prefix=".docs-stage-", dir=root)) - # The complete SDK documentation content comes from tracked tag files. - # Only this closed set of presentation assets and the reviewed landing - # page come from the current documentation revision. - for source in source_files(sdk, "docs"): - target = stage / source.relative_to(sdk / "docs") - target.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(source, target) - for directory in PRESENTATION_DIRECTORIES: - shutil.copytree(root / "docs" / directory, stage / directory, dirs_exist_ok=True) - landing = (root / "docs/index.md").read_text() - if CODE_BLOCKS.findall(landing) != CODE_BLOCKS.findall((sdk / "docs/index.md").read_text()): - raise ValueError("Landing examples differ from the selected published SDK documentation") - floors = re.findall(r"Python ([0-9]+\.[0-9]+)\+", landing) - if len(floors) != 1 or project.get("requires-python") != f">={floors[0]}": - raise ValueError("Landing Python support floor differs from the published SDK") - installed_python = importlib.metadata.distribution("durable-workflow").metadata.get("Requires-Python") - if installed_python != project["requires-python"]: - raise ValueError("Installed Python support metadata differs from the selected SDK source") - landing, matches = re.subn( - r"Python SDK · [0-9]+\.[0-9]+\.[0-9]+ published", f"Python SDK · {tag} published", landing, - ) - if matches != 1: - raise ValueError("Published package label is missing or ambiguous") - (stage / "index.md").write_text(landing) - config["docs_dir"] = str(stage) - config["nav"] = yaml.safe_load((sdk / "mkdocs.yml").read_text())["nav"] - documentation = next(plugin["mkdocstrings"] for plugin in config["plugins"] - if isinstance(plugin, dict) and "mkdocstrings" in plugin) - documentation["handlers"]["python"]["paths"] = [str(sdk / "src")] - config["extra"]["published_sdk_version"] = tag + stage_published_docs(root, sdk, config, project, tag) channel = "stable" else: revision = docs_revision diff --git a/tests/test_docs_release_channel.py b/tests/test_docs_release_channel.py index d3f72e69..0b8116be 100644 --- a/tests/test_docs_release_channel.py +++ b/tests/test_docs_release_channel.py @@ -106,7 +106,8 @@ def git(*arguments: str) -> None: def test_main_source_is_preview_even_with_a_numeric_version(tmp_path: Path, version: str) -> None: result, values, calls = select_channel(tmp_path, version=version, event="push") assert result.returncode == 0, result.stderr - assert values["stable"] == "false" and values["version"] == version + assert values["stable"] == "false" + assert values["version"] == version assert values["docs_revision"] == values["sdk_revision"] assert len(values["docs_revision"]) == 40 assert calls == "" @@ -115,7 +116,8 @@ def test_main_source_is_preview_even_with_a_numeric_version(tmp_path: Path, vers def test_explicit_published_tag_with_exact_source_can_select_stable(tmp_path: Path) -> None: result, values, calls = select_channel(tmp_path, tag="2.14.1") assert result.returncode == 0, result.stderr - assert values["stable"] == "true" and values["version"] == "2.14.1" + assert values["stable"] == "true" + assert values["version"] == "2.14.1" assert values["docs_revision"] == values["sdk_revision"] assert values["wheel_sha256"] == "1" * 64 assert values["wheel_url"].startswith("https://files.pythonhosted.org/packages/") @@ -154,13 +156,15 @@ def test_registry_identity_or_missing_artifacts_refuse_stable(tmp_path: Path, re def test_current_docs_can_select_the_separate_published_sdk(tmp_path: Path) -> None: result, values, _ = select_channel(tmp_path, tag="2.14.1", docs_version="2.15.0") assert result.returncode == 0, result.stderr - assert values["stable"] == "true" and values["version"] == "2.14.1" + assert values["stable"] == "true" + assert values["version"] == "2.14.1" assert values["docs_revision"] != values["sdk_revision"] def test_wrong_documentation_revision_refuses_before_publication_lookup(tmp_path: Path) -> None: result, values, calls = select_channel(tmp_path, tag="2.14.1", matching_docs=False) - assert result.returncode != 0 and values.get("stable") != "true" + assert result.returncode != 0 + assert values.get("stable") != "true" assert calls == "" diff --git a/tests/test_portal_source_split.py b/tests/test_portal_source_split.py index 0d96384e..56f4cdd8 100644 --- a/tests/test_portal_source_split.py +++ b/tests/test_portal_source_split.py @@ -18,7 +18,8 @@ SCRIPT = Path(__file__).parents[1] / "scripts/prepare_portal.py" SPEC = importlib.util.spec_from_file_location("prepare_portal", SCRIPT) -assert SPEC is not None and SPEC.loader is not None +assert SPEC is not None +assert SPEC.loader is not None portal = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(portal) @@ -93,7 +94,8 @@ def test_stable_build_combines_current_presentation_and_published_content(source assert config["nav"] == [{"Published metrics": "reference/metrics.md"}] assert config["plugins"][1]["mkdocstrings"]["handlers"]["python"]["paths"] == [str(root / ".docs-sdk/src")] assert audit["docs_source_revision"] != audit["sdk_source_revision"] - assert audit["sdk_version"] == "2.14.1" and audit["channel"] == "stable" + assert audit["sdk_version"] == "2.14.1" + assert audit["channel"] == "stable" inventory = json.loads((root / ".docs-api-inventory.json").read_text()) assert [row["path"] for row in inventory] == ["durable_workflow/__init__.py"] assert str(root) not in (root / ".docs-provenance.json").read_text() @@ -131,6 +133,7 @@ def test_source_build_stays_preview_with_current_api_and_no_package_claim(source root, env, _installed = sources env["STABLE_TAG"] = "" audit = portal.prepare(root, env) - assert audit["channel"] == "source-preview" and audit["sdk_version"] == "2.15.0" + assert audit["channel"] == "source-preview" + assert audit["sdk_version"] == "2.15.0" assert audit["docs_source_revision"] == audit["sdk_source_revision"] assert audit["published_wheel_sha256"] is None From c3a0b5fcfa6db9ecffda3b4d61b095d36001fb7b Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:08:04 +0000 Subject: [PATCH 05/12] fix: keep theme keyboard activation and code controls readable --- .github/workflows/docs.yml | 4 ++-- docs/javascripts/identity.js | 5 +++++ docs/stylesheets/layout.css | 9 +++++++++ scripts/check-docs-layout.py | 3 +++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2bc1b249..e3bb4253 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -154,7 +154,7 @@ jobs: run: | pip install --only-binary=:all: -r requirements-docs.txt if [ -n "$STABLE_TAG" ]; then - pip install --no-deps --only-binary=:all: "$PUBLISHED_WHEEL_URL#sha256=$PUBLISHED_WHEEL_SHA256" + pip install --require-hashes --no-deps --only-binary=:all: "$PUBLISHED_WHEEL_URL#sha256=$PUBLISHED_WHEEL_SHA256" else pip install --no-deps --only-binary=:all: -e . fi @@ -192,7 +192,7 @@ jobs: cp .docs-provenance.json site/deployment-audit.json cp .docs-api-inventory.json site/api-source-inventory.json - name: Retain rendered review screenshots - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: python-portal-review-${{ github.sha }} path: docs-review diff --git a/docs/javascripts/identity.js b/docs/javascripts/identity.js index 3a405dcb..7af03aaf 100644 --- a/docs/javascripts/identity.js +++ b/docs/javascripts/identity.js @@ -26,7 +26,12 @@ import { watchPublicStatus } from "./status.mjs"; control.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); + // Material also handles Enter globally; one activation changes the theme once. + event.stopPropagation(); control.click(); + requestAnimationFrame(() => { + document.querySelector('.md-header__option input:checked')?.nextElementSibling?.focus(); + }); } }); }); diff --git a/docs/stylesheets/layout.css b/docs/stylesheets/layout.css index 8d98db04..1abb19ef 100644 --- a/docs/stylesheets/layout.css +++ b/docs/stylesheets/layout.css @@ -1164,3 +1164,12 @@ body:has(.dw-landing) .md-content__inner { button[data-clipboard-target] { color: var(--dw-code-muted); } + +.md-code__nav { + background: var(--dw-code-background); +} + +button[data-clipboard-target]:is(:hover, :focus-visible) { + background: var(--dw-code-background); + color: var(--dw-code-foreground); +} diff --git a/scripts/check-docs-layout.py b/scripts/check-docs-layout.py index f6302b37..234a22e2 100644 --- a/scripts/check-docs-layout.py +++ b/scripts/check-docs-layout.py @@ -189,6 +189,9 @@ def exercise_identity(page: Page, theme: str) -> None: toggle.focus() toggle.press("Enter") page.wait_for_function("document.body.dataset.mdColorScheme === 'slate'") + page.wait_for_function( + "document.querySelector('.md-header__option input:checked')?.nextElementSibling === document.activeElement", + ) page.reload(wait_until="networkidle") page.wait_for_function("document.body.dataset.mdColorScheme === 'slate'") page.evaluate("document.fonts.ready") From 185a89e185287adffbac1e98361bdceab9dcd7cf Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:12:29 +0000 Subject: [PATCH 06/12] fix: restore concept-card layout and explicit wheel hash input --- .github/workflows/docs.yml | 10 ++++++++-- .gitignore | 2 ++ docs/stylesheets/layout.css | 9 +++++++-- scripts/check-docs-layout.py | 2 +- tests/test_docs_release_channel.py | 17 +++++++++++++++++ 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e3bb4253..59578c9c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -131,7 +131,7 @@ jobs: if wheel is None: raise SystemExit("Selected package has no universal Python wheel") url = urlsplit(wheel.get("url", "")) - if (url.scheme != "https" or url.netloc != "files.pythonhosted.org" + if (url.geturl() != wheel["url"] or url.scheme != "https" or url.netloc != "files.pythonhosted.org" or url.query or url.fragment or not url.path.startswith("/packages/") or not url.path.endswith("/" + wheel["filename"]) or not re.fullmatch(r"[A-Za-z0-9/_.-]+", url.path)): @@ -154,7 +154,13 @@ jobs: run: | pip install --only-binary=:all: -r requirements-docs.txt if [ -n "$STABLE_TAG" ]; then - pip install --require-hashes --no-deps --only-binary=:all: "$PUBLISHED_WHEEL_URL#sha256=$PUBLISHED_WHEEL_SHA256" + python - <<'PY' + import os + from pathlib import Path + requirement = f"durable-workflow @ {os.environ['PUBLISHED_WHEEL_URL']} --hash=sha256:{os.environ['PUBLISHED_WHEEL_SHA256']}\n" + Path(".docs-published-wheel.txt").write_text(requirement) + PY + pip install --require-hashes --no-deps --only-binary=:all: -r .docs-published-wheel.txt else pip install --no-deps --only-binary=:all: -e . fi diff --git a/.gitignore b/.gitignore index ab8ce130..f0620d69 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ site/ .docs-api-inventory.json .docs-provenance.json .docs-requirements.txt + +.docs-published-wheel.txt diff --git a/docs/stylesheets/layout.css b/docs/stylesheets/layout.css index 1abb19ef..0345bbe5 100644 --- a/docs/stylesheets/layout.css +++ b/docs/stylesheets/layout.css @@ -517,6 +517,7 @@ body:has(.dw-landing) .md-content__inner { } .dw-hero h1 { + margin: 0; font-size: 64px; line-height: 1; letter-spacing: -.03em; @@ -886,6 +887,9 @@ body:has(.dw-landing) .md-content__inner { } @media (max-width: 1100px) { + .dw-role-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } .dw-global-bar { flex-wrap: wrap; gap: 8px 24px; @@ -978,7 +982,7 @@ body:has(.dw-landing) .md-content__inner { width: 100%; } .dw-role-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: 1fr; } .dw-section > h2 { font-size: 32px; @@ -1144,7 +1148,8 @@ body:has(.dw-landing) .md-content__inner { } } -.md-typeset .dw-role-grid { +.md-typeset ul.dw-role-grid { + display: grid; margin: 0; padding: 0; list-style: none; diff --git a/scripts/check-docs-layout.py b/scripts/check-docs-layout.py index 234a22e2..d4762b8f 100644 --- a/scripts/check-docs-layout.py +++ b/scripts/check-docs-layout.py @@ -197,7 +197,7 @@ def exercise_identity(page: Page, theme: str) -> None: page.evaluate("document.fonts.ready") fonts = page.evaluate("[...document.fonts].filter(font => font.status === 'loaded').map(font => font.family)") assert {"Inter", "Space Grotesk", "JetBrains Mono"} <= set(fonts), fonts - assert page.locator(".highlight").first.evaluate( + assert page.locator(".highlight pre").first.evaluate( "element => getComputedStyle(element).backgroundColor", ) == "rgb(15, 21, 19)" switcher = page.locator("[data-sdk-switcher]") diff --git a/tests/test_docs_release_channel.py b/tests/test_docs_release_channel.py index 0b8116be..337e18eb 100644 --- a/tests/test_docs_release_channel.py +++ b/tests/test_docs_release_channel.py @@ -178,3 +178,20 @@ def test_current_workflow_owns_docs_and_the_central_apt_floor() -> None: "durable-workflow/.github/.github/actions/require-fixed-apt@")) chromium = next(i for i, step in enumerate(steps) if step.get("name") == "Install Chromium") assert apt < chromium + + +@pytest.mark.parametrize("url", [ + "https://files.pythonhosted.org/packages/fixture/durable_workflow-2.14.1-py3-none-any.whl?other=1", + "https://example.test/packages/fixture/durable_workflow-2.14.1-py3-none-any.whl", + "https://files.pythonhosted.org/packages/fixture/\ndurable_workflow-2.14.1-py3-none-any.whl", +]) +def test_wheel_requirement_uses_only_an_exact_public_artifact_url(tmp_path: Path, url: str) -> None: + registry = {"info": {"name": "durable-workflow", "version": "2.14.1"}, "urls": [ + {"filename": "durable_workflow-2.14.1-py3-none-any.whl", "packagetype": "bdist_wheel", + "yanked": False, "digests": {"sha256": "1" * 64}, "url": url}, + {"filename": "durable_workflow-2.14.1.tar.gz", "packagetype": "sdist", + "yanked": False, "digests": {"sha256": "2" * 64}}, + ]} + result, values, _ = select_channel(tmp_path, tag="2.14.1", registry=registry) + assert result.returncode != 0 + assert values.get("stable") != "true" From 286139cbbfd5edbbe15b9b4bb5a857fe7309b460 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:13:18 +0000 Subject: [PATCH 07/12] style: align concept cards with the section grid --- docs/stylesheets/layout.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/stylesheets/layout.css b/docs/stylesheets/layout.css index 0345bbe5..19f6d843 100644 --- a/docs/stylesheets/layout.css +++ b/docs/stylesheets/layout.css @@ -1158,6 +1158,7 @@ body:has(.dw-landing) .md-content__inner { display: flex; min-width: 0; margin: 0; + padding: 0; } .dw-role-card { min-height: 190px; From 347b69574c111838c873d97d8fb623dc3d35dd29 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:16:12 +0000 Subject: [PATCH 08/12] ci: review the portal with the same pinned source builder --- .github/workflows/docs-pr.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index 64d22a92..175adb65 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -9,6 +9,8 @@ on: - 'overrides/**' - 'mkdocs.yml' - 'pyproject.toml' + - 'requirements-docs.txt' + - 'scripts/prepare_portal.py' - 'scripts/check-docs-analytics.py' - 'scripts/check-docs-layout.py' - 'scripts/docstring_cross_references.py' @@ -29,6 +31,8 @@ jobs: timeout-minutes: 15 steps: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + persist-credentials: false - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 with: python-version: "3.12" @@ -37,7 +41,11 @@ jobs: with: clients: pip,python - name: Install package and documentation dependencies - run: pip install -e '.[docs]' + run: | + pip install --only-binary=:all: -r requirements-docs.txt + pip install --no-deps --only-binary=:all: -e . + - name: Require the accepted central APT floor before browser system dependencies + uses: durable-workflow/.github/.github/actions/require-fixed-apt@18783be6bef737cc778e4c7282a8797e39daa61a # fixed-apt - name: Install Chromium env: NODE_OPTIONS: --use-openssl-ca @@ -47,7 +55,16 @@ jobs: run: python -m playwright install --with-deps chromium - name: Build and check the portal run: | - mkdocs build --strict + python scripts/prepare_portal.py + mkdocs build --strict --config-file .docs-build.yml python scripts/docstring_cross_references.py --site site python scripts/check-docs-analytics.py site - python scripts/check-docs-layout.py site + python scripts/check-docs-layout.py site --screenshots docs-review + cp .docs-provenance.json docs-review/deployment-audit.json + cp .docs-api-inventory.json docs-review/api-source-inventory.json + - name: Retain rendered review screenshots + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: python-portal-review-${{ github.sha }} + path: docs-review + retention-days: 7 From 8ba9abd597e6db157851da40d6a12e11fffb6601 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 23:31:02 +0000 Subject: [PATCH 09/12] ci: build source documentation without importing the SDK --- .github/workflows/docs-pr.yml | 3 +-- .github/workflows/docs.yml | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index 175adb65..cb036608 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -40,10 +40,9 @@ jobs: uses: durable-workflow/.github/.github/actions/bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7 # clean-trust with: clients: pip,python - - name: Install package and documentation dependencies + - name: Install pinned documentation dependencies run: | pip install --only-binary=:all: -r requirements-docs.txt - pip install --no-deps --only-binary=:all: -e . - name: Require the accepted central APT floor before browser system dependencies uses: durable-workflow/.github/.github/actions/require-fixed-apt@18783be6bef737cc778e4c7282a8797e39daa61a # fixed-apt - name: Install Chromium diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 59578c9c..d4c02004 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -161,8 +161,6 @@ jobs: Path(".docs-published-wheel.txt").write_text(requirement) PY pip install --require-hashes --no-deps --only-binary=:all: -r .docs-published-wheel.txt - else - pip install --no-deps --only-binary=:all: -e . fi - name: Require the accepted central APT floor before browser system dependencies uses: durable-workflow/.github/.github/actions/require-fixed-apt@18783be6bef737cc778e4c7282a8797e39daa61a # fixed-apt From 65f9da489c2ab9289385c5d79bb83e36cd4b1da2 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 6 Nov 2026 00:18:19 +0000 Subject: [PATCH 10/12] style: override directional list margins in the concept grid --- docs/stylesheets/layout.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stylesheets/layout.css b/docs/stylesheets/layout.css index 19f6d843..7dc1ec0e 100644 --- a/docs/stylesheets/layout.css +++ b/docs/stylesheets/layout.css @@ -1154,7 +1154,7 @@ body:has(.dw-landing) .md-content__inner { padding: 0; list-style: none; } -.md-typeset .dw-role-grid > li { +.md-typeset ul.dw-role-grid > li { display: flex; min-width: 0; margin: 0; From 22ca11df6f3a989fdb7a27c75cebc46bad934e32 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 6 Nov 2026 01:04:29 +0000 Subject: [PATCH 11/12] ci: qualify bundled Chromium without system dependency installation --- .github/workflows/docs-pr.yml | 4 +--- .github/workflows/docs.yml | 4 +--- scripts/check-docs-layout.py | 24 ++++++++++++++++++++++++ tests/test_docs_release_channel.py | 17 ++++++++++++----- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index cb036608..3387e5f9 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -43,15 +43,13 @@ jobs: - name: Install pinned documentation dependencies run: | pip install --only-binary=:all: -r requirements-docs.txt - - name: Require the accepted central APT floor before browser system dependencies - uses: durable-workflow/.github/.github/actions/require-fixed-apt@18783be6bef737cc778e4c7282a8797e39daa61a # fixed-apt - name: Install Chromium env: NODE_OPTIONS: --use-openssl-ca NODE_EXTRA_CA_CERTS: '' NODE_USE_SYSTEM_CA: '0' NODE_TLS_REJECT_UNAUTHORIZED: '1' - run: python -m playwright install --with-deps chromium + run: python -m playwright install chromium - name: Build and check the portal run: | python scripts/prepare_portal.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d4c02004..5a78b5e9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -162,15 +162,13 @@ jobs: PY pip install --require-hashes --no-deps --only-binary=:all: -r .docs-published-wheel.txt fi - - name: Require the accepted central APT floor before browser system dependencies - uses: durable-workflow/.github/.github/actions/require-fixed-apt@18783be6bef737cc778e4c7282a8797e39daa61a # fixed-apt - name: Install Chromium env: NODE_OPTIONS: --use-openssl-ca NODE_EXTRA_CA_CERTS: '' NODE_USE_SYSTEM_CA: '0' NODE_TLS_REJECT_UNAUTHORIZED: '1' - run: python -m playwright install --with-deps chromium + run: python -m playwright install chromium - name: Build and check the portal env: CLOUDFLARE_WEB_ANALYTICS_TOKEN: ${{ vars.CLOUDFLARE_WEB_ANALYTICS_TOKEN }} diff --git a/scripts/check-docs-layout.py b/scripts/check-docs-layout.py index d4762b8f..92427f47 100644 --- a/scripts/check-docs-layout.py +++ b/scripts/check-docs-layout.py @@ -7,10 +7,14 @@ import contextlib import functools import http.server +import importlib.metadata +import json +import platform import threading from pathlib import Path from typing import Any +import playwright as playwright_package from playwright.sync_api import Browser, Page, sync_playwright FOCUSED = "element => element === document.activeElement" @@ -276,6 +280,24 @@ def check_api_views(browser: Browser, base_url: str) -> None: page.close() +def browser_evidence(browser: Browser) -> dict[str, str]: + manifest_path = Path(playwright_package.__file__).parent / "driver" / "package" / "browsers.json" + manifest = json.loads(manifest_path.read_text()) + selected = next(item for item in manifest["browsers"] if item["name"] == "chromium-headless-shell") + proof = { + "playwright_version": importlib.metadata.version("playwright"), + "selection": "Playwright default bundled headless Chromium", + "bundled_revision": selected["revision"], + "bundled_version": selected["browserVersion"], + "launched_version": browser.version, + "os": platform.system(), + "architecture": platform.machine(), + } + print(json.dumps(proof, sort_keys=True)) + assert proof["launched_version"] == proof["bundled_version"], "Launched browser differs from the locked bundle" + return proof + + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("site", nargs="?", type=Path, default=Path("site")) @@ -288,8 +310,10 @@ def main() -> int: with serve(site) as base_url, sync_playwright() as playwright: browser = playwright.chromium.launch() try: + proof = browser_evidence(browser) if args.screenshots: args.screenshots.mkdir(parents=True, exist_ok=True) + (args.screenshots / "browser-evidence.json").write_text(json.dumps(proof, indent=2) + "\n") check_landing_views(browser, base_url, args.screenshots) check_api_views(browser, base_url) diff --git a/tests/test_docs_release_channel.py b/tests/test_docs_release_channel.py index 337e18eb..fadfa509 100644 --- a/tests/test_docs_release_channel.py +++ b/tests/test_docs_release_channel.py @@ -4,6 +4,7 @@ import json import os +import shlex import subprocess import sys from pathlib import Path @@ -168,16 +169,22 @@ def test_wrong_documentation_revision_refuses_before_publication_lookup(tmp_path assert calls == "" -def test_current_workflow_owns_docs_and_the_central_apt_floor() -> None: +def test_current_workflow_owns_docs_and_published_sdk_checkouts() -> None: steps = yaml.safe_load(WORKFLOW.read_text())["jobs"]["build"]["steps"] checkouts = [step for step in steps if step.get("uses", "").startswith("actions/checkout@")] assert checkouts[0]["with"]["ref"] == "${{ github.sha }}" assert checkouts[1]["with"]["path"] == ".docs-sdk" assert all(step["with"]["persist-credentials"] is False for step in checkouts) - apt = next(i for i, step in enumerate(steps) if step.get("uses", "").startswith( - "durable-workflow/.github/.github/actions/require-fixed-apt@")) - chromium = next(i for i, step in enumerate(steps) if step.get("name") == "Install Chromium") - assert apt < chromium + + +@pytest.mark.parametrize("workflow", ["docs.yml", "docs-pr.yml"]) +def test_bundled_browser_command_has_no_system_dependency_install_or_fallback(workflow: str) -> None: + document = yaml.safe_load(WORKFLOW.with_name(workflow).read_text()) + steps = next(iter(document["jobs"].values()))["steps"] + chromium = next(step for step in steps if step.get("name") == "Install Chromium") + assert shlex.split(chromium["run"]) == ["python", "-m", "playwright", "install", "chromium"] + assert chromium["env"]["NODE_TLS_REJECT_UNAUTHORIZED"] == "1" + assert chromium["env"]["NODE_OPTIONS"] == "--use-openssl-ca" @pytest.mark.parametrize("url", [ From 32b2a627ea71800a80b87e728f6a537e7355baab Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 6 Nov 2026 01:10:10 +0000 Subject: [PATCH 12/12] ci: enforce exact documentation wheel hashes --- .github/workflows/docs-pr.yml | 2 +- .github/workflows/docs.yml | 2 +- requirements-docs.txt | 128 +++++++++++++++++++---------- tests/test_docs_release_channel.py | 2 + 4 files changed, 89 insertions(+), 45 deletions(-) diff --git a/.github/workflows/docs-pr.yml b/.github/workflows/docs-pr.yml index 3387e5f9..408f8d56 100644 --- a/.github/workflows/docs-pr.yml +++ b/.github/workflows/docs-pr.yml @@ -42,7 +42,7 @@ jobs: clients: pip,python - name: Install pinned documentation dependencies run: | - pip install --only-binary=:all: -r requirements-docs.txt + pip install --require-hashes --only-binary=:all: -r requirements-docs.txt - name: Install Chromium env: NODE_OPTIONS: --use-openssl-ca diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5a78b5e9..d31535ab 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -152,7 +152,7 @@ jobs: PUBLISHED_WHEEL_URL: ${{ steps.release-channel.outputs.wheel_url }} PUBLISHED_WHEEL_SHA256: ${{ steps.release-channel.outputs.wheel_sha256 }} run: | - pip install --only-binary=:all: -r requirements-docs.txt + pip install --require-hashes --only-binary=:all: -r requirements-docs.txt if [ -n "$STABLE_TAG" ]; then python - <<'PY' import os diff --git a/requirements-docs.txt b/requirements-docs.txt index f3884860..3f4d7947 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,43 +1,85 @@ -# Resolved documentation and SDK runtime dependencies for the Python 3.12 portal builder. -anyio==4.16.0 -babel==2.18.0 -backrefs==8.2 -certifi==2026.10.23 -charset-normalizer==3.5.2 -click==8.5.0 -colorama==0.4.6 -fastavro==1.13.1 -ghp-import==2.1.0 -greenlet==3.5.6 -griffelib==2.4.1 -h11==0.16.0 -httpcore==1.0.9 -httpx==0.28.1 -idna==3.20 -Jinja2==3.1.6 -Markdown==3.11.3 -MarkupSafe==3.0.4 -mergedeep==1.3.4 -mkdocs==1.6.1 -mkdocs-autorefs==1.4.5 -mkdocs-get-deps==0.2.2 -mkdocs-material==9.8.0 -mkdocs-material-extensions==1.3.1 -mkdocstrings==1.0.8 -mkdocstrings-python==2.0.10 -packaging==26.3 -paginate==0.5.7 -pathspec==1.1.1 -platformdirs==4.14.5 -playwright==1.64.1 -pyee==14.0.0 -Pygments==2.21.0 -pymdown-extensions==12.3 -python-dateutil==4.0.0 -PyYAML==6.0.3 -pyyaml_env_tag==1.1 -requests==2.34.2 -typing_extensions==4.16.0 -tzdata==2026.6 -urllib3==2.8.0 -watchdog==7.1.0 +# Resolved wheel artifacts for the Linux amd64 / Python 3.12 portal builder. +# anyio-4.16.0-py3-none-any.whl +anyio==4.16.0 --hash=sha256:c66ae634e0b8228396b801741beb3294326d4b6102f7bba4041c0dfb58c29026 +# babel-2.18.0-py3-none-any.whl +babel==2.18.0 --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35 +# backrefs-8.2-py312-none-any.whl +backrefs==8.2 --hash=sha256:e64a50f31e8eee2837009ca7744999ba59125acfd50996ba2349d8f18061e608 +# certifi-2026.10.23-py3-none-any.whl +certifi==2026.10.23 --hash=sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1 +# charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +charset-normalizer==3.5.2 --hash=sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649 +# click-8.5.0-py3-none-any.whl +click==8.5.0 --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 +# colorama-0.4.6-py2.py3-none-any.whl +colorama==0.4.6 --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 +# fastavro-1.13.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +fastavro==1.13.1 --hash=sha256:2f56a127d71e45083306d2650efff827cad0f4b0744dd42cb69c631d77943b1d +# ghp_import-2.1.0-py3-none-any.whl +ghp-import==2.1.0 --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 +# greenlet-3.5.6-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl +greenlet==3.5.6 --hash=sha256:975736b002ed080d124cf81a79cb7e05cb26d6b3f5c7a7b651c0fcce70353aa1 +# griffelib-2.4.1-py3-none-any.whl +griffelib==2.4.1 --hash=sha256:76bc562f936ad1ce198d2cf6d0731bd5c92a7d934b69ef828ef22ee0ffdb62ce +# h11-0.16.0-py3-none-any.whl +h11==0.16.0 --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 +# httpcore-1.0.9-py3-none-any.whl +httpcore==1.0.9 --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 +# httpx-0.28.1-py3-none-any.whl +httpx==0.28.1 --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad +# idna-3.20-py3-none-any.whl +idna==3.20 --hash=sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c +# jinja2-3.1.6-py3-none-any.whl +Jinja2==3.1.6 --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 +# markdown-3.11.3-py3-none-any.whl +Markdown==3.11.3 --hash=sha256:e1a31a112232504d753e5c01bb8c2de90d3f0ba986b063e10770cf7695b09e73 +# markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +MarkupSafe==3.0.4 --hash=sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581 +# mergedeep-1.3.4-py3-none-any.whl +mergedeep==1.3.4 --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307 +# mkdocs-1.6.1-py3-none-any.whl +mkdocs==1.6.1 --hash=sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e +# mkdocs_autorefs-1.4.5-py3-none-any.whl +mkdocs-autorefs==1.4.5 --hash=sha256:68ee484e0800f3ba50cda1bc4a94293adbdfcc010243c322b3f5d66e15d06770 +# mkdocs_get_deps-0.2.2-py3-none-any.whl +mkdocs-get-deps==0.2.2 --hash=sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650 +# mkdocs_material-9.8.0-py3-none-any.whl +mkdocs-material==9.8.0 --hash=sha256:19304a7263c5700428227122b3b2699aa73f432c9de8816876adaec7d7293ce3 +# mkdocs_material_extensions-1.3.1-py3-none-any.whl +mkdocs-material-extensions==1.3.1 --hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31 +# mkdocstrings-1.0.8-py3-none-any.whl +mkdocstrings==1.0.8 --hash=sha256:4b03ed2b863009f1153f0d04aacb22a85b92afc28a0636d85717c92eecca638c +# mkdocstrings_python-2.0.10-py3-none-any.whl +mkdocstrings-python==2.0.10 --hash=sha256:4a3aaeebb25ba5ae57a8f9e75432fe4571984889602f2b2f7d672faa760e9062 +# packaging-26.3-py3-none-any.whl +packaging==26.3 --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c +# paginate-0.5.7-py2.py3-none-any.whl +paginate==0.5.7 --hash=sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591 +# pathspec-1.1.1-py3-none-any.whl +pathspec==1.1.1 --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 +# platformdirs-4.14.5-py3-none-any.whl +platformdirs==4.14.5 --hash=sha256:8e778c6fa421516f4d3369449f8df3b0e1b72c0e0005fe88e3ec772d0a14e750 +# playwright-1.64.1-py3-none-manylinux1_x86_64.whl +playwright==1.64.1 --hash=sha256:577b8d88778c245cfbd145ae17cf53044f084b1b46c55c69081d4069d56057e2 +# pyee-14.0.0-py3-none-any.whl +pyee==14.0.0 --hash=sha256:3ac2d3229a9677f7de2c33d7f52fe25b638a46b19c413fea2edc8c6d0a644e4d +# pygments-2.21.0-py3-none-any.whl +Pygments==2.21.0 --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 +# pymdown_extensions-12.3-py3-none-any.whl +pymdown-extensions==12.3 --hash=sha256:345493a8e3497e24382902903a1c43a28e34a52f2b7571a1c9641429b820ca74 +# python_dateutil-4.0.0-py3-none-any.whl +python-dateutil==4.0.0 --hash=sha256:a5e246f79eb80475e691bf93862cead98decd8a498cf53ba0516fcdc0f50dc1c +# pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +PyYAML==6.0.3 --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc +# pyyaml_env_tag-1.1-py3-none-any.whl +pyyaml_env_tag==1.1 --hash=sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04 +# requests-2.34.2-py3-none-any.whl +requests==2.34.2 --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 +# typing_extensions-4.16.0-py3-none-any.whl +typing_extensions==4.16.0 --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 +# tzdata-2026.6-py2.py3-none-any.whl +tzdata==2026.6 --hash=sha256:0a10eec6e13ef16b627fabed9a5f2ba61a62b1c67f327305f4e7e412a70b4aec +# urllib3-2.8.0-py3-none-any.whl +urllib3==2.8.0 --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 +# watchdog-7.1.0-py3-none-manylinux_2_17_x86_64.whl +watchdog==7.1.0 --hash=sha256:ad6d0b473cb8cde0721692b22b30fcbea8b52102430528874b3593549888bd81 diff --git a/tests/test_docs_release_channel.py b/tests/test_docs_release_channel.py index fadfa509..995276fa 100644 --- a/tests/test_docs_release_channel.py +++ b/tests/test_docs_release_channel.py @@ -185,6 +185,8 @@ def test_bundled_browser_command_has_no_system_dependency_install_or_fallback(wo assert shlex.split(chromium["run"]) == ["python", "-m", "playwright", "install", "chromium"] assert chromium["env"]["NODE_TLS_REJECT_UNAUTHORIZED"] == "1" assert chromium["env"]["NODE_OPTIONS"] == "--use-openssl-ca" + install = next(step["run"] for step in steps if "-r requirements-docs.txt" in step.get("run", "")) + assert "--require-hashes" in shlex.split(install.splitlines()[0]) @pytest.mark.parametrize("url", [