diff --git a/.github/workflows/developer-guide-docs.yml b/.github/workflows/developer-guide-docs.yml index ee2c36a9db..bd8c1b34a8 100644 --- a/.github/workflows/developer-guide-docs.yml +++ b/.github/workflows/developer-guide-docs.yml @@ -2,6 +2,7 @@ name: Build Developer Guide Docs on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] paths: - 'docs/developer-guide/**' - '.github/workflows/developer-guide-docs.yml' @@ -21,23 +22,60 @@ jobs: - name: Check out repository uses: actions/checkout@v4 - - name: Update publication metadata for release - if: ${{ github.event_name == 'release' && github.event.action == 'published' }} - env: - RELEASE_TAG: ${{ github.event.release.tag_name }} + - name: Determine publication metadata run: | set -euo pipefail REV_DATE="$(date -u +%Y-%m-%d)" - VERSION="${RELEASE_TAG:-}" + VERSION="${GITHUB_HEAD_REF:-}" if [ -z "$VERSION" ] && [ -n "${GITHUB_REF_NAME:-}" ]; then VERSION="$GITHUB_REF_NAME" fi + if [ -z "$VERSION" ] && [ -n "${GITHUB_SHA:-}" ]; then + VERSION="${GITHUB_SHA:0:7}" + fi VERSION="${VERSION#v}" if [ -z "$VERSION" ]; then VERSION="UNKNOWN" fi - sed -i -E "s/^:revdate: .*/:revdate: ${REV_DATE}/" docs/developer-guide/developer-guide.asciidoc - sed -i -E "s/^:revnumber: .*/:revnumber: ${VERSION}/" docs/developer-guide/developer-guide.asciidoc + if date -u -d "$REV_DATE" '+%B %-d, %Y' >/tmp/rev_human 2>/dev/null; then + REV_HUMAN_DATE="$(cat /tmp/rev_human)" + else + REV_HUMAN_DATE="$(date -u '+%B %-d, %Y')" + fi + echo "Developer guide workflow metadata:" >&2 + echo " REV_DATE=$REV_DATE" >&2 + echo " REV_NUMBER=$VERSION" >&2 + echo " REV_HUMAN_DATE=$REV_HUMAN_DATE" >&2 + { + echo "REV_DATE=$REV_DATE" + echo "REV_NUMBER=$VERSION" + echo "REV_HUMAN_DATE=$REV_HUMAN_DATE" + } >> "$GITHUB_ENV" + + - name: Render publication cover artwork + run: | + set -euo pipefail + sudo apt-get update + sudo apt-get install -y --no-install-recommends librsvg2-bin + SOURCE="docs/developer-guide/book-cover.svg" + GENERATED_SVG="docs/developer-guide/book-cover.generated.svg" + GENERATED_PNG="docs/developer-guide/book-cover.generated.png" + cp "$SOURCE" "$GENERATED_SVG" + export GENERATED_SVG + python3 scripts/developer-guide/prepare_cover_artwork.py \ + "$GENERATED_SVG" \ + --rev-number "$REV_NUMBER" \ + --rev-human-date "$REV_HUMAN_DATE" \ + --rev-date "$REV_DATE" + echo "Rasterizing cover artwork to ${GENERATED_PNG}" >&2 + rsvg-convert -w 2551 -h 3579 "$GENERATED_SVG" -o "$GENERATED_PNG" + ls -l "$GENERATED_PNG" + file "$GENERATED_PNG" + { + echo "COVER_IMAGE_ATTR=$(basename "$GENERATED_PNG")" + echo "GENERATED_COVER_IMAGE=$GENERATED_PNG" + echo "GENERATED_COVER_SVG=$GENERATED_SVG" + } >> "$GITHUB_ENV" - name: Set up Ruby uses: ruby/setup-ruby@v1 @@ -55,11 +93,30 @@ jobs: HTML_BUILD_DIR="${OUTPUT_ROOT}/html" PDF_BUILD_DIR="${OUTPUT_ROOT}/pdf" PACKAGE_DIR="${OUTPUT_ROOT}/html-package" + COVER_IMAGE_ATTR_VALUE="${COVER_IMAGE_ATTR:-book-cover.svg}" + GENERATED_COVER_IMAGE="${GENERATED_COVER_IMAGE:-}" + GENERATED_COVER_SVG="${GENERATED_COVER_SVG:-}" + echo "Building with cover image attribute: ${COVER_IMAGE_ATTR_VALUE}" >&2 mkdir -p "$HTML_BUILD_DIR" "$PDF_BUILD_DIR" - asciidoctor -D "$HTML_BUILD_DIR" -o developer-guide.html docs/developer-guide/developer-guide.asciidoc - asciidoctor-pdf -D "$PDF_BUILD_DIR" -o developer-guide.pdf docs/developer-guide/developer-guide.asciidoc + asciidoctor \ + -a revdate="$REV_DATE" \ + -a revnumber="$REV_NUMBER" \ + -a cover-image="$COVER_IMAGE_ATTR_VALUE" \ + -D "$HTML_BUILD_DIR" \ + -o developer-guide.html \ + docs/developer-guide/developer-guide.asciidoc + asciidoctor-pdf \ + -a revdate="$REV_DATE" \ + -a revnumber="$REV_NUMBER" \ + -a cover-image="$COVER_IMAGE_ATTR_VALUE" \ + -D "$PDF_BUILD_DIR" \ + -o developer-guide.pdf \ + docs/developer-guide/developer-guide.asciidoc rm -rf "$PACKAGE_DIR" mkdir -p "$PACKAGE_DIR" + if [ -n "$GENERATED_COVER_IMAGE" ] && [ -f "$GENERATED_COVER_IMAGE" ]; then + cp "$GENERATED_COVER_IMAGE" "$HTML_BUILD_DIR/$(basename "$GENERATED_COVER_IMAGE")" + fi cp "$HTML_BUILD_DIR/developer-guide.html" "$PACKAGE_DIR/" for asset_dir in docs/developer-guide/*; do base_name="$(basename "$asset_dir")" @@ -67,7 +124,16 @@ jobs: cp -R "$asset_dir" "$PACKAGE_DIR/" fi done + if [ -n "$GENERATED_COVER_IMAGE" ] && [ -f "$GENERATED_COVER_IMAGE" ]; then + cp "$GENERATED_COVER_IMAGE" "$PACKAGE_DIR/$(basename "$GENERATED_COVER_IMAGE")" + fi (cd "$PACKAGE_DIR" && zip -r "../developer-guide-html.zip" .) + if [ -n "$GENERATED_COVER_IMAGE" ] && [ -f "$GENERATED_COVER_IMAGE" ]; then + rm -f "$GENERATED_COVER_IMAGE" + fi + if [ -n "$GENERATED_COVER_SVG" ] && [ -f "$GENERATED_COVER_SVG" ]; then + rm -f "$GENERATED_COVER_SVG" + fi - name: Upload HTML artifact uses: actions/upload-artifact@v4 @@ -92,6 +158,24 @@ jobs: const marker = ''; const { owner, repo } = context.repo; const runId = context.runId; + const prNumber = context.payload.pull_request?.number; + const runHeadSha = context.payload.pull_request?.head?.sha; + + if (prNumber && runHeadSha) { + const { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: prNumber + }); + + if (pr.head.sha !== runHeadSha) { + console.log( + `Skipping PR comment update because workflow run head ${runHeadSha} is not the latest PR head ${pr.head.sha}.` + ); + return; + } + } + const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner, repo, diff --git a/docs/developer-guide/.gitignore b/docs/developer-guide/.gitignore new file mode 100644 index 0000000000..7516c3134b --- /dev/null +++ b/docs/developer-guide/.gitignore @@ -0,0 +1,2 @@ +book-cover.generated.svg +book-cover.generated.png diff --git a/docs/developer-guide/developer-guide.asciidoc b/docs/developer-guide/developer-guide.asciidoc index 66232ad8ef..e4c896690f 100644 --- a/docs/developer-guide/developer-guide.asciidoc +++ b/docs/developer-guide/developer-guide.asciidoc @@ -16,7 +16,8 @@ :autofit-option: //:hide-uri-scheme: :uuid: 92CA37B2-EB2B-4B8F-AC7C-1ED13683F7FB -:front-cover-image: image:img/cover.png[Front Cover,1000,1600] +:cover-image: book-cover.svg +:front-cover-image: image:{cover-image}[Front Cover] :lang: en-US :revdate: 2025-10-20 :revnumber: DEV-SNAPSHOT diff --git a/scripts/developer-guide/prepare_cover_artwork.py b/scripts/developer-guide/prepare_cover_artwork.py new file mode 100755 index 0000000000..233fc2b3f2 --- /dev/null +++ b/scripts/developer-guide/prepare_cover_artwork.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Prepare developer guide cover artwork for rasterization.""" + +from __future__ import annotations + +import argparse +import pathlib +import re +import sys + + +VERSION_PATTERN = re.compile(r"Version\s+[^<]+") +CSS_VARIABLE_DECL_PATTERN = re.compile(r"--([\w-]+):\s*([^;]+);") +CSS_VARIABLE_REF_PATTERN = re.compile(r"var\(--([\w-]+)\)") + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("svg_path", help="Path to the SVG file to update") + parser.add_argument("--rev-number", required=True, help="Revision number to embed") + parser.add_argument("--rev-human-date", default="", help="Human readable revision date") + parser.add_argument("--rev-date", default="", help="Fallback machine readable revision date") + return parser.parse_args(argv) + + +def inject_version_label(svg_text: str, version_label: str) -> str: + updated_text, count = VERSION_PATTERN.subn(version_label, svg_text, count=1) + if count != 1: + raise RuntimeError("Could not find version text placeholder in cover artwork") + return updated_text + + +def resolve_css_variables(svg_text: str) -> tuple[str, int]: + variables = {name: value.strip() for name, value in CSS_VARIABLE_DECL_PATTERN.findall(svg_text)} + replacement_count = 0 + + def replace(match: re.Match[str]) -> str: + nonlocal replacement_count + name = match.group(1) + value = variables.get(name) + if value is None: + return match.group(0) + replacement_count += 1 + return value + + resolved_text = CSS_VARIABLE_REF_PATTERN.sub(replace, svg_text) + return resolved_text, replacement_count + + +def main(argv: list[str]) -> int: + args = parse_args(argv) + + svg_path = pathlib.Path(args.svg_path) + svg_text = svg_path.read_text(encoding="utf-8") + + revision_date = args.rev_human_date or args.rev_date + version_label = f"Version {args.rev_number}".strip() + if revision_date: + version_label = f"{version_label} - {revision_date.strip()}" + + svg_text = inject_version_label(svg_text, version_label) + svg_text, replacement_count = resolve_css_variables(svg_text) + + svg_path.write_text(svg_text, encoding="utf-8") + + print(f"Injected cover legend: {version_label}") + print(f"Resolved {replacement_count} CSS variable references for rasterization") + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:]))