Add script to statically build PR previews (#1680)
Part of https://github.com/Qiskit/documentation/issues/1043. The new `scripts/pr-previews/builder.py` file builds a static copy of the site using the Git branch's current copy of the docs, then writes the static files to the folder specified, like `scripts/pr-previews/builder.py pr-reviews/90`. The static site can then be served with a simple server like `python3 -m http.server` or GitHub Pages. The script takes 57 seconds on my MacBook Pro. ## Docker auth The script expects that the Docker image for the static builder can be accessed via `docker pull`, i.e. that you already have authentication and everything. For now, I'm using a local copy of the Docker image. Once the new image is published, we'll switch to the Container Registry copy. Setting up Container Registry will be handled in the GitHub Action, rather than this script. ## GitHub pages in a follow up I wanted to keep the GitHub Pages logic separate. This script only worries about how to build the static website to a given folder. Another script will deal with GitHub Pages. ## Skips building API docs The API docs are huge and take substantial time to build. For example, a trace shows that Runtime release notes take 10s (although that is not considering the build's parallelism). The content writers agreed they don't look at API docs in PR previews, so they'd rather have faster PR previews than have the API docs loaded in them.
This commit is contained in:
parent
8819a813c5
commit
e86afc7db6
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# This code is a Qiskit project.
|
||||
#
|
||||
# (C) Copyright IBM 2024.
|
||||
#
|
||||
# This code is licensed under the Apache License, Version 2.0. You may
|
||||
# obtain a copy of this license in the LICENSE file in the root directory
|
||||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
|
||||
#
|
||||
# Any modifications or derivative works of this code must retain this
|
||||
# copyright notice, and modified files need to carry a notice indicating
|
||||
# that they have been altered from the originals.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
from contextlib import contextmanager
|
||||
from typing import Iterator
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
|
||||
IMAGE_NAME = "iqp-channel-docs-preview-builder"
|
||||
|
||||
logger = logging.getLogger()
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
datefmt="%H:%M:%S",
|
||||
)
|
||||
|
||||
|
||||
def create_parser() -> ArgumentParser:
|
||||
parser = ArgumentParser()
|
||||
parser.add_argument("dest", help="The output folder", type=Path)
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = create_parser().parse_args()
|
||||
with setup_dir() as dir:
|
||||
yarn_build(dir)
|
||||
save_output(dir, args.dest)
|
||||
|
||||
|
||||
def yarn_build(root_dir: Path) -> None:
|
||||
# This ensures that dependencies like Sharp are properly installed. Most
|
||||
# dependencies, like the first-party deps, will already have been installed.
|
||||
_run_subprocess(["yarn", "install"], cwd=root_dir, stream_output=True)
|
||||
_run_subprocess(["yarn", "build"], cwd=root_dir, stream_output=True)
|
||||
|
||||
|
||||
def save_output(root_dir: Path, dest: Path) -> None:
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
for item in (root_dir / "packages/preview/out").iterdir():
|
||||
if item.is_dir():
|
||||
shutil.copytree(item, dest / item.name)
|
||||
else:
|
||||
shutil.copy2(item, dest)
|
||||
logger.info(f"Static site files copied to {dest}")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def setup_dir() -> Iterator[Path]:
|
||||
with TemporaryDirectory() as _tempdir:
|
||||
root_dir = Path(_tempdir)
|
||||
logger.info(f"Using tmpdir {root_dir}")
|
||||
|
||||
_copy_local_content(root_dir)
|
||||
_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.
|
||||
for dir in [
|
||||
"docs/api/migration-guides",
|
||||
"docs/guides",
|
||||
"public/videos",
|
||||
"public/images/guides",
|
||||
"public/images/migration",
|
||||
"public/images/optimize",
|
||||
"public/images/qiskit-ibm-runtime",
|
||||
"public/images/qiskit-patterns",
|
||||
]:
|
||||
dest = (
|
||||
root_dir / "packages/preview" / dir
|
||||
if dir.startswith("public")
|
||||
else root_dir / dir
|
||||
)
|
||||
shutil.copytree(dir, dest)
|
||||
|
||||
for fp in ["docs/support.mdx"]:
|
||||
shutil.copy2(fp, root_dir / fp)
|
||||
|
||||
logger.info("local content files copied")
|
||||
|
||||
|
||||
def _extract_docker_files(root_dir: Path) -> None:
|
||||
container_id = _run_subprocess(["docker", "create", IMAGE_NAME]).stdout.strip()
|
||||
try:
|
||||
_run_subprocess(["docker", "cp", f"{container_id}:/home/node/app/.", root_dir])
|
||||
finally:
|
||||
_run_subprocess(["docker", "rm", container_id])
|
||||
logger.info("Docker contents extracted")
|
||||
|
||||
|
||||
def _run_subprocess(
|
||||
cmd: list[str],
|
||||
*,
|
||||
cwd: Path | None = None,
|
||||
stream_output: bool = False,
|
||||
) -> subprocess.CompletedProcess:
|
||||
output_dest = None if stream_output else subprocess.PIPE
|
||||
if stream_output:
|
||||
logger.info(f"Starting subprocess: {', '.join(cmd)}")
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
stdout=output_dest,
|
||||
stderr=output_dest,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result
|
||||
logger.error(f"Subprocess failed with code {result.returncode}: {cmd}")
|
||||
if not stream_output:
|
||||
logger.error(f"stdout: {result.stdout}")
|
||||
logger.error(f"stderr: {result.stderr}")
|
||||
raise SystemExit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Loading…
Reference in New Issue