Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ knowledge_base:
- ".agent/skills/cccl-test/SKILL.md"
- ".agent/skills/cccl-test/references/common.md"
- ".agent/skills/cccl-test/references/libcudacxx.md"
- "docs/contributors/index.rst"
- "docs/site/contributors/index.rst"
- "docs/cccl/config_macros.rst"
- "docs/cccl/development/macro.rst"
- "docs/cccl/development/visibility.rst"
Expand Down
23 changes: 13 additions & 10 deletions .github/actions/docs-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,27 @@ inputs:
runs:
using: "composite"
steps:
# Build all docs (gen_docs.bash detects and installs missing deps)
- name: Build all docs
# registry.py imports PyYAML, so the docs environment is created before
# anything reads the registry. The build scripts reuse this venv, and the
# runner's system Python is left alone.
- name: Set up the docs environment
shell: bash --noprofile --norc -euo pipefail {0}
run: ./docs/gen_docs.bash --allow-dep-install
run: |
python3 -m venv docs/env
docs/env/bin/python -m pip install --quiet -r docs/requirements.txt

# Copy all docs to the right folder
- name: Move docs to right folder
# The deploy's assembly, minus seeding from and publishing to gh-pages: every
# product in docs/registry.yml plus the root project, which holds the
# contributor and maintainer docs. Warnings are errors throughout.
- name: Build all docs
shell: bash --noprofile --norc -euo pipefail {0}
run: |
mkdir -p _site
cp -rf ./docs/_build/html/* _site/
touch _site/.nojekyll
run: docs/env/bin/python docs/build_site.py

# Update docs as workflow artifact:
- name: Upload artifact
if: ${{ inputs.upload_workflow_artifact == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: docs
path: _site/
path: docs/_site/
compression-level: 0
2 changes: 1 addition & 1 deletion .github/actions/workflow-run-job-linux/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ runs:
echo ""
echo "For additional information, see:"
echo " - DevContainer Documentation: https://github.com/NVIDIA/cccl/blob/main/.devcontainer/README.md"
echo " - Continuous Integration (CI) Overview: https://github.com/NVIDIA/cccl/blob/main/docs/infrastructure/ci/references/ci_overview.rst"
echo " - Continuous Integration (CI) Overview: https://github.com/NVIDIA/cccl/blob/main/docs/site/infrastructure/ci/references/ci_overview.rst"
fi

ci/upload_job_result_artifacts.sh "${{inputs.id}}" $exit_code > /dev/null
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-rapids.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ jobs:
echo ""
echo "For additional information, see:"
echo " - DevContainer Documentation: https://github.com/NVIDIA/cccl/blob/main/.devcontainer/README.md"
echo " - Continuous Integration (CI) Overview: https://github.com/NVIDIA/cccl/blob/main/docs/infrastructure/ci/references/ci_overview.rst"
echo " - Continuous Integration (CI) Overview: https://github.com/NVIDIA/cccl/blob/main/docs/site/infrastructure/ci/references/ci_overview.rst"
fi
exit $code
}
Expand Down
158 changes: 158 additions & 0 deletions .github/workflows/build-site.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Build and publish the documentation site.
#
# One root Sphinx project indexes the products; each product is built separately
# and composed into a complete site, which is published as a single commit.
#
# push to main every product at unstable/
# dispatch a tag that tag's product, under its exact version
#
# The published branch is recreated from the artifact on every deploy, which
# keeps it at one commit and the repository's clones small. That is safe because
# the artifact is seeded from what is already published.
name: Build documentation site

on:
push:
branches:
- main
workflow_dispatch:
inputs:
git-tag:
description: "Release tag to publish, e.g. v3.5.0. Leave blank to publish every product at unstable."
required: false
type: string
default: ""
source-ref:
description: "What to build from. Defaults to the tag itself; set it to publish a tag whose own tree cannot build these docs."
required: false
type: string
default: ""
docs-branch:
description: "Target branch: gh-pages, or gh-pages-<name> to rehearse."
required: false
type: string
default: gh-pages

# Two runs publishing the same branch would each seed from the tip as it stood
# when they started, so the one that finished second would drop what the first
# added. Runs on a branch are therefore serialized, and a publish already under
# way is never cancelled. GitHub keeps only the newest waiting run, though, so a
# release dispatched while a publish runs is cancelled if a later push queues
# behind it, and has to be dispatched again.
concurrency:
group: docs-publish-${{ inputs.docs-branch || 'gh-pages' }}
cancel-in-progress: false
Comment on lines +42 to +44

@coderabbitai coderabbitai Bot Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Major · Stability & Availability · Heavy lift

important: cancel-in-progress: false does not queue every run. GitHub keeps one running and one pending run per concurrency group. A newer run replaces any older pending run in the group.

If a v3.5.0 dispatch and a python-1.2.0 dispatch are both queued behind a running publish, a later push to main can cancel the older pending release dispatch. That release is then never published, and the workflow gives no failure signal. The test at test_publishing_one_branch_at_a_time assumes no run is dropped.

Fix this one of two ways:

  • Give release dispatches a unique concurrency group per tag. Serialize the push-to-main runs separately.
  • Make the publish step safe to run from a single latest run, and document that dispatches must be sequential.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I believe the fix is queue: max, which keeps runs serialized without letting a newer run replace a waiting one, but the repo's pinned actionlint doesn't support that key yet, so I've left it out of this PR. I don't think per-tag groups are an option. Two runs could then publish gh-pages at once, and one would drop the other's version.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Analysis chain

web_search executed:

Check official GitHub Actions documentation for workflow concurrency queue: max: supported syntax, maximum pending runs, behavior at the limit, and compatibility with cancel-in-progress: false.

Length of output: 15059


Script executed:

#!/bin/bash
rg -n -i -C 3 'actionlint|queue: max' .github .pre-commit-config.yaml ci || true

Length of output: 1962


@sri-koundinyan important: My per-tag group suggestion was incorrect. Concurrent publishes could overwrite each other’s versions.

GitHub documents queue: max as allowing up to 100 pending runs. It cancels additional runs when the queue is full.

Until the fix is available, check that each release dispatch succeeds and rerun it if canceled. Would you like me to open a follow-up issue for actionlint support and queue: max?

You are interacting with an AI system.


permissions:
contents: read

jobs:
publish:
runs-on: ${{ github.repository == 'NVIDIA/cccl' && 'linux-amd64-cpu32' || 'ubuntu-latest' }}
permissions:
contents: write
steps:
# fetch-depth: 0 because the assembly reads the published branch. git-tag
# names the version and, by default, is also what gets built. source-ref
# overrides the latter for tags too old to build these docs themselves; a
# version published that way is reproducible from that ref, not its tag.
# The token is not persisted: later fetches read a public repository
# anonymously, and the publish step brings its own.
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: ${{ inputs.source-ref || inputs.git-tag || github.sha }}
fetch-depth: 0
persist-credentials: false

# registry.py imports PyYAML, so the docs environment is created before
# anything reads the registry. The build scripts reuse this venv, and the
# runner's system Python is left alone.
- name: Set up the docs environment
shell: bash --noprofile --norc -euo pipefail {0}
run: |
python3 -m venv docs/env
docs/env/bin/python -m pip install --quiet -r docs/requirements.txt

- name: Resolve what to build
id: plan
shell: bash --noprofile --norc -euo pipefail {0}
env:
GIT_TAG: ${{ inputs.git-tag }}
DOCS_BRANCH: ${{ inputs.docs-branch || 'gh-pages' }}
run: |
# Only gh-pages, or a gh-pages-<name> rehearsal branch: the deploy
# recreates its target branch from scratch.
if [[ ! "${DOCS_BRANCH}" =~ ^gh-pages(-[A-Za-z0-9._-]+)?$ ]]; then
echo "error: docs-branch must be gh-pages or gh-pages-<name>, got '${DOCS_BRANCH}'." >&2
exit 1
fi
echo "branch=${DOCS_BRANCH}" >> "${GITHUB_OUTPUT}"

# Pages serves <owner>.github.io/<repo> with the owner lowercased; for
# NVIDIA/cccl that is exactly the production URL.
owner="$(printf '%s' "${GITHUB_REPOSITORY%%/*}" | tr '[:upper:]' '[:lower:]')"
echo "site_url=https://${owner}.github.io/${GITHUB_REPOSITORY#*/}" >> "${GITHUB_OUTPUT}"

if [[ -z "${GIT_TAG}" ]]; then
# Development: every product in the registry, at unstable.
echo "label=unstable" >> "${GITHUB_OUTPUT}"
echo "product=" >> "${GITHUB_OUTPUT}"
else
# Release: the tag names the product and the version. The registry
# owns that mapping, so a new product is routable by being added.
docs/env/bin/python - "${GIT_TAG}" >> "${GITHUB_OUTPUT}" <<'PY'
import sys
sys.path.insert(0, "docs")
import registry
key, version = registry.resolve_tag(sys.argv[1])
print(f"label={version}")
print(f"product={key}")
PY
fi

# Into a remote-tracking ref, not FETCH_HEAD, which any later fetch
# overwrites: the assembly and the manifest generator both read it.
- name: Fetch the published site
shell: bash --noprofile --norc -euo pipefail {0}
env:
BRANCH: ${{ steps.plan.outputs.branch }}
run: |
if ! git fetch --depth=1 origin \
"+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}"; then
# An absent ref means a first publication and the assembly seeds
# nothing. A fetch that merely failed looks identical from there,
# and force_orphan would publish a site missing every version. Ask
# the remote which one this is.
if git ls-remote --exit-code origin "refs/heads/${BRANCH}" >/dev/null 2>&1; then
echo "error: ${BRANCH} exists on the remote but could not be fetched." >&2
echo " Refusing to publish a site seeded from nothing." >&2
exit 1
fi
echo "${BRANCH} is not on the remote yet -- first publication"
fi

- name: Build and assemble
shell: bash --noprofile --norc -euo pipefail {0}
env:
CCCL_DOCS_SITE_URL: ${{ steps.plan.outputs.site_url }}
CCCL_DOCS_BRANCH: origin/${{ steps.plan.outputs.branch }}
LABEL: ${{ steps.plan.outputs.label }}
PRODUCT: ${{ steps.plan.outputs.product }}
run: |
args=(--label "${LABEL}" --seed-from "${CCCL_DOCS_BRANCH}")
if [[ -n "${PRODUCT}" ]]; then
args+=(--product "${PRODUCT}")
fi
docs/env/bin/python docs/build_site.py "${args[@]}"

# The artifact is the complete site, so recreating the branch from it
# loses nothing. One commit, every version, a small clone.
- name: Publish
uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: ${{ steps.plan.outputs.branch }}
publish_dir: docs/_site
destination_dir: docs
force_orphan: true
123 changes: 0 additions & 123 deletions .github/workflows/docs-deploy.yml

This file was deleted.

21 changes: 21 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,26 @@ repos:
language: python
types: [text]

# Each failure these cover is silent: a tag routed to the wrong component,
# pages stamped with a label the switcher manifest does not list, or a
# deploy whose replacement reaches past the version directory it built.
# They use fixtures and run in well under a second.
- id: docs-build-contract
name: docs publication contract
entry: python -m pytest -q docs/test_docs_build.py
language: python
additional_dependencies: [pytest, pyyaml]
files: |
(?x)^(
docs/(registry|build_site|make_manifest|test_docs_build)\.py|
docs/registry\.yml|
docs/gen_(docs|python_docs)\.bash|
docs/(conf|site/conf)\.py|
docs/(cpp|python)_site/.*|
\.github/workflows/build-site\.yml
)$
pass_filenames: false


default_language_version:
python: python3
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ Test organization:

## Continuous Integration (CI)

See `docs/infrastructure/ci/references/ci_overview.rst` for detailed examples and troubleshooting guidance.
See `docs/site/infrastructure/ci/references/ci_overview.rst` for detailed examples and troubleshooting guidance.

CCCL's CI is built on GitHub Actions and relies on a dynamically generated job matrix plus several helper scripts.

Expand Down Expand Up @@ -363,7 +363,7 @@ When writing, updating, reviewing, or validating CCCL tests, read `.agent/skills

* Validate changes with builds/tests; report results.
* Run `pre-commit` before committing.
* Review `CONTRIBUTING.md` and `docs/infrastructure/ci/references/ci_overview.rst` before starting work.
* Review `CONTRIBUTING.md` and `docs/site/infrastructure/ci/references/ci_overview.rst` before starting work.

### Performance Tips

Expand Down
Loading
Loading