Skip to content
Open
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
158 changes: 158 additions & 0 deletions .github/workflows/build-and-push.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Drop-in GitHub Actions workflow for the Learning MFE fork repo.
#
# Copy into your fork repo as `.github/workflows/build-and-push.yml`.
# Builds the MFE Docker image on every commit to the default branch
# (we suggest `rooman/main` per the parent README) and pushes it to
# GitHub Container Registry as
# ghcr.io/<owner>/<repo>:<branch>-<short-sha>
# ghcr.io/<owner>/<repo>:latest
#
# Permissions: this workflow requires `packages: write` so it can push to
# GHCR using the repo's built-in GITHUB_TOKEN — no extra secrets needed.
#
# After the first successful run, the image is pull-able from any host
# (Tutor on the LMS box) with:
# docker pull ghcr.io/<owner>/<repo>:latest
#
# For private repos: GHCR images default to private too. Either make the
# package public (Settings → Packages on GitHub) or add a docker login
# step on the LMS box pointing at a personal access token with read:packages.

name: Build and push Learning MFE image

on:
push:
branches: [rooman/main]
workflow_dispatch: # lets you re-run a build from the Actions tab

# Cancel in-flight builds when a newer push lands on the same branch.
# MFE builds take 4-6 min; nobody wants the slow one to win.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set image tags
id: tags
run: |
# Lowercase owner — GHCR rejects mixed-case paths.
OWNER="${GITHUB_REPOSITORY_OWNER,,}"
REPO_LOWER="${GITHUB_REPOSITORY,,}" # owner/repo lowercased
SHORT_SHA="${GITHUB_SHA::7}"
BRANCH_TAG="${GITHUB_REF_NAME//\//-}-${SHORT_SHA}"
echo "image=ghcr.io/${REPO_LOWER}" >> "$GITHUB_OUTPUT"
echo "branch_tag=${BRANCH_TAG}" >> "$GITHUB_OUTPUT"

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
# Upstream openedx/frontend-app-learning ships a Dockerfile at
# the repo root — we use it unchanged.
file: ./Dockerfile
# `latest` makes the LMS box's `tutor images pull mfe` always
# grab the newest build. The sha-suffix tag is what you set in
# config.yml.example for reproducible prod deploys.
tags: |
${{ steps.tags.outputs.image }}:latest
${{ steps.tags.outputs.image }}:${{ steps.tags.outputs.branch_tag }}
push: true
# Reuse layers across builds so the typical "edit one React
# file" change rebuilds in ~90s instead of 6 min.
cache-from: type=gha
cache-to: type=gha,mode=max
# The MFE Dockerfile takes one build-arg: the MFE name. Tutor
# injects all runtime config (LMS_BASE_URL etc.) at container
# start via env vars — nothing to bake in here.
build-args: |
APP_NAME=learning

- name: Summary
run: |
echo "### Image pushed" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "${{ steps.tags.outputs.image }}:latest" >> "$GITHUB_STEP_SUMMARY"
echo "${{ steps.tags.outputs.image }}:${{ steps.tags.outputs.branch_tag }}" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Deploy with:" >> "$GITHUB_STEP_SUMMARY"
echo '```bash' >> "$GITHUB_STEP_SUMMARY"
echo "tutor config save --set MFE_LEARNING_DOCKER_IMAGE=${{ steps.tags.outputs.image }}:latest" >> "$GITHUB_STEP_SUMMARY"
echo "tutor images pull mfe && tutor local restart mfe" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"

# ─────────────────────────────────────────────────────────────────
# Cross-repo dispatch: tell ChandanaSRooman/LMS that the MFE source
# has new commits, so its `build-mfe-bundle.yml` workflow rebuilds
# the unified MFE bundle (all 11 MFEs + the Rooman fork at this sha)
# and deploys it to the dev EC2.
#
# Why dispatch and not just trigger via the image push?
# Because the LMS-side build needs to clone the MFE source at a
# specific sha, not pull an image. It does its OWN Tutor build
# combining 11 MFEs into one container. So we pass the sha + ref
# along for reproducibility.
#
# Requires a PAT in the secret `LMS_MONOREPO_DISPATCH_PAT` with:
# - fine-grained, scoped only to ChandanaSRooman/LMS
# - permission: Repository contents = read, Actions = write
# Without the secret, this step prints a warning and exits 0 —
# the MFE image push still succeeded, the LMS bundle just won't
# auto-rebuild until the secret is set OR someone manually triggers
# workflow_dispatch on build-mfe-bundle.yml.
# ─────────────────────────────────────────────────────────────────
- name: Dispatch bundle rebuild to ChandanaSRooman/LMS
# Only fire on real source pushes — skip workflow_dispatch
# re-runs so we don't infinite-loop.
if: github.event_name == 'push'
env:
DISPATCH_PAT: ${{ secrets.LMS_MONOREPO_DISPATCH_PAT }}
DISPATCH_TARGET: ChandanaSRooman/LMS
run: |
set -euo pipefail
if [[ -z "${DISPATCH_PAT:-}" ]]; then
echo "::warning::LMS_MONOREPO_DISPATCH_PAT secret not set on this repo."
echo "::warning::MFE image was built+pushed OK, but the LMS-side bundle won't auto-rebuild."
echo "::warning::Trigger build-mfe-bundle.yml manually, or set the secret to enable auto-dispatch."
exit 0
fi
HTTP_CODE=$(curl -sS -L -o /tmp/dispatch.body -w '%{http_code}' \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${DISPATCH_PAT}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/${DISPATCH_TARGET}/dispatches" \
-d "{\"event_type\":\"mfe-source-updated\",\"client_payload\":{\"sha\":\"${GITHUB_SHA}\",\"ref\":\"${GITHUB_REF}\",\"image\":\"${{ steps.tags.outputs.image }}:${{ steps.tags.outputs.branch_tag }}\"}}")
if [[ "$HTTP_CODE" != "204" ]]; then
echo "::error::Dispatch failed with HTTP $HTTP_CODE"
cat /tmp/dispatch.body
exit 1
fi
echo " -> dispatched mfe-source-updated to ${DISPATCH_TARGET}"
echo " sha=${GITHUB_SHA}"
echo " downstream workflow: https://github.com/${DISPATCH_TARGET}/actions/workflows/build-mfe-bundle.yml"
{
echo ""
echo "### Downstream bundle rebuild dispatched"
echo ""
echo "[Watch the LMS-side build-mfe-bundle run](https://github.com/${DISPATCH_TARGET}/actions/workflows/build-mfe-bundle.yml)"
} >> "$GITHUB_STEP_SUMMARY"
63 changes: 63 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Production Docker image for the Rooman Learning MFE.
#
# Multi-stage build:
# 1. `builder` — Node 20 + npm to compile the React app via fedx-scripts
# 2. `server` — Caddy serving the static `dist/` output with SPA fallback
# (so client-side routes like /learning/course/.../sequence
# work on direct page loads, not just navigation)
#
# PUBLIC_PATH=/learning/ is required so webpack (via fedx-scripts) emits
# script tags as src="/learning/runtime.xxx.js" matching the path prefix
# that Tutor's MFE Caddyfile serves assets under. fedx-scripts reads
# PUBLIC_PATH (not PUBLIC_URL) — see the Tutor MFE Dockerfile template.

# ─── Stage 1: build the React bundle ────────────────────────────────────────
FROM docker.io/node:20-bookworm-slim AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
git \
python3 \
build-essential \
&& rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund

COPY . .

RUN cat > env.config.jsx <<'EOF'
const config = {
...process.env,
pluginSlots: {},
};
export default config;
EOF

ARG APP_NAME=learning
ENV APP_NAME=${APP_NAME}
ENV APP_ID=learning
ENV NODE_ENV=production
ENV PUBLIC_PATH=/learning/
ENV MFE_CONFIG_API_URL=/api/mfe_config/v1
RUN npm run build

# ─── Stage 2: serve the static bundle ───────────────────────────────────────
FROM docker.io/caddy:2-alpine AS server

COPY --from=builder /app/dist /usr/share/caddy

RUN printf '%s\n' \
':8080 {' \
' root * /usr/share/caddy' \
' encode gzip' \
' try_files {path} {path}/index.html /index.html' \
' file_server' \
'}' \
> /etc/caddy/Caddyfile

EXPOSE 8080

CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
11 changes: 11 additions & 0 deletions src/courseware/course/sidebar/sidebars/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as notifications from './notifications';
import * as discussions from './discussions';
import * as roomanTutor from './rooman-tutor';

export const SIDEBARS = {
[notifications.ID]: {
Expand All @@ -12,9 +13,19 @@ export const SIDEBARS = {
Sidebar: discussions.Sidebar,
Trigger: discussions.Trigger,
},
// Rooman addition — in-course AI tutor. New custom sidebar, not from
// upstream Open edX. When rebasing this file across releases, keep
// this entry intact — it's the entire Layer-3 sidebar wiring.
[roomanTutor.ID]: {
ID: roomanTutor.ID,
Sidebar: roomanTutor.Sidebar,
Trigger: roomanTutor.Trigger,
},
};

export const SIDEBAR_ORDER = [
// Rooman tutor sits at the top so learners see the icon first.
roomanTutor.ID,
discussions.ID,
notifications.ID,
];
Loading