| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A Dagger module for building and deploying Open edX platform images, services, and micro-frontends.
This module provides:
lehrer is the single entrypoint for working in this repository. It is a cyclopts CLI that is intended to grow to cover every routine task — today it manages the local k3d dev environment and drives the Dagger build pipelines.
uv sync # install the CLI into the project venv
uv run lehrer --helpTop-level command groups:
| Command | Purpose |
|---|---|
| lehrer dev | Manage the local k3d Open edX dev environment |
| lehrer build | Run the Dagger build pipelines |
| lehrer compat | Enumerate the build cells a diff affects, for the CI matrices |
The local dev environment runs on k3d + Tilt. The cluster lifecycle is:
lehrer dev check # verify required tools (k3d, kubectl, tilt, helm, dagger, docker)
lehrer dev setup # create the k3d cluster + bootstrap secrets (run once)
lehrer dev start # tilt up — build & deploy the services
lehrer dev stop # tilt down — remove deployed resources, keep the cluster
lehrer dev teardown # delete the cluster and clean up all local state
lehrer dev status # show cluster / pod state
lehrer dev db-collation # audit MariaDB schema-collation drift (see below)Use a deployment-specific config and MFE hot-reload:
lehrer dev start --deployment-config ./deployments/mit-ol --mfe-hot-reloadOnce the stack is up, the port picks the service, so any hostname that resolves to 127.0.0.1 reaches it (localhost, or local.openedx.io for mit-ol). Log in at localhost, though: the LMS and Studio base URLs in the platform ConfigMaps name it, and Studio's SSO callback is registered for http://localhost:8010 only.
| Service | URL |
|---|---|
| LMS | http://localhost:8000 |
| Studio | http://localhost:8010 (logs in through the LMS) |
| notes | http://localhost:8001 |
Each of those host ports reaches a Traefik entrypoint of its own (local-dev/manifests/traefik-config.yaml), which k3d wires up when it creates the cluster. A cluster created before that change sends all three to one host-routed entrypoint where nothing matches, so every request 404s. lehrer dev setup and lehrer dev start print a warning when they find one; recreate it with lehrer dev teardown && lehrer dev setup (this deletes the cluster's databases).
--mfe-hot-reload serves the MFEs from host dev servers instead of from the cluster: the compiled nginx image, Deployment and ingress route are all skipped, so nothing is built for a site you are editing live.
Each site's dev server binds the port declared for it in mfe_slot_config/frontend/dev-ports.yaml. Keep those clear of the ports k3d's loadbalancer binds (k3d-config.yaml: 8000, 8001, 8010, 8090) and distinct from each other; the Tiltfile refuses to load otherwise. Defaults:
| Deployment | Site | Dev port | Dev baseUrl |
|---|---|---|---|
| generic | default | 8100 | http://localhost:8100 |
| mit-ol | mitx | 8101 | http://apps.local.openedx.io:8101 |
| mit-ol | mitxonline | 8102 | http://apps.local.openedx.io:8102 |
| mit-ol | xpro | 8103 | http://apps.local.openedx.io:8103 |
The port is declared rather than read out of baseUrl because the two answer different questions: the port is where webpack-dev-server listens on your machine, while baseUrl is where the browser reaches the app. They coincide only when a site gets a host to itself. An MFE served as a sub-path of the LMS (the topology ol-infrastructure deploys) has a baseUrl carrying the LMS origin and no port of its own. When baseUrl does name a port, the Tiltfile checks it matches, since a dev server listening anywhere else just serves a broken site.
Hostnames are a per-deployment setting, declared in mfe_slot_config/frontend/shared/src/dev-hosts.json:
{
"lmsBaseUrl": "http://local.openedx.io:8000",
"sites": {
"mitx": "http://apps.local.openedx.io:8101",
"mitxonline": "http://apps.local.openedx.io:8102",
"xpro": "http://apps.local.openedx.io:8103"
}
}Every site.config.dev.tsx imports that file as @shared/dev-hosts.json, and both lehrer dev check and the Tiltfile read the same file, so changing a deployment's local-dev domain is one edit and the tooling cannot end up checking something the bundle was not built with.
The defaults need no setup because upstream Open edX publishes *.local.openedx.io as a public A record pointing at 127.0.0.1. That does make hot reload depend on public DNS, so it breaks offline or behind a resolver that filters the name. To check before starting:
lehrer dev check --deployment-config ./deployments/mit-olIf a name does not resolve, either map it to 127.0.0.1 in /etc/hosts or point dev-hosts.json at a name that does. A deployment whose production LMS is learn.mit.edu may prefer a subdomain of it so local dev mirrors production.
Secret values are read from the environment (MYSQL_ROOT_PASSWORD, DJANGO_SECRET_KEY, MONGO_PASSWORD, PROVISION_SUPERUSER_PASSWORD, ...) and fall back to safe local-dev defaults. They all land in the openedx-secrets Secret, which the MariaDB and MongoDB CRs read too — so an override reaches the operators rather than only the application.
That includes the RSA pair the LMS signs every JWT with (JWT_PRIVATE_SIGNING_JWK / JWT_PUBLIC_SIGNING_JWK_SET). Without it no login completes.
Existing clusters: the MariaDB CR used to carry its own root-password Secret, and spec.rootPasswordSecretKeyRef is immutable. A cluster created before that change rejects the new manifest and lehrer dev start fails on the mysql resource. Both lehrer dev setup and lehrer dev start detect this and print the fix:
kubectl --context k3d-lehrer-dev -n openedx delete mariadb mysql kubectl --context k3d-lehrer-dev -n openedx delete pvc storage-mysql-0which drops the edxapp databases and lets the migrate and provision Jobs rebuild them. Both lines matter: the PVC is retained when the CR goes, and a replacement that reattaches it keeps the old databases and the old root password. Keep the --context too — an unqualified delete lands on whichever cluster your kubeconfig currently points at.
MYSQL_ROOT_PASSWORD is init-only. MariaDB sets root's password when it initializes an empty datadir and never rotates it, so an override has to be set before the first lehrer dev setup; changing it later moves the Secret but not the server. lehrer dev setup notices and prints the same recreate steps. MONGO_PASSWORD has no such limitation — the MongoDB operator rotates SCRAM credentials when the referenced Secret changes.
edxapp-migrate creates the edxapp schema; three more Jobs turn that into a usable stack:
| Job | Trigger | What it does |
|---|---|---|
| edxapp-provision | on tilt up, then manual | Superuser, notes and Studio OAuth clients, waffle flags |
| notes-migrate | automatic | edx-notes-api schema and search index |
| edxapp-demo-course | manual | Imports the Open edX demo course |
edxapp-migrate and edxapp-provision run by themselves when tilt up starts; after that they wait for a trigger (see Iterating on platform settings for why). Trigger edxapp-migrate after a change that brings new migrations, such as a build_manifest.yaml bump.
edxapp-provision creates the edx / edx superuser (override the password with PROVISION_SUPERUSER_PASSWORD before lehrer dev setup), the DOT OAuth Application that LMS↔notes SSO signs its tokens with, the cms-sso Application Studio logs in through, and the waffle flags in local-dev/provision/waffle-flags.yaml. It is idempotent, so trigger it again whenever you edit either file. Add OAuth clients in local-dev/provision/provision.py; both files are mounted into the Job as a ConfigMap.
notes-migrate creates the tables in the notes database (the MariaDB CR creates the database and the grant, but nothing creates the schema) and the OpenSearch index. Both are required: edx-notes-api indexes on every save via RealTimeSignalProcessor, so a missing index breaks annotation writes and not just search. The Job fails rather than leave a notes service running that cannot be written to.
edxapp-demo-course resolves the demo course branch matching the release the stack was built from, so a named-release stack does not import master content. Set DEMO_COURSE_GIT_BRANCH in the Job to pin a branch instead. It is left off the critical path because it clones the course repo over the network. Trigger it from the Tilt UI, or:
tilt trigger edxapp-demo-courseNeither migrate Job retries. MariaDB DDL is not transactional, so a migration that dies partway leaves the tables and columns it already created behind, and a second attempt fails on "table already exists" — burying whatever the first, real error was. backoffLimit: 0 keeps the original failure on screen.
Edits to these files are live-updated into the running platform pods instead of rebuilding the image:
Tilt copies the changed file into every running LMS, CMS and worker container and sends PID 1 a HUP. gunicorn answers by starting new workers, which import the settings again; celery re-execs itself in place. The container is not restarted, so the synced files stay.
Everything else the platform build reads still runs the full Dagger build. assets.py, i18n.py, the *.env.yml files and build_manifest.yaml feed collectstatic, compilemessages or dependency resolution, which a file copy cannot redo. set_waffle_flags.py is run by the edxapp-provision Job from the image, so an edit to it has to reach the image before you re-trigger the Job.
| Edit | Full rebuild (before) | Live update (after) |
|---|---|---|
| settings/lms/aqueduct.py | 31m 26s | 8s |
Measured 2026-09-14 on an i7-13800H / 49 GB WSL2 host with Tilt v0.37.7 and Dagger v0.21.9, the second build of the same inputs on that Dagger engine. "Full rebuild" runs from tilt up until the LMS answers /heartbeat on the new image, with the codejail, notes and MFE images building alongside it. 1641s of it was the platform image step: Dagger evaluating and exporting the image, then docker load and a push that re-uploaded 63 of the image's 71 layers. The rest was the migrate and provision Jobs and the pod rollout. "Live update" runs from saving the file until the same pod answers /heartbeat after gunicorn has booted new workers.
The full rebuild exports the platform image, about 6 GB, as a tarball under $TMPDIR (default /tmp). Where /tmp is tmpfs (systemd's tmp.mount makes it one), that tarball sits in RAM; point it at disk instead:
TMPDIR=/var/tmp lehrer dev startedxapp-migrate and edxapp-provision wait for a trigger once tilt up has started them. A finished Job's pod cannot be live-updated, and Tilt answers that by rebuilding the image for that resource, which would turn every settings edit back into a full build.
Every schema is created as utf8mb4 / utf8mb4_unicode_ci, pinned explicitly in local-dev/manifests/infra/mariadb.yaml. This is not a detail the server config can cover for you: a schema's default collation is stored once, at CREATE DATABASE time, and never follows a later change to collation-server. Any CREATE TABLE that omits an explicit COLLATE inherits whatever the schema was created with, and a foreign key between two tables on different collations is rejected with errno 150, "Foreign key constraint is incorrectly formed" — a migration failure that reads like a MariaDB bug and is really schema metadata drift. It is a longstanding source of migration failures in residential MITx.
The mariadb-operator makes this easy to get wrong. spec.database on the MariaDB CR does not create the schema itself; the operator turns it into a Database CR named <mariadb>-database with no characterSet/collate, the CRD defaults (utf8 / utf8_general_ci) get stamped on, and it issues an explicit CREATE DATABASE edxapp CHARACTER SET = 'utf8' COLLATE = 'utf8_general_ci'. Declaring mysql-database ourselves is what stops that: the reconciler only builds its own when that key is absent. Anything that provisions a MariaDB schema for a lehrer deployment needs the same treatment — set the collation at creation, and do not rely on a parameter group, which only reaches schemas created after it.
To check an existing cluster:
lehrer dev db-collation # report schema and table collations
lehrer dev db-collation --fix # ALTER DATABASE drifted schemas
lehrer dev db-collation --check-tables # also CHECK TABLE for corruption--fix only realigns the schema-level default, which is a metadata change that rewrites nothing — new tables land on the right collation, existing ones keep theirs. Converting those in place is the risky half, so it is not automated; on a dev cluster, recreate the instance and let the Jobs rebuild the schema. --check-tables reports and never repairs: rebuilding a corrupt unique index can delete the duplicate rows it was masking. This mirrors ol-infrastructure's bin/mariadb-collation-guard, which is the version to reach for against deployed environments.
lehrer dev setup runs the schema-level audit on an already-initialized datadir and points here if it finds drift.
Existing clusters: if your cluster predates this, the operator already generated a mysql-database Database resource on utf8, and characterSet / collate are immutable — lehrer dev start fails on it. Both setup and start detect that and print the way out. Delete the resource after patching its cleanupPolicy to Skip: the Database finalizer runs DROP DATABASE, so deleting it on the CRD's Delete default takes edxapp with it. The manifests now set cleanupPolicy: Skip on all three databases for that reason.
lehrer build is a thin, consistent facade over the Dagger module — it saves you from remembering the object-scoped dagger call paths, and any trailing arguments are forwarded straight to Dagger. lehrer build --help groups the commands the way you reason about them:
| Command | Wraps | Purpose |
|---|---|---|
| lehrer build platform | platform build-platform | Build the edx-platform LMS/CMS image |
| lehrer build codejail | codejail build | Build the codejail service image |
| lehrer build notes | notes build | Build the edx-notes-api image |
| lehrer build mfe-legacy | mfe build-legacy | Build a legacy (webpack) MFE dist/ |
| lehrer build mfe-site | mfe build-site | Build an OEP-65 Site Project |
| lehrer build check | platform check-deployment | Verify a cell's requirements install + import |
| lehrer build verify-settings | platform verify-settings | Boot a cell's committed aqueduct settings + Django system checks |
| lehrer build test | platform test | Run edx-platform + installed plugin tests in a built image |
| lehrer build test-report | platform test-report | The same run, returning an exportable JUnit + per-plugin summary |
| lehrer build codejail-test | codejail test | Run the codejail test suite |
| lehrer build notes-test | notes test | Run the edx-notes-api test suite |
| lehrer build cells | — | Print the (release, deployment) cells in a manifest |
| lehrer build functions | dagger functions | List every Dagger function |
| lehrer build call ... | dagger call ... | Raw passthrough for any function without a wrapper |
The cell-scoped commands (platform, check, test) accept a single <group>/<release>/<deployment> cell that expands to --build-manifest deployments/<group>/build_manifest.yaml --release-name <release> --deployment-name <deployment>, so you don't repeat them:
uv sync # install the CLI into the venv
# Build the edx-platform image for a cell (the manifest supplies the rest):
uv run lehrer build platform --cell mit-ol/master/mitxonline \
--custom-settings ./deployments/mit-ol/settings
# Verify a cell (cheap → thorough):
uv run lehrer build check --cell mit-ol/master/mitxonline
uv run lehrer build test --cell mit-ol/master/mitxonline \
--custom-settings ./deployments/mit-ol/settings
# Other services and MFEs:
uv run lehrer build codejail --release-name master
uv run lehrer build mfe-legacy --mfe-name learning ... export --path ./dist
# Raw escape hatch for anything without a wrapper (e.g. publish, watch-site):
uv run lehrer build call mfe watch-site ...The rest of this README documents the underlying Dagger functions directly; each common one has a lehrer build shortcut per the table above.
build-platform is a two-base build, the way a multi-stage Dockerfile would be. Dependencies are resolved on one base, and only /openedx/venv, /openedx/edx-platform and /openedx/nodeenv are copied onto a fresh one — so the intermediate state of the dependency resolution (uv and npm caches, build artifacts, discarded layers) never reaches the shipped image. The compilers and -dev headers do: the second base is another apt-base, which installs the same toolchain the first one did.
then, on a second apt-base, with the venv and source copied across:
Unless --verify-boot false, the finished image is then started and Django's system checks are run for both LMS and CMS. publish-platform chains onto the returned Container to push it.
Every Dagger function is namespaced under a service object — platform, mfe, codejail, or notes. Run lehrer build functions (or dagger functions) to list them, and dagger call <object> <function> --help for a function's flags.
build-platform assembles the whole image the way a multi-stage Docker build would: it first builds dependencies on one base (apt-base → get-code → install-deps), then starts a fresh clean base and copies only the needed directories across, conditionally applies locales (unless --include-locales false) and themes, and finishes with collected → fetch-translations → build-static-assets → inject-aqueduct-settings → docker-image, then verifies the finished image can actually start by running Django's system checks for both services (--verify-boot false to skip while iterating). The other functions are those individual stages, plus check-deployment / verify-settings / test (verification), publish-platform, and regenerate-aqueduct-settings.
The simplest way to drive a full build is a cell — the deployment's build_manifest.yaml supplies the platform/theme/translation repos, Python and Node versions, and requirement pins, so you pass only the cell coordinate and the settings directory:
# Recommended: the lehrer CLI resolves the manifest for you.
uv run lehrer build platform --cell mit-ol/master/mitxonline \
--custom-settings ./deployments/mit-ol/settings
# The same thing as a raw dagger call:
dagger call platform build-platform \
--build-manifest ./deployments/mit-ol/build_manifest.yaml \
--release-name master \
--deployment-name mitxonline \
--custom-settings ./deployments/mit-ol/settingsWithout a manifest, pass the build parameters explicitly:
dagger call platform build-platform \
--deployment-name mitxonline \
--release-name master \
--pip-package-lists ./pip_package_lists \
--pip-package-overrides ./pip_package_overrides \
--custom-settings ./settings \
--platform-repo "https://github.com/openedx/edx-platform" \
--platform-branch master \
--theme-repo "https://github.com/mitodl/mitxonline-theme" \
--theme-branch main \
--python-version 3.12build-platform returns a Container; chain publish-platform to push it (there is no dedicated CLI wrapper — use lehrer build call or a raw dagger call):
dagger call platform build-platform \
--build-manifest ./deployments/mit-ol/build_manifest.yaml \
--release-name master --deployment-name mitxonline \
--custom-settings ./deployments/mit-ol/settings \
publish-platform \
--registry ghcr.io \
--repository mitodl/openedx-mitxonline \
--tag master-latest \
--username "$GITHUB_USER" \
--password env:GITHUB_TOKENEverything the pipelines need is operator-owned and lives under deployments/<group>/. Nothing is picked up implicitly from the repo root, and the generic pipelines never fall back to MIT OL's directories — slot_config, codejail_config and notes_config are all required, and omitting one fails with an error naming the flag.
deployments/mit-ol/ ├── build_manifest.yaml # one cell per (release, deployment) ├── settings/ # → --custom-settings ├── mfe_slot_config/ │ ├── legacy/ # → --slot-config for webpack MFEs │ └── frontend/ # → --site-project for OEP-65 Site Projects ├── codejail_config/ # → --codejail-config (01-sandbox) └── notes_config/ # → --notes-config (env_config.py)
The declarative source of truth for a deployment group: one cell per (release, deployment) pair naming the platform/theme/translations repos and branches, the Python and Node versions, and the pinned requirement lines. It replaces the older pip_package_lists/ + pip_package_overrides/ directories, which remain supported as a lower-level alternative ({release_name}/{deployment_name}.txt under each). See src/lehrer/core/build_manifest.py and plans/06-build-manifest.md.
settings/ ├── lms.env.yml ├── cms.env.yml ├── lms/ │ ├── assets.py │ ├── i18n.py │ ├── aqueduct.py │ └── models/ │ └── aqueduct.py ├── cms/ │ ├── assets.py │ ├── i18n.py │ ├── aqueduct.py │ └── models/ │ └── aqueduct.py ├── set_waffle_flags.py ├── process_scheduled_emails.py └── saml_pull.py
Every file is required. There is deliberately no top-level models/base.py: inject-aqueduct-settings supplies that from lehrer's own src/lehrer/settings/base.py, keeping the ProductionSettingsMixin a single implementation rather than a per-operator copy. The full contract is in docs/creating-a-deployment.md.
The examples below show the lehrer build form; the equivalent raw dagger call platform build-platform ... accepts the same flags.
# Build mitxonline
uv run lehrer build platform --cell mit-ol/master/mitxonline \
--custom-settings ./deployments/mit-ol/settings
# Build mitx
uv run lehrer build platform --cell mit-ol/master/mitx \
--custom-settings ./deployments/mit-ol/settingsAny extra flags after the --cell are forwarded to build-platform:
uv run lehrer build platform --cell mit-ol/master/mitxonline \
--custom-settings ./deployments/mit-ol/settings \
--source ../edx-platform \
--theme-source ../mitxonline-themeuv run lehrer build platform --cell mit-ol/master/mitxonline \
--custom-settings ./deployments/mit-ol/settings \
--include-locales falseBy default:
The manifest cell can pin python_version; override it per-invocation with --python-version:
uv run lehrer build platform --cell mit-ol/master/mitxonline \
--custom-settings ./deployments/mit-ol/settings \
--python-version 3.11The codejail service provides sandboxed Python execution for running student code:
uv run lehrer build codejail --release-name master # Python 3.12
uv run lehrer build codejail --release-name sumac # Python 3.11
uv run lehrer build codejail --release-name master --python-version 3.11
# Raw form:
dagger call codejail build --release-name masterCodejail automatically installs the appropriate edx-platform sandbox requirements based on the release.
The edx-notes-api service provides student annotation functionality:
uv run lehrer build notes --release-name master
uv run lehrer build notes --release-name open-release/sumac.master
uv run lehrer build notes --release-name master --python-version 3.9
# Raw form:
dagger call notes build --release-name masterNote: edx-notes-api master branch requires Python 3.9+. Older releases may work with Python 3.8.
The codejail and notes builds return a Container; chain publish to push it. There is no dedicated CLI wrapper for the chain, so use lehrer build call (or a raw dagger call):
# Build and publish codejail
uv run lehrer build call codejail build --release-name sumac \
publish --address ghcr.io/mitodl/openedx-codejail:sumac
# Build and publish notes
uv run lehrer build call notes build --release-name master \
publish --address ghcr.io/mitodl/openedx-notes:latestThe module provides functions for building Open edX Micro-Frontends with deployment-specific configurations.
lehrer build mfe-legacy wraps mfe build-legacy. --slot-config (the operator's slot-configuration directory) is required, and export --path ./dist writes the built bundle out:
# Build the learning MFE
uv run lehrer build mfe-legacy \
--mfe-name learning \
--mfe-repo https://github.com/openedx/frontend-app-learning \
--mfe-branch open-release/sumac.latest \
--deployment-name mitxonline \
--slot-config ./deployments/mit-ol/mfe_slot_config/legacy \
export --path ./dist
# Build with custom styles + an extra npm bundle. Bundle specs are
# "npm_package_spec|target_directory":
uv run lehrer build mfe-legacy \
--mfe-name learning \
--mfe-repo https://github.com/openedx/frontend-app-learning \
--mfe-branch master \
--deployment-name mitxonline \
--slot-config ./deployments/mit-ol/mfe_slot_config/legacy \
--styles-file mitxonline-styles.scss \
--extra-npm-bundles "@mitodl/smoot-design|public/static/smoot-design" \
export --path ./dist
# Raw form:
dagger call mfe build-legacy --mfe-name account \
--mfe-repo https://github.com/openedx/frontend-app-account \
--deployment-name mitxonline \
--slot-config ./deployments/mit-ol/mfe_slot_config/legacy \
export --path ./distLearning-MFE customizations (AI drawer slots, smoot-design, extra bundles) are best captured once in a build_config.yaml and applied with build-legacy-configured — see Config-driven legacy builds below.
MFEs bake configuration in at build time. Pass each variable with a repeatable --env-vars KEY=VALUE flag:
uv run lehrer build mfe-legacy \
--mfe-name learning \
--mfe-repo https://github.com/openedx/frontend-app-learning \
--deployment-name mitxonline \
--slot-config ./deployments/mit-ol/mfe_slot_config/legacy \
--env-vars LMS_BASE_URL=https://courses.learn.mit.edu \
--env-vars SITE_NAME="MIT Learn" \
--env-vars APP_ID=learning \
export --path ./distCommon variables include LMS_BASE_URL, SITE_NAME, BASE_URL, APP_ID, and DEPLOYMENT_NAME. See the Concourse pipeline values.py for the full set a production build supplies.
For iterating on slot configs without rebuilding, run the local dev environment with MFE hot-reload:
uv run lehrer dev start --deployment-config ./deployments/mit-ol --mfe-hot-reloadFor an OEP-65 Site Project specifically, mfe watch-site serves a built Site Project with hot reload:
uv run lehrer build call mfe watch-site \
--site-project ./site-project up --ports 8080:8080
# Access at http://localhost:8080mfe_slot_config/legacy/ holds what --slot-config points at for a webpack MFE build. Two files are looked up by name on every build:
Everything else in the directory is opt-in, named by --extra-slot-files, --styles-file and --extra-npm-bundles (or resolved from build_config.yaml by build-legacy-configured). MIT OL's directory currently supplies AI-drawer and feedback slot components, ResponsiveCourseTabs.jsx, and the mitx-styles.scss / mitxonline-styles.scss deployment stylesheets.
mfe_slot_config/frontend/ is the OEP-65 side: one Site Project per deployment plus a shared/ component directory and dev-ports.yaml.
Rather than passing --extra-slot-files, --styles-file, and --extra-npm-bundles on every invocation, an operator can describe their customizations once in a build_config.yaml that lives alongside the slot configuration. build-legacy-configured reads it and resolves the explicit build-legacy arguments per deployment and Open edX release:
dagger call mfe build-legacy-configured \
--mfe-name learning \
--slot-config ./mfe_slot_config/legacy \
--mfe-source ./frontend-app-learning \
--deployment-name mitxonline \
--release-name master \
export --path ./distThe config structure is defined by the Pydantic models in src/lehrer/core/mfe_config.py, which are both the runtime validation layer (a malformed file fails fast with field-level errors) and the source of a publishable JSON Schema.
Generate the JSON Schema for editor or agentic validation:
dagger call mfe build-config-schema > build_config.schema.jsonA copy generated from the models is committed at the repo root as build_config.schema.json (kept in sync by a pre-commit hook). Reference it from the top of a build_config.yaml so editors validate as you type:
# yaml-language-server: $schema=https://raw.githubusercontent.com/mitodl/lehrer/main/build_config.schema.jsonAll build parameters are explicit — no implicit file copying from a build context. Use directory/file mounting for local sources:
Use lehrer build platform (or dagger call platform build-platform) for a complete end-to-end build. The individual platform functions are the pipeline's stages: apt-base takes a --python-version and creates the initial container, while the later stages (get-code, install-deps, locales, themes, collected, ...) each take a container and return the next one. build-platform is where they are wired together in src/lehrer/core/platform.py.
This repo's own CI drives the CLI (uv run lehrer build ...) — see .github/workflows/. To publish an image from a workflow you can also call the Dagger module directly:
name: Build OpenEdx Image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and publish
uses: dagger/dagger-for-github@v5
with:
version: "latest"
verb: call
args: |
platform build-platform
--build-manifest ./deployments/mit-ol/build_manifest.yaml
--release-name master
--deployment-name mitxonline
--custom-settings ./deployments/mit-ol/settings
publish-platform
--registry ghcr.io
--repository mitodl/openedx-mitxonline
--tag ${{ github.sha }}
--username ${{ github.actor }}
--password env:GITHUB_TOKEN
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}# Install the CLI + dependencies
uv sync
# List available functions
uv run lehrer build functions # or: dagger functions
# Get help on a command or the underlying function
uv run lehrer build platform --help
dagger call platform build-platform --help
# Evaluate a single build stage
dagger call platform apt-base stdouttests/ covers the parts of src/lehrer/ that run without a Dagger engine — argument and manifest resolution, config parsing, JUnit report generation, the CLI wrappers:
uv run ruff check .
uv run ruff format --check .
uv run mypy --config-file=pyproject.toml src/lehrer tests
uv run pytest tests/ -vci.yml runs exactly those four on every push and pull request, then three named pre-commit hooks rather than the whole suite:
uv run pre-commit run build-config-schema --all-files
uv run pre-commit run build-manifest-schema --all-files
uv run pre-commit run lehrer-core-boundary --all-filesThe rest of the hooks run on commit, so uv run pre-commit run --all-files locally is a superset of the PR gate, not the same thing. The other workflows cover what needs a Dagger engine or a schedule: settings-verify.yml boots each cell's committed aqueduct settings, plugin-compat.yml installs and imports each cell's pinned requirements, canary.yml runs full platform builds on a schedule, and actions-static-analysis.yml lints the workflows themselves with zizmor.
src/lehrer/main.py is only the Dagger entry point — a thin Lehrer root type whose methods return the per-service objects. The pipelines themselves live in src/lehrer/core/, one module per service.
BSD-3-Clause
| Back | FazBrowse Home | New Git URL |