# 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/<owner>/<repo>/releases/download/<tag>/<asset>.jar`.
- API: `api.github.com/repos/<owner>/<repo>/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.
