diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 7f8828ab62e..d02f5b4bc5f 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -60,12 +60,53 @@ jobs: env: pr_preview ref: ${{ github.event.pull_request.head.ref }} + - name: Get all changed files + id: changed-files + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + CHANGED_FILES="$(node scripts/ci/determine-changed-files.ts "${{ github.event.number }}")" + { + echo "ALL_CHANGED_FILES<<__GH_OUTPUT__" + printf '%s\n' "$CHANGED_FILES" + echo "__GH_OUTPUT__" + } >> "$GITHUB_OUTPUT" + + - name: Determine changed content files + env: + GH_TOKEN: ${{ github.token }} + CHANGED_FILES: ${{ steps.changed-files.outputs.ALL_CHANGED_FILES }} + # If changing these regexes, test them on https://regex101.com/ with the + # following paths (you can paste the block in and uncomment it): + # Add new tests if necessary + CONTENT_FILE_REGEX: (docs|learning)(?!\/api)\/.*\.(mdx|ipynb) + # -- Should match: + # docs/guides/thing.ipynb + # docs/guides/thing.mdx + # learning/courses/my-course/introduction.ipynb + # + # -- Should not match: + # docs/guides/_toc.json + # docs/api/qiskit/index.mdx + # scripts/nb-tester/example-notebook.ipynb + id: changed-content-files + run: | + CHANGED_CONTENT_FILES=$(echo "${CHANGED_FILES[@]}" | grep -P $CONTENT_FILE_REGEX || true) + mkdir -p .github/outputs + echo "Detected these changed content files:" + echo $CHANGED_CONTENT_FILES + echo $CHANGED_CONTENT_FILES >> .github/outputs/changed-content-files.txt + - name: Build static site run: > scripts/pr-previews/builder.py ${{ env.PR_PREVIEW_PATH }} --basepath /documentation/${{ env.PR_PREVIEW_PATH }} + - name: Delete temp files + # Otherwise this gets pushed to the gh-pages branch + run: rm -r .github/outputs + - name: Deploy to GitHub Pages run: scripts/pr-previews/deploy.py ${{ env.PR_PREVIEW_PATH }} diff --git a/scripts/pr-previews/builder.py b/scripts/pr-previews/builder.py index bff8a9a7959..63bc7115448 100755 --- a/scripts/pr-previews/builder.py +++ b/scripts/pr-previews/builder.py @@ -19,9 +19,10 @@ import shutil from argparse import ArgumentParser from contextlib import contextmanager -from typing import Iterator +from functools import partial from pathlib import Path from tempfile import TemporaryDirectory +from typing import Iterator from utils import configure_logging, run_subprocess, write_timestamp @@ -54,7 +55,16 @@ def main() -> None: write_proof_of_concept(args.dest) return - with setup_dir() as dir: + try: + lines = ( + Path(".github/outputs/changed-content-files.txt").read_text().split("\n") + ) + changed_content_files = set(line for line in lines if line != "") + except FileNotFoundError: + logger.info("No changed files detected, will build all pages") + changed_content_files: set[str] = set() + + with setup_dir(changed_content_files) as dir: yarn_build(dir, args.basepath) save_output(dir, args.dest) write_timestamp(args.dest) @@ -105,33 +115,30 @@ def save_output(root_dir: Path, dest: Path) -> None: @contextmanager -def setup_dir() -> Iterator[Path]: +def setup_dir(changed_content_files: set[str]) -> Iterator[Path]: with TemporaryDirectory() as _tempdir: root_dir = Path(_tempdir) logger.info(f"Using tmpdir {root_dir}") - _copy_local_content(root_dir) + _copy_local_content(root_dir, changed_content_files) _extract_docker_files(root_dir) yield root_dir -def _copy_local_content(root_dir: Path) -> None: - # We intentionally don't copy over API docs to speed up the build. +def _copy_local_content(root_dir: Path, changed_files: set[str]) -> None: + copytree_ignore = partial(_ignore_unchanged_files, changed_files=changed_files) + for dir in [ - "docs/guides", - "docs/tutorials", - "public/docs/images/tutorials", + "docs", + "public", "learning", - "public/docs/images/guides", - "public/docs/images/qiskit-patterns", - "public/learning", ]: dest = ( root_dir / "packages/preview" / dir if dir.startswith("public") else root_dir / f"content/{dir}" ) - shutil.copytree(dir, dest) + shutil.copytree(dir, dest, ignore=copytree_ignore) for fp in [ "docs/responsible-quantum-computing.mdx", @@ -142,6 +149,55 @@ def _copy_local_content(root_dir: Path) -> None: logger.info("local content files copied") +def _ignore_unchanged_files( + dir: str, contents: list[str], changed_files: set[str] +) -> list[str]: + """For input to shutil.copytree. This function takes the directory path + (such as `docs/guides`) and a list of file and folder names (entries) in + that directory (such as `["api", "index.mdx", ... ]`). It should output a list + of entries to ignore. + """ + ignores = [] + + # Don't copy any entries named "api". This has the effect of ignoring + # any paths matching "/api/". We intentionally don't copy over API docs + # to speed up the build. + if "api" in contents: + ignores.append("api") + + # No changed files means we should copy over everything. + if len(changed_files) == 0: + return ignores + + for entry in contents: + full_path = f"{dir}/{entry}" + + # Always copy over the `public` folder as its contents could be used + # anywhere. TODO: Maybe make this more selective? + if full_path.startswith("public"): + continue + + # We also need to copy over `_toc.json` used by any changed files. + # Copytree should only reach these files if a sibling or child of + # the current directory contains a changed file. + if entry == "_toc.json": + continue + + # We also need to copy over the index files because the app doesn't + # build without them + if entry.startswith("index."): + continue + + # Finally, include files that were directly changed. + if any(file.startswith(full_path) for file in changed_files): + continue + + # Ignore everything else + ignores.append(entry) + + return ignores + + def _extract_docker_files(root_dir: Path) -> None: container_id = run_subprocess(["docker", "create", IMAGE_NAME]).stdout.strip() try: