Add native desktop themes and platform fidelity testing #6408
Workflow file for this run
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: | |
| paths: | |
| - 'scripts/ci/retry.sh' | |
| - 'docs/developer-guide/**' | |
| - 'docs/demos/**' | |
| - 'scripts/developer-guide/**' | |
| # The API-name check derives the published pages from the javadoc's own | |
| # sources, and the guide links into the java.* compatibility classes -- | |
| # java/util/List.html among them. A change that renames or deletes one | |
| # of those without touching docs/ would otherwise skip this job and | |
| # leave a dead link behind. | |
| - 'Ports/CLDC11/**' | |
| # And the script that decides what javadoc publishes at all. The check | |
| # mirrors its source roots, its excluded package and its -protected | |
| # setting, so a change to any of those moves which links are alive | |
| # without touching a page. | |
| - '.github/scripts/build_javadocs.sh' | |
| - '.github/workflows/developer-guide-docs.yml' | |
| # The build hint table is rendered rather than committed, so a change to | |
| # anything it is rendered FROM changes what this guide contains without | |
| # touching a single file under docs/. While the table was checked in that | |
| # edit showed up as a docs diff and triggered this workflow for free; | |
| # generating it on the fly took that away, and a malformed table would then | |
| # have merged and surfaced in the release documentation build. | |
| # | |
| # All four inputs, not just the catalog: the annotations are the source of | |
| # truth for the hints they expose and their prose goes straight into the | |
| # Description column, and build-hint-tools is the renderer itself. | |
| - 'CodenameOne/src/com/codename1/annotations/buildhints/**' | |
| - 'maven/build-hint-catalog/**' | |
| - 'maven/build-hint-tools/**' | |
| - 'scripts/gen-build-hint-table.sh' | |
| # The themed figures are rendered against the compiled native themes, so | |
| # what those themes look like is an input to this guide exactly the way the | |
| # build hint sources above are. build-native-themes.sh compiles | |
| # native-themes/<name>/theme.css with maven/css-compiler, so a change to | |
| # any of the three changes the pictures without touching docs/ at all -- | |
| # and the committed figures would then go stale silently until some later | |
| # run failed against them. | |
| - 'native-themes/**' | |
| - 'maven/css-compiler/**' | |
| - 'scripts/build-native-themes.sh' | |
| # The figures are rendered by the framework itself, so a change to how a | |
| # component paints, how a layout measures, or how the JavaSE port | |
| # rasterizes changes every generated PNG without touching docs/ at all. | |
| # The comparison is byte-exact, so without these the committed figures go | |
| # stale silently and some later docs or release build is the one that | |
| # fails. | |
| - 'CodenameOne/**' | |
| - 'Ports/JavaSE/**' | |
| - 'maven/core/**' | |
| - 'maven/javase/**' | |
| 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@v6 | |
| - name: Determine changed components | |
| id: changes | |
| if: github.event_name == 'pull_request' | |
| uses: dorny/paths-filter@v3 | |
| with: | |
| filters: | | |
| demos: | |
| - 'docs/demos/**' | |
| - '.github/workflows/developer-guide-docs.yml' | |
| docs: | |
| - 'docs/developer-guide/**' | |
| - 'docs/demos/common/src/main/snippets/**' | |
| # The whole directory, not two named files. on.pull_request.paths | |
| # already triggers on scripts/developer-guide/**, so a change to any | |
| # other script here started the workflow while leaving this filter | |
| # false -- which skipped the very steps that script governs. A | |
| # change to compare-screenshots.py could have merged without the | |
| # screenshot check ever running it. | |
| - 'scripts/developer-guide/**' | |
| - '.github/workflows/developer-guide-docs.yml' | |
| # Triggering the workflow is not enough on its own: the HTML and PDF | |
| # build and the steps beside it are gated on this filter, so the | |
| # SAME four inputs listed under on.pull_request.paths have to | |
| # satisfy it too. Two copies of one list, and they had already | |
| # drifted from what the table is actually rendered from. | |
| - 'CodenameOne/src/com/codename1/annotations/buildhints/**' | |
| - 'maven/build-hint-catalog/**' | |
| - 'maven/build-hint-tools/**' | |
| - 'scripts/gen-build-hint-table.sh' | |
| # Same two-copies-of-one-list problem as the build hint inputs: | |
| # triggering the workflow is not enough, because the figure | |
| # comparison step is gated on this filter too. | |
| - 'native-themes/**' | |
| - 'maven/css-compiler/**' | |
| - 'scripts/build-native-themes.sh' | |
| # Same reason, and the same two-lists rule: the figure comparison | |
| # step is gated on this filter as well as on the trigger. | |
| - 'CodenameOne/**' | |
| - 'Ports/JavaSE/**' | |
| - 'maven/core/**' | |
| - 'maven/javase/**' | |
| workflow: | |
| - '.github/workflows/developer-guide-docs.yml' | |
| - name: Set up Java | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '17' | |
| # Before anything that reads the guide. The build hint table is rendered | |
| # from maven/build-hint-catalog rather than checked in, so the lint, the | |
| # image and snippet checks, the HTML and PDF build and Vale all need it on | |
| # disk -- and asciidoctor reports a missing include as an error, so getting | |
| # this order wrong fails loudly rather than dropping the table quietly. | |
| # Unconditional, because every one of those steps is not. | |
| - name: Render the build hint table | |
| run: scripts/gen-build-hint-table.sh | |
| # The javase jar carries iOSModernTheme.res and AndroidMaterialTheme.res, | |
| # which the figure renderer installs. They are copied in from Themes/ by an | |
| # antrun step with failonerror=false, so if they have not been compiled the | |
| # copy fails open and the jar ships without them -- and the renderer then | |
| # dies on a theme resource that is simply absent. Compiling them first is | |
| # what run-javase-simulator-integration-tests.sh already does, for the same | |
| # reason. | |
| - name: Compile the native themes | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| run: ./scripts/build-native-themes.sh | |
| - name: Install local Codename One Maven artifacts | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| run: | | |
| set -euo pipefail | |
| xvfb-run -a mvn -B -ntp -f maven/pom.xml \ | |
| -pl core,javase,android,css-compiler,codenameone-maven-plugin,backend \ | |
| -am install \ | |
| -Plocal-dev-javase \ | |
| -DskipTests \ | |
| -Dmaven.javadoc.skip=true \ | |
| -Dmaven.source.skip=true \ | |
| -Dspotbugs.skip=true \ | |
| -Dpmd.skip=true \ | |
| -Dcheckstyle.skip=true | |
| - name: Build Codename One demos | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| run: | | |
| set -euo pipefail | |
| mkdir -p "$HOME/.codenameone" | |
| touch "$HOME/.codenameone/guibuilder.jar" | |
| cp maven/CodeNameOneBuildClient.jar "$HOME/.codenameone/CodeNameOneBuildClient.jar" | |
| xvfb-run -a mvn -B -ntp -Dgenerate-gui-sources-done=true -Dcodename1.platform=javase -f docs/demos/pom.xml process-classes | |
| - name: Install demo common module for screenshot generation | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| run: | | |
| set -euo pipefail | |
| bash $GITHUB_WORKSPACE/scripts/ci/retry.sh mvn -B -ntp \ | |
| -Dgenerate-gui-sources-done=true \ | |
| -Dcodename1.platform=javase \ | |
| -f docs/demos/pom.xml \ | |
| -pl common \ | |
| -am \ | |
| install \ | |
| -DskipTests | |
| - name: Verify generated developer guide figures are current | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| run: | | |
| set -euo pipefail | |
| GENERATED_DIR="$RUNNER_TEMP/pre-advanced-theming-screenshots" | |
| echo "GUIDE_GENERATED_SCREENSHOTS=$GENERATED_DIR" >> "$GITHUB_ENV" | |
| rm -rf "$GENERATED_DIR" | |
| mkdir -p "$GENERATED_DIR" | |
| xvfb-run -a mvn -B -ntp \ | |
| -Dgenerate-gui-sources-done=true \ | |
| -Dcodename1.platform=javase \ | |
| -f docs/demos/javase/pom.xml \ | |
| -P guide-screenshot-generator \ | |
| -Dguide.screenshot.output="$GENERATED_DIR" \ | |
| verify | |
| pip install --user --quiet Pillow | |
| # Byte equality is still the rule; a figure only gets a bounded | |
| # difference if it carries a .tolerance sidecar saying why. | |
| python3 scripts/developer-guide/compare-screenshots.py \ | |
| --generated "$GENERATED_DIR" \ | |
| --committed docs/developer-guide/img/generated | |
| # When the byte compare fails, the message names the file but not what it | |
| # actually rendered, which leaves no way to tell a real regression from an | |
| # environment difference. Publishing what this runner produced makes the | |
| # two distinguishable without adding a debugging round trip to CI. | |
| - name: Upload generated screenshots when they do not match | |
| if: failure() && env.GUIDE_GENERATED_SCREENSHOTS != '' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: guide-generated-screenshots | |
| path: ${{ env.GUIDE_GENERATED_SCREENSHOTS }} | |
| if-no-files-found: warn | |
| - name: Verify developer guide images are referenced | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| run: | | |
| set -euo pipefail | |
| python3 scripts/developer-guide/find_unused_images.py docs/developer-guide | |
| - name: Compile Android demo sources | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| run: | | |
| set -euo pipefail | |
| scripts/developer-guide/compile-android-demo-sources.sh | |
| - name: Validate developer guide snippets | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| run: | | |
| set -euo pipefail | |
| python3 scripts/developer-guide/validate-guide-snippets.py | |
| - 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: Determine Codename One release version | |
| run: | | |
| set -euo pipefail | |
| VERSION="$(python3 scripts/developer-guide/determine_release_version.py)" | |
| if [ -z "$VERSION" ]; then | |
| echo "Unable to determine Codename One release version" >&2 | |
| exit 1 | |
| fi | |
| echo "Using Codename One release version: $VERSION" >&2 | |
| { | |
| echo "CN1_RELEASE_VERSION=$VERSION" | |
| echo "CN1_PLUGIN_RELEASE_VERSION=$VERSION" | |
| } >> "$GITHUB_ENV" | |
| - name: Render publication cover artwork | |
| run: | | |
| set -euo pipefail | |
| bash scripts/ci/apt-get-update.sh | |
| 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 | |
| with: | |
| ruby-version: '3.1' | |
| - name: Install Asciidoctor tooling | |
| run: | | |
| gem install --no-document asciidoctor asciidoctor-pdf rouge | |
| - name: Run Asciidoctor lint | |
| run: | | |
| set -euo pipefail | |
| REPORT_DIR="build/developer-guide/reports" | |
| REPORT_FILE="${REPORT_DIR}/asciidoc-lint-report.txt" | |
| mkdir -p "$REPORT_DIR" | |
| set +e | |
| asciidoctor \ | |
| --require rouge \ | |
| --failure-level WARN \ | |
| --verbose \ | |
| --trace \ | |
| -o /dev/null \ | |
| docs/developer-guide/developer-guide.asciidoc \ | |
| 2>&1 | tee "$REPORT_FILE" | |
| STATUS=${PIPESTATUS[0]} | |
| set -e | |
| echo "ASCII_DOC_LINT_REPORT=$REPORT_FILE" >> "$GITHUB_ENV" | |
| echo "ASCII_DOC_LINT_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| if [ "$STATUS" -ne 0 ]; then | |
| echo "Asciidoctor exited with status $STATUS — the final quality-gate step will fail the build." >&2 | |
| fi | |
| # These four cover defects the gates above are structurally blind to: a | |
| # chapter swallowed by the one before it, a cross-reference that resolves | |
| # to nothing, prose that promises a code block which is not there, and a | |
| # link the website does not serve. Asciidoctor reports none of them, and | |
| # every one of them shipped. The structure and cross-reference checks | |
| # render the book, so they must run after the Asciidoctor install. | |
| - name: Check developer guide structure, cross-references, code blocks and links | |
| run: | | |
| set -euo pipefail | |
| python3 scripts/developer-guide/check-guide-structure.py | |
| python3 scripts/developer-guide/check-guide-xrefs.py | |
| python3 scripts/developer-guide/check-missing-code-blocks.py | |
| # An unquoted comma in alt text silently truncates it and puts the rest | |
| # in the width slot. Asciidoctor accepts it and the image renders, so | |
| # nothing else in this job notices. | |
| python3 scripts/developer-guide/check-image-alt-text.py | |
| # Note this only catches a link the GUIDE breaks. When the website | |
| # moves the route instead -- a page renamed, deleted or re-slugged -- | |
| # the same script runs from website-docs.yml, which triggers on | |
| # docs/website/**. Putting those paths here would run this whole job | |
| # (maven install, demo build, screenshots) for every blog post. | |
| python3 scripts/developer-guide/check-guide-links.py | |
| # A class name the guide gets slightly wrong sends the reader looking | |
| # for something that is not there, and nothing else here reads the | |
| # names: EnclodedImage, ConectionRequest, GenenricListCellRenderer, | |
| # BubbleTransiton, SwipableContainer, GridbagLayout, | |
| # InifiniteContainer, DataChangeListener and MiGLayout all shipped. | |
| python3 scripts/developer-guide/check-guide-api-names.py | |
| - name: Build Developer Guide HTML and PDF | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| 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: Install Vale | |
| run: | | |
| set -euo pipefail | |
| VALE_VERSION="3.13.0" | |
| VALE_ARCHIVE="vale_${VALE_VERSION}_Linux_64-bit.tar.gz" | |
| curl -fsSL -o "$VALE_ARCHIVE" "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/${VALE_ARCHIVE}" | |
| tar -xzf "$VALE_ARCHIVE" | |
| sudo mv vale /usr/local/bin/vale | |
| rm -f "$VALE_ARCHIVE" | |
| - name: Sync Vale styles | |
| run: | | |
| set -euo pipefail | |
| vale sync --config docs/developer-guide/.vale.ini | |
| - name: Run Vale style linter | |
| run: | | |
| set -euo pipefail | |
| REPORT_DIR="build/developer-guide/reports" | |
| REPORT_FILE="${REPORT_DIR}/vale-report.json" | |
| HTML_REPORT="${REPORT_DIR}/vale-report.html" | |
| mkdir -p "$REPORT_DIR" | |
| set +e | |
| vale --config docs/developer-guide/.vale.ini --minAlertLevel=suggestion --output=JSON docs/developer-guide > "$REPORT_FILE" | |
| STATUS=$? | |
| set -e | |
| python3 scripts/developer-guide/vale_report_to_html.py --input "$REPORT_FILE" --output "$HTML_REPORT" | |
| ISSUE_COUNT="$(python3 -c "import json, sys; data = json.load(open(sys.argv[1])); print(sum(len(v) for v in data.values() if isinstance(v, list)))" "$REPORT_FILE" 2>/dev/null || echo 0)" | |
| echo "VALE_REPORT=$REPORT_FILE" >> "$GITHUB_ENV" | |
| echo "VALE_HTML_REPORT=$HTML_REPORT" >> "$GITHUB_ENV" | |
| echo "VALE_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| echo "VALE_ISSUE_COUNT=$ISSUE_COUNT" >> "$GITHUB_ENV" | |
| if [ "$STATUS" -ne 0 ] || [ "$ISSUE_COUNT" != "0" ]; then | |
| echo "Vale exited with status $STATUS and reported $ISSUE_COUNT issue(s). The final quality-gate step will fail the build." >&2 | |
| fi | |
| - name: Run paragraph capitalization check | |
| run: | | |
| set -euo pipefail | |
| REPORT_DIR="build/developer-guide/reports" | |
| REPORT_FILE="${REPORT_DIR}/paragraph-capitalization-report.json" | |
| mkdir -p "$REPORT_DIR" | |
| set +e | |
| ruby scripts/developer-guide/check_paragraph_capitalization.rb \ | |
| --output "$REPORT_FILE" \ | |
| docs/developer-guide/developer-guide.asciidoc | |
| STATUS=$? | |
| set -e | |
| echo "PARAGRAPH_CAP_REPORT=$REPORT_FILE" >> "$GITHUB_ENV" | |
| echo "PARAGRAPH_CAP_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| if [ "$STATUS" -ne 0 ]; then | |
| echo "Paragraph capitalization check failed. The final quality-gate step will fail the build." >&2 | |
| fi | |
| - name: Set up Java 17 for LanguageTool | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| uses: actions/setup-java@v5 | |
| with: | |
| distribution: 'temurin' | |
| java-version: '17' | |
| - name: Install language-tool-python | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| run: | | |
| set -euo pipefail | |
| pip install --user language-tool-python==2.9.4 | |
| - name: Run LanguageTool grammar check | |
| # LanguageTool consumes the rendered HTML produced by "Build | |
| # Developer Guide HTML and PDF". That step is gated on the same | |
| # paths filter (docs/demos/workflow). Without this gate the | |
| # LanguageTool step ran on PRs that didn't touch docs and exited | |
| # non-zero because `developer-guide.html` was never generated, | |
| # turning every unrelated PR (e.g. iOS port fixes) red on the | |
| # final quality-gate check. | |
| if: github.event_name != 'pull_request' || steps.changes.outputs.docs == 'true' || steps.changes.outputs.demos == 'true' || steps.changes.outputs.workflow == 'true' | |
| env: | |
| LANGUAGETOOL_VERSION: '6.6' | |
| run: | | |
| set -euo pipefail | |
| REPORT_DIR="build/developer-guide/reports" | |
| REPORT_FILE="${REPORT_DIR}/languagetool-report.json" | |
| mkdir -p "$REPORT_DIR" | |
| # The pip pin above fixes the WRAPPER; this fixes the engine it | |
| # downloads. Unset, the wrapper fetches "latest", which is a | |
| # nightly snapshot rather than a release -- so the rule set moved | |
| # between runs and turned branches red on prose nobody edited. | |
| echo "LanguageTool engine: ${LANGUAGETOOL_VERSION}" | |
| set +e | |
| python3 scripts/developer-guide/run_languagetool.py \ | |
| --html build/developer-guide/html/developer-guide.html \ | |
| --output "$REPORT_FILE" | |
| STATUS=$? | |
| set -e | |
| LT_COUNT="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("total",0))' "$REPORT_FILE" 2>/dev/null || echo 0)" | |
| echo "LANGUAGETOOL_REPORT=$REPORT_FILE" >> "$GITHUB_ENV" | |
| echo "LANGUAGETOOL_COUNT=$LT_COUNT" >> "$GITHUB_ENV" | |
| echo "LANGUAGETOOL_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| echo "LanguageTool flagged ${LT_COUNT} match(es). The final quality-gate step will fail the build if any matches remain." >&2 | |
| - name: Check for unused developer guide images | |
| run: | | |
| set -euo pipefail | |
| REPORT_DIR="build/developer-guide/reports" | |
| JSON_REPORT="${REPORT_DIR}/unused-images.json" | |
| TEXT_REPORT="${REPORT_DIR}/unused-images.txt" | |
| mkdir -p "$REPORT_DIR" | |
| set +e | |
| python3 scripts/developer-guide/find_unused_images.py docs/developer-guide \ | |
| --output "$JSON_REPORT" | tee "$TEXT_REPORT" | |
| STATUS=${PIPESTATUS[0]} | |
| set -e | |
| echo "UNUSED_IMAGES_JSON=$JSON_REPORT" >> "$GITHUB_ENV" | |
| echo "UNUSED_IMAGES_TEXT=$TEXT_REPORT" >> "$GITHUB_ENV" | |
| echo "UNUSED_IMAGES_STATUS=$STATUS" >> "$GITHUB_ENV" | |
| if [ "$STATUS" -ne 0 ]; then | |
| echo "Unused images detected — the final quality-gate step will fail the build." >&2 | |
| fi | |
| - name: Summarize AsciiDoc linter findings | |
| id: summarize_asciidoc_lint | |
| if: always() | |
| run: | | |
| python3 scripts/developer-guide/summarize_reports.py ascii \ | |
| --report "${ASCII_DOC_LINT_REPORT}" \ | |
| --status "${ASCII_DOC_LINT_STATUS:-0}" \ | |
| --output "${GITHUB_OUTPUT}" | |
| - name: Summarize Vale findings | |
| id: summarize_vale | |
| if: always() | |
| run: | | |
| python3 scripts/developer-guide/summarize_reports.py vale \ | |
| --report "${VALE_REPORT}" \ | |
| --status "${VALE_STATUS:-0}" \ | |
| --output "${GITHUB_OUTPUT}" | |
| - name: Summarize unused image findings | |
| id: summarize_unused_images | |
| if: always() | |
| run: | | |
| python3 scripts/developer-guide/summarize_reports.py unused-images \ | |
| --report "${UNUSED_IMAGES_JSON}" \ | |
| --output "${GITHUB_OUTPUT}" \ | |
| --details-key details \ | |
| --preview-limit 10 | |
| - name: Summarize paragraph capitalization findings | |
| id: summarize_paragraph_cap | |
| if: always() | |
| run: | | |
| python3 scripts/developer-guide/summarize_reports.py paragraph-capitalization \ | |
| --report "${PARAGRAPH_CAP_REPORT}" \ | |
| --status "${PARAGRAPH_CAP_STATUS:-0}" \ | |
| --output "${GITHUB_OUTPUT}" | |
| - name: Summarize LanguageTool findings | |
| id: summarize_languagetool | |
| if: always() | |
| run: | | |
| python3 scripts/developer-guide/summarize_reports.py languagetool \ | |
| --report "${LANGUAGETOOL_REPORT}" \ | |
| --output "${GITHUB_OUTPUT}" | |
| - name: Upload HTML artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: developer-guide-html | |
| path: build/developer-guide/developer-guide-html.zip | |
| if-no-files-found: warn | |
| - name: Upload PDF artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: developer-guide-pdf | |
| path: build/developer-guide/pdf/developer-guide.pdf | |
| if-no-files-found: warn | |
| - name: Upload AsciiDoc linter report | |
| if: always() && env.ASCII_DOC_LINT_REPORT != '' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: developer-guide-asciidoc-lint | |
| path: ${{ env.ASCII_DOC_LINT_REPORT }} | |
| if-no-files-found: warn | |
| - name: Upload Vale report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: developer-guide-vale-report | |
| path: | | |
| ${{ env.VALE_REPORT }} | |
| ${{ env.VALE_HTML_REPORT }} | |
| if-no-files-found: warn | |
| - name: Upload unused image report | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: developer-guide-unused-images | |
| path: | | |
| ${{ env.UNUSED_IMAGES_JSON }} | |
| ${{ env.UNUSED_IMAGES_TEXT }} | |
| if-no-files-found: warn | |
| - name: Upload paragraph capitalization report | |
| if: always() && env.PARAGRAPH_CAP_REPORT != '' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: developer-guide-paragraph-capitalization | |
| path: ${{ env.PARAGRAPH_CAP_REPORT }} | |
| if-no-files-found: warn | |
| - name: Upload LanguageTool report | |
| if: always() && env.LANGUAGETOOL_REPORT != '' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: developer-guide-languagetool | |
| path: ${{ env.LANGUAGETOOL_REPORT }} | |
| if-no-files-found: warn | |
| - name: Fail build on developer guide quality issues | |
| if: always() | |
| run: | | |
| set -euo pipefail | |
| FAIL=0 | |
| if [ "${ASCII_DOC_LINT_STATUS:-0}" != "0" ]; then | |
| echo "AsciiDoc lint exited with status ${ASCII_DOC_LINT_STATUS}. See the developer-guide-asciidoc-lint artifact." >&2 | |
| FAIL=1 | |
| fi | |
| if [ "${VALE_STATUS:-0}" != "0" ] || [ "${VALE_ISSUE_COUNT:-0}" != "0" ]; then | |
| echo "Vale reported ${VALE_ISSUE_COUNT:-?} issue(s) (exit status ${VALE_STATUS:-?}). See the developer-guide-vale-report artifact." >&2 | |
| echo "Either fix the prose, add the term to docs/developer-guide/styles/config/vocabularies/CodenameOne/accept.txt, or document an exception per docs/developer-guide/.vale.ini." >&2 | |
| FAIL=1 | |
| fi | |
| if [ "${UNUSED_IMAGES_STATUS:-0}" != "0" ]; then | |
| echo "Unused image check reported orphan files (exit status ${UNUSED_IMAGES_STATUS}). See the developer-guide-unused-images artifact." >&2 | |
| echo "Either delete the orphan image or add an image:: reference for it from a developer-guide asciidoc file." >&2 | |
| FAIL=1 | |
| fi | |
| if [ "${PARAGRAPH_CAP_STATUS:-0}" != "0" ]; then | |
| echo "Paragraph capitalization check reported findings (exit status ${PARAGRAPH_CAP_STATUS}). See the developer-guide-paragraph-capitalization artifact." >&2 | |
| echo "Rewrite the flagged paragraph so its first prose word starts with a capital letter." >&2 | |
| FAIL=1 | |
| fi | |
| if [ "${LANGUAGETOOL_STATUS:-0}" != "0" ] || [ "${LANGUAGETOOL_COUNT:-0}" != "0" ]; then | |
| echo "LanguageTool reported ${LANGUAGETOOL_COUNT:-?} match(es) (exit status ${LANGUAGETOOL_STATUS:-?}). See the developer-guide-languagetool artifact." >&2 | |
| echo "Either fix the prose, add the term to docs/developer-guide/languagetool-accept.txt, or document a new exception by disabling the rule in scripts/developer-guide/run_languagetool.py." >&2 | |
| FAIL=1 | |
| fi | |
| if [ "$FAIL" -ne 0 ]; then | |
| echo "Developer guide quality gates failed. Warnings from vale, asciidoctor, and LanguageTool are treated as build-breaking errors." >&2 | |
| exit 1 | |
| fi | |
| echo "Developer guide quality gates passed (vale issues=${VALE_ISSUE_COUNT:-0}, asciidoctor status=${ASCII_DOC_LINT_STATUS:-0}, unused-image status=${UNUSED_IMAGES_STATUS:-0}, languagetool count=${LANGUAGETOOL_COUNT:-0})." | |
| - name: Comment with artifact download links | |
| if: ${{ always() && github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} | |
| uses: actions/github-script@v9 | |
| env: | |
| ASCII_SUMMARY: ${{ steps.summarize_asciidoc_lint.outputs.summary }} | |
| VALE_SUMMARY: ${{ steps.summarize_vale.outputs.summary }} | |
| UNUSED_SUMMARY: ${{ steps.summarize_unused_images.outputs.summary }} | |
| UNUSED_DETAILS: ${{ steps.summarize_unused_images.outputs.details }} | |
| PARAGRAPH_CAP_SUMMARY: ${{ steps.summarize_paragraph_cap.outputs.summary }} | |
| LANGUAGETOOL_SUMMARY: ${{ steps.summarize_languagetool.outputs.summary }} | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| 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, | |
| run_id: runId, | |
| per_page: 100 | |
| }); | |
| const artifactLinks = new Map(); | |
| for (const artifact of artifacts.data.artifacts) { | |
| artifactLinks.set( | |
| artifact.name, | |
| `https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id}` | |
| ); | |
| } | |
| const links = []; | |
| if (artifactLinks.has('developer-guide-html')) { | |
| links.push(`- [Developer Guide HTML package](${artifactLinks.get('developer-guide-html')})`); | |
| } | |
| if (artifactLinks.has('developer-guide-pdf')) { | |
| links.push(`- [Developer Guide PDF](${artifactLinks.get('developer-guide-pdf')})`); | |
| } | |
| if (artifactLinks.has('developer-guide-asciidoc-lint')) { | |
| links.push(`- [AsciiDoc linter report](${artifactLinks.get('developer-guide-asciidoc-lint')})`); | |
| } | |
| if (artifactLinks.has('developer-guide-vale-report')) { | |
| links.push(`- [Vale report](${artifactLinks.get('developer-guide-vale-report')})`); | |
| } | |
| if (artifactLinks.has('developer-guide-unused-images')) { | |
| links.push(`- [Unused image report](${artifactLinks.get('developer-guide-unused-images')})`); | |
| } | |
| if (artifactLinks.has('developer-guide-paragraph-capitalization')) { | |
| links.push(`- [Paragraph capitalization report](${artifactLinks.get('developer-guide-paragraph-capitalization')})`); | |
| } | |
| if (artifactLinks.has('developer-guide-languagetool')) { | |
| links.push(`- [LanguageTool report](${artifactLinks.get('developer-guide-languagetool')})`); | |
| } | |
| if (!links.length) { | |
| console.log('No artifacts found to report.'); | |
| return; | |
| } | |
| const qualityLines = []; | |
| const asciiSummary = process.env.ASCII_SUMMARY?.trim(); | |
| const valeSummary = process.env.VALE_SUMMARY?.trim(); | |
| const unusedSummary = process.env.UNUSED_SUMMARY?.trim(); | |
| const paragraphCapSummary = process.env.PARAGRAPH_CAP_SUMMARY?.trim(); | |
| const languagetoolSummary = process.env.LANGUAGETOOL_SUMMARY?.trim(); | |
| const asciiLink = artifactLinks.get('developer-guide-asciidoc-lint'); | |
| const valeLink = artifactLinks.get('developer-guide-vale-report'); | |
| const unusedLink = artifactLinks.get('developer-guide-unused-images'); | |
| const paragraphCapLink = artifactLinks.get('developer-guide-paragraph-capitalization'); | |
| const languagetoolLink = artifactLinks.get('developer-guide-languagetool'); | |
| if (asciiSummary) { | |
| qualityLines.push(`- AsciiDoc linter: ${asciiSummary}${asciiLink ? ` ([report](${asciiLink}))` : ''}`); | |
| } | |
| if (valeSummary) { | |
| qualityLines.push(`- Vale: ${valeSummary}${valeLink ? ` ([report](${valeLink}))` : ''}`); | |
| } | |
| if (paragraphCapSummary) { | |
| qualityLines.push(`- Paragraph capitalization: ${paragraphCapSummary}${paragraphCapLink ? ` ([report](${paragraphCapLink}))` : ''}`); | |
| } | |
| if (languagetoolSummary) { | |
| qualityLines.push(`- LanguageTool: ${languagetoolSummary}${languagetoolLink ? ` ([report](${languagetoolLink}))` : ''}`); | |
| } | |
| if (unusedSummary) { | |
| qualityLines.push(`- Image references: ${unusedSummary}${unusedLink ? ` ([report](${unusedLink}))` : ''}`); | |
| } | |
| let unusedDetails = process.env.UNUSED_DETAILS ? process.env.UNUSED_DETAILS.split('\n') : []; | |
| unusedDetails = unusedDetails.filter(Boolean); | |
| const detailsSection = unusedDetails.length | |
| ? `\nUnused image preview:\n\n${unusedDetails.map(line => ` ${line}`).join('\n')}\n` | |
| : ''; | |
| const sections = [ | |
| `${marker}`, | |
| 'Developer Guide build artifacts are available for download from this workflow run:', | |
| '', | |
| links.join('\n') | |
| ]; | |
| if (qualityLines.length) { | |
| sections.push('', 'Developer Guide quality checks:', '', qualityLines.join('\n')); | |
| } | |
| if (detailsSection) { | |
| sections.push(detailsSection.trimEnd()); | |
| } | |
| const body = sections.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 |