A Moodle local plugin that syncs course content from a GitHub repository. Content creators author HTML files in a structured repo, and Moodle pulls it in to build and update the course automatically.
- One-click sync from a GitHub repository into a Moodle course
- Automatic section creation based on directory structure
- Page, Label, URL, Book, and Lesson activities created from HTML files and directories
- YAML front matter support for controlling activity types
- Asset management — CSS, images, and JS uploaded to Moodle file storage with automatic URL rewriting
- Incremental sync — only changed files are updated (content hash tracking)
- Delete detection — removed files are automatically hidden in the course
- Scheduled auto-sync — hourly cron task for configured courses
- GitHub webhook endpoint for instant sync on push
- CLI tool for bulk syncing all configured courses
- PAT encryption at rest using Moodle's Sodium-based encryption
- Built-in file editor — browse, edit, and commit files back to GitHub directly from within Moodle
- Per-course configuration with capability-based access control
- Sync history with commit SHA tracking and detailed operation logs
- Moodle 4.1 or later
- PHP 8.0 or later
- A GitHub repository with the expected directory structure
- A GitHub Personal Access Token (PAT) with
reposcope (orpublic_repofor public repos)
- Copy the
githubsyncdirectory to/local/githubsyncin your Moodle installation - Visit Site Administration > Notifications to trigger the database install
- Or run from the command line:
php admin/cli/upgrade.php
Your GitHub repository should follow this structure:
course.yaml # Course metadata (optional)
sections/
01-introduction/
section.yaml # Section title and summary (optional)
01-welcome.html # -> Page activity "Welcome"
02-course-handbook/ # -> Book activity "Course Handbook"
book.yaml # Optional: title, numbering, intro
01-getting-started.html # -> Chapter 1
02-assessment-guide.html # -> Chapter 2
03-resources.html # -> Chapter 3
03-overview.html # -> Page activity "Overview"
04-notice.html # -> Label activity (with front matter)
02-module-one/
section.yaml
01-lesson.html
02-external-link.html # -> URL activity (with front matter)
03-safety-training/ # -> Lesson activity "Safety Training"
lesson.yaml # Optional: title, intro, practice, retake
01-introduction.html # -> Content page
02-hazard-check.html # -> True/false question (with front matter)
03-procedure-quiz.html # -> Multichoice question (with front matter)
assets/
css/
custom.css # Shared stylesheets
images/
diagram.png # Shared images
js/
interactions.js # Shared scripts
Optional. Updates the Moodle course metadata on sync.
fullname: "Introduction to Digital Media"
shortname: "IDM101"
summary: "An introductory course covering..."
format: topics
visible: trueOptional per section. Sets the section title and summary.
title: "Module 1: Getting Started"
summary: "<p>In this module you will learn the fundamentals.</p>"
visible: truePlain HTML fragments (no <html> or <body> tags). Each file becomes a Moodle activity. The filename determines the activity name:
01-welcome.html→ "Welcome"02-interactive-lesson.html→ "Interactive Lesson"
The numeric prefix controls ordering and is stripped from the name.
Add a YAML block at the top of an HTML file to control the activity type:
Label activity:
---
type: label
---
<div class="alert alert-info">
<strong>Note:</strong> This becomes a Label on the course page.
</div>URL activity:
---
type: url
name: "Moodle Documentation"
url: "https://docs.moodle.org"
---
<p>Optional description text.</p>Supported front matter fields:
| Field | Description |
|---|---|
type |
Activity type: page (default), label, or url. Books and Lessons are created from directories, not front matter. Other Moodle activity types (quiz, forum, assign, etc.) are not yet implemented. Unrecognized types are treated as page. |
name |
Override the activity name (otherwise derived from filename) |
url |
External URL (required for type: url) |
visible |
true or false |
A subdirectory inside a section directory becomes a Book activity. Each .html file in the subdirectory becomes a chapter, ordered by numeric prefix. This maps one directory to one Moodle book module.
sections/
01-introduction/
02-course-handbook/ # -> Book "Course Handbook"
book.yaml # Optional metadata
01-getting-started.html # -> Chapter 1 "Getting Started"
02-assessment-guide.html # -> Chapter 2 "Assessment Guide"
03-resources.html # -> Chapter 3 "Resources"
Optional. Sets book-level metadata:
title: "Course Handbook"
numbering: bullets
intro: "<p>Reference handbook for this course.</p>"| Field | Description |
|---|---|
title |
Override the book name (otherwise derived from directory name) |
numbering |
Chapter numbering style: none, numbers (default), bullets, or indented |
intro |
HTML description shown on the book's intro page |
Chapter HTML files support YAML front matter for per-chapter settings:
---
title: "Getting Started Guide"
subchapter: true
---
<h2>Welcome</h2>
<p>This chapter covers...</p>| Field | Description |
|---|---|
title |
Override the chapter title (otherwise derived from filename) |
subchapter |
true to make this a sub-chapter (indented under the previous chapter). The first chapter in a book cannot be a subchapter. |
- New book directory: Creates the book activity and all chapters in one operation
- Modified chapter: Only the changed chapter is updated (content hash tracking)
- New chapter file: Added at the correct position based on filename ordering
- Removed chapter file: The chapter is hidden (not deleted), matching the behavior for removed pages
- Modified book.yaml: Updates book name, numbering, and intro without touching chapters
- Reordered chapters (renamed with different numeric prefixes): Page numbers are updated even if content is unchanged
A subdirectory inside a section directory that contains a lesson.yaml file (or has its name recognized as a lesson) becomes a Lesson activity. Each .html file in the subdirectory becomes a lesson page. Pages can be content pages, true/false questions, or multichoice questions, controlled by YAML front matter.
sections/
01-introduction/
03-safety-training/ # -> Lesson "Safety Training"
lesson.yaml # Optional metadata
01-introduction.html # -> Content page
02-hazard-check.html # -> True/false question
03-procedure-quiz.html # -> Multichoice question
Optional. Sets lesson-level metadata:
title: "Safety Training"
intro: "<p>Complete this lesson to demonstrate your understanding.</p>"
practice: false
retake: true
feedback: true
review: false
maxattempts: 3
progressbar: true| Field | Description |
|---|---|
title |
Override the lesson name (otherwise derived from directory name) |
intro |
HTML description shown on the lesson's intro page |
practice |
Practice lesson — no grades recorded (default: false) |
retake |
Allow students to retake the lesson (default: true) |
feedback |
Show feedback after answering (default: true) |
review |
Allow students to review their answers (default: false) |
maxattempts |
Maximum number of attempts (default: 1) |
progressbar |
Show progress bar (default: true) |
Each HTML file in a lesson directory becomes a page. The page type is set via front matter:
Content page (default — no front matter needed):
<h2>Introduction to Safety</h2>
<p>This module covers workplace safety fundamentals.</p>A "Continue" button is automatically added. The last page links to End of Lesson.
True/false question:
---
type: truefalse
correct: true
feedback_correct: "That's right! PPE is always required."
feedback_incorrect: "Actually, PPE is required in all work areas."
---
<p>Personal protective equipment (PPE) must be worn in all work areas.</p>| Field | Description |
|---|---|
type |
truefalse |
correct |
true or false — which answer is correct |
feedback_correct |
Feedback shown when the student answers correctly |
feedback_incorrect |
Feedback shown when the student answers incorrectly |
Multichoice question:
---
type: multichoice
answers:
- text: "Stop work and report it"
correct: true
feedback: "Correct! Always stop and report hazards."
- text: "Ignore it and continue"
correct: false
feedback: "Incorrect. Ignoring hazards puts everyone at risk."
- text: "Fix it yourself without training"
correct: false
feedback: "Incorrect. Only trained personnel should address hazards."
---
<p>What should you do if you notice a safety hazard?</p>| Field | Description |
|---|---|
type |
multichoice |
answers |
List of answer options, each with text, correct, and feedback |
Correct answers advance to the next page; incorrect answers stay on the current page.
- New lesson directory: Creates the lesson activity and all pages in one operation
- Modified page: Only the changed page is updated (content hash tracking via
itemidin the mapping table) - New page file: Added at the correct position based on filename ordering
- Removed page file: The page mapping is updated; pages are managed through the lesson's linked list
- Modified lesson.yaml: Updates lesson name, intro, and settings without touching pages
Files in the assets/ directory are uploaded to Moodle's file storage. References in HTML are automatically rewritten:
<!-- You write: -->
<link rel="stylesheet" href="assets/css/custom.css">
<img src="assets/images/diagram.png" alt="Diagram">
<!-- Moodle sees: -->
<link rel="stylesheet" href="/pluginfile.php/.../local_githubsync/assets/.../css/custom.css">
<img src="/pluginfile.php/.../local_githubsync/assets/.../images/diagram.png" alt="Diagram">- Navigate to your course
- Go to Course Settings > GitHub Sync (in the settings navigation)
- Enter:
- Repository URL: e.g.
https://github.com/yourorg/course-content - Personal Access Token: a GitHub PAT with
reposcope - Branch: the branch to sync from (default:
main) - Enable auto-sync: check to include in hourly scheduled sync
- Repository URL: e.g.
- Click Save settings
- Click Sync from GitHub to run the first sync
First sync:
- Creates Moodle sections matching the
sections/directories - Creates Page/Label/URL activities from HTML files
- Uploads assets to Moodle file storage
- Records the commit SHA
Subsequent syncs:
- Compares current commit SHA to stored SHA
- Skips if already up to date
- Only updates activities whose content hash has changed
- Creates new activities for new files
- Hides activities for removed files (does not delete)
- Updates assets if changed
The repository is the single source of truth. If someone edits a Page directly in Moodle and then a sync runs, the repo version overwrites the Moodle edit.
Visit Site Administration > Plugins > Local plugins > GitHub Sync to configure:
- Webhook secret for HMAC-SHA256 signature verification (required for webhooks)
- Default branch name for new configurations
- View webhook URL for GitHub integration
- CLI sync commands
For immediate sync when content is pushed:
- In Moodle, go to Site Administration > Plugins > Local plugins > GitHub Sync
- Set a Webhook secret (any random string) and save
- In your GitHub repository, go to Settings > Webhooks > Add webhook
- Set Payload URL to:
https://yourmoodle.com/local/githubsync/webhook.php - Set Content type to:
application/json - Set Secret to the same value you entered in Moodle
- Select Just the push event
- Click Add webhook
The webhook verifies the HMAC-SHA256 signature, matches the repository URL and branch to find configured courses, and syncs them automatically.
Enable auto-sync in the per-course configuration. The scheduled task runs hourly and syncs all courses with auto-sync enabled.
To manually trigger the scheduled task:
php admin/cli/scheduled_task.php --execute='\local_githubsync\task\sync_courses'Sync all configured courses:
php local/githubsync/cli/sync_all.phpSync a specific course:
php local/githubsync/cli/sync_all.php --courseid=9Sync only auto-sync courses:
php local/githubsync/cli/sync_all.php --auto-onlyThe plugin includes a built-in file editor that lets you browse, view, edit, and commit repository files directly from Moodle — no need to switch to GitHub or a local IDE for quick content changes.
Accessing the editor: From the GitHub Sync configuration page for a course, click the File Editor link. Requires the local/githubsync:configure capability.
What you can do:
- Browse the full repository file tree in a collapsible sidebar
- View any text file by clicking it in the tree
- Edit file content in a textarea with keyboard shortcuts (Tab inserts spaces, Ctrl+S / Cmd+S saves)
- Commit changes back to GitHub with a commit message
Conflict detection: The editor tracks each file's Git blob SHA. If someone else modifies the file between your load and save, GitHub returns a conflict and the editor prompts you to reload before saving.
Binary files (images, PDFs, archives, fonts, media) are shown in the tree but are not editable.
PAT requirements: Your GitHub Personal Access Token must have write access to commit changes. For fine-grained tokens, set the "Contents" permission to "Read and write". For classic tokens, ensure the "repo" scope is enabled. Read-only tokens can still browse and view files but cannot save.
| Capability | Description | Default roles |
|---|---|---|
local/githubsync:configure |
Configure GitHub Sync settings for a course | Manager, Editing Teacher |
local/githubsync:sync |
Trigger a sync from GitHub | Manager, Editing Teacher |
| Table | Purpose |
|---|---|
local_githubsync_config |
Per-course configuration (repo URL, encrypted PAT, branch, last sync SHA) |
local_githubsync_mapping |
Maps repo file paths to Moodle course module IDs and content hashes |
local_githubsync_log |
Sync operation history with status, commit SHA, and detailed JSON logs |
- Webhook authentication: The webhook endpoint requires HMAC-SHA256 signature verification. Configure a shared secret in both Moodle admin settings and your GitHub webhook. Requests without a valid
X-Hub-Signature-256header are rejected. - PAT encryption: Personal Access Tokens are encrypted at rest using Moodle's Sodium-based encryption API (
\core\encryption). Encryption keys must be configured — there is no insecure fallback. Legacy base64-encoded tokens from older versions are automatically migrated to Sodium on first access. - HTML sanitization: All HTML content from GitHub repositories is passed through
purify_html()(HTMLPurifier) before storage, stripping<script>tags, event handlers, and other XSS vectors. - Asset type allowlist: Only safe file types (CSS, JS, images, fonts, etc.) are synced from the
assets/directory. SVG, PHP, and other potentially dangerous file types are blocked. - Input validation: Repository URLs are validated against a strict anchored regex, branch names are restricted to safe characters, and PATs are validated against GitHub's known token formats.
- Capability checks: All actions require appropriate capabilities in the course context.
- Session key validation: The sync trigger page requires a valid sesskey (CSRF protection).
- No information leakage: Error messages shown to users are generic; detailed error information is stored in internal logs only. The webhook endpoint returns generic responses regardless of outcome.
- No guest access: Asset files served via
pluginfile.phprequire authenticated course enrollment — no guest auto-login. - No git required: Uses the GitHub REST API only — no git binary needed on the server.
For full security documentation, see SECURITY.md.
GitHub allows 5,000 API requests per hour with a PAT. A typical sync uses:
- 1 request for the commit SHA
- 1 request for the tree
- 1 request per file fetched
A course with 50 HTML files and 10 assets would use ~62 requests per full sync. Incremental syncs use fewer requests since unchanged files are skipped via content hashing.
The plugin tracks rate limit headers and provides a clear error message if the limit is exceeded.
Every push and pull request to main runs two automated checks:
Static application security testing (SAST) using Semgrep with 8 community rulesets plus custom Moodle-specific rules.
Community rulesets:
| Ruleset | Coverage |
|---|---|
p/owasp-top-ten |
OWASP Top 10 2021 |
p/php |
PHP-specific security patterns |
p/security-audit |
General security audit |
p/command-injection |
OS command injection |
p/sql-injection |
SQL injection |
p/xss |
Cross-site scripting |
p/secrets |
Hardcoded secrets and credentials |
p/insecure-transport |
HTTP/TLS issues |
Custom Moodle rules (.semgrep.yml):
Standard Semgrep rules don't understand Moodle's framework patterns. The custom ruleset catches:
- Unsanitized HTML stored in
->content,->intro, or->summaryfields withoutpurify_html() - Variables echoed without
s(),format_text(), orhtml_writer - Direct
$_POSTaccess bypassing Moodle'srequired_param()/optional_param() - Direct
$USER = get_admin()instead of proper session management - Base64 encoding used as a substitute for real encryption
- Stack traces stored in the database (information leakage)
require_course_login()with guest auto-login enabled
PHPStan at level 6 — covers type safety, return types, undefined variables, dead code, and argument type validation.
The CI job checks out Moodle 4.5 stable, places the plugin inside it, and runs PHPStan with a bootstrap that loads Moodle's core classes for full type information.
Running locally:
composer install
vendor/bin/phpstan analyse --memory-limit=512MPHP CodeSniffer with the moodlehq/moodle-cs ruleset enforces Moodle's coding standards — file docblocks, naming conventions, inline comment formatting, and PHPDoc annotations.
Running locally:
composer install
vendor/bin/phpcs --standard=moodle --extensions=php --ignore=vendor/,phpstan-bootstrap.php .- Branch protection is enabled on
main— all changes must go through a pull request with passing CI checks. - Use the PR template and ensure all three checks pass (Semgrep, PHPStan, Moodle CS).
- Dependabot keeps composer and GitHub Actions dependencies up to date automatically.
- See CHANGELOG.md for release history.
local/githubsync/
version.php # Plugin version and requirements
lib.php # Navigation hooks and pluginfile handler
config.php # Per-course configuration page
sync.php # Sync trigger page
editor.php # File editor entry page
webhook.php # GitHub webhook endpoint
settings.php # Global admin settings
LICENSE # GNU GPL v3
SECURITY.md # Security architecture and audit history
CHANGELOG.md # Release history
.editorconfig # Editor formatting rules
.semgrep.yml # Custom Moodle security rules for Semgrep
phpstan.neon # PHPStan configuration
db/
install.xml # Database schema
access.php # Capability definitions
services.php # External function (AJAX) registration
tasks.php # Scheduled task registration
lang/en/
local_githubsync.php # Language strings
templates/
editor.mustache # File editor UI template
amd/src/
editor.js # File editor frontend logic
repository.js # AJAX service calls
classes/
form/
config_form.php # Per-course config form (moodleform)
github/
client.php # GitHub REST API client
external/
get_file_tree.php # AJAX: list repo files
get_file_content.php # AJAX: fetch file content + SHA
update_file.php # AJAX: commit file changes to GitHub
sync/
engine.php # Core sync orchestrator
course_builder.php # Creates/updates Moodle course structure
asset_handler.php # Asset upload and URL rewriting
task/
sync_courses.php # Scheduled task for auto-sync
cli/
sync_all.php # CLI script for bulk sync
.github/
dependabot.yml # Automated dependency updates
pull_request_template.md # PR template
ISSUE_TEMPLATE/
bug_report.yml # Bug report form
feature_request.yml # Feature request form
workflows/
semgrep.yml # Semgrep OWASP security scan
phpstan.yml # PHPStan static analysis
moodle-cs.yml # Moodle coding standards
This plugin is licensed under the GNU GPL v3, consistent with Moodle's license.