Skip to main content

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-tag jobs (ci/build.gitlab-ci.yml) run ./gradlew clean build. Gradle's build lifecycle includes the test task, so the suite runs automatically; a failure fails the job. No dedicated test job is needed.
  • LE Play Server Utils (Maven): the build job runs mvn clean verify. Surefire executes the suite during the verify phase; 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).

JobTriggerPayload templateWebhook URL variableTarget channel
publish-discordRelease tag pushci/discord_webhook_body.jsonDISCORD_WEBHOOK_URLPublic release-announcement channel
publish-discord-betaPush to main (excluding maintenance pipelines)ci/discord_webhook_body_beta.jsonDISCORD_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 (stage check) produces it as an artifact, the tag job consumes it via needs: artifacts: true.
  • On main pipelines: the beta job runs ci/extract-changelog.sh itself in its before_script, passing the latest release tag and CI_COMMIT_SHA. The script extracts pure-addition hunks from CHANGELOG.md and 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​

VariableDescription
DISCORD_WEBHOOK_URLWebhook URL for the public release channel. Used by publish-discord (tag job).
DISCORD_BETA_WEBHOOK_URLWebhook 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).
  • www-data is the user that runs nginx.
  • plugin-update is the user that uploads file during the CI/CD execution.
  • www-data is 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 by www-data:root to allow nginx to manage its content. nginx can read this directory because www-data user is in the plugin-update group.
  • /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:root as 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 the plugin-update user. This limitation would not allow /var/www/lasers-enigma.eu/plugin-update/ to be the readable by nginx (user home directory must be owned by root for sftp to work and restrict the user to his home directory. Meanwhile, nginx uses www-data who has no right over directories owned by root).

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
  • /var/www/lasers-enigma.eu/lasers-enigma-commit-hash.txt is 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 url https://lasers-enigma.eu/lasers-enigma-commit-hash.txt instead of https://lasers-enigma.eu/plugin-update/lasers-enigma-commit-hash.txt

  • /var/www/lasers-enigma.eu/lasers-enigma-version.txt is 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 url https://lasers-enigma.eu/lasers-enigma-version.txt instead of https://lasers-enigma.eu/plugin-update/lasers-enigma-version.txt.

  • /var/www/lasers-enigma.eu/download.php is 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 main branch.
  • 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 the run-maintenance-pipeline skill). 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=true run (web or API), the rest of the main pipeline (build, artifactory publish, www upload, beta deploy) is deliberately skipped — only generate-server-pack runs.

Pipeline steps​

  1. Writes Pterodactyl credentials into $GRADLE_USER_HOME/gradle.properties (not the project root file, to avoid committing secrets).
  2. Runs ./gradlew generateServerPack, which:
    • Downloads the server archive from Pterodactyl
    • Extracts, cleans up, and sanitizes the le-play server (removes schematics, logs, sensitive config)
    • Creates a .tar.gz archive in build/server-pack/
  3. Lists existing server-pack archives on the web server via SFTP.
  4. Deletes old archives and uploads the new one via SFTP (batch operation).
  5. 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:

  1. Removes the public archive locally (frees disk), applies the beta-only in-place edits to run/le-play/ (flips proxy-protocol to true in the Paper config, prefixes the server.properties MOTD with a red [BETA] tag so the beta server stands out in the multiplayer list, and restores the documentation agent address + token that sanitizeServer blanked for the public pack — captured from the play config before sanitization, so /puzzle doc keeps working on beta), then rebuilds a root-level archive (no le-play/ prefix) from the sanitized run/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, since buildAndDeployBranches/updateLepsu skip the le-play mirror), with the versioned *-*.jar globs 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 /plugins wipe filter in step 3, so the two can never drift apart.
  2. Stops the beta server (Pterodactyl power → stop) and waits for the offline state.
  3. 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 folders plugins/LasersEnigma/ and plugins/LEPlayServerUtils/ are removed and repopulated from play.
  4. Uploads the archive via SFTP and decompresses it server-side (Pterodactyl files/decompress, which merges into the existing /plugins, so the kept jars survive).
  5. Restarts the beta server.

The beta server therefore runs the in-development builds of the two in-house plugins (deployed on every push to main by the update-beta-server job) 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):

VariableDescription
PTERODACTYL_PANEL_URLPterodactyl panel base URL (e.g. https://panel.skytale.fr/)
PTERODACTYL_API_KEYPterodactyl client API key (ptlc_...)
PTERODACTYL_SERVER_IDPterodactyl server short identifier
WEB_SSH_PRIVATE_KEYBase64-encoded SSH private key for SFTP upload (shared with other deploy jobs)
WEB_SSH_HOSTWeb server hostname (shared with other deploy jobs)
WEB_SSH_PORTWeb server SSH port (shared with other deploy jobs)
WEB_SSH_LOGINWeb server SSH/SFTP username (shared with other deploy jobs)
GENERATE_SERVER_PACKSet 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_PASSWORDBeta server SFTP target (password auth) for uploading the mirror archive.
BETA_MC_SERVER_PTERO_HOST / _PTERO_API_KEY / _PTERO_SERVER_IDBeta 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=true on any branch — either from a "Run pipeline" web pipeline or via the API (CI_PIPELINE_SOURCE == "api", e.g. the GitLab MCP or the run-maintenance-pipeline skill). Useful for republishing the docs between releases (wiki content updates, Docusaurus config tweaks).

On a DEPLOY_DOCS=true run on main (web or API), the rest of the main pipeline is deliberately skipped — only build-and-deploy-docs runs.

Pipeline steps​

  1. Clones the lasersenigma.wiki repository at its current HEAD into build/docs/_wiki-src/ (independent of any local wiki/ clone).
  2. Runs npm run build in www/docs/ which:
    • Executes prepare-docs.mjs to normalize wiki markdown for Docusaurus consumption.
    • Builds the static site under build/docs/site/.
  3. Deploys the site to the web server's docs/ directory with rclone sync over 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. The docs/ 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​

VariableDescription
DEPLOY_DOCSSet to true on a manual pipeline (web or API) to trigger a docs-only republish. Not needed on release-tag pipelines.
WEB_SSH_PRIVATE_KEYBase64-encoded SSH private key for SFTP upload (shared with other deploy jobs).
WEB_SSH_HOSTWeb server hostname (shared with other deploy jobs).
WEB_SSH_PORTWeb server SSH port (shared with other deploy jobs).
WEB_SSH_LOGINWeb 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=true web run on main, the rest of the main pipeline is deliberately skipped — only compress-wiki-uploads runs.

Pipeline steps​

  1. Installs ffmpeg and oxipng via apt.
  2. Clones the wiki repository using a write-capable token.
  3. Runs www/docs/scripts/compress-wiki-uploads.mjs against the wiki clone, which:
    • Converts each .gif to .mp4 (silent H.264 yuv420p with faststart).
    • Converts the .mkv files to .mp4 (H.264 + AAC).
    • Recompresses PNGs losslessly with oxipng.
    • Rewrites .gif and .mkv references inside wiki markdown files to .mp4.
  4. Commits and pushes the changes back to the wiki repo's master branch as a "Compress uploads (CI auto-run)" commit. Skips the commit if the run found nothing to compress.

CI variables​

VariableDescription
COMPRESS_WIKISet to true on a manual web pipeline to trigger compression. The job is gated on this variable — without it, nothing runs.
WIKI_PUSH_TOKENPersonal 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.