Fix developer guide cover rasterization #16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' | ||
| release: | ||
| types: [published] | ||
| workflow_dispatch: | ||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| issues: write | ||
| pull-requests: write | ||
| actions: read | ||
| steps: | ||
| - name: Check out repository | ||
| uses: actions/checkout@v4 | ||
| - name: Determine publication metadata | ||
| run: | | ||
| set -euo pipefail | ||
| REV_DATE="$(date -u +%Y-%m-%d)" | ||
| 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 | ||
| 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 - <<'PY' | ||
| import os | ||
| import pathlib | ||
| import re | ||
| target = pathlib.Path(os.environ['GENERATED_SVG']) | ||
| text = target.read_text(encoding='utf-8') | ||
| rev = os.environ.get('REV_NUMBER', 'UNKNOWN') | ||
| date = os.environ.get('REV_HUMAN_DATE') or os.environ.get('REV_DATE', '') | ||
| replacement = f"Version {rev}" + (f" - {date}" if date else '') | ||
| new_text, count = re.subn(r'Version\s+[^<]+', replacement.strip(), text, count=1) | ||
| if count != 1: | ||
| raise RuntimeError('Could not find version text placeholder in cover artwork') | ||
| variables = { | ||
| name: value.strip() | ||
| for name, value in re.findall(r'--([\w-]+):\s*([^;]+);', new_text) | ||
| } | ||
| var_counter = {'count': 0} | ||
| def replace_var(match): | ||
| name = match.group(1) | ||
| value = variables.get(name) | ||
| if value is None: | ||
| return match.group(0) | ||
| var_counter['count'] += 1 | ||
| return value | ||
| new_text = re.sub(r'var\(--([\w-]+)\)', replace_var, new_text) | ||
| target.write_text(new_text, encoding='utf-8') | ||
| print(f"Injected cover legend: {replacement.strip()}") | ||
| print(f"Resolved {var_counter['count']} CSS variable references for rasterization") | ||
| PY | ||
| 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 | ||
| with: | ||
| ruby-version: '3.1' | ||
| - name: Install Asciidoctor tooling | ||
| run: | | ||
| gem install --no-document asciidoctor asciidoctor-pdf | ||
| - name: Build Developer Guide HTML and PDF | ||
| run: | | ||
| set -euo pipefail | ||
| OUTPUT_ROOT="build/developer-guide" | ||
| 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 \ | ||
| -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 | ||
| with: | ||
| name: developer-guide-html | ||
| path: build/developer-guide/developer-guide-html.zip | ||
| if-no-files-found: error | ||
| - name: Upload PDF artifact | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: developer-guide-pdf | ||
| path: build/developer-guide/pdf/developer-guide.pdf | ||
| if-no-files-found: error | ||
| - name: Comment with artifact download links | ||
| if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} | ||
| uses: actions/github-script@v7 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const marker = '<!-- developer-guide-artifacts -->'; | ||
| const { owner, repo } = context.repo; | ||
| const runId = context.runId; | ||
| const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ | ||
| owner, | ||
| repo, | ||
| run_id: runId, | ||
| per_page: 100 | ||
| }); | ||
| const links = []; | ||
| for (const artifact of artifacts.data.artifacts) { | ||
| if (artifact.name === 'developer-guide-html') { | ||
| links.push(`- [Developer Guide HTML package](https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id})`); | ||
| } | ||
| if (artifact.name === 'developer-guide-pdf') { | ||
| links.push(`- [Developer Guide PDF](https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id})`); | ||
| } | ||
| } | ||
| if (!links.length) { | ||
| console.log('No artifacts found to report.'); | ||
| return; | ||
| } | ||
| const body = `${marker}\nDeveloper Guide build artifacts are available for download from this workflow run:\n\n${links.join('\n')}\n`; | ||
| const comments = await github.rest.issues.listComments({ | ||
| owner, | ||
| repo, | ||
| issue_number: context.issue.number, | ||
| per_page: 100 | ||
| }); | ||
| const existing = comments.data.find(comment => comment.body && comment.body.includes(marker)); | ||
| if (existing) { | ||
| await github.rest.issues.updateComment({ | ||
| owner, | ||
| repo, | ||
| comment_id: existing.id, | ||
| body | ||
| }); | ||
| } else { | ||
| await github.rest.issues.createComment({ | ||
| owner, | ||
| repo, | ||
| issue_number: context.issue.number, | ||
| body | ||
| }); | ||
| } | ||
| - name: Log skipped PR comment | ||
| if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} | ||
| run: echo "Skipping PR comment because the workflow run does not have permission to post on forked pull requests." | ||
| - name: Attach artifacts to release | ||
| if: ${{ github.event_name == 'release' && github.event.action == 'published' }} | ||
| uses: softprops/action-gh-release@v1 | ||
| with: | ||
| files: | | ||
| build/developer-guide/developer-guide-html.zip | ||
| build/developer-guide/pdf/developer-guide.pdf | ||