Skip to content

About

A release pipeline for BepInEx mods

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ModRelease

A release pipeline for BepInEx mods, run from the mod's own repository on a machine that has the game installed. One command clean-builds the plugin, checks the built assemblies, zips them so the archive drops straight onto the game folder, creates the GitHub release, and writes a folder with everything needed to post the same archive on Nexus by hand.

It exists because a plugin compiles against the game's own assemblies, which cannot be committed or sent to a CI runner, so releases are made locally. The tool makes a local release as hard to get wrong as a scripted one.

What a release goes through

  • Preflight. Refuses unless the working tree is clean, HEAD carries exactly one vX.Y.Z tag, that tag is on origin at the same commit, CHANGELOG.md has a ## [X.Y.Z] section, the installed game matches the recorded fingerprint, and no GitHub release for the tag exists yet.
  • Clean build of the plugin project in Release with ContinuousIntegrationBuild, into artifacts/build/, with any extra MSBuild properties from the config (for instance one that stops a dev-loop copy step).
  • Verification of the built assemblies' metadata, without loading them: the plugin's BepInPlugin version is the build's major.minor.patch, the build's informational version is the tag's, every assembly reference resolves to the package, the game or the loader, and nothing of the game's or the loader's is in the package.
  • Archive of the listed files under BepInEx/plugins/<mod>/ (configurable), with entry timestamps fixed to the commit time, so the same commit always gives the same bytes. A SHA-256 file sits beside it.
  • GitHub release with the changelog section as its notes and the archive and checksum attached.
  • Nexus kit under artifacts/nexus-kit-X.Y.Z/: the archive, the checksum, changelog.txt, the page description and screenshots from the repo's nexus/ folder, and a POSTING.md checklist.

package runs the same build, checks and kit on any commit, with warnings in place of refusals, so the pipeline can be tried without tagging. A package from an untagged commit is named with its pre-release version, so it cannot be mistaken for a release.

Setting a mod up

Versions come from git tags. The tool expects the plugin to be built with MinVer, so the assembly's informational version is the tag's, and expects the BepInPlugin attribute to carry the same version. The cleanest way to get both is to let MSBuild write the version into a generated class. In the plugin's .csproj (or a Directory.Build.props):

<PropertyGroup>
  <MinVerTagPrefix>v</MinVerTagPrefix>
</PropertyGroup>

<ItemGroup>
  <PackageReference Include="MinVer" Version="6.0.0" PrivateAssets="all" />
</ItemGroup>

<Target Name="GeneratePluginInfo" BeforeTargets="CoreCompile" DependsOnTargets="MinVer">
  <PropertyGroup>
    <PluginInfoFile>$(IntermediateOutputPath)PluginInfo.g.cs</PluginInfoFile>
  </PropertyGroup>
  <ItemGroup>
    <PluginInfoLine Include="// &lt;auto-generated /&gt;" />
    <PluginInfoLine Include="namespace $(RootNamespace)%3B" />
    <PluginInfoLine Include="internal static class PluginInfo" />
    <PluginInfoLine Include="{" />
    <PluginInfoLine Include="%20%20%20%20public const string Version = &quot;$(MinVerMajor).$(MinVerMinor).$(MinVerPatch)&quot;%3B" />
    <PluginInfoLine Include="%20%20%20%20public const string FullVersion = &quot;$(MinVerVersion)&quot;%3B" />
    <PluginInfoLine Include="}" />
    <Compile Include="$(PluginInfoFile)" />
  </ItemGroup>
  <WriteLinesToFile File="$(PluginInfoFile)" Lines="@(PluginInfoLine)" Overwrite="true" WriteOnlyWhenDifferent="true" Condition="'$(MinVerVersion)' != ''" />
</Target>

Then [BepInPlugin(Guid, Name, PluginInfo.Version)]. BepInEx 5 parses the plugin version with System.Version, which is why the attribute gets major.minor.patch and the full version (with the pre-release suffix an untagged build carries, e.g. 1.0.1-alpha.0.3) is left for the log line.

Install the tool into the mod's repository with a tool manifest, so its version is pinned there:

dotnet new tool-manifest
dotnet tool install ModRelease

Add modrelease.json at the repository root:

{
  "modName": "SomeMod",
  "pluginProject": "SomeMod/SomeMod.csproj",
  "packagedFiles": ["SomeMod.dll", "SomeMod.pdb", "SomeMod.Core.dll", "SomeMod.Core.pdb"],
  // "packageRoot": "BepInEx/plugins/{modName}",  // the default; the folder inside the archive
  "buildProperties": { "CopyToPlugins": "false" },  // extra -p: values for the release build
  "game": {
    "dirEnv": "SOME_GAME_DIR",                      // environment variable naming the game folder
    "defaultDir": "~/.local/share/Steam/steamapps/common/SomeGame",
    "runtimeAssemblyDirs": ["SomeGame_Data/Managed", "BepInEx/core"],
    "fingerprint": {                                // optional, strongly recommended
      "file": "SomeGame_Data/Managed/Assembly-CSharp.dll",
      "sha256": "...",                              // from: dotnet modrelease fingerprint
      "note": "Game 1.2.3, Steam build 12345"
    }
  },
  "loader": "bepinex",                              // or "none" to skip the attribute check
  "requirements": [
    { "name": "BepInEx 5.4.23.5", "url": "https://github.com/BepInEx/BepInEx/releases/tag/v5.4.23.5" }
  ],
  "nexus": { "gameDomain": "somegame", "modId": null }   // omit the section for no Nexus kit
}

Keep CHANGELOG.md in Keep a Changelog form, and put the Nexus page text in nexus/description.bbcode with screenshots under nexus/images/. Add artifacts/ to .gitignore.

Cutting a release

git tag v1.2.3 && git push origin main v1.2.3
GITHUB_TOKEN=$(cat ~/.config/modrelease/github-token) dotnet modrelease release

Use a fine-grained personal access token limited to the repository with Contents read and write, or a classic token with repo scope. Keep it in a file only you can read, such as ~/.config/modrelease/github-token at mode 600, and read it at the point of use as above rather than exporting it from your shell profile.

--dry-run does everything but the GitHub calls; --draft creates the release as a draft. Then post to Nexus from artifacts/nexus-kit-1.2.3/, following its POSTING.md.

After a game patch, re-test the mod and record the new fingerprint with dotnet modrelease fingerprint.

Developing the tool

dotnet test
dotnet pack src/ModRelease -p:MinVerVersionOverride=0.1.0-dev

To try the unpublished build against a mod, in that mod's repository pin 0.1.0-dev in .config/dotnet-tools.json and restore from the pack output:

dotnet tool restore --add-source ../ModRelease/artifacts/packages

NuGet caches packages by version, so after repacking the same version clear ~/.nuget/packages/modrelease/0.1.0-dev before restoring again.

Releases of the tool itself follow its own convention: a ## [X.Y.Z] section in CHANGELOG.md, a vX.Y.Z tag, and the CI workflow packs and publishes to nuget.org through Trusted Publishing.

About

A release pipeline for BepInEx mods

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages