Skip to content

Commit a9504cd

Browse files
authored
feat: add paragon hosting configuration for dev and local environments (FC-87) (#38)
* feat: add bundle minification build step for hosting * feat: add paragon static server and caddy configuration * feat: add configuration for Paragon theme URLs in dev and local * feat: add conditional logic for serving compiled themes * feat: update static server from nginx to caddy for serving compiled themes * fix: update tutor dependency to remove upper version limit * feat: update Paragon theme configuration and static server settings - add an upper limit version for tutor dependency - create a shared template for the mfe config on both dev and local envs - update the commonly used port for the hosting service to 12400 * feat: add tutor-mfe service integration * fix: integration test dependencies * feat: add integration tests for hosted files * fix(test): update LMS_HOST variable for hosting tests * fix: update MFE_CONFIG to use 'brandOverride' for theme URLs
1 parent ec3862f commit a9504cd

File tree

13 files changed

+730
-41
lines changed

13 files changed

+730
-41
lines changed

plugins/tutor-contrib-paragon/pyproject.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ classifiers = [
2727

2828
]
2929
dependencies = [
30-
"tutor>=19.0.0,<20.0.0",
30+
"tutor>=19.0.0,<21.0.0",
31+
"tutor-mfe @ git+https://github.com/overhangio/tutor-mfe.git@release",
3132
]
3233

33-
optional-dependencies = { dev = ["tutor[dev]>=19.0.0,<20.0.0", "pytest>=8.3.4", "pytest-order>=1.3.0"] }
34+
optional-dependencies = { dev = ["tutor[dev]>=19.0.0,<21.0.0", "pytest>=8.3.4", "pytest-order>=1.3.0", "requests>=2.32.2"] }
3435

3536
# These fields will be set by hatch_build.py
3637
dynamic = ["version"]
@@ -45,6 +46,9 @@ Source = "https://github.com/openedx/openedx-tutor-plugins.git#subdirectory=plug
4546
requires = ["hatchling"]
4647
build-backend = "hatchling.build"
4748

49+
[tool.hatch.metadata]
50+
allow-direct-references = true
51+
4852
# hatch-specific configuration
4953
[tool.hatch.metadata.hooks.custom]
5054
path = ".hatch_build.py"

plugins/tutor-contrib-paragon/tests/integration/conftest.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44
import subprocess
55

6-
from .helpers import PARAGON_NAME, PARAGON_IMAGE
6+
from .helpers import PARAGON_NAME, PARAGON_IMAGE, MFE_SERVICE
77

88

99
@pytest.fixture(scope="package", autouse=True)
@@ -15,7 +15,7 @@ def setup_tutor_paragon_plugin():
1515
"""
1616

1717
subprocess.run(
18-
["tutor", "plugins", "enable", PARAGON_NAME],
18+
["tutor", "plugins", "enable", MFE_SERVICE, PARAGON_NAME],
1919
check=True,
2020
capture_output=True,
2121
)
@@ -26,10 +26,16 @@ def setup_tutor_paragon_plugin():
2626
capture_output=True,
2727
)
2828

29+
subprocess.run(
30+
["tutor", "config", "save", "--set", "LMS_HOST=local.openedx.io"],
31+
check=True,
32+
capture_output=True,
33+
)
34+
2935
yield
3036

3137
subprocess.run(
32-
["tutor", "plugins", "disable", PARAGON_NAME],
38+
["tutor", "plugins", "disable", PARAGON_NAME, MFE_SERVICE],
3339
check=True,
3440
capture_output=True,
3541
)

plugins/tutor-contrib-paragon/tests/integration/helpers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
logger = logging.getLogger(__name__)
77

8+
MFE_SERVICE = "mfe"
89
PARAGON_NAME = "paragon"
910
PARAGON_IMAGE = "paragon-builder"
1011
PARAGON_JOB = "paragon-build-tokens"
@@ -52,3 +53,14 @@ def get_tutor_root_path():
5253
raise RuntimeError("Failed to get Tutor root path: " + result.stderr)
5354

5455
return result.stdout.strip()
56+
57+
58+
def get_config_value(key: str) -> str:
59+
"""Get a configuration value from Tutor.
60+
61+
Returns:
62+
str: The value of the configuration key.
63+
"""
64+
result = execute_tutor_command(["config", "printvalue", key])
65+
assert result.returncode == 0, f"Error getting {key}: {result.stderr}"
66+
return result.stdout.strip()

plugins/tutor-contrib-paragon/tests/integration/plugin_functionality_test.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
import shutil
1111
import pytest
1212
import re
13+
import requests
14+
import time
1315

1416
from .helpers import (
1517
execute_tutor_command,
18+
get_config_value,
1619
get_tutor_root_path,
1720
PARAGON_JOB,
1821
PARAGON_COMPILED_THEMES_FOLDER,
@@ -145,3 +148,68 @@ def test_build_tokens_with_source_tokens_only():
145148
assert not os.path.exists(
146149
utility_classes_css
147150
), f"{utility_classes_css} should not exist when --source-tokens-only is used."
151+
152+
153+
@pytest.mark.order(6)
154+
def test_build_tokens_generates_minified_bundle():
155+
"""
156+
Ensure that the build-tokens job generates the minified bundle files for hosting.
157+
"""
158+
theme = "light"
159+
result = execute_tutor_command(["local", "do", PARAGON_JOB, "--themes", theme])
160+
assert result.returncode == 0, f"Error running build-tokens job: {result.stderr}"
161+
162+
tutor_root = get_tutor_root_path()
163+
compiled_path = os.path.join(tutor_root, PARAGON_COMPILED_THEMES_FOLDER)
164+
165+
minified_theme_bundle = os.path.join(
166+
compiled_path, "themes", theme, f"{theme}.min.css"
167+
)
168+
minified_core_bundle = os.path.join(compiled_path, "core", "core.min.css")
169+
170+
assert os.path.exists(
171+
minified_core_bundle
172+
), f"Minified core bundle file {minified_core_bundle} does not exist."
173+
assert os.path.exists(
174+
minified_theme_bundle
175+
), f"Minified theme bundle file {minified_theme_bundle} does not exist."
176+
177+
178+
@pytest.mark.order(7)
179+
def test_build_tokens_hosted_files():
180+
"""
181+
Verify that the compiled themes can be served through the tutor-mfe service.
182+
183+
This test builds tokens, starts the required services, and checks that the
184+
static files are accessible via HTTP requests.
185+
"""
186+
result = execute_tutor_command(["local", "do", PARAGON_JOB])
187+
assert result.returncode == 0, f"Error running build-tokens job: {result.stderr}"
188+
189+
static_url_prefix = get_config_value("PARAGON_STATIC_URL_PREFIX").lstrip("/")
190+
mfe_host = get_config_value("MFE_HOST")
191+
192+
services_result = execute_tutor_command(["local", "start", "-d", "caddy", "mfe"])
193+
assert services_result.returncode == 0, "Error starting hosting services"
194+
195+
time.sleep(5)
196+
197+
try:
198+
base_url = f"http://{mfe_host}/{static_url_prefix}"
199+
test_files = ["core/core.min.css", "themes/light/light.min.css"]
200+
201+
for test_file in test_files:
202+
url = f"{base_url}{test_file}"
203+
response = requests.get(url, timeout=2)
204+
205+
assert (
206+
response.status_code == 200
207+
), f"Expected status 200 for {url}, but got {response.status_code}. "
208+
209+
content_type = response.headers.get("Content-Type", "")
210+
assert "text/css" in content_type.lower(), (
211+
f"Expected 'text/css' Content-Type for {url}, but got '{content_type}'."
212+
)
213+
214+
finally:
215+
execute_tutor_command(["local", "stop", "caddy", "mfe"])
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% if MFE_HOST_EXTRA_FILES %}
2+
# Paragon static files hosting
3+
handle_path /{{ PARAGON_STATIC_URL_PREFIX }}* {
4+
@mincss {
5+
# Match only minified CSS files
6+
path_regexp mincss \.min\.css$
7+
}
8+
handle @mincss {
9+
root * /paragon-statics
10+
file_server
11+
}
12+
}
13+
{% endif %}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{% if MFE_HOST_EXTRA_FILES %}
2+
- ../../{{ PARAGON_COMPILED_THEMES_PATH }}:/paragon-statics
3+
{% endif %}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{% if MFE_HOST_EXTRA_FILES %}
2+
{% set PROTOCOL = 'https' if ENABLE_HTTPS else 'http' %}
3+
{% set PARAGON_BASE_URL = PROTOCOL ~ "://" ~ "localhost" ~ ":" ~ 8002 ~ "/" ~ PARAGON_STATIC_URL_PREFIX%}
4+
{% include "paragon/settings/mfe-common-settings" %}
5+
{% endif %}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{% if MFE_HOST_EXTRA_FILES %}
2+
{% set PROTOCOL = 'https' if ENABLE_HTTPS else 'http' %}
3+
{% set PARAGON_BASE_URL = PROTOCOL ~ "://" ~ MFE_HOST ~ "/" ~ PARAGON_STATIC_URL_PREFIX %}
4+
{% include "paragon/settings/mfe-common-settings" %}
5+
{% endif %}

plugins/tutor-contrib-paragon/tutorparagon/plugin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@
2929
# List of enabled themes to compile and serve
3030
# Only themes listed here will be processed, even if others exist in sources
3131
("PARAGON_ENABLED_THEMES", []),
32-
# Whether Tutor should expose the compiled themes to be served (e.g. via nginx, cady or static server)
33-
("PARAGON_SERVE_COMPILED_THEMES", True),
3432
# Paragon Builder Docker image
3533
# This image is used to compile themes and should be built with `tutor images build paragon-builder`
3634
("PARAGON_BUILDER_IMAGE", "paragon-builder:latest"),
35+
("PARAGON_STATIC_URL_PREFIX", "static/paragon/"),
36+
("MFE_HOST_EXTRA_FILES", True), # Enable MFE host extra files
3737
]
3838
)
3939

plugins/tutor-contrib-paragon/tutorparagon/templates/paragon/build/paragon-builder/entrypoint.sh

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,33 @@ parse_args() {
4141
printf '%s\n' "$@"
4242
}
4343

44+
build_css_bundle() {
45+
# This function builds a CSS bundle using PostCSS.
46+
# It takes the path to the index.css file as an argument.
47+
# Usage: build_css_bundle <index_css_file>
48+
49+
local index_css_file="$1"
50+
51+
local bundle_directory=$(dirname "$index_css_file")
52+
local bundle_name=$(basename "$bundle_directory")
53+
local minified_output_file="$bundle_directory/${bundle_name}.min.css"
54+
55+
if npx postcss "$index_css_file" \
56+
--use postcss-import \
57+
--use postcss-custom-media \
58+
--use postcss-combine-duplicated-selectors \
59+
--use postcss-minify \
60+
--no-map \
61+
--output "$minified_output_file"; then
62+
63+
echo "Successfully created bundle: $bundle_name"
64+
return 0
65+
else
66+
echo "Failed to build CSS bundle: $bundle_name" >&2
67+
return 1
68+
fi
69+
}
70+
4471
set -- $(parse_args "$@")
4572

4673
# Executes the Paragon CLI to build themes.
@@ -50,8 +77,14 @@ npx paragon build-tokens \
5077
--build-dir "$TMP_BUILD_DIR" \
5178
"$@"
5279

80+
find "$TMP_BUILD_DIR" -type f -name 'index.css' | while read -r index; do
81+
if [ -f "$index" ]; then
82+
build_css_bundle "$index"
83+
fi
84+
done
85+
5386
# Moves the built themes to the final volume directory.
54-
mkdir -p "$FINAL_BUILD_DIR"
87+
rm -rf "$FINAL_BUILD_DIR"/*
5588
cp -a "$TMP_BUILD_DIR/." "$FINAL_BUILD_DIR/"
5689
chmod -R a+rw "$FINAL_BUILD_DIR"
5790

0 commit comments

Comments
 (0)