diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 562a3e5..dc2d19d 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -27,6 +27,23 @@ jobs: with: python-version: "3.11" + - name: Set preview build label + run: | + set -e + short=$(git rev-parse --short HEAD) + ver="preview (PR ${{ github.event.pull_request.number }}, commit $short)" + python3 - "$ver" source/data.xml <<'PY' + import sys, pathlib + ver = sys.argv[1] + p = pathlib.Path('source/data.xml') + s = p.read_text(encoding='utf-8') + if '{{BUILD_VERSION}}' in s: + p.write_text(s.replace('{{BUILD_VERSION}}', ver), encoding='utf-8') + print(f"preview build label: {ver}") + else: + print("no {{BUILD_VERSION}} placeholder found (skipping)") + PY + - name: Render preview HTML run: | python3 tools/render_preview.py source/data.xml preview/index.html diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07cbb34..9581322 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,6 +26,20 @@ jobs: ref: docker-container path: xote + - name: Compute build version + id: ver + run: | + short=$(git rev-parse --short HEAD) + date=$(date -u +%Y-%m-%d) + if [ "${{ github.event_name }}" = "release" ]; then + # On release publish, github.ref_name is the tag (e.g. v0.0.6) + ver="${{ github.ref_name }} ($date, commit $short)" + else + ver="dev ($date, commit $short, ${{ github.event_name }})" + fi + echo "version=$ver" >> "$GITHUB_OUTPUT" + echo "Build version: $ver" + - name: Build the xerte image working-directory: xote run: docker build -t xerte . @@ -47,6 +61,20 @@ jobs: done curl -s -o /dev/null -w "HTTP %{http_code}\n" http://localhost:8081/ + - name: Inject build version into course content + run: | + set -e + python3 - "${{ steps.ver.outputs.version }}" source/data.xml source/preview.xml <<'PY' + import sys, pathlib + ver = sys.argv[1] + for fn in sys.argv[2:]: + p = pathlib.Path(fn) + s = p.read_text(encoding='utf-8') + assert '{{BUILD_VERSION}}' in s, f"{fn}: {{BUILD_VERSION}} placeholder not found" + p.write_text(s.replace('{{BUILD_VERSION}}', ver), encoding='utf-8') + print(f"injected {ver!r} into {fn}") + PY + - name: Create project and import course content run: | set -e @@ -76,14 +104,32 @@ jobs: "http://localhost:8081/website_code/php/scorm/export.php?scorm=true&template_id=${{ env.template_id }}" ls -la out/ + - name: Add VERSION.txt to the package + run: | + set -e + printf 'Secure code development\nVersion: %s\nBuilt: %s\nCommit: %s\nSource: %s\n' \ + "${{ steps.ver.outputs.version }}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + "${{ github.sha }}" "${{ github.server_url }}/${{ github.repository }}/tree/${{ github.sha }}" \ + > out/VERSION.txt + (cd out && zip Secure_code_development_scorm.zip VERSION.txt) + echo "--- VERSION.txt in package ---" + unzip -p out/Secure_code_development_scorm.zip VERSION.txt + - name: Verify the package run: | + set -e unzip -l out/Secure_code_development_scorm.zip | head unzip -p out/Secure_code_development_scorm.zip imsmanifest.xml | grep -oE 'schemaversion>[^<]*|scormtype="[a-z]*"' unzip -p out/Secure_code_development_scorm.zip template.xml > /tmp/check.xml echo "questions: $(grep -oE ']*?)/>',x) if html.unescape(html.unescape(re.search(r'text=\"([^\"]*)\"',m.group(1)).group(1)).replace('

','').replace('

','').strip())==''))")" + # build version was injected (placeholder must be gone, label must show) + if grep -q '{{BUILD_VERSION}}' /tmp/check.xml; then + echo "ERROR: {{BUILD_VERSION}} placeholder was not substituted"; exit 1 + fi + echo "build version in About page: $(grep -oE 'Version: [^<&]*' /tmp/check.xml | head -1)" + echo "VERSION.txt in package: $(unzip -l out/Secure_code_development_scorm.zip | grep -c VERSION.txt)" - name: Upload artifact (for workflow_dispatch / fallback) uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/docs/COURSE_VERIFICATION.md b/docs/COURSE_VERIFICATION.md index 636879d..0cd6ee4 100644 --- a/docs/COURSE_VERIFICATION.md +++ b/docs/COURSE_VERIFICATION.md @@ -182,3 +182,27 @@ nets. Guards make it a no-op outside a live SCORM `normal` session. **Pending (convention 9 — re-test on LMS)**: confirm a learner attempt now shows the gradebook updating incrementally (not only on Save), and that a completed attempt still reports `lesson_status=passed` with a non-zero grade. + +## Build version stamping (`{{BUILD_VERSION}}` placeholder) + +So an operator can tell which version is running in an LMS, the About page +(page 2) carries a `Build` section with a `{{BUILD_VERSION}}` placeholder, +and the release workflow adds a `VERSION.txt` to the zip. The placeholder is +committed in `source/data.xml` + `source/preview.xml` and substituted at build +time: the **release workflow** injects `v (date, commit )` before +the XOT export (and fails the build if the token remains in the export), and +the **preview workflow** injects `preview (PR #N, commit )` before +rendering so PR preview HTML shows a clean build line. + +**Verified locally** (simulating the release workflow end-to-end through the +running XOT): substituted `{{BUILD_VERSION}}` → `v0.0.6 (2026-07-14, commit +abc1234)` in `source/data.xml` + `source/preview.xml`, pushed into XOT, +`play.php` returned 200, re-exported SCORM — the exported `template.xml` +shows the substituted `Version: v0.0.6 (…)` line on the About page, +0 `{{BUILD_VERSION}}` placeholders remain, and a `VERSION.txt` was added to +the zip root. All content/tracking attrs unchanged (45 questions, 80% pass, +`trackingMode=full`, `trackingWeight` 1×7 + 21, `unmarkForCompletion`×1, 0 +empty options, 27 `delaySecs=0`, autosave script present). A Publish +round-trip preserved the `{{BUILD_VERSION}}` placeholder intact in +`data.xml` (CKEditor does not mangle the braces). See `PROJECT_CONTEXT.md` § +"Build version stamping" for full detail. diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index 0a5642d..d0c9895 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -257,6 +257,43 @@ re-introducing the terminate-only commit will silently revert to would just call `doLMSCommit` a second time, which is idempotent) and can be dropped. +## Build version stamping (`{{BUILD_VERSION}}` placeholder) + +So an operator can tell **which version is running** in an LMS, the SCORM +package carries a build stamp in two places: + +1. **Visible in the player** — the About page (page 2) has a `Build` section + with a line `

Version: {{BUILD_VERSION}} …

`. The + `{{BUILD_VERSION}}` token is a **placeholder** committed in `source/data.xml` + + `source/preview.xml`. +2. **Machine-readable** — the release workflow adds a `VERSION.txt` (version, + build timestamp, full commit SHA, source tree URL) to the zip root. + +**Who substitutes the placeholder:** +- **Release workflow** (`.github/workflows/release.yml`): a `Compute build + version` step derives `v (YYYY-MM-DD, commit )` for release events + (`github.ref_name` = tag) or `dev (…, workflow_dispatch)` for manual runs; an + `Inject build version into course content` step `python3`-replaces + `{{BUILD_VERSION}}` in `source/data.xml` + `source/preview.xml` **before** the + XOT import, so the substituted value is what gets exported. The `Verify the + package` step fails the build if `{{BUILD_VERSION}}` remains in the exported + `template.xml`. +- **Preview workflow** (`.github/workflows/preview.yml`): a `Set preview build + label` step substitutes `{{BUILD_VERSION}}` → `preview (PR #N, commit )` + before `render_preview.py`, so PR preview HTML shows a clean build line instead + of the raw token. + +**Why a placeholder (not a committed version):** the version is only known at +release time (the git tag), so it cannot be baked into committed source. The +placeholder keeps `source/data.xml` version-agnostic and is a valid, static +value that round-trips through the XOT editor (verified: a Publish cycle +preserves `{{BUILD_VERSION}}` intact — CKEditor does not mangle the braces). + +**Do not remove the `{{BUILD_VERSION}}` placeholder** from the About page +without a replacement — the release workflow's verify step expects it to be +present before substitution and absent after, and that check guards against +silent version-stamping regressions. + ## SCORM scoring (how the LMS grade is computed) SCORM 1.2 reports **one** `cmi.core.score.raw` (0–100) and **one** @@ -362,6 +399,10 @@ grep -oE 'trackingMode="[a-z_]+"' /tmp/c.xml # "full" grep -oE 'delaySecs="0"' /tmp/c.xml | wc -l # 27 (all bullets pages) grep -c 'xPersistProgress' /tmp/c.xml # 1 (SCORM autosave script present on root) grep -oE 'unmarkForCompletion="true"' /tmp/c.xml | wc -l # 1 (Welcome page) +# build version: in a release export {{BUILD_VERSION}} is substituted; in source/preview it is the placeholder +grep -oE 'Version: [^<&]*' /tmp/c.xml # e.g. 'Version: v0.0.6 (...)' +grep -c '{{BUILD_VERSION}}' /tmp/c.xml # 0 in a release export (1 in committed source) +unzip -p Secure_code_development_scorm.zip VERSION.txt # present in release builds # empty options (must be 0): python3 -c "import re,html as H;x=open('/tmp/c.xml').read();print(sum(1 for m in re.finditer(r'