CI/CD
This page contains dev notes about CI/CD. Most important things about CI/CD are already explained in Development process documentation.
Build & automated tests​
Every pipeline builds the plugin and runs its automated tests inside the existing build job — a test failure fails the pipeline.
What runs​
- Lasers-Enigma (Gradle): the
build-branch-or-main/build-tagjobs (ci/build.gitlab-ci.yml) run./gradlew clean build. Gradle'sbuildlifecycle includes thetesttask, so the suite runs automatically; a failure fails the job. No dedicated test job is needed. - LE Play Server Utils (Maven): the
buildjob runsmvn clean verify. Surefire executes the suite during theverifyphase; a failure fails the job.
What the tests are​
The suites are ArchUnit architecture rules only (introduced in issue #732). They freeze each plugin's layered/feature architecture and conventions — layer dependency direction, the persistence boundary (JDBC/JPA confined to repositories), DI/singleton conventions, logging discipline, events/DTOs as payloads, … There are no behavioural unit tests: gameplay behaviour is still verified by compile + Javadoc + in-game testing. The exact rule sets live in core/src/test (Lasers-Enigma) and src/test (LEPSU), and are summarised in each repo's AGENTS.md.
Test reports in GitLab​
Both build jobs publish JUnit XML through artifacts:reports:junit (core/build/test-results/test/TEST-*.xml for Lasers-Enigma, target/surefire-reports/TEST-*.xml for LEPSU), so results show up in the pipeline's Tests tab and the merge-request test widget. Artifacts use when: always so the reports survive a failing run.
Discord webhook notifications​
Two CI jobs post announcements to Discord webhooks via the shared .publish-discord template defined in ci/publish.gitlab-ci.yml. The template handles the common bits (apt-get install jq git, the 1800-char truncation loop, the jq injection into the embed description, the curl POST). Each concrete job sets its own WEBHOOK_BODY_TEMPLATE (payload JSON path) and WEBHOOK_URL (CI variable holding the webhook URL).
| Job | Trigger | Payload template | Webhook URL variable | Target channel |
|---|---|---|---|---|
publish-discord | Release tag push | ci/discord_webhook_body.json | DISCORD_WEBHOOK_URL | Public release-announcement channel |
publish-discord-beta | Push to main (excluding maintenance pipelines) | ci/discord_webhook_body_beta.json | DISCORD_BETA_WEBHOOK_URL | #beta-test, pings the beta-tester role |
The publish-discord-beta job needs: update-beta-server, so the notification fires only after the beta server has actually received the new jar.
Changelog diff source​
Both jobs inject the same changelog-diff.txt file into the embed's description. How the file is produced differs:
- On tag pipelines:
check-changelog-and-version-diff-between-tags(stagecheck) produces it as an artifact, the tag job consumes it vianeeds: artifacts: true. - On main pipelines: the beta job runs
ci/extract-changelog.shitself in itsbefore_script, passing the latest release tag andCI_COMMIT_SHA. The script extracts pure-addition hunks fromCHANGELOG.mdand validates the result has > 5 characters.
The 1800-char truncation step (in the shared template) caps the diff so it fits inside Discord's embed-description limit.
CI variables​
| Variable | Description |
|---|---|
DISCORD_WEBHOOK_URL | Webhook URL for the public release channel. Used by publish-discord (tag job). |
DISCORD_BETA_WEBHOOK_URL | Webhook URL for the #beta-test channel. Used by publish-discord-beta (main job). |
Both should be configured as masked + protected in Settings → CI/CD → Variables. The Discord role IDs (release-announcement role, beta-tester role) are inlined in the JSON payload files — no variable needed.
SFTP upload​
Some CI/CD jobs uploads files to a SFTP server.
The following files are concerned:
web/download.php: download latest jar from artifactory (because artifactory does not expose a permanent link to the latest snapshot/release).core/src/main/resources/lasers-enigma-commit-hash.txt: latest beta commit hash. Used to notify admins to update lasers-enigma (if the configuration allows beta notification).core/src/main/resources/lasers-enigma-version.txt: latest release version. Used to notify admins to update lasers-enigma (if the configuration allows release notification).
Specific SFTP related user groups​
www-datais the user that runs nginx.plugin-updateis the user that uploads file during the CI/CD execution.www-datais in the group plugin-update. This allows nginx to read and execute files uploaded by plugin-update.
Specific directory structure​
/var/www/lasers-enigma.eu/is the web site directory. it is owned bywww-data:rootto allow nginx to manage its content. nginx can read this directory becausewww-datauser is in theplugin-updategroup./home/plugin-update/is the home directory of the user that uploads file from the CI/CD.
The user is restricted to this folder. It is owned by
root:rootas any user's start directory should be for the sFTP server to allow connection. This is the reason why the folder/var/www/lasers-enigma.eu/plugin-update/is not the home directory of theplugin-updateuser. This limitation would not allow/var/www/lasers-enigma.eu/plugin-update/to be the readable by nginx (user home directory must be owned byrootfor sftp to work and restrict the user to his home directory. Meanwhile, nginx useswww-datawho has no right over directories owned byroot).
Bind mounts​
To allow plugin-update user to upload in /var/www/lasers-enigma.eu/<subfolder>/, we use bind mounts.
Existing bind mounts​
plugin-update​
/var/www/lasers-enigma.eu/plugin-update is a hardlink to /home/plugin-update/web
schematics​
/var/www/lasers-enigma.eu/schematics is a hardlink to /home/plugin-update/schematics
docs​
/var/www/lasers-enigma.eu/docs is a hardlink to /home/plugin-update/docs
Commands reminder​
## See mounts
findmnt
## Create a bind mount
mount --bind /var/www/lasersenigma.eu/docs /home/plugin-update/docs
## Remove a bind mount
umount /var/www/lasersenigma.eu/docs
To persist the mount, edit /etc/fstab:
/var/www/lasersenigma.eu/plugin-update /home/plugin-update/web none bind,nofail,x-systemd.device-timeout=2 0 0
/var/www/lasersenigma.eu/schematics /home/plugin-update/schematics none bind,nofail,x-systemd.device-timeout=2 0 0
/var/www/lasersenigma.eu/docs /home/plugin-update/docs non bind,nofail,x-systemd.device-timeout=2 0 0
Symlinks​
-
/var/www/lasers-enigma.eu/lasers-enigma-commit-hash.txtis a symlink to/var/www/lasers-enigma.eu/plugin-update/lasers-enigma-commit-hash.txt. This is for retro-compatibily purposes. Older versions of lasers-enigma looked for this file at the urlhttps://lasers-enigma.eu/lasers-enigma-commit-hash.txtinstead ofhttps://lasers-enigma.eu/plugin-update/lasers-enigma-commit-hash.txt -
/var/www/lasers-enigma.eu/lasers-enigma-version.txtis a symlink to/var/www/lasers-enigma.eu/plugin-update/lasers-enigma-version.txt. This is for retro-compatibily purposes. Older versions of lasers-enigma looked for this file at the urlhttps://lasers-enigma.eu/lasers-enigma-version.txtinstead ofhttps://lasers-enigma.eu/plugin-update/lasers-enigma-version.txt. -
/var/www/lasers-enigma.eu/download.phpis a symlink to/var/www/lasers-enigma.eu/plugin-update/download.php. This is for retro-compatibily purposes since we may have put this link in different places.
Server pack generation​
The generate-server-pack CI job (ci/server-pack.gitlab-ci.yml) generates a downloadable server pack archive from the play server, then redeploys a mirror of play onto the beta test server.
Trigger​
- Manual job: can be triggered manually from the pipeline on the
mainbranch. - Manual pipeline: by setting the CI variable
GENERATE_SERVER_PACK=true— either from a "Run pipeline" web pipeline or via the API (CI_PIPELINE_SOURCE == "api", e.g. the GitLab MCP or therun-maintenance-pipelineskill). Runs the whole job (generation and beta deployment) on demand, without a new commit. - Scheduled: runs automatically on scheduled pipelines (weekly). The schedule's target branch should be
main.
On a scheduled pipeline and on a
GENERATE_SERVER_PACK=truerun (web or API), the rest of themainpipeline (build, artifactory publish, www upload, beta deploy) is deliberately skipped — onlygenerate-server-packruns.
Pipeline steps​
- Writes Pterodactyl credentials into
$GRADLE_USER_HOME/gradle.properties(not the project root file, to avoid committing secrets). - Runs
./gradlew generateServerPack, which:- Downloads the server archive from Pterodactyl
- Extracts, cleans up, and sanitizes the
le-playserver (removes schematics, logs, sensitive config) - Creates a
.tar.gzarchive inbuild/server-pack/
- Lists existing server-pack archives on the web server via SFTP.
- Deletes old archives and uploads the new one via SFTP (batch operation).
- Deploys a sanitized mirror of play onto the beta test server (see Beta server mirror below).
Beta server mirror​
After publishing the public archive, the same job redeploys play onto the beta test server so it stays a realistic mirror of production, while keeping the beta server's own in-development plugin builds.
Steps:
- Removes the public archive locally (frees disk), applies the beta-only in-place edits to
run/le-play/(flipsproxy-protocoltotruein the Paper config, prefixes theserver.propertiesMOTD with a red[BETA]tag so the beta server stands out in the multiplayer list, and restores the documentation agent address + token thatsanitizeServerblanked for the public pack — captured from the play config before sanitization, so/puzzle dockeeps working on beta), then rebuilds a root-level archive (nole-play/prefix) from the sanitizedrun/le-play/, excluding the two in-house plugin jars by their version-less deploy names (plugins/Lasers-Enigma.jar,plugins/le-play-server-utils.jar— the names they carry in play's backup, sincebuildAndDeployBranches/updateLepsuskip thele-playmirror), with the versioned*-*.jarglobs kept as a defensive net. These two names are defined once as shell variables (KEEP_JAR_LE/KEEP_JAR_LEPSU) and reused by both the tar exclude here and the/pluginswipe filter in step 3, so the two can never drift apart. - Stops the beta server (Pterodactyl
power→stop) and waits for theofflinestate. - Wipes the server, keeping only the two in-house plugin jars deployed by
update-beta-server(/plugins/Lasers-Enigma.jar,/plugins/le-play-server-utils.jar). The play config foldersplugins/LasersEnigma/andplugins/LEPlayServerUtils/are removed and repopulated from play. - Uploads the archive via SFTP and decompresses it server-side (Pterodactyl
files/decompress, which merges into the existing/plugins, so the kept jars survive). - Restarts the beta server.
The beta server therefore runs the in-development builds of the two in-house plugins (deployed on every push to
mainby theupdate-beta-serverjob) on top of play's world, config and other plugins. Both play and beta deploy these plugins under the same fixed names (Lasers-Enigma.jar/le-play-server-utils.jar), so excluding them from the archive is what keeps the wipe-kept beta builds from being overwritten by play's (potentially older) versions when the archive is decompressed.
The beta steps use the Pterodactyl client API (https://<panel>/api/client/servers/<id>/...) with the header Accept: Application/vnd.pterodactyl.v1+json.
CI variables​
The following CI/CD variables must be configured in GitLab (Settings → CI/CD → Variables):
| Variable | Description |
|---|---|
PTERODACTYL_PANEL_URL | Pterodactyl panel base URL (e.g. https://panel.skytale.fr/) |
PTERODACTYL_API_KEY | Pterodactyl client API key (ptlc_...) |
PTERODACTYL_SERVER_ID | Pterodactyl server short identifier |
WEB_SSH_PRIVATE_KEY | Base64-encoded SSH private key for SFTP upload (shared with other deploy jobs) |
WEB_SSH_HOST | Web server hostname (shared with other deploy jobs) |
WEB_SSH_PORT | Web server SSH port (shared with other deploy jobs) |
WEB_SSH_LOGIN | Web server SSH/SFTP username (shared with other deploy jobs) |
GENERATE_SERVER_PACK | Set to true on a manual pipeline (web or API) to run the job (generation + beta deployment) on demand. |
BETA_MC_SERVER_SSH_HOST / _SSH_PORT / _SSH_LOGIN / _SSH_PASSWORD | Beta server SFTP target (password auth) for uploading the mirror archive. |
BETA_MC_SERVER_PTERO_HOST / _PTERO_API_KEY / _PTERO_SERVER_ID | Beta server Pterodactyl client API (stop / wipe / decompress / restart). |
Download endpoint​
The server pack archive is downloadable via download.php?type=server-pack, which uses glob() to find the archive on the web server and redirects (HTTP 302) to it. See Server pack installation page for user-facing documentation.
Documentation site deployment​
The build-and-deploy-docs CI job (ci/docs.gitlab-ci.yml) builds the Docusaurus documentation site from the wiki repo and deploys it to https://lasers-enigma.eu/docs/.
Trigger​
- Release tag: runs automatically on every release tag.
- Manual: can be triggered with the CI variable
DEPLOY_DOCS=trueon any branch — either from a "Run pipeline" web pipeline or via the API (CI_PIPELINE_SOURCE == "api", e.g. the GitLab MCP or therun-maintenance-pipelineskill). Useful for republishing the docs between releases (wiki content updates, Docusaurus config tweaks).
On a
DEPLOY_DOCS=truerun onmain(web or API), the rest of themainpipeline is deliberately skipped — onlybuild-and-deploy-docsruns.
Pipeline steps​
- Clones the
lasersenigma.wikirepository at its current HEAD intobuild/docs/_wiki-src/(independent of any localwiki/clone). - Runs
npm run buildinwww/docs/which:- Executes
prepare-docs.mjsto normalize wiki markdown for Docusaurus consumption. - Builds the static site under
build/docs/site/.
- Executes
- Deploys the site to the web server's
docs/directory withrclone syncover SFTP (delta sync): only files changed since the previous deploy are uploaded, and orphaned remote files are removed with--delete-after— after the transfer completes, never during — so the live site is never in a partially-wiped state. Thedocs/directory itself is hardlinked to/var/www/lasers-enigma.eu/docs/on the server, so only its contents are synced; the directory stays in place.
CI variables​
| Variable | Description |
|---|---|
DEPLOY_DOCS | Set to true on a manual pipeline (web or API) to trigger a docs-only republish. Not needed on release-tag pipelines. |
WEB_SSH_PRIVATE_KEY | Base64-encoded SSH private key for SFTP upload (shared with other deploy jobs). |
WEB_SSH_HOST | Web server hostname (shared with other deploy jobs). |
WEB_SSH_PORT | Web server SSH port (shared with other deploy jobs). |
WEB_SSH_LOGIN | Web server SSH/SFTP username (shared with other deploy jobs). |
Wiki uploads compression​
The compress-wiki-uploads CI job (ci/wiki-compress.gitlab-ci.yml) compresses the assets in the lasersenigma.wiki repository's uploads/ directory and pushes the result back to the wiki repo. Targets heavy GIF and MKV files (converted to MP4 H.264) and PNGs (lossless oxipng pass).
Trigger​
- Manual only: triggered exclusively from a "Run pipeline" web pipeline by setting the CI variable
COMPRESS_WIKI=true. Never runs automatically — keeps the cost zero outside explicit maintainer runs.
On a
COMPRESS_WIKI=trueweb run onmain, the rest of themainpipeline is deliberately skipped — onlycompress-wiki-uploadsruns.
Pipeline steps​
- Installs
ffmpegandoxipngvia apt. - Clones the wiki repository using a write-capable token.
- Runs
www/docs/scripts/compress-wiki-uploads.mjsagainst the wiki clone, which:- Converts each
.gifto.mp4(silent H.264 yuv420p with faststart). - Converts the
.mkvfiles to.mp4(H.264 + AAC). - Recompresses PNGs losslessly with oxipng.
- Rewrites
.gifand.mkvreferences inside wiki markdown files to.mp4.
- Converts each
- Commits and pushes the changes back to the wiki repo's
masterbranch as a "Compress uploads (CI auto-run)" commit. Skips the commit if the run found nothing to compress.
CI variables​
| Variable | Description |
|---|---|
COMPRESS_WIKI | Set to true on a manual web pipeline to trigger compression. The job is gated on this variable — without it, nothing runs. |
WIKI_PUSH_TOKEN | Personal Access Token (free-tier compatible) with write_repository scope on the wiki repository. Used as the OAuth2 token to push the compression commit back to the wiki. Mark masked + protected. |