diff --git a/.github/workflows/deploytest.yml b/.github/workflows/deploytest.yml index e96c175511..cda563cf81 100644 --- a/.github/workflows/deploytest.yml +++ b/.github/workflows/deploytest.yml @@ -45,6 +45,15 @@ jobs: pnpm rebuild node-sass - name: Build frontend run: pnpm run build + - name: Upload frontend bundle + uses: actions/upload-artifact@v7 + with: + name: studio-frontend-bundle + path: | + contentcuration/contentcuration/static/studio/ + contentcuration/build/webpack-stats.json + if-no-files-found: error + retention-days: 1 make_messages: name: Build all message files needs: pre_job @@ -79,3 +88,145 @@ jobs: sudo apt-get install -y gettext - name: Test Django makemessages run: python contentcuration/manage.py makemessages --all + browser_smoke_test: + name: Browser smoke test + needs: [pre_job, build_assets] + if: ${{ needs.pre_job.outputs.should_skip != 'true' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: learningequality + POSTGRES_PASSWORD: kolibri + POSTGRES_DB: kolibri-studio + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + redis: + image: redis:6.0.9 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + env: + DJANGO_SETTINGS_MODULE: contentcuration.settings + DATA_DB_HOST: localhost + AWS_S3_ENDPOINT_URL: http://localhost:9000 + AWS_BUCKET_NAME: content + CELERY_BROKER_ENDPOINT: localhost + CELERY_REDIS_DB: "0" + CELERY_REDIS_PASSWORD: "" + RUN_MODE: ci + SMOKE_EMAIL: smokeadmin@example.com + SMOKE_PASSWORD: smokepass1234 + SCREENSHOT_DIR: ${{ runner.temp }}/smoke_test_screenshots + steps: + - uses: actions/checkout@v6 + - name: Set up MinIO + run: | + docker run -d -p 9000:9000 --name minio \ + -e "MINIO_ROOT_USER=development" \ + -e "MINIO_ROOT_PASSWORD=development" \ + -e "MINIO_DEFAULT_BUCKETS=content:public" \ + bitnamilegacy/minio:2024.5.28 + - name: Install system deps (gettext, nginx) + run: | + sudo apt-get update -y + sudo apt-get install -y gettext nginx + # nginx's postinst auto-starts the system service on the ubuntu-latest + # runner image. We launch our own nginx with a custom config below, so + # the default instance has to go first — otherwise our nginx -c fails + # on the default pid path or :80 collision. + sudo systemctl stop nginx + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: '3.10' + activate-environment: "true" + enable-cache: "true" + - name: Install python dependencies + run: uv pip sync requirements.txt + - name: Download frontend bundle + uses: actions/download-artifact@v8 + with: + name: studio-frontend-bundle + - name: Cache Playwright browsers + uses: actions/cache@v5 + with: + path: ~/.cache/ms-playwright + key: playwright-chromium-${{ runner.os }}-v1 + - name: Install Chromium and system deps + run: uvx --from "playwright<2" playwright install --with-deps chromium + - name: Prepare database + run: | + python contentcuration/manage.py migrate --noinput + python contentcuration/manage.py loadconstants + - name: Create smoke test user + shell: python + run: | + import os + import sys + # contentcuration package lives one level down; put it on sys.path + # so django.setup() can import contentcuration.settings. + sys.path.insert(0, "contentcuration") + import django + django.setup() + from django.contrib.auth import get_user_model + # Studio's custom User model: create_superuser(email, first_name, last_name, password). + # is_active defaults to False, so we have to flip it explicitly — + # otherwise the user exists but can't log in. + u = get_user_model().objects.create_superuser( + os.environ["SMOKE_EMAIL"], + "Smoke", + "Admin", + password=os.environ["SMOKE_PASSWORD"], + ) + u.is_active = True + u.save() + - name: Prepare static and translations + run: | + python contentcuration/manage.py collectstatic --noinput + cd contentcuration && python manage.py compilemessages + - name: Start gunicorn + run: | + cd contentcuration && \ + nohup gunicorn contentcuration.wsgi:application \ + --timeout=120 --workers=1 --threads=1 \ + --bind=0.0.0.0:8081 --log-level=info \ + > "${{ runner.temp }}/gunicorn.log" 2>&1 & + echo "gunicorn started in background" + - name: Start nginx (serves /static/, proxies / to gunicorn) + run: | + # Substitute the static dir into the checked-in template. Escape + # sed replacement metacharacters (& and \) so paths containing them + # don't corrupt the substitution. + STATIC_DIR="$GITHUB_WORKSPACE/contentcuration/static" + ESCAPED=$(printf '%s' "$STATIC_DIR" | sed -e 's/[\\&]/\\&/g') + NGINX_CONF="${{ runner.temp }}/smoke-nginx.conf" + sed "s|__STATIC_DIR__|$ESCAPED|" integration_testing/nginx-smoke.conf > "$NGINX_CONF" + sudo nginx -c "$NGINX_CONF" -g 'daemon on;' + - name: Run browser smoke test + run: uv run --script integration_testing/smoke_test.py + - name: Upload screenshots + if: always() + uses: actions/upload-artifact@v7 + with: + name: smoke_test_screenshots + path: ${{ runner.temp }}/smoke_test_screenshots + if-no-files-found: ignore + - name: Upload gunicorn log on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: smoke_test_gunicorn_log + path: ${{ runner.temp }}/gunicorn.log + if-no-files-found: ignore diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..d5f42b0e8f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,176 @@ + + +# Kolibri Studio Development Guide for AI Coding Agents + +**Project:** Kolibri Studio — web app for authoring and publishing learning channels to Kolibri +**Stack:** Python/Django backend, Vue.js 2.7 frontend, Kolibri Design System (KDS) + legacy Vuetify 1.5, Postgres/Redis/MinIO/Celery services, pytest/Jest testing +**Platform:** web (server-deployed, not packaged for clients) + +## Quick Start + +```bash +uv pip sync requirements.txt requirements-dev.txt # Python deps +pnpm install # Node deps +pre-commit install # Required — commits fail without this +make dcservicesup # Bring up postgres / redis / minio in docker +pnpm devsetup # Migrate + load sample data + create admin +pnpm devserver # Django :8080 + Webpack watcher +``` + +→ Full setup: `README.md` | Local production-shape stack: `make dcup` (uses `docker-compose.yml`) + +## Critical Gotchas + +### ⚠️ BEFORE Writing Any Vue Component, Search for Existing Ones + +Do not create a new component without first searching for an existing solution: +1. **Kolibri Design System** ([docs](https://design-system.learningequality.org/)) — `KButton`, `KCircularLoader`, `KTextbox`, `KSelect`, `KModal`, `KCheckbox`, `KIcon`, `KTable`, etc. +2. **`contentcuration/contentcuration/frontend/shared/`** — Studio-specific shared components. +3. **Vuetify 1.5** — for legacy widgets KDS doesn't cover (data tables, complex layout primitives). See the next gotcha before reaching for Vuetify in new code. + +If a component does 80% of what you need, wrap it — do not rewrite. + +### ⚠️ Use KDS, Not Vuetify, for New Code + +KDS is the design-system source of truth. Vuetify is legacy and being phased out. The rule applies to **new files / brand-new components** — if you're working inside a file that already uses Vuetify, match it (see "Match the surrounding file's API style" below) rather than mixing the two. **Do not introduce Vuetify into a fresh component.** When you encounter Vuetify being used where a KDS equivalent exists, leave it alone unless the work is genuinely about replacing it — opportunistic refactors in unrelated PRs add review noise. + +### ⚠️ Use Theme Tokens, Not Hard-Coded Colors + +Never use raw color values. Access theme colors via `$themeTokens` and `$themePalette`: +```vue + +``` +For computed dynamic styles, use `$computedClass`. + +### ⚠️ Style Blocks, Not Inline — RTL Depends On It + +Non-dynamic styles go in `