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
83 changes: 36 additions & 47 deletions .github/workflows/check-editor-releases.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
---
name: Check Editor Releases

# Watches exelearning/exelearning for a new editor release and opens a SYNC
# PULL REQUEST updating .editor-version and the playground blueprint pin.
#
# It deliberately does NOT build a package, publish a plugin release or push to
# main any more (DEC-0068): releases follow the release-preparation flow — a
# human PR commits the final version.php, the tag points at that commit, and
# release.yml builds from the tag. This workflow only keeps the editor pin in
# sync and leaves the release decision to a maintainer.

on:
schedule:
- cron: "0 8 * * *" # Daily at 8:00 UTC
workflow_dispatch:

permissions:
contents: write
actions: write
pull-requests: write

jobs:
check_and_build:
sync_editor_pin:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand All @@ -21,7 +30,6 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Fetch latest release from exelearning/exelearning
LATEST=$(gh api repos/exelearning/exelearning/releases/latest --jq '.tag_name' 2>/dev/null || echo "")
if [ -z "$LATEST" ]; then
echo "No release found"
Expand All @@ -31,13 +39,11 @@ jobs:
echo "Latest editor release: $LATEST"
echo "tag=$LATEST" >> $GITHUB_OUTPUT

# Check if we already built this version
MARKER_FILE=".editor-version"
CURRENT=""
if [ -f "$MARKER_FILE" ]; then
CURRENT=$(cat "$MARKER_FILE")
if [ -f .editor-version ]; then
CURRENT=$(cat .editor-version)
fi
echo "Current built version: $CURRENT"
echo "Current pinned version: $CURRENT"

if [ "$LATEST" = "$CURRENT" ]; then
echo "Already up to date"
Expand All @@ -47,57 +53,40 @@ jobs:
echo "found=true" >> $GITHUB_OUTPUT
fi

- name: Setup Bun
if: steps.check.outputs.found == 'true'
uses: oven-sh/setup-bun@v2

- name: Build static editor
- name: Open editor-pin sync pull request
if: steps.check.outputs.found == 'true'
env:
EXELEARNING_EDITOR_REPO_URL: https://github.com/exelearning/exelearning.git
EXELEARNING_EDITOR_REF: ${{ steps.check.outputs.tag }}
EXELEARNING_EDITOR_REF_TYPE: tag
run: make build-editor

- name: Compute version
if: steps.check.outputs.found == 'true'
id: version
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.check.outputs.tag }}"
VERSION="${TAG#v}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT
BRANCH="sync/editor-${TAG}"

- name: Create package
if: steps.check.outputs.found == 'true'
run: make package RELEASE=${{ steps.version.outputs.version }}
if gh pr list --head "$BRANCH" --state open --json number --jq 'length' | grep -qv '^0$'; then
echo "A sync PR for $TAG is already open; nothing to do."
exit 0
fi

git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git checkout -b "$BRANCH"

- name: Update editor version marker
if: steps.check.outputs.found == 'true'
run: |
TAG="${{ steps.check.outputs.tag }}"
echo "$TAG" > .editor-version
# Keep the playground blueprint's editor URL in sync with .editor-version,
# otherwise the preview stays pinned to the hardcoded version.
# otherwise the preview stays pinned to the previous version.
sed -i -E \
-e "s#(release=)v[0-9][0-9A-Za-z.-]*#\1$TAG#g" \
-e "s#(releases/download/)v[0-9][0-9A-Za-z.-]*#\1$TAG#g" \
-e "s#(exelearning-static-)v[0-9][0-9A-Za-z.-]*(\.zip)#\1$TAG\2#g" \
blueprint.json
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git add .editor-version blueprint.json
git commit -m "Update editor version to $TAG"
git push
git commit -m "Sync editor pin to $TAG"
git push origin "$BRANCH"

- name: Create GitHub Release
if: steps.check.outputs.found == 'true'
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.version.outputs.tag }}
name: "${{ steps.version.outputs.tag }}"
body: |
Automated build with eXeLearning editor ${{ steps.version.outputs.tag }}.
files: mod_exelearning-${{ steps.version.outputs.version }}.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
gh pr create \
--base main \
--head "$BRANCH" \
--title "Sync editor pin to $TAG" \
--body "The editor published [$TAG](https://github.com/exelearning/exelearning/releases/tag/$TAG). This updates \`.editor-version\` and the playground blueprint pin.

To ship a plugin release bundling this editor, follow the release-preparation flow (DEVELOPMENT.md, \"Versioning and releases\"): commit the final \`version.php\`, merge, tag that commit, and publish the release — release.yml builds the ZIP from the tag."
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,5 +316,10 @@ jobs:
- name: Validate release workflow editor pinning
run: bash scripts/check-release-workflow.sh

- name: Validate version metadata (DEC-0068)
run: |
bash scripts/check-version-selftest.sh
bash scripts/check-version.sh

- name: Validate release packaging
run: bash scripts/check-package.sh
43 changes: 34 additions & 9 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
---
name: Release

# Builds the distributable ZIP for a published GitHub release (or a manual test
# build). Under the DEC-0068 version policy this workflow VALIDATES metadata and
# never mutates it: the release-preparation PR commits the final
# $plugin->version / $plugin->release before the tag is created, the tag points
# at that exact commit, and the package ships version.php verbatim. Nothing here
# derives a version from the runner date, the run date, the tag date or the
# release publication date — rebuilding the same tag always produces the same
# version.php. This workflow never commits or pushes.

on:
release:
types: [published]
workflow_dispatch:
inputs:
release_tag:
description: "Release label for package name (e.g. 1.2.3 or 1.2.3-beta)"
required: false
default: ""
editor_repo_url:
description: "Editor source repository URL"
required: false
Expand Down Expand Up @@ -53,16 +58,36 @@ jobs:
echo "EXELEARNING_EDITOR_REF=v${VERSION_TAG}" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REF_TYPE=tag" >> $GITHUB_ENV
else
INPUT_RELEASE="${{ github.event.inputs.release_tag }}"
if [ -z "$INPUT_RELEASE" ]; then
INPUT_RELEASE="manual-$(date +%Y%m%d)-${GITHUB_SHA::7}"
fi
echo "RELEASE_TAG=${INPUT_RELEASE}" >> $GITHUB_ENV
# Manual test build: version.php on a development branch carries
# release = 'dev', and check-version validates exactly that. To test
# the full release path, run this from a release-preparation branch
# whose version.php already carries the final release, tag it, and
# publish the release instead.
echo "RELEASE_TAG=dev" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REPO_URL=${{ github.event.inputs.editor_repo_url }}" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REF=${{ github.event.inputs.editor_ref }}" >> $GITHUB_ENV
echo "EXELEARNING_EDITOR_REF_TYPE=${{ github.event.inputs.editor_ref_type }}" >> $GITHUB_ENV
fi

- name: Validate release metadata (DEC-0068)
run: |
if [ "${{ github.event_name }}" = "release" ]; then
# The checked-out commit must be exactly the tagged commit, and
# version.php must already carry the final release (never 'dev'):
# check-version.sh asserts release == tag (without 'v'), the
# 10-digit monotonic version, and the savepoint bound.
RAW_TAG="${GITHUB_REF##*/}"
TAGGED_SHA="$(git rev-parse "refs/tags/${RAW_TAG}^{commit}")"
HEAD_SHA="$(git rev-parse HEAD)"
if [ "$TAGGED_SHA" != "$HEAD_SHA" ]; then
echo "Checked-out commit $HEAD_SHA is not the tagged commit $TAGGED_SHA" >&2
exit 1
fi
bash scripts/check-version.sh --release "${RELEASE_TAG}"
else
bash scripts/check-version.sh
fi

- name: Build static editor
run: make build-editor

Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Cerradas: **TAREA-012 / RIE-001** investigación (DEC-0019); **TAREA-009 / RIE-0
| DEC-0027 | **Aceptada** (2026-06-03) | Aceptar `.zip` (con `content.xml`) además de `.elpx` en la subida |
| DEC-0028 | **Aceptada** (2026-06-03) | Enlaces del gradebook: análisis y destino del 'grade analysis' → issue #13 #4 |
| DEC-0029 | **Aceptada** (2026-06-03) | Interruptor 'Calificable' por actividad (`gradeenabled`) → issue #13 |
| DEC-0030 | **Aceptada** (2026-06-03) | Versión 'sentinela' (`9999999999`/dev) en main; la real la inyecta `make package` |
| DEC-0030 | **Superseded** by DEC-0068 | Versión 'sentinela' (`9999999999`/dev) en main; la real la inyectaba `make package` |
| DEC-0031 | **Aceptada** (2026-06-03) | Separar el formulario en 'Grading' y 'Attempts management' → issue #13 |
| DEC-0032 | **Propuesta** (2026-06-04) | Ingesta dual de tracking: shim SCORM 1.2 + xAPI (`exe_xapi.js`) sobre tubería común → TAREA-015 |
| DEC-0033 | **Propuesta** (2026-06-04) | Actualización de contenido: reemplazo del `.elpx` + origen por URL con sincronización (patrón `mod_scorm`) → TAREA-016 |
Expand All @@ -182,7 +182,7 @@ Cerradas: **TAREA-012 / RIE-001** investigación (DEC-0019); **TAREA-009 / RIE-0
| DEC-0048 | **Aceptada** (2026-06-12) | Estrategia de cobertura de tests: mockear la red con `\curl::mock_response()` + mock parcial de `download_to_temp()` en vez de excluir; no excluir del scope código testeable (`excludelistfiles` vacío); xdebug/Codecov es la medida autoritativa (pcov local subacredita llamadas anidadas — artefacto, no límite); gate `codecov project: target: auto` (trinquete). Cobertura honesta 85.71%→87.2% (PR #65) |
| DEC-0049 | **Aceptada** (2026-06-12) | Auditoría estándar de repositorio (2026-06-11, tras DEC-0016/DEC-0044): 9 mejoras P1–P3 implementadas (PRs #46–#54: hardening XML de estilos, thirdpartylibs en el ZIP, fidelidad backup/restore, lock de intentos, participación vs grademethod, recálculo de notas en lote, `zip_utils`, descarga del informe, Behat) + registro de **hallazgos descartados** y opciones de dirección para no re-auditar |
| DEC-0050 | **Aceptada** (2026-06-12) | La herramienta de migración exeweb/exescorm vive en `mod_exelearning` (destino, dueño de los internals); orígenes como fuentes legacy de solo lectura tras `source_interface`. Endurecimiento de la rama issue #13: fix `mod_exeweb` itemid=revision (antes leía 0 → todo `nosource`); clasificación `mod_exescorm` (`.elpx` directo / 1 embebido / 0=nosource / >1=ambiguous / external+aiccurl+localsync=unsupported, `localsync` excluido por sincronización aunque tenga snapshot local); limpieza compensatoria con `course_delete_module` ante fallo parcial (sin transacción, caveat recycle bin); preservación de metadatos del cm (idnumber **nunca** se copia); validación post-extracción anti shell-vacío (`migrateextractfailed`); eventos (started/migrated/skipped/failed, patrón DEC-0041); columnas `userid`/`timemodified` (upgrade 2026061201); preflight + `\core\progress\display`. Refactor a `classes/local/migration/` (elimina `import_service`). CLI diferido |
| DEC-0051..0063 | (varias, 2026-06-12 → 2026-06-17) | **Ver índice completo en `research/docs/indices/adrs.yaml`.** Resumen: DEC-0051 eventos selectivos · DEC-0052 completion por estado · DEC-0053 búsqueda global · DEC-0054 refactor `lib.php` (extracción a clases) · DEC-0055 auditoría post-refactor · DEC-0056 tests JS (Vitest) · DEC-0057 extracción no-destructiva (BETA→STABLE) · DEC-0058 fijar tag del editor en release · DEC-0063 validación canónica del endpoint xAPI + política de versión (1.0.3 tolerante a 2.0) · DEC-0064 implementación ingesta xAPI · DEC-0065 editor solo empaquetado en release (sin instalador runtime) · DEC-0066 interruptor global del editor (modo reproductor puro) · DEC-0067 página de estilos solo-endpoint (cierra UX-01). *(DEC-0059..0062 = iframe seguro en rama `feature/secure-iframe-scorm-bridge`, aún no en `main`.)* |
| DEC-0051..0063 | (varias, 2026-06-12 → 2026-06-17) | **Ver índice completo en `research/docs/indices/adrs.yaml`.** Resumen: DEC-0051 eventos selectivos · DEC-0052 completion por estado · DEC-0053 búsqueda global · DEC-0054 refactor `lib.php` (extracción a clases) · DEC-0055 auditoría post-refactor · DEC-0056 tests JS (Vitest) · DEC-0057 extracción no-destructiva (BETA→STABLE) · DEC-0058 fijar tag del editor en release · DEC-0063 validación canónica del endpoint xAPI + política de versión (1.0.3 tolerante a 2.0) · DEC-0064 implementación ingesta xAPI · DEC-0065 editor solo empaquetado en release (sin instalador runtime) · DEC-0066 interruptor global del editor (modo reproductor puro) · DEC-0067 página de estilos solo-endpoint (cierra UX-01) · DEC-0068 versión real y monótona en main (supersede DEC-0030; empaquetado valida, no muta). *(DEC-0059..0062 = iframe seguro en rama `feature/secure-iframe-scorm-bridge`, aún no en `main`.)* |

## Restricciones inmutables

Expand Down
49 changes: 41 additions & 8 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,12 @@ This produces `mod_exelearning-<RELEASE>.zip` with everything under a top-level
Packaging (`scripts/package.sh`) uses **only `git`** &mdash; no `zip`, `rsync`,
`python` or `php` &mdash; so it also works in Git Bash on Windows. It stages the
working tree (including the built editor under `dist/static/`, which is
`.gitignore`d) into a throwaway index, stamps `version.php` there (`version` =
`YYYYMMDD00`, `release` = `<RELEASE>`; the working tree is never modified), and
emits the ZIP via `git archive --format=zip`. Temporary git objects are written
to a scratch store, so your real `.git` is left untouched.
`.gitignore`d) into a throwaway index and emits the ZIP via
`git archive --format=zip`. Temporary git objects are written to a scratch
store, so your real `.git` is left untouched and the working tree is never
modified. **`version.php` ships exactly as committed** — packaging validates the
metadata (`make package` depends on `check-release-version`) but never rewrites
it, so rebuilding the same tag on any day produces the same `version.php`.

The bundled editor is mandatory (DEC-0065): packaging **fails** — with a clear
error and no partial ZIP — unless `dist/static/` holds a valid editor
Expand All @@ -192,7 +194,38 @@ component or full relative path matches a pattern). `README.md` and
`docker*`, `blueprint.json`, `phpmd*`, `scripts/`, `research/`, `docs/`, hidden
files, internal docs) is not — the README links to the docs on GitHub instead.

> The committed `version.php` carries a sentinel (`9999999999` / `dev`,
> [DEC-0030](./research/decisiones/adr/DEC-0030-version-sentinela-en-main.md)); the real
> values are injected into the ZIP only. Releases are also built automatically by
> `.github/workflows/release.yml` on a published GitHub release.
## Versioning and releases

Policy: [DEC-0068](./research/decisiones/adr/DEC-0068-version-real-monotona-en-main.md)
(supersedes DEC-0030). `main` always carries a **real, monotonic Moodle
version** and `$plugin->release = 'dev'`:

- **No sentinels, in either direction.** `9999999999` bricks any site installed
from a checkout (every real release becomes a downgrade Moodle refuses, with
no in-product recovery); low values (`0`, `1`, `99999`) break the upgrade
protocol the other way, because `$plugin->version` must stay above every
`upgrade_mod_savepoint()` in `db/upgrade.php`. The development marker belongs
in `$plugin->release`, which is informational.
- **When to bump `$plugin->version`** (format `YYYYMMDDXX`): whenever Moodle
must detect a change — `db/`, `classes/`, JavaScript source or builds,
settings, language strings, scheduled tasks, capabilities, external services,
or other cache-sensitive metadata. The new value must be strictly greater
than the latest published version and every savepoint / `$oldversion <` guard
in `db/upgrade.php`. `scripts/check-version.sh` (run by CI and by
`make check-version`) enforces the bounds.
- **Release flow** (in this exact order):
1. Open a release-preparation PR committing the final version and semantic
release in `version.php` (e.g. `$plugin->version = 2026072500;`
`$plugin->release = '4.0.3';`).
2. Merge it.
3. Create the git tag (`vX.Y.Z`) **on that exact commit**. Never modify
`version.php` after the tag exists — rebuilding a tag must never change it.
4. Publish the GitHub release; `.github/workflows/release.yml` verifies the
checked-out commit is the tagged one, validates the metadata
(`check-version.sh --release`), builds the editor and packages the ZIP
without touching `version.php`. Workflows never commit or push.
5. Open a follow-up PR switching `$plugin->release` back to `'dev'` and
bumping `$plugin->version` to the next valid development value.

`make check-version` validates the committed state at any time;
`make check-release-version RELEASE=X.Y.Z` validates release metadata.
31 changes: 22 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -212,18 +212,31 @@ clean-editor:

PLUGIN_NAME = mod_exelearning

# Create a distributable ZIP package
# Usage: make package RELEASE=0.0.2
# VERSION (YYYYMMDDXX) is auto-generated from the current date.
# Delegates to scripts/package.sh, which uses only git ("git archive") so it
# needs no zip/rsync/python/php and works in Git Bash on Windows. version.php is
# stamped in a temporary index (the working tree is never modified) and the ZIP
# is rooted at the Moodle install folder "exelearning/".
package:
# Validate the committed version metadata (DEC-0068): real, monotonic
# YYYYMMDDXX version, release = 'dev' on the development branch, strictly above
# every db/upgrade.php savepoint.
check-version:
bash scripts/check-version.sh

# Validate release metadata before packaging: version.php must already carry the
# final semantic release (committed by the release-preparation PR) matching
# RELEASE, and the tagged commit when building from a tag.
check-release-version:
@if [ -z "$(RELEASE)" ]; then \
echo "Error: RELEASE not specified. Use 'make package RELEASE=0.0.2'"; \
echo "Error: RELEASE is required."; \
exit 1; \
fi
bash scripts/check-version.sh --release "$(RELEASE)"

# Create a distributable ZIP package
# Usage: make package RELEASE=4.0.3
# The Moodle version is NOT generated here: version.php ships exactly as
# committed (DEC-0068); check-release-version validates it first. Delegates to
# scripts/package.sh, which uses only git ("git archive") so it needs no
# zip/rsync/python/php and works in Git Bash on Windows. The working tree is
# never modified and the ZIP is rooted at the Moodle install folder
# "exelearning/".
package: check-release-version
@command -v git >/dev/null 2>&1 || { echo "Error: git is required to build the package."; exit 1; }
@bash scripts/package.sh "$(RELEASE)" "$(PLUGIN_NAME)"

Expand Down
Loading
Loading