A reusable GitHub composite action for packaging and releasing Joomla extensions. This action automates the entire process of versioning, packaging, and releasing Joomla modules, plugins, and components.
there is a workflows version documented here: Packager Documentation
- Automatic Versioning: Date-based versioning (YYYY.MM.DD format) with support for multiple releases per day
- Manual Version Override: Specify custom versions (e.g., 1.0.0, 2.0.0-beta) for semantic versioning
- File Updates: Automatically updates version and copyright information across all files (can be disabled)
- Changelog Generation: Creates changelogs from commit messages following Keep a Changelog format
- Package Creation: Builds properly structured ZIP files for Joomla installation
- GitHub Releases: Creates releases with artifacts and release notes
- Joomla Updates: Updates the Joomla update server XML
- Multi-Extension Support: Works with modules, plugins, and components
- Extensible: Easy to extend and customize for specific needs
- Reference the action in your workflow using the
uses:field, pointing to the public repository and release/tag (replaceN6REJ/joomla-packager@v1with the correct owner/repo and version/tag):
name: Package Extension
on:
pull_request:
types: [closed]
branches: [main]
workflow_dispatch:
jobs:
package:
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.merged == true)
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_PAT }}
- uses: N6REJ/joomla-packager@v2025.6.24
with:
extension-name: 'mod_example'
extension-xml: 'mod_example.xml'
extension-type: 'module'
author: 'Your Name'
copyright-holder: 'Your Company'
copyright-start-year: '2024'
github-token: ${{ secrets.GH_PAT }}- Set up your GitHub PAT in repository secrets as
GH_PATor whatever you use forgithub-token:
| Input | Description |
|---|---|
extension-name |
Extension folder and file prefix (e.g., mod_example) |
extension-xml |
Main XML manifest file (e.g., mod_example.xml) |
extension-type |
Type: module, plugin, or component |
author |
Your name or handle |
copyright-holder |
Copyright holder name |
copyright-start-year |
Year copyright started |
github-token |
GitHub PAT with repo permissions |
| Input | Default | Description |
|---|---|---|
manual-version |
'' |
Explicit version, overrides every scheme |
version-scheme |
date |
date → 2025.10.02.3, or semver → bumps the patch of the manifest <version> when that version was already released |
timezone |
America/Chicago |
IANA zone used for the date-based version, the manifest <creationDate> and the changelog date. GitHub runners are UTC, which stamps an evening US build with tomorrow's date |
create-release |
true |
Create the GitHub release and upload the package |
update-joomla-server |
true |
Publish updates.xml after the release asset is verified (requires create-release) |
commit-changes |
false |
Commit and push the manifest, updates.xml and changelog back to the branch |
updates-xml-file |
updates.xml |
Update feed file name |
extension-client |
'' |
site or administrator for the feed entry. Empty detects it from the manifest client attribute, then falls back to administrator for components and site for everything else |
targetplatform-name |
joomla |
name of the <targetplatform> element written into the feed |
targetplatform-version |
6.* |
version regex of the <targetplatform> element. Applied only when the feed does not already declare one, so a feed pinned to another Joomla major is left alone |
changelog-file |
CHANGELOG.md |
Generated changelog file name |
license-file |
License.txt |
License file name |
file-updates |
true |
Rewrite version/date/copyright in manifest, PHP, INI and CSS files |
generate-changelog |
true |
Generate a changelog from the commits since the previous tag |
upload-artifact |
true |
Also upload the unpacked package as a workflow artifact |
readme |
false |
Include README.md in the package ZIP |
php-version |
8.1 |
PHP version used for the packaging steps |
dir-tree-file |
directory-structure.txt |
Directory listing attached to the release, empty to skip. Not included in the installable package |
Note:
helper-file,favicon-file,package-dir,css-dir,js-dir,tmpl-dirandlanguage-dirare accepted for backwards compatibility but are not currently used by the action.
The action is careful about when a version becomes public:
- The feed is published last. The copy of
updates.xmlthat gets committed is only rewritten after the release has been created, and only once the release asset has been confirmed to exist. A failed release therefore never leaves installed sites being offered a download that 404s. - The packaged feed matches the manifest. An extension ships its own copy of
updates.xml, but the package is assembled before the release exists, so that copy would otherwise still advertise the previous version and the previous download URL. It is synced to the build version up front, so the two never disagree. This does not weaken the guarantee above: installed sites read the feed fromupdate_serverin the manifest rather than from the bundled file, and the post-release pass finds nothing left to change. - Feed entries are matchable. Joomla matches an update against an installed extension by element, type, client and target platform, and silently ignores any entry that is missing
<targetplatform>, carries an empty<type />, or names the wrong<client>. The action fills those in — detecting the type and client from the manifest, and defaulting the platform totargetplatform-name/targetplatform-version— but never overwrites a value the extension declared for itself, so a feed deliberately pinned to another Joomla major is left alone. - Re-runs do not mint empty versions. Before choosing a version, the action fingerprints the files that actually ship and compares that against the most recent release. If nothing has changed, that version is reused and the release steps are skipped — a re-run, a manual dispatch or a retried job cannot push an identical package onto every installed site as an "update available". The fingerprint mirrors the packaging step's exclusions, so it covers exactly what a user installs: it ignores what the action itself regenerates (
CHANGELOG.md,updates.xml), the manifest's own<version>,<creationDate>and<copyright>,@version/@copyrightheaders in PHP, CSS and language files, and everything the package excludes such as.github/,.gitignore,build/andREADME.md. Editing a CI workflow or re-pinning this action therefore does not bump your users' version, while a real source change — including a new field in the manifest — still does. - Nothing is left behind. The action updates files in the workspace only. Set
commit-changes: 'true'to have it commit and push the version bump, the feed and the changelog back to your branch — otherwise your workflow must do that itself, and a diff check that ignores untracked files will silently drop the feed update. When a version is reused, the changelog is not regenerated and nothing is committed, so the branch stays untouched. - Only release metadata is generated.
dir-tree-fileis written next to the build directory and attached to the release, not inside the installable package, so it never ends up on the end user's server. - Dates follow your day, not the runner's. The version, the manifest
<creationDate>and the changelog date are all computed intimezone(defaultAmerica/Chicago). Set it toUTCor your own zone as needed. An unrecognised zone fails the run rather than silently falling back to UTC.
The github-token input (or the default GITHUB_TOKEN) is used for creating GitHub Releases, uploading artifacts, updating files, and optionally interacting with pull requests, issues, and GitHub Packages. For the action to perform all its features, the token must have the following permissions:
| Permission | Why Needed |
|---|---|
| contents: write | Create releases, upload release assets, update files in the repository |
| pull-requests: write | Update or comment on pull requests (e.g., for changelog or status) |
| actions: write | Trigger or manage other workflows, upload artifacts |
| packages: write | Publish to GitHub Packages (optional) |
| issues: write | Create or comment on issues (optional, e.g., for release notes) |
- Minimum required:
contents: write(for releases, assets, and file updates) - Recommended for full functionality: Add
pull-requests: write,actions: write,packages: write, andissues: writeas needed for your workflow. - The default
GITHUB_TOKENprovided by GitHub Actions usually hascontents: writeandpull-requests: writeby default, but you may need to explicitly set these in your workflow’spermissionsblock for full access. - If using a Personal Access Token (PAT), it must have the
reposcope for private repositories (includes all the above), or at leastpublic_repofor public repositories. Addworkflowandwrite:packagesif you need to trigger workflows or publish packages.
Example permissions block for your workflow:
permissions:
contents: write
pull-requests: write
actions: write
packages: write
issues: writeIf you encounter permission errors, check your workflow's permissions block and your token's scopes.
The action categorizes commits based on their prefix:
- Added:
Add,Create,Implement,Feature - Changed:
Update,Improve,Enhance,Refactor,Change - Fixed:
Fix,Bug,Correct,Resolve - Removed:
Remove,Delete,Deprecate - Security:
Security
- uses: N6REJ/joomla-packager@v1
with:
extension-name: 'mod_hello_world'
extension-xml: 'mod_hello_world.xml'
extension-type: 'module'
author: 'John Doe'
copyright-holder: 'Acme Corp'
copyright-start-year: '2024'
github-token: ${{ secrets.GH_PAT }}- uses: N6REJ/joomla-packager@v1
with:
extension-name: 'plg_system_cache'
extension-xml: 'plg_system_cache.xml'
extension-type: 'plugin'
author: 'Jane Smith'
copyright-holder: 'Tech Solutions'
copyright-start-year: '2023'
github-token: ${{ secrets.GH_PAT }}
css-dir: 'assets/css'
js-dir: 'assets/js'
package-dir: 'dist'- uses: N6REJ/joomla-packager@v1
with:
extension-name: 'com_myapp'
extension-xml: 'com_myapp.xml'
extension-type: 'component'
author: 'Dev Team'
copyright-holder: 'My Company'
copyright-start-year: '2022'
github-token: ${{ secrets.GH_PAT }}
php-version: '8.2'
generate-changelog: 'true'
create-release: 'true'
update-joomla-server: 'true'This example shows how to package a complete Joomla package containing a component, module, and multiple plugins (based on N6REJ/bears_aichatbot):
- uses: N6REJ/joomla-packager@v1
with:
extension-name: 'pkg_bears_aichatbot'
extension-xml: 'pkg_bears_aichatbot.xml'
extension-type: 'package'
author: 'N6REJ'
copyright-holder: 'N6REJ'
copyright-start-year: '2024'
github-token: ${{ secrets.GH_PAT }}
file-updates: 'true' # Updates version in all included extensionsWhen packaging multiple extensions, the action will:
- Update the package XML manifest
- Update all referenced component, module, and plugin XML files
- Update version and copyright in all PHP, CSS, and language files
- Maintain consistent versioning across all included extensions
- uses: N6REJ/joomla-packager@v1
with:
extension-name: 'mod_example'
extension-xml: 'mod_example.xml'
extension-type: 'module'
author: 'Your Name'
copyright-holder: 'Your Company'
copyright-start-year: '2024'
github-token: ${{ secrets.GH_PAT }}
manual-version: '2.0.0' # Specify your own version- uses: N6REJ/joomla-packager@main
with:
extension-name: 'mod_example'
extension-xml: 'mod_example.xml'
extension-type: 'module'
author: 'Your Name'
copyright-holder: 'Your Company'
copyright-start-year: '2024'
github-token: ${{ secrets.GH_PAT }}
version-scheme: 'semver' # uses the manifest <version>, bumping the patch if already released
commit-changes: 'true' # push the version bump and update feed back to mainYou can extend this action in several ways:
- Fork and Modify: Customize the action for your specific needs
- Wrapper Workflows: Add pre/post processing steps
- Use Outputs: Access version, package path, and release URL in subsequent steps
- Custom Deployment: Add deployment steps after packaging
Example with custom deployment:
- name: Package Extension
id: package
uses: N6REJ/joomla-packager@v1
with:
# ... your inputs ...
- name: Deploy to Production
run: |
echo "Deploying version ${{ steps.packager.outputs.version }}"
# Your deployment script hereContributions are welcome! Feel free to:
- Report bugs
- Suggest new features
- Submit pull requests
- Share your use cases
| Workflow | What it covers |
|---|---|
test-component.yml, test-package.yml, test-plugin.yml |
End-to-end packaging against a real release. Smoke tests: they prove the action completes, not that it produced correct output. |
test-module.yml |
Packaging plus assertions that the manifest, the working-tree feed and the copy inside the package all agree. test-module is the only test extension that ships an updates.xml, so this is the only end-to-end feed coverage. |
test-feed-logic.yml |
Unit tests for the two steps that maintain the feed. |
The feed steps are gated on create-release: 'true', so the test-module workflow can never reach them: this repository is the packager, and the action packages the repository root, which would publish a copy of the whole packager as a release asset. test/test_feed_sync.py and test/test_feed_publish.py cover that logic instead, by extracting each step from action.yml and running it offline against temporary fixtures. They can be run directly:
python -m pip install pyyaml
python test/test_feed_sync.py
python test/test_feed_publish.pyBoth extract their step from action.yml at run time rather than copying it, so they cannot drift from the logic they test, and both fail loudly if a step is renamed or gains an expression the harness does not model. The sync step makes no network calls; the publish step reaches for gh and sleep, which are replaced with bash function stubs so the release-asset guard and its retry loop can be driven deterministically. test_feed_sync.py additionally asserts that the two steps' sed expressions remain byte-identical, because they are separate copies of the same rewrite — a divergence there is what allowed a packaged feed to advertise the previous version.
This project is open source and available under the GPL3+ License.
Based on the workflow from N6REJ/mod_bears_pricing_tables.
Example package implementation: N6REJ/bears_aichatbot - A complete Joomla package with component, module, and multiple plugins.
Made with ❤️ for the Joomla community
For more detailed documentation and usage examples, visit: https://www.hallhome.us/joomla-packager