diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 049bb7c4571..b45aa1d9908 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -3,21 +3,31 @@ on: schedule: - cron: "0 9 * * *" workflow_dispatch: + inputs: + dry-run: + description: "dry-run: build and log what would be published without uploading or flushing the CDN" + required: false + type: boolean + default: false push: branches: - main concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false defaults: run: shell: bash permissions: - id-token: write contents: read +env: + DRY_RUN: ${{ inputs.dry-run || false }} + RAPIDS_DOCS_BASE_URL: https://docs.nvidia.com/datascience/ + UPLOAD: ${{ github.ref == 'refs/heads/main' }} jobs: build: - name: Build (and deploy) + name: Build and publish NVIDIA docs portal + if: ${{ github.repository == 'rapidsai/docs' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -32,26 +42,77 @@ jobs: run: uv sync --locked - name: Build and validate portal run: make check - - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 #v5.1.1 + - name: Check out gha-tools + if: ${{ env.UPLOAD == 'true' }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: rapidsai/gha-tools + ref: main + path: gha-tools + persist-credentials: false + sparse-checkout: tools + - name: Add gha-tools to PATH + if: ${{ env.UPLOAD == 'true' }} + run: echo "${GITHUB_WORKSPACE}/gha-tools/tools" >> "${GITHUB_PATH}" + - name: Publish portal to NVIDIA docs + if: ${{ env.UPLOAD == 'true' }} + uses: rapidsai/shared-actions/publish-docs@82e2c50e4703a224de0bc8d0f6e5d12dcab68db7 + with: + dry-run: ${{ env.DRY_RUN }} + source-path: _site + target-s3-key: datascience + target-s3-exclude: deployment/* + target-s3-bucket: ${{ secrets.NVIDIA_DOCS_S3_BUCKET }} + target-aws-access-key-id: ${{ secrets.NVIDIA_DOCS_AWS_ACCESS_KEY_ID }} + target-aws-secret-access-key: ${{ secrets.NVIDIA_DOCS_AWS_SECRET_ACCESS_KEY }} + target-aws-region: ${{ secrets.NVIDIA_DOCS_AWS_REGION }} + 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 }} + akamai-request-name: rapidsai-docs-${{ github.run_id }} + + compat: + name: Assemble and deploy docs.rapids.ai compatibility site + needs: build + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + 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: 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 # 2h - - name: Assemble complete documentation site + role-duration-seconds: 7200 + - name: Assemble and validate API documentation run: make assemble - - name: Deploy site + - name: Generate compatibility redirects + run: | + uv run python scripts/generate_redirect_site.py \ + --output _site/_redirects + - name: Deploy compatibility site to docs.rapids.ai 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" + # Only main publishes production; other refs and dry runs produce a draft deploy. + ARGS=() + if [[ "${UPLOAD}" == "true" && "${DRY_RUN}" != "true" ]]; then + ARGS+=(--prod) fi - netlify deploy "$ARGS" \ - --debug \ - --dir=_site + netlify deploy "${ARGS[@]}" --debug --dir=_site diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index d35a3931dd1..85b4af60778 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -42,6 +42,8 @@ jobs: 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 +57,42 @@ jobs: run: uv sync --locked - name: Build and validate portal run: make check - - name: Configure AWS credentials + - name: Check out gha-tools + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: rapidsai/gha-tools + ref: main + path: gha-tools + persist-credentials: false + sparse-checkout: tools + - name: Add gha-tools to PATH + run: echo "${GITHUB_WORKSPACE}/gha-tools/tools" >> "${GITHUB_PATH}" + - name: Publish portal to NVIDIA docs + uses: rapidsai/shared-actions/publish-docs@82e2c50e4703a224de0bc8d0f6e5d12dcab68db7 + with: + dry-run: true + source-path: _site + target-s3-key: datascience + target-s3-exclude: deployment/* + target-s3-bucket: ${{ secrets.NVIDIA_DOCS_S3_BUCKET }} + target-aws-access-key-id: ${{ secrets.NVIDIA_DOCS_AWS_ACCESS_KEY_ID }} + target-aws-secret-access-key: ${{ secrets.NVIDIA_DOCS_AWS_SECRET_ACCESS_KEY }} + target-aws-region: ${{ secrets.NVIDIA_DOCS_AWS_REGION }} + 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 }} + akamai-request-name: rapidsai-docs-${{ github.run_id }} + - 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 complete documentation site + - name: Assemble and validate API documentation run: make assemble + - name: Generate compatibility redirects + run: | + uv run python scripts/generate_redirect_site.py \ + --output _site/_redirects 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..30235a56234 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 @@ -16,31 +16,33 @@ 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 +Builds use `https://docs.nvidia.com/datascience/` as the default base URL. +Set `RAPIDS_DOCS_BASE_URL` to override it. -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: +## Validation + +Run linting, tests, a strict Sphinx build, and rendered-site validation: ```shell -AWS_PROFILE=rapids-docs make full +make check ``` -This applies the RAPIDS library/version selectors to the imported documentation. +Pull requests run validation and receive a Netlify preview. -## Validation +## Publishing -```shell -make check -``` +Merges to `main` and the daily scheduled workflow publish the portal to +`docs.nvidia.com/datascience/` using the shared `publish-docs` action. +The independently published `datascience/deployment/` subtree is excluded from +uploads and deletions. A manual run with the `dry-run` input builds everything +and skips the upload, the CDN flush, and the production Netlify deploy. -Run checks including linting, tests, and a local build. +## Compatibility site -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. +`docs.rapids.ai` continues to host unmigrated API documentation and redirect +migrated content. The `compat` job in the [deploy workflow](.github/workflows/deploy.yaml) +imports the remaining API docs from S3 and publishes them to Netlify, and runs +only after the portal publish to `docs.nvidia.com` has succeeded. ## Repository layout diff --git a/extensions/rapids_docs/api.py b/extensions/rapids_docs/api.py index 17a7abeba7c..2cd8e0dfbfe 100644 --- a/extensions/rapids_docs/api.py +++ b/extensions/rapids_docs/api.py @@ -3,23 +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(".")) - ): - target_version = "latest" if version_name == "nightly" else version - return f"https://docs.nvidia.com/{project['path']}/{target_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..1aebd166270 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.nvidia.com/datascience/") 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..c4f98fff62d --- /dev/null +++ b/extensions/rapids_docs/routes.py @@ -0,0 +1,84 @@ +# 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): + target_version = "latest" if version_name == "nightly" else version + base_url = f"https://docs.nvidia.com/{project['path']}/{target_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/index.md b/index.md index 9829ccb800b..543a5405383 100644 --- a/index.md +++ b/index.md @@ -36,7 +36,7 @@ you. Visit [RAPIDS.ai](https://rapids.ai) for more information on the overall pr ::: :::{grid-item-card} Deployment Guides -:link: /deployment/stable/ +:link: /deployment/latest/ :link-type: url ::: @@ -84,7 +84,7 @@ Platform Support User Guide API Docs Visualization Guide -Deployment Guides +Deployment Guides Maintainer Docs contributing/index notices/index diff --git a/install/index.md b/install/index.md index 8231d4625f8..dd4d15e67d4 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/latest/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..f2e49f576e1 --- /dev/null +++ b/scripts/generate_redirect_site.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Generate redirects for the hybrid docs.rapids.ai compatibility site.""" + +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") +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") +REDIRECT_STATUS = 301 + + +def _rule(source: str, destination: str) -> str: + return f"{source} {destination} {REDIRECT_STATUS}!" + + +def _project_rules(project: dict, releases: dict) -> list[str]: + rules = [] + 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/" + ): + 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), + _rule( + f"{source}/*", + _documentation_url(project, version_name, version, ":splat"), + ), + ] + ) + + return rules + + +def generate_redirects() -> str: + docs = yaml.safe_load(DOCS_CONFIG.read_text()) + releases = json.loads(RELEASES_CONFIG.read_text()) + rules = [ + "# 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.", + ] + for section in SECTIONS: + for project in docs[section].values(): + rules.extend(_project_rules(project, releases)) + + rules.extend( + [ + "", + "# Portal routes move beneath docs.nvidia.com/datascience.", + _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: + rules.extend( + [ + _rule( + f"/{prefix}", + f"https://docs.nvidia.com/datascience/{prefix}/", + ), + _rule( + f"/{prefix}/*", + f"https://docs.nvidia.com/datascience/{prefix}/:splat", + ), + ] + ) + for filename in PORTAL_FILES: + rules.append( + _rule( + f"/{filename}", + f"https://docs.nvidia.com/datascience/{filename}", + ) + ) + rules.extend( + [ + "", + "# Do not add a /* fallback: /api and /assets contain real site content.", + "", + ] + ) + return "\n".join(rules) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + output = generate_redirects() + 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 958e09a9478..e2afcfba36f 100644 --- a/scripts/validate_site.py +++ b/scripts/validate_site.py @@ -7,6 +7,7 @@ from __future__ import annotations import argparse +import os import re from pathlib import Path from urllib.parse import urlsplit @@ -66,11 +67,15 @@ 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.nvidia.com/datascience/" + ) + expected_baseurl = expected_baseurl.rstrip("/") + "/" + if f' None: if "fa-twitter" in home or "fa-x-twitter" in home: missing.append("Twitter/X icon remains on the home page") - analytics = (args.site / "_static" / "js" / "portal-analytics.js").read_text() - if "G-DLJNCEWKZD" not in analytics or "_satellite.pageBottom" not in analytics: - missing.append("GA4 or Adobe page-bottom telemetry is missing") + if os.environ.get("RAPIDS_DOCS_BASE_URL"): + missing.extend( + f"theme-injected telemetry missing from home page: {value}" + for value in ("cdn.cookielaw.org", "assets.adobedtm.com") + if value not in home + ) + if "G-DLJNCEWKZD" not in home: + missing.append("GA4 telemetry is missing from the home page") # Imported API docs may include upstream source links and template examples. # Limit portal-specific checks to portal pages. @@ -113,8 +123,32 @@ def main() -> 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/legacy", + "api/dask-cudf/legacy", "api/ucxx/stable", "api/ucxx/latest", "api/ucxx/nightly", diff --git a/sphinx/_static/js/portal-analytics.js b/sphinx/_static/js/portal-analytics.js deleted file mode 100644 index c64422dc58d..00000000000 --- a/sphinx/_static/js/portal-analytics.js +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. -// SPDX-License-Identifier: Apache-2.0 - -function OptanonWrapper() { - window.dispatchEvent(new Event("bannerLoaded")); -} - -function hasAnalyticsConsent() { - return ( - typeof window.OnetrustActiveGroups === "string" && - window.OnetrustActiveGroups.split(",").includes("C0002") - ); -} - -function loadGA4() { - if (window._ga4Loaded) return; - window._ga4Loaded = true; - const gtagScript = document.createElement("script"); - gtagScript.async = true; - gtagScript.src = - "https://www.googletagmanager.com/gtag/js?id=G-DLJNCEWKZD"; - document.head.appendChild(gtagScript); - window.dataLayer = window.dataLayer || []; - window.gtag = - window.gtag || - function gtag() { - window.dataLayer.push(arguments); - }; - window.gtag("js", new Date()); - window.gtag("config", "G-DLJNCEWKZD"); -} - -function initializeAnalytics() { - if (hasAnalyticsConsent()) loadGA4(); - if (window._satellite && window._satellite.pageBottom) { - window._satellite.pageBottom(); - } -} - -window.addEventListener("load", initializeAnalytics); -if (window.OneTrust && typeof window.OneTrust.OnConsentChanged === "function") { - window.OneTrust.OnConsentChanged(initializeAnalytics); -} diff --git a/sphinx/conf.py b/sphinx/conf.py index 5f68a3ef968..e8a2cd799b2 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,10 +56,20 @@ html_theme = "nvidia_sphinx_theme" html_static_path = ["_static"] html_extra_path = ["../_redirects"] -html_baseurl = "https://docs.rapids.ai/" +# Production workflows explicitly set RAPIDS_DOCS_BASE_URL, which takes precedence. +# Netlify supplies DEPLOY_PRIME_URL so rewritten portal links stay within previews. +# Otherwise, use the NVIDIA portal as the default. +html_baseurl = ( + os.environ.get("RAPIDS_DOCS_BASE_URL") + or os.environ.get("DEPLOY_PRIME_URL") + or "https://docs.nvidia.com/datascience/" +).rstrip("/") + "/" html_scaled_image_link = False html_theme_options = { + "analytics": { + "google_analytics_id": "G-DLJNCEWKZD", + }, "icon_links": [ { "name": "GitHub", @@ -69,23 +79,11 @@ } ], "navbar_align": "right", + "public_docs_features": bool(os.environ.get("RAPIDS_DOCS_BASE_URL")), "show_toc_level": 2, } html_css_files = ["css/custom.css"] -html_js_files = [ - ( - "https://cdn.cookielaw.org/scripttemplates/otSDKStub.js", - { - "charset": "UTF-8", - "data-document-language": "true", - "data-domain-script": "018e2d71-40f3-7e89-90b8-e10ec6012ab0-test", - }, - ), - "https://images.nvidia.com/aem-dam/Solutions/ot-js/ot-custom.js", - "https://assets.adobedtm.com/5d4962a43b79/814eb6e9b4e1/launch-4bc07f1e0b0b.min.js", - "js/portal-analytics.js", -] copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " copybutton_prompt_is_regexp = True diff --git a/tests/test_redirects.py b/tests/test_redirects.py new file mode 100644 index 00000000000..79556c2ca31 --- /dev/null +++ b/tests/test_redirects.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import json + +from scripts import generate_redirect_site + + +def test_redirects_route_migrated_docs_and_portal() -> None: + 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 301!" in redirects + assert f"/api/cudf/{stable} https://docs.nvidia.com/cudf/{stable}/ 301!" in redirects + # Deployment redirects are hand-maintained in _redirects and target latest/. + assert ( + "/deployment/stable/* https://docs.nvidia.com/datascience/deployment/latest/:splat 301!" + in redirects + ) + assert "/deployment/*" not 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_nightly_redirects_use_latest() -> None: + releases = json.loads(generate_redirect_site.RELEASES_CONFIG.read_text()) + nightly = releases["nightly"]["version"] + project = { + "path": "cudf", + "first_docs_nvidia_com_release": "26.08", + "versions": {"nightly": 1}, + } + + assert generate_redirect_site._project_rules(project, releases) == [ + "/api/cudf/nightly https://docs.nvidia.com/cudf/latest/ 301!", + "/api/cudf/nightly/* https://docs.nvidia.com/cudf/latest/:splat 301!", + f"/api/cudf/{nightly} https://docs.nvidia.com/cudf/latest/ 301!", + f"/api/cudf/{nightly}/* https://docs.nvidia.com/cudf/latest/:splat 301!", + ] + + +def test_redirects_route_external_unversioned_docs() -> None: + 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() + releases = json.loads(generate_redirect_site.RELEASES_CONFIG.read_text()) + legacy = releases["legacy"]["version"] + + assert "/api/ucxx/stable " not in redirects + assert "/api/ucxx/nightly " not in redirects + assert f"/api/ucxx/{releases['stable']['ucxx_version']} " not in redirects + assert "/api/dask-cudf/legacy " 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 diff --git a/tests/test_rendering.py b/tests/test_rendering.py index b521d19e3ea..6d352045fb8 100644 --- a/tests/test_rendering.py +++ b/tests/test_rendering.py @@ -1,10 +1,13 @@ # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +import sys from pathlib import Path from types import SimpleNamespace +import pytest from docutils import nodes +from sphinx.config import eval_config_file from extensions import rapids_docs from extensions.rapids_docs import api, lifecycle, notices, platform_support, releases, urls @@ -105,6 +108,24 @@ def test_standard_jinja_syntax_and_raw_blocks() -> None: assert rendered == stable_version + "\n${{ matrix.PY_VER }}\n" +@pytest.mark.parametrize("base_url", [None, "https://docs.rapids.ai/"]) +def test_html_baseurl(monkeypatch: pytest.MonkeyPatch, base_url: str | None) -> None: + monkeypatch.delenv("RAPIDS_DOCS_BASE_URL", raising=False) + monkeypatch.delenv("DEPLOY_PRIME_URL", raising=False) + monkeypatch.setattr(sys, "path", sys.path.copy()) + if base_url is not None: + monkeypatch.setenv("RAPIDS_DOCS_BASE_URL", base_url) + + config = eval_config_file(ROOT / "sphinx" / "conf.py", tags=None) + assert config["html_baseurl"] == (base_url or "https://docs.nvidia.com/datascience/") + + +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/") @@ -182,6 +203,37 @@ def test_absolute_url_rewriting() -> None: ) +@pytest.mark.parametrize("version_name", ["stable", "nightly"]) +def test_api_documentation_url_rewriting(version_name: str) -> 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=f"/api/cudf/{version_name}/user_guide/10min/?source=portal#intro", + ) + unmigrated = nodes.reference( + "", + "UCXX guide", + refuri=f"/api/ucxx/{version_name}/user_guide/", + ) + doctree = nodes.container("", migrated, unmigrated) + + urls._rewrite_absolute_urls(app, doctree, "user-guide/index") + + target_version = ( + "latest" + if version_name == "nightly" + else app.rapids_portal_data["releases"]["stable"]["version"] + ) + assert migrated["refuri"] == ( + f"https://docs.nvidia.com/cudf/{target_version}/user_guide/10min/?source=portal#intro" + ) + assert unmigrated["refuri"] == f"https://docs.rapids.ai/api/ucxx/{version_name}/user_guide/" + + def test_theme_url_rewriting() -> None: app = SimpleNamespace( config=SimpleNamespace(html_baseurl="https://docs.example.com/datascience/") diff --git a/user-guide/index.md b/user-guide/index.md index 320d57fd692..222a6a51776 100644 --- a/user-guide/index.md +++ b/user-guide/index.md @@ -11,7 +11,7 @@ The RAPIDS data science framework is a collection of libraries for running end-t A repository with example notebooks and "getting started" code samples to help you integrate RAPIDS with the hyperparameter optimization services from Azure ML, AWS Sagemaker, Google Cloud, and Databricks. -** Tools and Guides for [RAPIDS Deployment](/deployment/stable/)**: +** Tools and Guides for [RAPIDS Deployment](/deployment/latest/)**: Deployment documentation to get you up and running with RAPIDS in AWS, GCP, Azure, IBM and more. Also includes guides for HPC, HPO, Kubernetes, Dask, and more. ** ETL and Dataframe Processing with [cuDF](https://github.com/rapidsai/cudf)**: