Skip to content

Real, monotonic and reproducible plugin versioning (DEC-0068) - #111

Merged
erseco merged 1 commit into
mainfrom
fix/monotonic-release-versioning
Jul 24, 2026
Merged

Real, monotonic and reproducible plugin versioning (DEC-0068)#111
erseco merged 1 commit into
mainfrom
fix/monotonic-release-versioning

Conversation

@erseco

@erseco erseco commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Supersedes PR 100. That PR correctly identified the unsafe maximum sentinel — 9999999999 bricks any site installed from the repository checkout, because every real release becomes a downgrade Moodle refuses, and its authors hit exactly that on their own test site. However, the 2026070701 it proposed is lower than the 2026072400 upgrade savepoint that now exists in db/upgrade.php, which makes it conceptually invalid: $plugin->version takes part in Moodle's install/upgrade protocol and must stay above every savepoint. The minimum valid development version at the time of this implementation is 2026072401, which is what this PR commits.

The policy (ADR DEC-0068, included; supersedes DEC-0030 with its context preserved)

main always carries a real, monotonic Moodle version — no sentinels in either direction (9999999999 bricks upgrades; 0/1/99999 sit below existing savepoints) — with the development marker where it belongs: $plugin->release = 'dev'. The version is bumped 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; always strictly above the latest published version and every upgrade_mod_savepoint().

Packaging: the date dependency is removed

scripts/package.sh no longer computes YYYYMMDD from the runner date and no longer rewrites $plugin->version / $plugin->release with sed in the temporary index. The ZIP ships version.php byte-identical to the committed file — rebuilding the same tag on any day produces the same version.php. The rest of the temporary-index machinery is untouched: bundled editor under dist/static/, package-only thirdpartylibs.xml, translation-marker stripping, .distignore, git archive, working tree never modified. scripts/check-package.sh now asserts the byte-identity.

Release flow: metadata is committed before tagging; workflows validate, never mutate

  1. A release-preparation PR commits the final version and semantic release (e.g. $plugin->version = 2026072500; $plugin->release = '4.0.3'; — the actual values are chosen when preparing the release).
  2. Merge; create the git tag on that exact commit; version.php is never modified after the tag exists.
  3. Publish the GitHub release: release.yml now verifies the checked-out commit is the tagged commit, runs check-version.sh --release, fails on dev metadata, builds the editor, packages without rewriting anything, uploads. It never commits or pushes, and derives nothing from runner/workflow/release/tag dates.
  4. A follow-up PR returns main to 'dev' with the next valid development version.

The daily editor watcher (check-editor-releases.yml) no longer creates automated plugin releases or pushes to main — both incompatible with this policy. It now opens an editor-pin sync PR (.editor-version + blueprint) and leaves the release decision to a maintainer. Manual workflow_dispatch builds of release.yml produce a dev-labelled test artifact validated under the development rules.

Validation

New scripts/check-version.sh (Bash 3.2+/Ubuntu/Git Bash), run by CI (release-workflow-check), make check-version, make check-release-version RELEASE=X.Y.Z, and as a dependency of make package:

  • rejects sentinels (9999999999, 99999, 0, 1, …);
  • requires exactly ten digits with a plausible YYYYMMDDXX date structure;
  • extracts every savepoint and $oldversion < guard from db/upgrade.php and enforces the bound (strict in development, >= for releases);
  • in release mode, requires $plugin->release to equal the expected value (never 'dev') and, on an exactly-tagged commit, the tag without its leading v to match;
  • rejects 'dev' metadata on tagged commits in any mode;
  • prints errors to stderr.

scripts/check-version-selftest.sh (also in CI) covers the required matrix: valid 2026072401/dev; each sentinel; malformed versions; version equal to and lower than the highest savepoint; version above it; release/tag match and mismatch; release still carrying dev; a release-preparation commit validating without arguments.

Test results

  • bash -n clean on all four scripts; self-test 17/17; make check-version OK; scripts/check-package.sh OK (working tree untouched, packaged version.php byte-identical); scripts/check-release-workflow.sh still green on the new release.yml; phpcs --standard=moodle 0/0 on version.php.
  • Release simulation performed: a temporary release-preparation commit (2026072500 / test release) was packaged and unzip -p … exelearning/version.php diffed against the committed file — identical; the commit was then reverted.

Documentation

DEVELOPMENT.md gains a "Versioning and releases" section (why no sentinels, when to bump, the five-step flow, tag immutability, validate-don't-mutate); the release checklist gains section 10b; version.php itself documents the bump rules where contributors will see them.


Moodle Playground Preview

The changes in this pull request can be previewed and tested using a Moodle Playground instance.

Preview in Moodle Playground

ℹ️ The eXeLearning editor is fetched from the shared release and unpacked into the plugin when the playground boots, so the first load may take a few extra seconds. ELPX upload, viewer and preview work normally.

Supersedes the sentinel scheme of DEC-0030. main carried
$plugin->version = 9999999999 with the real version stamped at package time
from the runner's date, which had three failure modes: installing from a
checkout bricks the site (every real release is a lower number, so Moodle
refuses it as a downgrade with no in-product recovery — the Marketplace listing
links the repository, so this path is real); rebuilding the same tag on another
day produced a different version.php; and the installed number told support
nothing. A low sentinel would be just as wrong in the other direction:
$plugin->version takes part in the install/upgrade protocol and must stay above
every savepoint in db/upgrade.php — which is also why the 2026070701 proposed
in the earlier fix was invalid against the existing 2026072400 savepoint.

version.php now commits a real version: 2026072401, the minimum valid
development value (strictly greater than the highest savepoint), with the
development marker where it belongs — $plugin->release = 'dev'. The version is
bumped whenever Moodle must detect a change (db/, classes/, JS, settings,
strings, tasks, capabilities, external services, cache-sensitive metadata).

Packaging no longer computes or rewrites anything: scripts/package.sh drops the
date-derived version and the sed rewrite of version.php in the temporary index,
so the ZIP ships version.php byte-identical to the committed file
(check-package.sh asserts it). Everything else about the temporary-index
packaging (bundled editor, packaged thirdpartylibs.xml, translation-marker
stripping, .distignore, git archive) is unchanged, and the working tree is
never touched.

scripts/check-version.sh (with a CI self-test) enforces the policy: sentinels
rejected in both directions, ten-digit plausible YYYYMMDDXX, savepoint bounds
(strict in development, >= for releases), release/tag agreement, and no 'dev'
metadata on tagged commits. make package now depends on the release validation,
and make check-version validates the committed state.

The release flow is: release-preparation PR commits the final version + semver
release -> merge -> tag that exact commit -> release.yml verifies the checkout
is the tagged commit and validates (never mutates) the metadata, builds the
editor and packages -> follow-up PR returns main to 'dev' with the next valid
version. The daily editor watcher no longer creates automated releases or
pushes to main; it opens an editor-pin sync PR instead. Documented in
DEVELOPMENT.md ("Versioning and releases"), the release checklist (section
10b) and ADR DEC-0068; DEC-0030 is marked superseded with its original context
preserved.
@codecov-commenter

codecov-commenter commented Jul 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.31%. Comparing base (da528f4) to head (7a1b520).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff            @@
##               main     #111   +/-   ##
=========================================
  Coverage     92.31%   92.31%           
  Complexity      824      824           
=========================================
  Files            51       51           
  Lines          3734     3734           
=========================================
  Hits           3447     3447           
  Misses          287      287           
Flag Coverage Δ
javascript 94.11% <ø> (ø)
php 92.23% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
PHP (server-side) 92.23% <ø> (ø)
JavaScript (SCORM tracker) 94.11% <ø> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@erseco
erseco merged commit daef78e into main Jul 24, 2026
25 checks passed
@erseco
erseco deleted the fix/monotonic-release-versioning branch July 24, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants