Skip to content
Merged
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
102 changes: 93 additions & 9 deletions .github/workflows/developer-guide-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand All @@ -55,19 +93,47 @@ 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")"
if [ -d "$asset_dir" ] && [ "$base_name" != "sketch" ]; then
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
Expand All @@ -92,6 +158,24 @@ jobs:
const marker = '<!-- developer-guide-artifacts -->';
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,
Expand Down
2 changes: 2 additions & 0 deletions docs/developer-guide/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
book-cover.generated.svg
book-cover.generated.png
3 changes: 2 additions & 1 deletion docs/developer-guide/developer-guide.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions scripts/developer-guide/prepare_cover_artwork.py
Original file line number Diff line number Diff line change
@@ -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:]))