From 8a634d92fba2e73e5b7b6620f2adac31397557e1 Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 20 Aug 2026 13:00:52 -0500 Subject: [PATCH 1/3] Migrate docs portal publishing to docs.nvidia.com --- .github/workflows/deploy-redirects.yaml | 53 ++++++++++ .github/workflows/deploy.yaml | 95 +++++++++++++----- .github/workflows/pr.yaml | 19 ++-- 404.md | 5 +- README.md | 25 +++-- extensions/rapids_docs/api.py | 17 +--- extensions/rapids_docs/lifecycle.py | 3 + extensions/rapids_docs/notices.py | 8 +- extensions/rapids_docs/routes.py | 83 ++++++++++++++++ extensions/rapids_docs/urls.py | 42 ++++---- install/index.md | 4 +- notices/rsn0054.md | 2 +- resources/burn-down-guide.md | 4 +- scripts/generate_redirect_site.py | 125 ++++++++++++++++++++++++ scripts/validate_site.py | 29 +++++- sphinx/conf.py | 8 +- tests/test_redirects.py | 34 +++++++ tests/test_rendering.py | 26 +++++ 18 files changed, 490 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/deploy-redirects.yaml create mode 100644 extensions/rapids_docs/routes.py create mode 100644 scripts/generate_redirect_site.py create mode 100644 tests/test_redirects.py diff --git a/.github/workflows/deploy-redirects.yaml b/.github/workflows/deploy-redirects.yaml new file mode 100644 index 00000000000..84caee4db5f --- /dev/null +++ b/.github/workflows/deploy-redirects.yaml @@ -0,0 +1,53 @@ +name: Deploy docs.rapids.ai redirects + +on: + workflow_dispatch: + inputs: + status: + description: HTTP status for generated redirects + type: choice + options: + - "302" + - "301" + default: "302" + +concurrency: + group: deploy-docs-rapids-ai-redirects + cancel-in-progress: false + +defaults: + run: + shell: bash + +permissions: + contents: read + +jobs: + deploy: + name: Generate and deploy redirect-only site + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + enable-cache: true + - name: Install dependencies + run: uv sync --locked + - name: Generate redirect-only site + env: + REDIRECT_STATUS: ${{ inputs.status }} + run: | + uv run python scripts/generate_redirect_site.py \ + --output _redirect_site/_redirects \ + --status "${REDIRECT_STATUS}" \ + --require-complete + - name: Deploy redirects to docs.rapids.ai + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_API_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DOCS_SITE_ID }} + run: | # zizmor: ignore[adhoc-packages] + npm install --global --force @aschmidt8/netlify-cli + netlify deploy --prod --debug --dir=_redirect_site diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 049bb7c4571..75e45060b1b 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -13,12 +13,13 @@ defaults: run: shell: bash permissions: - id-token: write contents: read jobs: build: - name: Build (and deploy) + name: Build and publish NVIDIA docs portal runs-on: ubuntu-latest + env: + RAPIDS_DOCS_BASE_URL: https://docs.nvidia.com/datascience/ steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -32,26 +33,74 @@ jobs: run: uv sync --locked - name: Build and validate portal run: make check - - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 #v5.1.1 + - name: Configure NVIDIA docs AWS credentials + uses: aws-actions/configure-aws-credentials@e7f100cf4c008499ea8adda475de1042d6975c7b # v6.2.0 with: - role-to-assume: ${{ vars.AWS_ROLE_ARN }} - aws-region: ${{ vars.AWS_REGION }} - role-duration-seconds: 7200 # 2h - - name: Assemble complete documentation site - run: make assemble - - name: Deploy site + aws-access-key-id: ${{ secrets.NVIDIA_DOCS_AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.NVIDIA_DOCS_AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.NVIDIA_DOCS_AWS_REGION }} + unset-current-credentials: true + - name: Publish portal to NVIDIA docs S3 env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_API_TOKEN }} - NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DOCS_SITE_ID }} - # TODO: use official netlify-cli pkg after https://github.com/netlify/cli/issues/1809 - # is resolved and deployed. - run: | # zizmor: ignore[adhoc-packages] - npm install --global --force @aschmidt8/netlify-cli - - ARGS="" - if [ "$GITHUB_REF_NAME" = "main" ]; then - ARGS="--prod" - fi - netlify deploy "$ARGS" \ - --debug \ - --dir=_site + TARGET_BUCKET: ${{ secrets.NVIDIA_DOCS_S3_BUCKET }} + run: | + # The deployment repository owns this subtree. Excluded objects are also + # protected from --delete by the AWS CLI. + aws s3 sync --delete \ + --exclude 'deployment/*' \ + _site "s3://${TARGET_BUCKET}/developer/docs/datascience" + - name: Check out Akamai request template + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: rapidsai/shared-actions + ref: main + path: shared-actions + persist-credentials: false + sparse-checkout: publish-docs + - name: Install Akamai request dependencies + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends xsltproc + - name: Prepare Akamai cache flush request + env: + AKAMAI_ACCESS_TOKEN: ${{ secrets.NVIDIA_DOCS_AKAMAI_ACCESS_TOKEN }} + AKAMAI_CLIENT_SECRET: ${{ secrets.NVIDIA_DOCS_AKAMAI_CLIENT_SECRET }} + AKAMAI_CLIENT_TOKEN: ${{ secrets.NVIDIA_DOCS_AKAMAI_CLIENT_TOKEN }} + AKAMAI_EMAILS_TO_NOTIFY: ${{ secrets.NVIDIA_DOCS_AKAMAI_EMAILS_TO_NOTIFY }} + AKAMAI_HOST: ${{ secrets.NVIDIA_DOCS_AKAMAI_HOST }} + run: | + umask 077 + { + echo '[default]' + echo "client_secret = ${AKAMAI_CLIENT_SECRET}" + echo "host = ${AKAMAI_HOST}" + echo "access_token = ${AKAMAI_ACCESS_TOKEN}" + echo "client_token = ${AKAMAI_CLIENT_TOKEN}" + } > ~/.edgerc + xsltproc \ + --stringparam target-path datascience \ + shared-actions/publish-docs/akamai-eccu-flush.xslt \ + shared-actions/publish-docs/akamai-eccu-flush.xslt \ + | sed 's/xmlns:match="x" //' > /tmp/flush.xml + jq -Rnc \ + --arg emails "${AKAMAI_EMAILS_TO_NOTIFY}" \ + '$emails | split(",") | map(select(length > 0))' \ + > /tmp/email-addresses.json + - name: Submit Akamai cache flush request + env: + REQUEST_NAME: rapidsai-docs-${{ github.run_id }} + run: | + uv tool run --from httpie-edgegrid http \ + --ignore-stdin \ + --auth-type edgegrid \ + -a default: \ + :/eccu-api/v1/requests \ + metadata=@/tmp/flush.xml \ + propertyName=docs.nvidia.com \ + propertyNameExactMatch=true \ + propertyType=HOST_HEADER \ + requestName="${REQUEST_NAME}" \ + statusUpdateEmails:=@/tmp/email-addresses.json + - name: Delete Akamai credentials + if: always() + run: rm -f ~/.edgerc diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index d35a3931dd1..82e7051c0ac 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -19,7 +19,6 @@ jobs: permissions: actions: read contents: read - id-token: write packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] @@ -39,9 +38,8 @@ jobs: docs: runs-on: ubuntu-latest - permissions: - contents: read - id-token: write + env: + RAPIDS_DOCS_BASE_URL: https://docs.nvidia.com/datascience/ steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -55,11 +53,8 @@ jobs: run: uv sync --locked - name: Build and validate portal run: make check - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 - with: - role-to-assume: ${{ vars.AWS_ROLE_ARN }} - aws-region: ${{ vars.AWS_REGION }} - role-duration-seconds: 7200 - - name: Assemble and validate complete documentation site - run: make assemble + - name: Generate redirect preview + run: | + uv run python scripts/generate_redirect_site.py \ + --output _redirect_site/_redirects \ + --status 302 diff --git a/404.md b/404.md index 236231eb33b..4e7bd8bd838 100644 --- a/404.md +++ b/404.md @@ -6,12 +6,13 @@ orphan: true We could not find the page you were looking for. diff --git a/README.md b/README.md index 29cd6271ad2..36145f61ac9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # NVIDIA RAPIDS Documentation This repository contains the source for the -[NVIDIA RAPIDS documentation site](https://docs.rapids.ai/). The site is built +[NVIDIA RAPIDS documentation site](https://docs.nvidia.com/datascience/). The site is built with Sphinx and the NVIDIA Sphinx theme. ## Build the site @@ -18,9 +18,10 @@ override it with `PORT` (for example, `make serve PORT=8080`). ## Build the full site -The complete docs site imports versioned API documentation and the deployment -documentation from the private `rapidsai-docs` S3 bucket. Configure a read-only -AWS profile named `rapids-docs`, then run: +The legacy complete-site build imports versioned API documentation and the +deployment documentation from the private `rapidsai-docs` S3 bucket. It is kept +temporarily as a rollback aid while the portal moves to `docs.nvidia.com`. +Configure a read-only AWS profile named `rapids-docs`, then run: ```shell AWS_PROFILE=rapids-docs make full @@ -37,10 +38,18 @@ make check Run checks including linting, tests, and a local build. Pull requests opened against `rapidsai/docs` are copied to a -`pull-request/` branch by the RAPIDS copy-PR bot. That branch runs the -same validation and dry-runs assembly of the complete S3-backed documentation -tree without deploying it. Netlify's repository integration separately creates -a site preview. Merges to `main` deploy the production site. +`pull-request/` branch by the RAPIDS copy-PR bot. That branch builds the +portal with its canonical `https://docs.nvidia.com/datascience/` base URL and +generates a preview of the eventual `docs.rapids.ai` redirects. Netlify's +repository integration separately creates a site preview. + +Merges to `main` and the daily scheduled workflow publish the portal to +`docs.nvidia.com/datascience/`. The independently published +`docs.nvidia.com/datascience/deployment/` subtree is explicitly preserved. +Production publishing to `docs.rapids.ai` remains frozen until the manually +triggered redirect workflow passes its migration-completeness gate. Run that +workflow with temporary redirects first, then switch to permanent redirects +after the cutover is verified. ## Repository layout diff --git a/extensions/rapids_docs/api.py b/extensions/rapids_docs/api.py index 5e113c0c01a..3a66fa42e2f 100644 --- a/extensions/rapids_docs/api.py +++ b/extensions/rapids_docs/api.py @@ -3,22 +3,7 @@ """Render the API documentation listings.""" - -def _version_label(project: dict, version_name: str, releases: dict) -> str: - override = project.get("version-overrides", {}).get(version_name) - if override: - return str(override) - version_key = "ucxx_version" if "ucxx" in project["path"].lower() else "version" - return str(releases[version_name][version_key]) - - -def _documentation_url(project: dict, version_name: str, version: str) -> str: - first_docs_nvidia_com_release = project["first_docs_nvidia_com_release"] - if first_docs_nvidia_com_release and tuple(map(int, version.split("."))) >= tuple( - map(int, first_docs_nvidia_com_release.split(".")) - ): - return f"https://docs.nvidia.com/{project['path']}/{version}/" - return f"https://docs.rapids.ai/api/{project['path']}/{version_name}/" +from .routes import _documentation_url, _version_label def _api_docs(data: dict, section: str) -> str: diff --git a/extensions/rapids_docs/lifecycle.py b/extensions/rapids_docs/lifecycle.py index 4f594e01b74..d6d3cebf8bc 100644 --- a/extensions/rapids_docs/lifecycle.py +++ b/extensions/rapids_docs/lifecycle.py @@ -27,6 +27,8 @@ def _jinja_environment(app) -> Environment: def _context(app, docname: str = "index") -> dict: data = app.rapids_portal_data + config = getattr(app, "config", None) + site_baseurl = getattr(config, "html_baseurl", "https://docs.rapids.ai/") return { **data, "api_docs": lambda section: _api_docs(data, section), @@ -36,6 +38,7 @@ def _context(app, docname: str = "index") -> dict: ), "platform_support_content": lambda: _platform_support(data), "previous_schedules": lambda: _previous_schedules(data), + "site_baseurl": site_baseurl.rstrip("/") + "/", } diff --git a/extensions/rapids_docs/notices.py b/extensions/rapids_docs/notices.py index 79038357c88..6df14ef48df 100644 --- a/extensions/rapids_docs/notices.py +++ b/extensions/rapids_docs/notices.py @@ -13,6 +13,7 @@ from bs4 import BeautifulSoup from .dates import _date, _long_date +from .routes import _site_url _NOTICE_STATUS_COLORS = {"blue", "green", "purple", "red", "yellow"} @@ -103,11 +104,12 @@ def _build_rss(app, exception) -> None: ElementTree.SubElement( channel, "description" ).text = "Notices communicate and document changes in RAPIDS for contributors, developers, users, and the community." - ElementTree.SubElement(channel, "link").text = "https://docs.rapids.ai/notices/" + base_url = app.config.html_baseurl + ElementTree.SubElement(channel, "link").text = _site_url(base_url, "/notices/") ElementTree.SubElement( channel, "{http://www.w3.org/2005/Atom}link", - href="https://docs.rapids.ai/notices/feed.xml", + href=_site_url(base_url, "/notices/feed.xml"), rel="self", type="application/rss+xml", ) @@ -129,7 +131,7 @@ def _build_rss(app, exception) -> None: ElementTree.SubElement(item, "description").text = html.unescape(description) published = notice.get("notice_updated") or notice["notice_created"] ElementTree.SubElement(item, "pubDate").text = _rss_date(published) - url = f"https://docs.rapids.ai/notices/{Path(notice['docname']).name}/" + url = _site_url(base_url, f"/notices/{Path(notice['docname']).name}/") ElementTree.SubElement(item, "link").text = url ElementTree.SubElement(item, "guid", isPermaLink="true").text = url for category in [*notice.get("tags", []), *notice.get("categories", [])]: diff --git a/extensions/rapids_docs/routes.py b/extensions/rapids_docs/routes.py new file mode 100644 index 00000000000..548fe3c9b85 --- /dev/null +++ b/extensions/rapids_docs/routes.py @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Resolve portal and API documentation URLs for each hosting target.""" + +from urllib.parse import urljoin, urlsplit, urlunsplit + + +def _version_label(project: dict, version_name: str, releases: dict) -> str: + override = project.get("version-overrides", {}).get(version_name) + if override: + return str(override) + version_key = "ucxx_version" if "ucxx" in project["path"].lower() else "version" + return str(releases[version_name][version_key]) + + +def _version_tuple(version: str) -> tuple[int, ...]: + return tuple(map(int, version.split("."))) + + +def _with_suffix(base_url: str, suffix: str) -> str: + return urljoin(base_url.rstrip("/") + "/", suffix.lstrip("/")) + + +def _documentation_url( + project: dict, + version_name: str, + version: str, + suffix: str = "", +) -> str: + if external_docs_url := project.get("external_docs_url"): + return _with_suffix(external_docs_url, suffix) + + first_nvidia_release = project["first_docs_nvidia_com_release"] + if first_nvidia_release and _version_tuple(version) >= _version_tuple(first_nvidia_release): + base_url = f"https://docs.nvidia.com/{project['path']}/{version}/" + else: + base_url = f"https://docs.rapids.ai/api/{project['path']}/{version_name}/" + return _with_suffix(base_url, suffix) + + +def _project_for_path(data: dict, project_path: str) -> dict | None: + for section in ("apis", "libs", "inactive-projects"): + for project in data["docs"][section].values(): + if project["path"].lower() == project_path.lower(): + return project + return None + + +def _api_documentation_url(url: str, data: dict | None) -> str | None: + if data is None: + return None + + parsed = urlsplit(url) + parts = parsed.path.strip("/").split("/") + if len(parts) < 3 or parts[0] != "api" or parts[2] not in {"legacy", "stable", "nightly"}: + return None + + project = _project_for_path(data, parts[1]) + if project is None or not project["versions"].get(parts[2]): + return None + + version_name = parts[2] + version = _version_label(project, version_name, data["releases"]) + suffix = "/".join(parts[3:]) + if suffix and parsed.path.endswith("/"): + suffix += "/" + destination = urlsplit(_documentation_url(project, version_name, version, suffix)) + return urlunsplit( + ( + destination.scheme, + destination.netloc, + destination.path, + parsed.query, + parsed.fragment, + ) + ) + + +def _site_url(base_url: str, url: str, data: dict | None = None) -> str: + if not url.startswith("/") or url.startswith("//"): + return url + return _api_documentation_url(url, data) or _with_suffix(base_url, url) diff --git a/extensions/rapids_docs/urls.py b/extensions/rapids_docs/urls.py index 49d5943996a..85d7cf23ec8 100644 --- a/extensions/rapids_docs/urls.py +++ b/extensions/rapids_docs/urls.py @@ -1,13 +1,14 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """Rewrite root-relative portal URLs for the configured site base URL.""" import re -from urllib.parse import urljoin from docutils import nodes +from .routes import _site_url + _HTML_URL_RE = re.compile(r"(?P\b(?:href|src)=['\"])(?P/(?!/)[^'\"]*)") _TOCTREE_RE = re.compile(r"^```\{toctree\}\n.*?^```$", re.MULTILINE | re.DOTALL) _TOCTREE_ENTRY_RE = re.compile( @@ -15,19 +16,13 @@ ) -def _absolute_url(base_url: str, url: str) -> str: - return urljoin(base_url.rstrip("/") + "/", url.lstrip("/")) - +def _rewrite_url(url: str, base_url: str, data: dict | None = None) -> str: + return _site_url(base_url, url, data) -def _rewrite_url(url: str, base_url: str) -> str: - if url.startswith("/") and not url.startswith("//"): - return _absolute_url(base_url, url) - return url - -def _rewrite_html_urls(text: str, base_url: str) -> str: +def _rewrite_html_urls(text: str, base_url: str, data: dict | None = None) -> str: return _HTML_URL_RE.sub( - lambda match: match["attribute"] + _rewrite_url(match["url"], base_url), text + lambda match: match["attribute"] + _rewrite_url(match["url"], base_url, data), text ) @@ -35,10 +30,13 @@ def _rewrite_toctree_urls(app, docname: str, source: list[str]) -> None: base_url = app.config.html_baseurl if not base_url: return + data = getattr(app, "rapids_portal_data", None) def rewrite_toctree(match: re.Match) -> str: return _TOCTREE_ENTRY_RE.sub( - lambda entry: entry["prefix"] + _absolute_url(base_url, entry["url"]) + entry["suffix"], + lambda entry: ( + entry["prefix"] + _rewrite_url(entry["url"], base_url, data) + entry["suffix"] + ), match[0], ) @@ -49,16 +47,17 @@ def _rewrite_absolute_urls(app, doctree, docname: str) -> None: base_url = app.config.html_baseurl if not base_url: return + data = getattr(app, "rapids_portal_data", None) for node in doctree.findall(nodes.reference): uri = node.get("refuri", "") if uri.startswith("/") and not uri.startswith("//"): - node["refuri"] = _absolute_url(base_url, uri) + node["refuri"] = _rewrite_url(uri, base_url, data) for node in doctree.findall(nodes.raw): if node.get("format") != "html": continue - text = _rewrite_html_urls(node.astext(), base_url) + text = _rewrite_html_urls(node.astext(), base_url, data) if text != node.astext(): node.rawsource = text node.clear() @@ -69,6 +68,7 @@ def _rewrite_theme_urls(app, pagename: str, templatename: str, context: dict, do base_url = app.config.html_baseurl if not base_url: return + data = getattr(app, "rapids_portal_data", None) pathto = context["pathto"] css_tag = context["css_tag"] @@ -76,12 +76,14 @@ def _rewrite_theme_urls(app, pagename: str, templatename: str, context: dict, do toctree = context["toctree"] def rewrite_path(*args, **kwargs) -> str: - return _rewrite_url(pathto(*args, **kwargs), base_url) + return _rewrite_url(pathto(*args, **kwargs), base_url, data) context["pathto"] = rewrite_path - context["css_tag"] = lambda css: _rewrite_html_urls(css_tag(css), base_url) - context["js_tag"] = lambda js: _rewrite_html_urls(js_tag(js), base_url) - context["toctree"] = lambda **kwargs: _rewrite_html_urls(toctree(**kwargs) or "", base_url) + context["css_tag"] = lambda css: _rewrite_html_urls(css_tag(css), base_url, data) + context["js_tag"] = lambda js: _rewrite_html_urls(js_tag(js), base_url, data) + context["toctree"] = lambda **kwargs: _rewrite_html_urls( + toctree(**kwargs) or "", base_url, data + ) for key in ("favicon_url", "logo_url"): if key in context: - context[key] = _rewrite_url(context[key], base_url) + context[key] = _rewrite_url(context[key], base_url, data) diff --git a/install/index.md b/install/index.md index 8231d4625f8..2b7c2b9114f 100644 --- a/install/index.md +++ b/install/index.md @@ -109,7 +109,7 @@ See the WSL2 setup [troubleshooting section](#wsl2-troubleshooting). All provisioned systems need to be RAPIDS capable. Below is a list of requirements for the current release. For requirements of historical RAPIDS versions, see [Platform Support](/platform-support/). **GPU:** NVIDIA Volta™ or higher with [compute capability](https://developer.nvidia.com/cuda-gpus) 7.0+ -- Pascal™ GPU support was [removed in 24.02](https://docs.rapids.ai/notices/rsn0034/). Compute capability 7.0+ is required for RAPIDS 24.02 and later. +- Pascal™ GPU support was [removed in 24.02](/notices/rsn0034/). Compute capability 7.0+ is required for RAPIDS 24.02 and later. **OS:** - Linux distributions with `glibc>=2.28` (released in August 2018), which include the following: @@ -150,7 +150,7 @@ Aside from the system requirements, other considerations for best performance in
### Cloud Instance GPUs -If you do not have access to GPU hardware, there are several cloud service providers (CSP) that are RAPIDS enabled. Learn how to deploy RAPIDS on AWS, Azure, GCP, and IBM cloud on our [Cloud Deployment Page](https://docs.rapids.ai/deployment/stable/cloud/index.html). +If you do not have access to GPU hardware, there are several cloud service providers (CSP) that are RAPIDS enabled. Learn how to deploy RAPIDS on AWS, Azure, GCP, and IBM cloud on our [Cloud Deployment Page](/deployment/stable/cloud/). Several services also offer **free and limited** trials with GPU resources: - [Amazon SageMaker Studio Lab](https://studiolab.sagemaker.aws/) diff --git a/notices/rsn0054.md b/notices/rsn0054.md index b1472a90fb2..b1b94e70eec 100644 --- a/notices/rsn0054.md +++ b/notices/rsn0054.md @@ -29,7 +29,7 @@ notice_updated: 2025-12-12 RAPIDS is raising the minimum required CUDA version for the entire software suite from 12.0 to 12.2 starting with the v25.12 release. All of RAPIDS will require a minimum of CUDA 12.2 including containers, all published packages (wheels and conda), and compilation from source in Release `v25.12`, scheduled for December 11, 2025. `v25.10` will be the last RAPIDS release to support CUDA 12.0 and 12.1 runtimes in any format. -We are continuing support for CUDA 12 and 13 containers, with CUDA major version tags. See [RSN 53](https://docs.rapids.ai/notices/rsn0053/) for more information. +We are continuing support for CUDA 12 and 13 containers, with CUDA major version tags. See [RSN 53](/notices/rsn0053/) for more information. ## Impact diff --git a/resources/burn-down-guide.md b/resources/burn-down-guide.md index cb2b7f27abc..90a652b4488 100644 --- a/resources/burn-down-guide.md +++ b/resources/burn-down-guide.md @@ -77,7 +77,7 @@ Suggested template: :warning: cuDF/cuML/cuGraph/RMM/cuStrings/dask-cuda v0.9 have moved to the burn down stage - `branch-0.10` is available but *not the default branch yet* *Burn down ends Tuesday, August 13* -See https://docs.rapids.ai/maintainers for full v0.9 schedule +See the [maintainer documentation](/maintainers/) for the full v0.9 schedule. Please keep the following in mind: - *Stop adding issues/PRs for v0.9*; unless deemed critical by the PICs @@ -86,7 +86,7 @@ Please keep the following in mind: - *Move open issues/PRs* to the new v0.9 boards and branch - Forward-mergers are in place to merge updates from v0.9 to v0.10 - https://gpuci.gpuopenanalytics.com/view/gpuCI%20-%20forward-mergers/ -See https://docs.rapids.ai/releases/process/#burn-down for more details on the burn down and development process. +See the [release process](/releases/process/#burn-down) for more details on the burn down and development process. *v0.9 boards:* - cuDF - https://github.com/rapidsai/cudf/projects/15 diff --git a/scripts/generate_redirect_site.py b/scripts/generate_redirect_site.py new file mode 100644 index 00000000000..8d47e0cceb9 --- /dev/null +++ b/scripts/generate_redirect_site.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Generate the redirect-only Netlify site used after the NVIDIA docs cutover.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +from extensions.rapids_docs.routes import _documentation_url, _version_label # noqa: E402 + +DOCS_CONFIG = ROOT / "_data" / "docs.yml" +RELEASES_CONFIG = ROOT / "_data" / "releases.json" +MANUAL_REDIRECTS = ROOT / "_redirects" +SECTIONS = ("apis", "libs", "inactive-projects") +VERSION_NAMES = ("legacy", "stable", "nightly") + + +def _rule(source: str, destination: str, status: int) -> str: + return f"{source} {destination} {status}!" + + +def _project_rules(project: dict, releases: dict, status: int) -> tuple[list[str], list[str]]: + rules = [] + incomplete = [] + emitted_sources = set() + project_path = project["path"] + + for version_name in VERSION_NAMES: + if not project["versions"].get(version_name): + continue + version = _version_label(project, version_name, releases) + destination = _documentation_url(project, version_name, version) + if not project.get("external_docs_url") and destination.startswith( + "https://docs.rapids.ai/" + ): + incomplete.append(f"{project_path}:{version_name}:{version}") + continue + + for source_version in (version_name, version): + source = f"/api/{project_path}/{source_version}" + if source in emitted_sources: + continue + emitted_sources.add(source) + rules.extend( + [ + _rule(source, destination, status), + _rule( + f"{source}/*", + _documentation_url(project, version_name, version, ":splat"), + status, + ), + ] + ) + + return rules, incomplete + + +def generate_redirects(*, status: int, require_complete: bool = False) -> str: + docs = yaml.safe_load(DOCS_CONFIG.read_text()) + releases = json.loads(RELEASES_CONFIG.read_text()) + rules = [ + "# Generated redirect-only site for the docs.nvidia.com migration.", + "# Manual compatibility redirects run first and may intentionally chain.", + MANUAL_REDIRECTS.read_text().rstrip(), + "", + "# API documentation aliases and numeric versions.", + ] + incomplete = [] + + for section in SECTIONS: + for project in docs[section].values(): + project_rules, project_incomplete = _project_rules(project, releases, status) + rules.extend(project_rules) + incomplete.extend(project_incomplete) + + if require_complete and incomplete: + details = "\n- ".join(incomplete) + raise SystemExit( + "Redirect cutover is blocked because these published docs still resolve to " + f"docs.rapids.ai:\n- {details}" + ) + + rules.extend( + [ + "", + "# Deployment docs are independently published below the datascience prefix.", + _rule( + "/deployment/*", + "https://docs.nvidia.com/datascience/deployment/:splat", + status, + ), + "", + "# Remaining portal routes move beneath docs.nvidia.com/datascience.", + _rule("/*", "https://docs.nvidia.com/datascience/:splat", status), + "", + ] + ) + return "\n".join(rules) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--status", choices=(301, 302), default=302, type=int) + parser.add_argument("--require-complete", action="store_true") + args = parser.parse_args() + + output = generate_redirects(status=args.status, require_complete=args.require_complete) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(output) + print(f"Wrote {args.output}") + + +if __name__ == "__main__": + main() diff --git a/scripts/validate_site.py b/scripts/validate_site.py index f349710f7ba..8940f10be8a 100644 --- a/scripts/validate_site.py +++ b/scripts/validate_site.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """Validate the rendered RAPIDS portal and optional assembled documentation tree.""" @@ -7,6 +7,7 @@ from __future__ import annotations import argparse +import os import re from pathlib import Path from urllib.parse import urlsplit @@ -66,6 +67,10 @@ def main() -> None: missing.append(f"{len(missing_search_notices)} individual notices are absent from search") home = (args.site / "index.html").read_text(errors="ignore") + expected_baseurl = os.environ.get("RAPIDS_DOCS_BASE_URL", "https://docs.rapids.ai/") + expected_baseurl = expected_baseurl.rstrip("/") + "/" + if f' None: if markdown_links: missing.append("same-site Markdown links remain:\n " + "\n ".join(markdown_links)) + if expected_baseurl == "https://docs.nvidia.com/datascience/": + misplaced_api_links = [] + legacy_portal_links = [] + for path in html_files: + text = path.read_text(errors="ignore") + if re.search( + r"https://docs\.nvidia\.com/datascience/api/[^\"']+/(legacy|stable|nightly)/", + text, + ): + misplaced_api_links.append(str(path.relative_to(args.site))) + if re.search(r"https://docs\.rapids\.ai/(?!api/)", text): + legacy_portal_links.append(str(path.relative_to(args.site))) + if misplaced_api_links: + missing.append( + "migrated API links incorrectly point below /datascience/api in: " + + ", ".join(misplaced_api_links) + ) + if legacy_portal_links: + missing.append( + "portal links still point to docs.rapids.ai in: " + ", ".join(legacy_portal_links) + ) + if args.full: full_paths = [ "api/cudf/stable", diff --git a/sphinx/conf.py b/sphinx/conf.py index 5f68a3ef968..8a7b50232c1 100644 --- a/sphinx/conf.py +++ b/sphinx/conf.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations @@ -56,7 +56,11 @@ html_theme = "nvidia_sphinx_theme" html_static_path = ["_static"] html_extra_path = ["../_redirects"] -html_baseurl = "https://docs.rapids.ai/" +html_baseurl = ( + os.environ.get("RAPIDS_DOCS_BASE_URL") + or os.environ.get("DEPLOY_PRIME_URL") + or "https://docs.rapids.ai/" +).rstrip("/") + "/" html_scaled_image_link = False html_theme_options = { diff --git a/tests/test_redirects.py b/tests/test_redirects.py new file mode 100644 index 00000000000..505521be2fc --- /dev/null +++ b/tests/test_redirects.py @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json + +import pytest +import yaml + +from scripts import generate_redirect_site + + +def test_redirects_route_migrated_docs_and_portal() -> None: + redirects = generate_redirect_site.generate_redirects(status=302) + releases = json.loads(generate_redirect_site.RELEASES_CONFIG.read_text()) + stable = releases["stable"]["version"] + + assert f"/api/cudf/stable/* https://docs.nvidia.com/cudf/{stable}/:splat 302!" in redirects + assert f"/api/cudf/{stable} https://docs.nvidia.com/cudf/{stable}/ 302!" in redirects + assert "/deployment/* https://docs.nvidia.com/datascience/deployment/:splat 302!" in redirects + assert redirects.rstrip().endswith("/* https://docs.nvidia.com/datascience/:splat 302!") + + +def test_redirects_route_external_unversioned_docs() -> None: + redirects = generate_redirect_site.generate_redirects(status=301) + + assert "/api/cuvs/stable/* https://docs.nvidia.com/cuvs/:splat 301!" in redirects + + +def test_redirect_cutover_requires_complete_migration_metadata() -> None: + docs = yaml.safe_load(generate_redirect_site.DOCS_CONFIG.read_text()) + assert docs["apis"]["dask-cudf"]["first_docs_nvidia_com_release"] is None + + with pytest.raises(SystemExit, match="dask-cudf:stable"): + generate_redirect_site.generate_redirects(status=302, require_complete=True) diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 1416c4a1288..9d37b7078df 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -171,6 +171,32 @@ def test_absolute_url_rewriting() -> None: ) +def test_api_documentation_url_rewriting() -> None: + app = SimpleNamespace( + config=SimpleNamespace(html_baseurl="https://docs.nvidia.com/datascience/"), + rapids_portal_data=portal_data._load_data(APP), + ) + migrated = nodes.reference( + "", + "cuDF guide", + refuri="/api/cudf/stable/user_guide/10min/?source=portal#intro", + ) + unmigrated = nodes.reference( + "", + "Dask-cuDF guide", + refuri="/api/dask-cudf/stable/user_guide/", + ) + doctree = nodes.container("", migrated, unmigrated) + + urls._rewrite_absolute_urls(app, doctree, "user-guide/index") + + stable_version = app.rapids_portal_data["releases"]["stable"]["version"] + assert migrated["refuri"] == ( + f"https://docs.nvidia.com/cudf/{stable_version}/user_guide/10min/?source=portal#intro" + ) + assert unmigrated["refuri"] == ("https://docs.rapids.ai/api/dask-cudf/stable/user_guide/") + + def test_theme_url_rewriting() -> None: app = SimpleNamespace( config=SimpleNamespace(html_baseurl="https://docs.example.com/datascience/") From 501baca6dc388aa56c8ef57fd4d3b6d382a95fbe Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 20 Aug 2026 13:48:28 -0500 Subject: [PATCH 2/3] Keep unmigrated API docs on docs.rapids.ai --- .github/workflows/deploy-redirects.yaml | 35 ++++++++--- .github/workflows/pr.yaml | 18 +++++- README.md | 34 +++++++---- scripts/generate_redirect_site.py | 78 +++++++++++++++++-------- scripts/validate_site.py | 3 + tests/test_redirects.py | 24 ++++---- 6 files changed, 135 insertions(+), 57 deletions(-) diff --git a/.github/workflows/deploy-redirects.yaml b/.github/workflows/deploy-redirects.yaml index 84caee4db5f..dbce03b888f 100644 --- a/.github/workflows/deploy-redirects.yaml +++ b/.github/workflows/deploy-redirects.yaml @@ -1,6 +1,8 @@ -name: Deploy docs.rapids.ai redirects +name: Deploy docs.rapids.ai compatibility site on: + schedule: + - cron: "0 9 * * *" workflow_dispatch: inputs: status: @@ -10,6 +12,9 @@ on: - "302" - "301" default: "302" + push: + branches: + - main concurrency: group: deploy-docs-rapids-ai-redirects @@ -21,11 +26,14 @@ defaults: permissions: contents: read + id-token: write jobs: deploy: - name: Generate and deploy redirect-only site + name: Assemble and deploy compatibility site runs-on: ubuntu-latest + env: + RAPIDS_DOCS_BASE_URL: https://docs.rapids.ai/ steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -36,18 +44,27 @@ jobs: enable-cache: true - name: Install dependencies run: uv sync --locked - - name: Generate redirect-only site + - name: Build and validate portal + run: make check + - name: Configure source AWS credentials + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + with: + role-to-assume: ${{ vars.AWS_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION }} + role-duration-seconds: 7200 + - name: Assemble and validate API documentation + run: make assemble + - name: Generate compatibility redirects env: - REDIRECT_STATUS: ${{ inputs.status }} + REDIRECT_STATUS: ${{ inputs.status || vars.DOCS_RAPIDSAI_REDIRECT_STATUS || '302' }} run: | uv run python scripts/generate_redirect_site.py \ - --output _redirect_site/_redirects \ - --status "${REDIRECT_STATUS}" \ - --require-complete - - name: Deploy redirects to docs.rapids.ai + --output _site/_redirects \ + --status "${REDIRECT_STATUS}" + - name: Deploy compatibility site to docs.rapids.ai env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_API_TOKEN }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DOCS_SITE_ID }} run: | # zizmor: ignore[adhoc-packages] npm install --global --force @aschmidt8/netlify-cli - netlify deploy --prod --debug --dir=_redirect_site + netlify deploy --prod --debug --dir=_site diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 82e7051c0ac..a1dd1e4d39d 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -19,6 +19,7 @@ jobs: permissions: actions: read contents: read + id-token: write packages: read pull-requests: read secrets: inherit # zizmor: ignore[secrets-inherit] @@ -38,8 +39,11 @@ jobs: docs: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write env: - RAPIDS_DOCS_BASE_URL: https://docs.nvidia.com/datascience/ + RAPIDS_DOCS_BASE_URL: https://docs.rapids.ai/ steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -53,8 +57,16 @@ jobs: run: uv sync --locked - name: Build and validate portal run: make check - - name: Generate redirect preview + - name: Configure source AWS credentials + uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 + with: + role-to-assume: ${{ vars.AWS_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION }} + role-duration-seconds: 7200 + - name: Assemble and validate API documentation + run: make assemble + - name: Generate compatibility redirects run: | uv run python scripts/generate_redirect_site.py \ - --output _redirect_site/_redirects \ + --output _site/_redirects \ --status 302 diff --git a/README.md b/README.md index 36145f61ac9..86abe10f199 100644 --- a/README.md +++ b/README.md @@ -16,18 +16,28 @@ make serve The rendered site is written to `_site`. The server uses port 8000 by default; override it with `PORT` (for example, `make serve PORT=8080`). -## Build the full site +## Build the docs.rapids.ai compatibility site -The legacy complete-site build imports versioned API documentation and the -deployment documentation from the private `rapidsai-docs` S3 bucket. It is kept -temporarily as a rollback aid while the portal moves to `docs.nvidia.com`. -Configure a read-only AWS profile named `rapids-docs`, then run: +The compatibility site imports versioned API documentation and deployment +documentation from the private `rapidsai-docs` S3 bucket. It continues to serve +real API content from `docs.rapids.ai/api/` until each library migrates +to `docs.nvidia.com`. Configure a read-only AWS profile named `rapids-docs`, then +run: ```shell AWS_PROFILE=rapids-docs make full ``` This applies the RAPIDS library/version selectors to the imported documentation. +Generate the Netlify redirect file after assembly: + +```shell +uv run python scripts/generate_redirect_site.py --output _site/_redirects --status 302 +``` + +The generated rules redirect portal pages and only those library versions whose +migration metadata points at `docs.nvidia.com`. Unmatched API routes and shared +assets remain real files in the assembled site. ## Validation @@ -39,17 +49,17 @@ Run checks including linting, tests, and a local build. Pull requests opened against `rapidsai/docs` are copied to a `pull-request/` branch by the RAPIDS copy-PR bot. That branch builds the -portal with its canonical `https://docs.nvidia.com/datascience/` base URL and -generates a preview of the eventual `docs.rapids.ai` redirects. Netlify's -repository integration separately creates a site preview. +full `docs.rapids.ai` compatibility site and validates its generated redirects. +Netlify's repository integration separately creates a site preview. Merges to `main` and the daily scheduled workflow publish the portal to `docs.nvidia.com/datascience/`. The independently published `docs.nvidia.com/datascience/deployment/` subtree is explicitly preserved. -Production publishing to `docs.rapids.ai` remains frozen until the manually -triggered redirect workflow passes its migration-completeness gate. Run that -workflow with temporary redirects first, then switch to permanent redirects -after the cutover is verified. +The companion compatibility workflow assembles and publishes the remaining API +documentation to `docs.rapids.ai`, with redirects for migrated API versions and +portal routes. Redirects default to temporary status `302`; set the +`DOCS_RAPIDSAI_REDIRECT_STATUS` repository variable to `301` after the cutover is +verified, or select a status in a manual workflow run. ## Repository layout diff --git a/scripts/generate_redirect_site.py b/scripts/generate_redirect_site.py index 8d47e0cceb9..7f6ca0d794b 100644 --- a/scripts/generate_redirect_site.py +++ b/scripts/generate_redirect_site.py @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -"""Generate the redirect-only Netlify site used after the NVIDIA docs cutover.""" +"""Generate redirects for the hybrid docs.rapids.ai compatibility site.""" from __future__ import annotations @@ -23,15 +23,28 @@ MANUAL_REDIRECTS = ROOT / "_redirects" SECTIONS = ("apis", "libs", "inactive-projects") VERSION_NAMES = ("legacy", "stable", "nightly") +PORTAL_PREFIXES = ( + "_static", + "contributing", + "install", + "licenses", + "maintainers", + "notices", + "platform-support", + "releases", + "resources", + "user-guide", + "visualization", +) +PORTAL_FILES = ("404", "404.html", "LICENSE", "SECURITY.md", "genindex", "search") def _rule(source: str, destination: str, status: int) -> str: return f"{source} {destination} {status}!" -def _project_rules(project: dict, releases: dict, status: int) -> tuple[list[str], list[str]]: +def _project_rules(project: dict, releases: dict, status: int) -> list[str]: rules = [] - incomplete = [] emitted_sources = set() project_path = project["path"] @@ -43,7 +56,6 @@ def _project_rules(project: dict, releases: dict, status: int) -> tuple[list[str if not project.get("external_docs_url") and destination.startswith( "https://docs.rapids.ai/" ): - incomplete.append(f"{project_path}:{version_name}:{version}") continue for source_version in (version_name, version): @@ -62,33 +74,23 @@ def _project_rules(project: dict, releases: dict, status: int) -> tuple[list[str ] ) - return rules, incomplete + return rules -def generate_redirects(*, status: int, require_complete: bool = False) -> str: +def generate_redirects(*, status: int) -> str: docs = yaml.safe_load(DOCS_CONFIG.read_text()) releases = json.loads(RELEASES_CONFIG.read_text()) rules = [ - "# Generated redirect-only site for the docs.nvidia.com migration.", + "# Generated redirects for the hybrid docs.rapids.ai compatibility site.", + "# Unmatched API routes continue to serve assembled documentation files.", "# Manual compatibility redirects run first and may intentionally chain.", MANUAL_REDIRECTS.read_text().rstrip(), "", "# API documentation aliases and numeric versions.", ] - incomplete = [] - for section in SECTIONS: for project in docs[section].values(): - project_rules, project_incomplete = _project_rules(project, releases, status) - rules.extend(project_rules) - incomplete.extend(project_incomplete) - - if require_complete and incomplete: - details = "\n- ".join(incomplete) - raise SystemExit( - "Redirect cutover is blocked because these published docs still resolve to " - f"docs.rapids.ai:\n- {details}" - ) + rules.extend(_project_rules(project, releases, status)) rules.extend( [ @@ -100,8 +102,39 @@ def generate_redirects(*, status: int, require_complete: bool = False) -> str: status, ), "", - "# Remaining portal routes move beneath docs.nvidia.com/datascience.", - _rule("/*", "https://docs.nvidia.com/datascience/:splat", status), + "# Portal routes move beneath docs.nvidia.com/datascience.", + _rule("/", "https://docs.nvidia.com/datascience/", status), + _rule("/api", "https://docs.nvidia.com/datascience/api/", status), + _rule("/api/", "https://docs.nvidia.com/datascience/api/", status), + ] + ) + for prefix in PORTAL_PREFIXES: + rules.extend( + [ + _rule( + f"/{prefix}", + f"https://docs.nvidia.com/datascience/{prefix}/", + status, + ), + _rule( + f"/{prefix}/*", + f"https://docs.nvidia.com/datascience/{prefix}/:splat", + status, + ), + ] + ) + for filename in PORTAL_FILES: + rules.append( + _rule( + f"/{filename}", + f"https://docs.nvidia.com/datascience/{filename}", + status, + ) + ) + rules.extend( + [ + "", + "# Do not add a /* fallback: /api and /assets contain real site content.", "", ] ) @@ -112,10 +145,9 @@ def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, required=True) parser.add_argument("--status", choices=(301, 302), default=302, type=int) - parser.add_argument("--require-complete", action="store_true") args = parser.parse_args() - output = generate_redirects(status=args.status, require_complete=args.require_complete) + output = generate_redirects(status=args.status) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(output) print(f"Wrote {args.output}") diff --git a/scripts/validate_site.py b/scripts/validate_site.py index 8940f10be8a..ba6aedff6cc 100644 --- a/scripts/validate_site.py +++ b/scripts/validate_site.py @@ -145,6 +145,9 @@ def main() -> None: "api/cudf/stable", "api/cudf/latest", "api/cudf/nightly", + "api/dask-cudf/legacy", + "api/dask-cudf/stable", + "api/dask-cudf/nightly", "deployment/stable/index.html", "deployment/nightly/index.html", ] diff --git a/tests/test_redirects.py b/tests/test_redirects.py index 505521be2fc..52c77c23b24 100644 --- a/tests/test_redirects.py +++ b/tests/test_redirects.py @@ -3,9 +3,6 @@ import json -import pytest -import yaml - from scripts import generate_redirect_site @@ -17,7 +14,9 @@ def test_redirects_route_migrated_docs_and_portal() -> None: assert f"/api/cudf/stable/* https://docs.nvidia.com/cudf/{stable}/:splat 302!" in redirects assert f"/api/cudf/{stable} https://docs.nvidia.com/cudf/{stable}/ 302!" in redirects assert "/deployment/* https://docs.nvidia.com/datascience/deployment/:splat 302!" in redirects - assert redirects.rstrip().endswith("/* https://docs.nvidia.com/datascience/:splat 302!") + assert "/notices/* https://docs.nvidia.com/datascience/notices/:splat 302!" in redirects + assert "/ https://docs.nvidia.com/datascience/ 302!" in redirects + assert "\n/* " not in redirects def test_redirects_route_external_unversioned_docs() -> None: @@ -26,9 +25,14 @@ def test_redirects_route_external_unversioned_docs() -> None: assert "/api/cuvs/stable/* https://docs.nvidia.com/cuvs/:splat 301!" in redirects -def test_redirect_cutover_requires_complete_migration_metadata() -> None: - docs = yaml.safe_load(generate_redirect_site.DOCS_CONFIG.read_text()) - assert docs["apis"]["dask-cudf"]["first_docs_nvidia_com_release"] is None - - with pytest.raises(SystemExit, match="dask-cudf:stable"): - generate_redirect_site.generate_redirects(status=302, require_complete=True) +def test_redirects_leave_unmigrated_api_docs_and_shared_assets_local() -> None: + redirects = generate_redirect_site.generate_redirects(status=302) + releases = json.loads(generate_redirect_site.RELEASES_CONFIG.read_text()) + legacy = releases["legacy"]["version"] + + assert "/api/dask-cudf/stable " not in redirects + assert f"/api/dask-cudf/{legacy} " not in redirects + assert "/api/cudf/legacy " not in redirects + assert f"/api/cudf/{legacy} " not in redirects + assert "/api/* " not in redirects + assert "/assets/* " not in redirects From 044be0b7348f8db828b81ce1a2d17ca3ab0c6cbf Mon Sep 17 00:00:00 2001 From: Bradley Dice Date: Thu, 20 Aug 2026 14:39:36 -0500 Subject: [PATCH 3/3] Address migration PR review feedback --- .github/workflows/deploy-redirects.yaml | 13 +----------- .github/workflows/pr.yaml | 3 +-- README.md | 7 +++---- extensions/rapids_docs/lifecycle.py | 2 +- scripts/generate_redirect_site.py | 27 ++++++++++--------------- tests/test_redirects.py | 17 ++++++++-------- tests/test_rendering.py | 6 ++++++ 7 files changed, 32 insertions(+), 43 deletions(-) diff --git a/.github/workflows/deploy-redirects.yaml b/.github/workflows/deploy-redirects.yaml index dbce03b888f..350125f7bd2 100644 --- a/.github/workflows/deploy-redirects.yaml +++ b/.github/workflows/deploy-redirects.yaml @@ -4,14 +4,6 @@ on: schedule: - cron: "0 9 * * *" workflow_dispatch: - inputs: - status: - description: HTTP status for generated redirects - type: choice - options: - - "302" - - "301" - default: "302" push: branches: - main @@ -55,12 +47,9 @@ jobs: - name: Assemble and validate API documentation run: make assemble - name: Generate compatibility redirects - env: - REDIRECT_STATUS: ${{ inputs.status || vars.DOCS_RAPIDSAI_REDIRECT_STATUS || '302' }} run: | uv run python scripts/generate_redirect_site.py \ - --output _site/_redirects \ - --status "${REDIRECT_STATUS}" + --output _site/_redirects - name: Deploy compatibility site to docs.rapids.ai env: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_API_TOKEN }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index a1dd1e4d39d..d778f4639e1 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -68,5 +68,4 @@ jobs: - name: Generate compatibility redirects run: | uv run python scripts/generate_redirect_site.py \ - --output _site/_redirects \ - --status 302 + --output _site/_redirects diff --git a/README.md b/README.md index 86abe10f199..12d4e488b8c 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This applies the RAPIDS library/version selectors to the imported documentation. Generate the Netlify redirect file after assembly: ```shell -uv run python scripts/generate_redirect_site.py --output _site/_redirects --status 302 +uv run python scripts/generate_redirect_site.py --output _site/_redirects ``` The generated rules redirect portal pages and only those library versions whose @@ -57,9 +57,8 @@ Merges to `main` and the daily scheduled workflow publish the portal to `docs.nvidia.com/datascience/deployment/` subtree is explicitly preserved. The companion compatibility workflow assembles and publishes the remaining API documentation to `docs.rapids.ai`, with redirects for migrated API versions and -portal routes. Redirects default to temporary status `302`; set the -`DOCS_RAPIDSAI_REDIRECT_STATUS` repository variable to `301` after the cutover is -verified, or select a status in a manual workflow run. +portal routes. Both automated and manually triggered compatibility deployments +use permanent HTTP 301 redirects. ## Repository layout diff --git a/extensions/rapids_docs/lifecycle.py b/extensions/rapids_docs/lifecycle.py index d6d3cebf8bc..1aebd166270 100644 --- a/extensions/rapids_docs/lifecycle.py +++ b/extensions/rapids_docs/lifecycle.py @@ -28,7 +28,7 @@ def _jinja_environment(app) -> Environment: def _context(app, docname: str = "index") -> dict: data = app.rapids_portal_data config = getattr(app, "config", None) - site_baseurl = getattr(config, "html_baseurl", "https://docs.rapids.ai/") + site_baseurl = getattr(config, "html_baseurl", "https://docs.nvidia.com/datascience/") return { **data, "api_docs": lambda section: _api_docs(data, section), diff --git a/scripts/generate_redirect_site.py b/scripts/generate_redirect_site.py index 7f6ca0d794b..d93f0012348 100644 --- a/scripts/generate_redirect_site.py +++ b/scripts/generate_redirect_site.py @@ -37,13 +37,14 @@ "visualization", ) PORTAL_FILES = ("404", "404.html", "LICENSE", "SECURITY.md", "genindex", "search") +REDIRECT_STATUS = 301 -def _rule(source: str, destination: str, status: int) -> str: - return f"{source} {destination} {status}!" +def _rule(source: str, destination: str) -> str: + return f"{source} {destination} {REDIRECT_STATUS}!" -def _project_rules(project: dict, releases: dict, status: int) -> list[str]: +def _project_rules(project: dict, releases: dict) -> list[str]: rules = [] emitted_sources = set() project_path = project["path"] @@ -65,11 +66,10 @@ def _project_rules(project: dict, releases: dict, status: int) -> list[str]: emitted_sources.add(source) rules.extend( [ - _rule(source, destination, status), + _rule(source, destination), _rule( f"{source}/*", _documentation_url(project, version_name, version, ":splat"), - status, ), ] ) @@ -77,7 +77,7 @@ def _project_rules(project: dict, releases: dict, status: int) -> list[str]: return rules -def generate_redirects(*, status: int) -> str: +def generate_redirects() -> str: docs = yaml.safe_load(DOCS_CONFIG.read_text()) releases = json.loads(RELEASES_CONFIG.read_text()) rules = [ @@ -90,7 +90,7 @@ def generate_redirects(*, status: int) -> str: ] for section in SECTIONS: for project in docs[section].values(): - rules.extend(_project_rules(project, releases, status)) + rules.extend(_project_rules(project, releases)) rules.extend( [ @@ -99,13 +99,12 @@ def generate_redirects(*, status: int) -> str: _rule( "/deployment/*", "https://docs.nvidia.com/datascience/deployment/:splat", - status, ), "", "# Portal routes move beneath docs.nvidia.com/datascience.", - _rule("/", "https://docs.nvidia.com/datascience/", status), - _rule("/api", "https://docs.nvidia.com/datascience/api/", status), - _rule("/api/", "https://docs.nvidia.com/datascience/api/", status), + _rule("/", "https://docs.nvidia.com/datascience/"), + _rule("/api", "https://docs.nvidia.com/datascience/api/"), + _rule("/api/", "https://docs.nvidia.com/datascience/api/"), ] ) for prefix in PORTAL_PREFIXES: @@ -114,12 +113,10 @@ def generate_redirects(*, status: int) -> str: _rule( f"/{prefix}", f"https://docs.nvidia.com/datascience/{prefix}/", - status, ), _rule( f"/{prefix}/*", f"https://docs.nvidia.com/datascience/{prefix}/:splat", - status, ), ] ) @@ -128,7 +125,6 @@ def generate_redirects(*, status: int) -> str: _rule( f"/{filename}", f"https://docs.nvidia.com/datascience/{filename}", - status, ) ) rules.extend( @@ -144,10 +140,9 @@ def generate_redirects(*, status: int) -> str: def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, required=True) - parser.add_argument("--status", choices=(301, 302), default=302, type=int) args = parser.parse_args() - output = generate_redirects(status=args.status) + output = generate_redirects() args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(output) print(f"Wrote {args.output}") diff --git a/tests/test_redirects.py b/tests/test_redirects.py index 52c77c23b24..2e8948b7d71 100644 --- a/tests/test_redirects.py +++ b/tests/test_redirects.py @@ -7,26 +7,27 @@ def test_redirects_route_migrated_docs_and_portal() -> None: - redirects = generate_redirect_site.generate_redirects(status=302) + redirects = generate_redirect_site.generate_redirects() releases = json.loads(generate_redirect_site.RELEASES_CONFIG.read_text()) stable = releases["stable"]["version"] - assert f"/api/cudf/stable/* https://docs.nvidia.com/cudf/{stable}/:splat 302!" in redirects - assert f"/api/cudf/{stable} https://docs.nvidia.com/cudf/{stable}/ 302!" in redirects - assert "/deployment/* https://docs.nvidia.com/datascience/deployment/:splat 302!" in redirects - assert "/notices/* https://docs.nvidia.com/datascience/notices/:splat 302!" in redirects - assert "/ https://docs.nvidia.com/datascience/ 302!" in redirects + assert f"/api/cudf/stable/* https://docs.nvidia.com/cudf/{stable}/:splat 301!" in redirects + assert f"/api/cudf/{stable} https://docs.nvidia.com/cudf/{stable}/ 301!" in redirects + assert "/deployment/* https://docs.nvidia.com/datascience/deployment/:splat 301!" in redirects + assert "/notices/* https://docs.nvidia.com/datascience/notices/:splat 301!" in redirects + assert "/ https://docs.nvidia.com/datascience/ 301!" in redirects + assert " 302!" not in redirects assert "\n/* " not in redirects def test_redirects_route_external_unversioned_docs() -> None: - redirects = generate_redirect_site.generate_redirects(status=301) + redirects = generate_redirect_site.generate_redirects() assert "/api/cuvs/stable/* https://docs.nvidia.com/cuvs/:splat 301!" in redirects def test_redirects_leave_unmigrated_api_docs_and_shared_assets_local() -> None: - redirects = generate_redirect_site.generate_redirects(status=302) + redirects = generate_redirect_site.generate_redirects() releases = json.loads(generate_redirect_site.RELEASES_CONFIG.read_text()) legacy = releases["legacy"]["version"] diff --git a/tests/test_rendering.py b/tests/test_rendering.py index 9d37b7078df..fa2f4a371a0 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -94,6 +94,12 @@ def test_standard_jinja_syntax_and_raw_blocks() -> None: assert rendered == stable_version + "\n${{ matrix.PY_VER }}\n" +def test_context_defaults_to_nvidia_portal() -> None: + app = SimpleNamespace(rapids_portal_data={}) + + assert lifecycle._context(app)["site_baseurl"] == "https://docs.nvidia.com/datascience/" + + def test_toctree_url_rewriting() -> None: app = SimpleNamespace( config=SimpleNamespace(html_baseurl="https://docs.example.com/datascience/")