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
17 changes: 17 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
Expand All @@ -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
Expand Down Expand Up @@ -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 '<question' /tmp/check.xml | wc -l | tr -d ' ')"
echo "pass mark: $(grep -oE 'trackingPassed="[^"]*"' /tmp/check.xml)"
echo "empty options: $(python3 -c "import re,html; x=open('/tmp/check.xml').read(); print(sum(1 for m in re.finditer(r'<option ([^>]*?)/>',x) if html.unescape(html.unescape(re.search(r'text=\"([^\"]*)\"',m.group(1)).group(1)).replace('<p>','').replace('</p>','').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:</strong> [^<&]*' /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
Expand Down
24 changes: 24 additions & 0 deletions docs/COURSE_VERIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<tag> (date, commit <short>)` before
the XOT export (and fails the build if the token remains in the export), and
the **preview workflow** injects `preview (PR #N, commit <short>)` 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:</strong> 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.
41 changes: 41 additions & 0 deletions docs/PROJECT_CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<p><strong>Version:</strong> {{BUILD_VERSION}} …</p>`. 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<tag> (YYYY-MM-DD, commit <short>)` 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 <short>)`
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**
Expand Down Expand Up @@ -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:</strong> [^<&]*' /tmp/c.xml # e.g. 'Version:</strong> 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'<option ([^>]*?)/>',x) if H.unescape(H.unescape(re.search(r'text=\"([^\"]*)\"',m.group(1)).group(1)).replace('<p>','').replace('</p>','').strip())==''))"
curl -s -o /dev/null -w "play=%{http_code}\n" http://localhost:8088/play.php?template_id=1 # 200
Expand Down
4 changes: 4 additions & 0 deletions source/data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
<h2>Provenance</h2>

<p>Generated with the <strong>pi coding-agent harness v0.79.8</strong> using LLM models <strong>glm-5.2</strong> and <strong>kimi k2.7-coder</strong>. Not endorsed by OWASP.</p>

<h2>Build</h2>

<p><strong>Version:</strong> {{BUILD_VERSION}} &mdash; this identifies the SCORM package build so you can tell which version is running in your LMS.</p>
]]></text><text linkID="PG1781883865808" name="Introduction to secure code development"><![CDATA[<h2>What is secure code development?</h2>

<p>Secure code development is the practice of designing, writing, reviewing, and maintaining software so that it can withstand malicious or accidental misuse. It is not a final coat of paint applied after the features are finished; it shapes the requirements, the architecture, and the day-to-day coding decisions that engineers make.</p>
Expand Down
4 changes: 4 additions & 0 deletions source/preview.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
<h2>Provenance</h2>

<p>Generated with the <strong>pi coding-agent harness v0.79.8</strong> using LLM models <strong>glm-5.2</strong> and <strong>kimi k2.7-coder</strong>. Not endorsed by OWASP.</p>

<h2>Build</h2>

<p><strong>Version:</strong> {{BUILD_VERSION}} &mdash; this identifies the SCORM package build so you can tell which version is running in your LMS.</p>
]]></text><text linkID="PG1781883865808" name="Introduction to secure code development"><![CDATA[<h2>What is secure code development?</h2>

<p>Secure code development is the practice of designing, writing, reviewing, and maintaining software so that it can withstand malicious or accidental misuse. It is not a final coat of paint applied after the features are finished; it shapes the requirements, the architecture, and the day-to-day coding decisions that engineers make.</p>
Expand Down