Mod build, deploy, and publish lifecycle. Use when building, deploying, or publishing mods.
End-to-end automated pipeline for building companion mods with CalVer versioning, metadata generation, and website integration. Version numbers are derived from git commit history—never manually specified.
5 stages: setup → build → deploy/publish → website build → deploy
Developer Code Changes
↓
git commit
↓ (pre-commit hook)
validate-mods-metadata.py ← catches config issues early
↓
uv run erenshor mod build
├─ Compiles with dotnet
├─ Generates CalVer version from git (YYYY.M.D.{decimal_hash})
├─ Merges dependencies via ILRepack
└─ Generates metadata to both:
├─ src/mods/mods-metadata.json (source of truth, versioned in git)
└─ src/maps/static/mods-metadata.json (for website)
↓
uv run erenshor mod publish
├─ Calls build internally
├─ Copies DLLs to src/maps/static/mods/
├─ Verifies metadata is present
└─ Ready for website deployment
↓
npm run build (from src/maps/)
├─ prebuild: uv run erenshor mod publish && tiles manifest
├─ vite build: includes static/mods/ and static/mods-metadata.json
└─ dist/ ready for deployment
↓
wrangler deploy
└─ Website live with latest mods + metadata
uv run erenshor mod build # Build all mods
uv run erenshor mod build --mod interactive-map-companion # Specific mod
Outputs:
src/mods/mods-metadata.json - Metadata with current versionssrc/maps/static/mods-metadata.json - Mirror for websitesrc/mods/{ModName}/bin/Debug/netstandard2.1/uv run erenshor mod deploy # Build + copy to game BepInEx/plugins/
uv run erenshor mod setup # Copy game DLLs to mod lib/ dirs first
uv run erenshor mod launch # Launch the game
uv run erenshor mod publish # Build + stage for website deployment
Outputs:
src/maps/static/mods/src/maps/static/mods-metadata.jsonuv run erenshor mod thunderstore --mod mod-id # Build and upload
uv run erenshor mod thunderstore --mod mod-id --dry-run # Test without uploading
Requirements:
dotnet tool install -g tcliTCLI_AUTH_TOKEN in .envthunderstore.toml config in mod directorythunderstore/README.md, thunderstore/CHANGELOG.md, and thunderstore/icon.pngVersion auto-increments if releasing multiple times same day (YYYY.MDD.R format).
Two distinct build modes — ILRepack vs. no ILRepack:
| Build path | ILRepack | Output | Used for |
|---|---|---|---|
mod build / mod deploy / mod publish | Yes | Single merged DLL | Local testing, website download |
mod thunderstore | No (-p:SkipILRepack=true) | Separate DLLs | Thunderstore (reviewers require separate DLLs) |
The thunderstore.toml [[build.copy]] sections list each DLL individually
(InteractiveMapCompanion.dll, Fleck.dll, Newtonsoft.Json.dll) because the
Thunderstore build uses the non-merged output. The ILRepack.targets file skips
all merge steps when SkipILRepack=true is set.
When adding a new NuGet dependency to a mod that has Thunderstore support:
thunderstore.toml [[build.copy]] so it's included in
the Thunderstore package toouv run python3 scripts/validate-mods-metadata.py
Checks:
Format: YYYY.M.D.{DECIMAL_HASH}
Example: 2026.1.25.2690525247
How it works:
.csproj target runs generate-mod-version.py before compilePluginInfo.g.csPluginInfo.VersionDirty tree handling:
-dirty-{timestamp} if uncommitted changes exist| File | Purpose | Committed? |
|---|---|---|
src/mods/mods-config.yaml | Master configuration (names, status, features, ports) | Yes |
src/mods/mods-metadata.json | Generated metadata with versions from git | Yes |
src/maps/static/mods-metadata.json | Copy for website static files | No (generated) |
Metadata generation is idempotent—safe to run multiple times.
Runs when files change: src/mods/mods-config.yaml or scripts/generate-mods-metadata.py
Validates metadata structure before commits allowed. Developers get immediate feedback if there are issues.
New validate-mods job runs on every push:
npm run build in src/maps/:
uv run erenshor mod publish → ensures DLLs + metadata readystatic/mods/ and static/mods-metadata.jsonsrc/routes/(app)/mod/+page.svelte:
/mods-metadata.json at runtime/mods/{ModName}.dllsrc/mods/{ModName}/src/mods/mods-config.yaml with metadatauv run erenshor mod setup to copy game DLLsuv run erenshor mod build to verify it compilesgit commit (pre-commit validates metadata)uv run erenshor mod publish (stages for website)npm run build in src/maps/ (prebuild calls publish)uv run erenshor mod build --mod mod-iduv run erenshor mod deploy --mod mod-iduv run erenshor mod launchBepInEx/LogOutput.log for errorsIf metadata validation fails:
git status to see what changedsrc/mods/mods-config.yaml for syntax errorsuv run python3 scripts/validate-mods-metadata.py for detailsSingle Source of Truth: Version from git, everything else derives from config.
Atomic Metadata: Both metadata files written together, never out of sync.
Fail Fast: Validation in pre-commit catches issues before pushing.
No Manual Steps: Website build automatically stages mods.
Reproducible: Same commit always produces same version.
Versioned History: Metadata committed so version history is trackable.
| Problem | Solution |
|---|---|
| Build fails: "No DLLs in lib/" | Run uv run erenshor mod setup first |
| Version shows "0.0.0-unknown" | Check git history exists for mod directory |
| Metadata invalid (pre-commit blocks) | Run validation script to see details |
| Website shows stale mods | Run npm run build from src/maps/ |
| DLL not in website static/ | Run uv run erenshor mod publish |
src/erenshor/cli/commands/mod.py - CLI with setup/build/deploy/publish commandsscripts/generate-mod-version.py - CalVer generation from gitscripts/generate-mods-metadata.py - Metadata generation from config + versionsscripts/validate-mods-metadata.py - Metadata validation for CI/pre-commitsrc/mods/mods-config.yaml - Master mod configuration.pre-commit-config.yaml - Pre-commit hook definition.github/workflows/ci.yml - CI validation jobsrc/maps/package.json - Website prebuild integration