# Roadmap — plugin acquisition overhaul Goal: replace runtime env-driven plugin downloads with a reproducible, source-of-truth-first acquisition pipeline. Make the server fully open source, fully auditable, fully reproducible. ## Problem (current state) Plugins are pulled at every container boot via `MODRINTH_PROJECTS` + `SPIGET_RESOURCES` env vars in `docker-compose.yml`. Pain points hit during 2026-04-27 deploy: - **Slug ≠ name** — `vault`, `protocollib` not on Modrinth at expected slug. Three boot loops to discover. - **Channel mismatch** — GrimAC alpha-only, WorldEdit beta-only on bleeding-edge MC versions. Default `release` filter silently rejected. Two more boot loops. - **Wrong env var** — `MODRINTH_DEFAULT_VERSION_TYPE` is for modpack flow; `MODRINTH_PROJECTS` flow needs `MODRINTH_PROJECTS_DEFAULT_VERSION_TYPE`. One more boot loop. - **VERSION=LATEST + Purpur** — Purpur 26.x versioning scheme confused itzg, sent "26.1.2" as MC version to Modrinth API; EssentialsX query returned no files. - **No lockfile** — `latest` drifts daily. No checksum, no audit trail. - **No pre-flight** — every typo is a 30s container restart cycle. - **License opacity** — no automated check that plugins are FOSS-compatible before adding. - **`REMOVE_OLD_MODS=*.jar`** wipes manually-placed jars on every boot, hostile to manual-only plugins (LoginSecurity, MarriageMaster, etc). ## Acquisition order — proposed 1. **GitHub Releases** (primary) 2. **Hangar** (PaperMC official) 3. **Modrinth** 4. **Spiget / SpigotMC** 5. **Manual jar** (last resort, premium/dead) ### Why GitHub first - Source-truth: jar built from tag, signed commit, reproducible. - License visible — repo `LICENSE` file. FOSS audit trivial. - Stable URL pattern: `github.com///releases/download//.jar`. - API: `api.github.com/repos///releases/latest` — JSON, version + asset URLs + checksums. - No platform lock-in (Modrinth/Hangar can delist; GH source survives). - Most Bukkit plugins ARE on GitHub — Modrinth/Hangar often just mirror. ## Design ### `plugins.yml` (manifest, committed) ```yaml plugins: - name: LuckPerms sources: - github: { owner: LuckPerms, repo: LuckPerms, asset_pattern: "LuckPerms-Bukkit-*.jar" } - modrinth: luckperms pin: latest # or "5.5.20" or sha256:abc... - name: ProtocolLib sources: - github: { owner: dmulloy2, repo: ProtocolLib, asset_pattern: "ProtocolLib.jar" } - spiget: 1997 - name: Vault sources: - github: { owner: MilkBowl, repo: Vault, asset_pattern: "Vault.jar" } - name: WorldEdit sources: - github: { owner: EngineHub, repo: WorldEdit, asset_pattern: "worldedit-bukkit-*.jar" } - hangar: { author: EngineHub, project: WorldEdit } - modrinth: worldedit channel: beta - name: LandClaimPlugin sources: - modrinth: landclaimplugin - name: LoginSecurity sources: - manual: ./manual-jars/LoginSecurity-3.3.1.jar license: GPL-3.0 upstream_url: https://www.spigotmc.org/resources/loginsecurity.19362/ ``` ### `plugins.lock` (generated, committed) ``` LuckPerms-Bukkit-5.5.20.jar sha256:abc... github:LuckPerms/LuckPerms@v5.5.20 ProtocolLib.jar sha256:def... github:dmulloy2/ProtocolLib@5.4.0 ... ``` ### `scripts/fetch-plugins.sh` (resolver) Runs **before** `docker compose up`. Pseudo: ```bash for plugin in plugins.yml; do for src in plugin.sources; do # fallback chain — first hit wins case src.type in github) asset=$(gh-api releases/latest); curl -L -o $asset ;; hangar) curl hangar.papermc.io/api/v1/projects/... ;; modrinth) curl api.modrinth.com/v2/project/$slug/version ;; spiget) curl api.spiget.org/v2/resources/$id/download ;; manual) cp $path ;; esac [ $? -eq 0 ] && break done sha256sum $jar >> plugins.lock done ``` Output: `plugins/*.jar` directory ready to bind-mount, plus `plugins.lock` for diff/audit. ### Compose changes ```yaml volumes: - /opt/docker/minecraft:/data environment: REMOVE_OLD_MODS: "false" # plugins/ pre-populated, don't wipe # delete: MODRINTH_PROJECTS, SPIGET_RESOURCES, MODRINTH_PROJECTS_DEFAULT_VERSION_TYPE ``` itzg image becomes pure runtime. Plugin acquisition is a separate, testable, reproducible build step. ## Phased rollout ### Phase 1 — pin everything (1 hour) Keep itzg env-driven. Replace `slug` with `slug:VERSION_ID` in `MODRINTH_PROJECTS`. Use `id:VERSION` in `SPIGET_RESOURCES`. No more `latest` drift. **Acceptance:** `docker compose up -d` produces identical plugin set on any host, any day. ### Phase 2 — fetch script + manifest (1 day) - Write `plugins.yml` w/ all current plugins + sources. - Write `scripts/fetch-plugins.sh` (bash, jq, curl, gh CLI). - Write `plugins.lock` first run, commit it. - Strip `MODRINTH_PROJECTS`/`SPIGET_RESOURCES` from compose; `REMOVE_OLD_MODS: false`. - Document new deploy: `./scripts/fetch-plugins.sh && docker compose up -d`. **Acceptance:** plugins/ populated from GH-first, lock committed, deploy reproducible. ### Phase 3 — CI automation (1 day) - GH Action daily: `fetch-plugins.sh --check-updates` → open PR per update, body has changelog link. - GH Action per-PR: license audit (`/repos/{owner}/{repo}/license` → SPDX id → `LICENSES.md`). - Renovate-style auto-merge for patch updates (config-gated). **Acceptance:** plugin updates land via PR, license audit in CI, no manual fetches. ## Tooling to evaluate first | Tool | Status | Verdict | |------|--------|---------| | `mcpkg` | exists, immature | reuse if active | | `packwiz` | mod-focused, Modrinth/Curse | adapt? | | `paper-plugin-manager` | Hangar client | use as Hangar source | | Custom bash + jq + gh + curl | trivial to build | likely fastest | Probably 200 lines of bash beats adopting an unmaintained tool. ## Side benefits - **License audit** — generated `LICENSES.md` proves the stack is FOSS. - **Pre-flight** — `fetch-plugins.sh --check` validates manifest in CI before merge, no boot-time surprise. - **Offline deploy** — pre-baked `plugins/` dir + tarball = air-gap deploy possible. - **Forks** — easy to swap `LandClaimPlugin` upstream → your own fork by changing one line in `plugins.yml`. ## Open questions - License whitelist policy: GPL-3, MIT, Apache-2 OK? AGPL? Proprietary? - Update cadence: daily auto-PR, weekly, manual? - Pin granularity: per-plugin tag, sha256 hash, or commit SHA? - Failure mode if a source delists a pinned version: pin migration script? - Manual-jar storage: in-repo `manual-jars/` (license risk) or separate private repo? ## Status - 2026-04-27 — roadmap drafted post-deploy painshare. Not started. - 2026-04-28 — Phase 1 (pin versions) still pending. `REMOVE_OLD_MODS` bug discovered: itzg disables it when `PLUGINS` env set, so manual jars are safe. Phase 2 design finalized here. No code yet.