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
99 changes: 98 additions & 1 deletion plugins/tutor-contrib-paragon/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ This structure is optimized for design token–based themes (see `Paragon Design

.. note::

Documentation for how to use extensions is not yet available.
A link to the official Open edX or Paragon documentation will be added here once it is published.

Configuration
Expand All @@ -60,9 +59,107 @@ All configuration variables can be overridden via `tutor config save`:
- theme-1
- theme-2
PARAGON_SERVE_COMPILED_THEMES: true
PARAGON_BUILDER_IMAGE: "paragon-builder:latest"

You may customize paths or theme names to suit your deployment.

Usage
*****

Prerequisites
-------------

- A built Paragon CLI image:

.. code-block:: bash

tutor images build paragon-builder

- The ``PARAGON_THEME_SOURCES_PATH`` directory structured as follows:

.. code-block:: text

<PARAGON_THEME_SOURCES_PATH>/
├── core/
│ └── ... (token files)
└── themes/
├── light/ # example theme variant
│ └── ... (light theme token files)
└── dark/ # example theme variant
└── ... (dark theme token files)

In this structure:

- The ``core/`` directory contains base design tokens common across all themes.
- The ``themes/`` directory contains subdirectories for each theme variant (e.g., ``light``, ``dark``), each with tokens specific to that theme.

Building Themes
---------------

Invoke the build process via Tutor:

.. code-block:: bash

tutor local do paragon-build-tokens [OPTIONS]

Available options:

- ``--source-tokens-only``
Include only source design tokens in the build.

- ``--output-token-references``
Include references for tokens with aliases to other tokens in the build output.

- ``--themes <theme1,theme2>``
Comma-separated list of theme names to compile. Defaults to the list defined in ``PARAGON_ENABLED_THEMES`` if not provided.

- ``-v, --verbose``
Enable verbose logging.

Examples
--------

.. code-block:: bash

# Compile all themes listed in PARAGON_ENABLED_THEMES
tutor local do paragon-build-tokens

# Compile only specific themes
tutor local do paragon-build-tokens --themes theme-1,theme-2

# Compile with full debug logs
tutor local do paragon-build-tokens --verbose

# Compile only source tokens for a single theme
tutor local do paragon-build-tokens --themes theme-1 --source-tokens-only

Output
------

Artifacts will be written to the directory specified by ``PARAGON_COMPILED_THEMES_PATH`` (default: ``env/plugins/paragon/compiled-themes``).

Troubleshooting
***************

- **No custom themes built or only default tokens generated**
Ensure that your custom theme directories exist under ``PARAGON_THEME_SOURCES_PATH`` and that their names exactly match those in ``PARAGON_ENABLED_THEMES`` or passed via ``--themes``. If no custom tokens are found, Paragon will fall back to its built-in defaults.

- **Themes are not picked up when using --themes:**
The value for ``--themes`` must be a comma-separated list (no spaces), e.g. ``--themes theme-1,theme-2``.

- **Write permission denied**
Verify that Docker and the Tutor process have write access to the path defined by ``PARAGON_COMPILED_THEMES_PATH``. Adjust filesystem permissions if necessary.

- **Error: "Expected at least 4 args"**
This occurs when the build job is invoked directly inside the container. Always run via Tutor:

.. code-block:: bash

tutor local do paragon-build-tokens [OPTIONS]

- **Other issues**
Re-run the build with ``--verbose`` to obtain detailed logs and identify misconfigurations or missing files.

License
*******

Expand Down
58 changes: 58 additions & 0 deletions plugins/tutor-contrib-paragon/tutorparagon/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Custom job definitions for the Paragon Tutor plugin.

This module contains Click command definitions that will be registered
as Tutor "do commands", this specialized jobs can be executed using
`tutor local do <command>` or similar.
"""

import click


@click.command()
@click.option(
"--source-tokens-only",
is_flag=True,
default=False,
help="Include only source design tokens in the build.",
)
@click.option(
"--output-token-references",
is_flag=True,
default=False,
help="Include references for tokens with aliases to other tokens in the build output.",
)
@click.option("--themes", help="Comma-separated list of themes to build.")
@click.option(
"-v", "--verbose", is_flag=True, default=False, help="Enable verbose logging."
)
def paragon_build_tokens(
source_tokens_only: bool,
output_token_references: bool,
themes: str,
verbose: bool,
) -> list[tuple[str, str]]:
"""
Build theme token files using Paragon.

Args:
source_tokens_only (bool): Only source design tokens.
output_token_references (bool): Output token references.
themes (str): Comma-separated list of themes.
verbose (bool): Verbose logging.

Returns:
list[tuple[str, str]]: List of commands to run.
"""
args = []
if source_tokens_only:
args.append("--source-tokens-only")
if output_token_references:
args.append("--output-token-references")
if themes:
args.append("--themes")
args.append(themes)
if verbose:
args.append("--verbose")

return [("paragon-builder", " ".join(args))]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Paragon Builder
paragon-builder-job:
image: {{ PARAGON_BUILDER_IMAGE }}
environment:
- PARAGON_ENABLED_THEMES={{ PARAGON_ENABLED_THEMES | join(',') }}
volumes:
- "./../../{{ PARAGON_THEME_SOURCES_PATH }}:/theme-sources"
- "./../../{{ PARAGON_COMPILED_THEMES_PATH }}:/compiled-themes"
27 changes: 27 additions & 0 deletions plugins/tutor-contrib-paragon/tutorparagon/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from tutor import config as tutor_config

from .__about__ import __version__
from .commands import paragon_build_tokens

########################################
# CONFIGURATION
Expand All @@ -27,6 +28,9 @@
("PARAGON_ENABLED_THEMES", []),
# Whether Tutor should expose the compiled themes to be served (e.g. via nginx, cady or static server)
("PARAGON_SERVE_COMPILED_THEMES", True),
# Paragon Builder Docker image
# This image is used to compile themes and should be built with `tutor images build paragon-builder`
("PARAGON_BUILDER_IMAGE", "paragon-builder:latest"),
]
)

Expand Down Expand Up @@ -55,6 +59,22 @@ def create_paragon_folders(project_root: str) -> None:
print(f"[paragon] Created {label} folder at: {path}")


########################################
# DOCKER IMAGE MANAGEMENT
########################################

hooks.Filters.IMAGES_BUILD.add_items(
[
(
"paragon-builder", # Image name used with 'tutor images build myservice'
("plugins", "paragon", "build", "paragon-builder"), # Path to Dockerfile
"{{ PARAGON_BUILDER_IMAGE }}", # Docker image tag
(), # Build arguments
),
]
)


########################################
# TEMPLATE RENDERING
# (It is safe & recommended to leave
Expand Down Expand Up @@ -92,3 +112,10 @@ def create_paragon_folders(project_root: str) -> None:
for path in glob(str(importlib_resources.files("tutorparagon") / "patches" / "*")):
with open(path, encoding="utf-8") as patch_file:
hooks.Filters.ENV_PATCHES.add_item((os.path.basename(path), patch_file.read()))


########################################
# CUSTOM JOBS (a.k.a. "do-commands")
########################################

hooks.Filters.CLI_DO_COMMANDS.add_item(paragon_build_tokens)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM node:20-alpine

WORKDIR /paragon-builder

COPY entrypoint.sh ./entrypoint.sh
COPY package*.json ./

RUN npm ci && \
chmod +x ./entrypoint.sh

ENTRYPOINT ["sh", "./entrypoint.sh"]

CMD []
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/bin/sh
set -e

SOURCE_DIR="/theme-sources" # Volume mounted by Paragon plugin
FINAL_BUILD_DIR="/compiled-themes" # Volume mounted by Paragon plugin
TMP_BUILD_DIR="$(mktemp -d /tmp/paragon-build.XXXXXX)"


has_themes() {
# This function checks if the --themes flag is present in the arguments
# Returns 0 if --themes is found, 1 otherwise
# Usage: has_themes "$@"

for arg in "$@"; do
[ "$arg" = "--themes" ] && return 0
done
return 1
}

parse_args() {
# This function parses arguments injected by Tutor.
# Tutor runs jobs via `sh -e -c '…'`, reserving $1–$3 for its wrapper and
# bundling all user flags into $4. The function then extracts the raw flags
# string from $4, shifts off the wrapper args, and verifies—or appends—the
# selected themes definition.

if [ "$#" -lt 4 ]; then
echo "Error: Expected at least 4 args, got $#. Must run via tutor."
exit 1
fi

shift 3

if ! has_themes "$@"; then
if [ -n "${PARAGON_ENABLED_THEMES:-}" ]; then
echo "Using PARAGON_ENABLED_THEMES: ${PARAGON_ENABLED_THEMES}"
set -- "$@" --themes "${PARAGON_ENABLED_THEMES}"
fi
fi

printf '%s\n' "$@"
}

set -- $(parse_args "$@")

# Executes the Paragon CLI to build themes.
# It uses a temporary directory to avoid volume permissions issues
npx paragon build-tokens \
--source "$SOURCE_DIR" \
--build-dir "$TMP_BUILD_DIR" \
"$@"

# Moves the built themes to the final volume directory.
mkdir -p "$FINAL_BUILD_DIR"
cp -a "$TMP_BUILD_DIR/." "$FINAL_BUILD_DIR/"

# Clean up
rm -rf "$TMP_BUILD_DIR"

exit 0
Loading