Skip to content

Merge pull request #288 from ifBars/releases/3.2.0 #41

Merge pull request #288 from ifBars/releases/3.2.0

Merge pull request #288 from ifBars/releases/3.2.0 #41

name: Publish GitHub Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: Existing release tag to publish or refresh (for example v3.1.0 or v3.1.0-beta.1)
required: true
type: string
publish_nexus:
description: Upload the release package to Nexus Mods
required: false
default: true
type: boolean
publish_thunderstore:
description: Upload the Thunderstore package to Thunderstore
required: false
default: true
type: boolean
permissions:
contents: write
env:
NEXUSMODS_API_KEY: ${{ secrets.NEXUSMODS_API_KEY }}
NEXUSMODS_FILE_GROUP_ID: ${{ secrets.NEXUSMODS_FILE_GROUP_ID || vars.NEXUSMODS_FILE_GROUP_ID }}
NEXUSMODS_MOD_ID: ${{ secrets.NEXUSMODS_MOD_ID || vars.NEXUSMODS_MOD_ID }}
RELEASE_DESCRIPTION: S1API is an open source collaboration project to help standardize Schedule One modding processes. The goal is to provide a standard place for common functionalities so you can focus on making content versus reverse engineering the game.
THUNDERSTORE_COMMUNITY: schedule-i
THUNDERSTORE_TOKEN: ${{ secrets.THUNDERSTORE_TOKEN }}
THUNDERSTORE_NAMESPACE: ifBars
THUNDERSTORE_PACKAGE_NAME: S1API_Forked
THUNDERSTORE_CATEGORIES: libraries
THUNDERSTORE_WEBSITE: https://github.com/ifBars/S1API
jobs:
publish-release:
runs-on: ubuntu-latest
steps:
- name: Resolve release metadata
id: metadata
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
tag="${{ inputs.tag }}"
else
tag="${GITHUB_REF_NAME}"
fi
if [[ -z "$tag" ]]; then
echo "::error::Unable to determine release tag"
exit 1
fi
if [[ ! "$tag" =~ ^v([0-9]+\.[0-9]+\.[0-9]+)(-([0-9A-Za-z][0-9A-Za-z.-]*))?(r[0-9]+)?$ ]]; then
echo "::error::Tag '$tag' does not match the expected release format"
exit 1
fi
base_version="${BASH_REMATCH[1]}"
release_version="${BASH_REMATCH[1]}${BASH_REMATCH[2]}"
release_name="S1API ${tag#v}"
if [[ -n "${BASH_REMATCH[2]}" ]]; then
prerelease="true"
release_branch="beta"
assembly_branch="beta"
else
prerelease="false"
release_branch="releases/${base_version}"
assembly_branch="main"
fi
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
echo "base_version=${base_version}" >> "$GITHUB_OUTPUT"
echo "release_version=${release_version}" >> "$GITHUB_OUTPUT"
echo "release_name=${release_name}" >> "$GITHUB_OUTPUT"
echo "release_branch=${release_branch}" >> "$GITHUB_OUTPUT"
echo "assembly_branch=${assembly_branch}" >> "$GITHUB_OUTPUT"
echo "prerelease=${prerelease}" >> "$GITHUB_OUTPUT"
- name: Checkout S1API
uses: actions/checkout@v4
with:
ref: ${{ steps.metadata.outputs.tag }}
fetch-depth: 0
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Checkout Mono game assemblies
uses: actions/checkout@v4
with:
repository: ${{ secrets.GAME_ASSEMBLIES_REPO }}
token: ${{ secrets.GAME_ASSEMBLIES_TOKEN }}
ref: ${{ steps.metadata.outputs.assembly_branch }}
path: game-assemblies
fetch-depth: 1
- name: Prepare Mono build inputs
shell: bash
run: |
set -euo pipefail
mkdir -p S1API/ScheduleOneAssemblies/Managed
mkdir -p S1API/ScheduleOneAssemblies/MelonLoader
mkdir -p S1API/ScheduleOneAssemblies/MelonLoaderMono
cp -r game-assemblies/Managed/* S1API/ScheduleOneAssemblies/Managed/
if [[ -d "game-assemblies/MelonLoader" ]]; then
cp -r game-assemblies/MelonLoader/* S1API/ScheduleOneAssemblies/MelonLoader/
fi
if [[ -d "game-assemblies/MelonLoaderMono" ]]; then
cp -r game-assemblies/MelonLoaderMono/* S1API/ScheduleOneAssemblies/MelonLoaderMono/
elif [[ -d "game-assemblies/MelonLoader/net35" ]]; then
cp -r game-assemblies/MelonLoader/net35/* S1API/ScheduleOneAssemblies/MelonLoaderMono/
elif [[ -d "game-assemblies/MelonLoader" ]]; then
cp -r game-assemblies/MelonLoader/* S1API/ScheduleOneAssemblies/MelonLoaderMono/
else
echo "Unable to locate Mono MelonLoader assemblies in game-assemblies"
exit 1
fi
if [[ ! -f "S1API/ScheduleOneAssemblies/Managed/Unity.TextMeshPro.dll" ]]; then
found_path="$(find game-assemblies -name "Unity.TextMeshPro.dll" -type f | head -1)"
if [[ -n "${found_path}" ]]; then
cp "${found_path}" S1API/ScheduleOneAssemblies/Managed/
fi
fi
if [[ ! -f "S1API/ScheduleOneAssemblies/Managed/UnityEngine.AIModule.dll" ]]; then
found_path="$(find game-assemblies -name "UnityEngine.AIModule.dll" -type f | head -1)"
if [[ -n "${found_path}" ]]; then
cp "${found_path}" S1API/ScheduleOneAssemblies/Managed/
fi
fi
cat > ci.build.props << 'EOF'
<Project>
<PropertyGroup>
<AutomateLocalDeployment>false</AutomateLocalDeployment>
<MelonLoaderAssembliesPath>$(MSBuildThisFileDirectory)S1API/ScheduleOneAssemblies/MelonLoader</MelonLoaderAssembliesPath>
<MelonLoaderMonoAssembliesPath>$(MSBuildThisFileDirectory)S1API/ScheduleOneAssemblies/MelonLoaderMono</MelonLoaderMonoAssembliesPath>
<LocalMonoDeploymentPath>null</LocalMonoDeploymentPath>
<MonoAssembliesPath>$(MSBuildThisFileDirectory)S1API/ScheduleOneAssemblies/Managed</MonoAssembliesPath>
</PropertyGroup>
</Project>
EOF
- name: Build Mono release DLLs
shell: bash
run: |
set -euo pipefail
mkdir -p artifacts/build
dotnet restore S1API/S1API.csproj -p:Configuration=MonoMelon
dotnet build S1API/S1API.csproj --no-restore -c MonoMelon -v minimal /p:AutomateLocalDeployment=false
cp S1API/bin/MonoMelon/netstandard2.1/S1API.dll artifacts/build/S1API.Mono.MelonLoader.dll
dotnet restore S1APILoader/S1APILoader.csproj -p:Configuration=MonoMelon
dotnet build S1APILoader/S1APILoader.csproj -c MonoMelon -f netstandard2.1 /p:AutomateLocalDeployment=false --no-restore
cp S1APILoader/bin/MonoMelon/netstandard2.1/S1APILoader.dll artifacts/build/S1APILoader.MelonLoader.dll
- name: Checkout IL2CPP game assemblies
uses: actions/checkout@v4
with:
repository: ${{ secrets.IL2CPP_ASSEMBLIES_REPO }}
token: ${{ secrets.IL2CPP_ASSEMBLIES_TOKEN }}
ref: ${{ steps.metadata.outputs.assembly_branch }}
path: il2cpp-scheduleone-assemblies
fetch-depth: 1
- name: Prepare IL2CPP build inputs
shell: bash
run: |
set -euo pipefail
rm -rf S1API/obj
mkdir -p S1API/ScheduleOneAssemblies/MelonLoader
mkdir -p S1API/ScheduleOneAssemblies/Il2CppAssemblies
if [[ -d "il2cpp-scheduleone-assemblies/MelonLoader/Il2CppAssemblies" ]]; then
cp -r il2cpp-scheduleone-assemblies/MelonLoader/Il2CppAssemblies/* S1API/ScheduleOneAssemblies/Il2CppAssemblies/
else
echo "Unable to locate IL2CPP generated assemblies"
exit 1
fi
if [[ -d "il2cpp-scheduleone-assemblies/MelonLoader/net6" ]]; then
cp -r il2cpp-scheduleone-assemblies/MelonLoader/net6/* S1API/ScheduleOneAssemblies/MelonLoader/
else
echo "Unable to locate IL2CPP MelonLoader net6 assemblies"
exit 1
fi
for assembly in \
Il2CppInterop.Runtime.dll \
Il2CppInterop.Common.dll \
Il2CppInterop.HarmonySupport.dll \
Il2CppInterop.Generator.dll \
UnityEngine.Il2CppAssetBundleManager.dll; do
if [[ ! -f "S1API/ScheduleOneAssemblies/MelonLoader/${assembly}" ]]; then
found_path="$(find il2cpp-scheduleone-assemblies -name "${assembly}" -type f | head -1)"
if [[ -n "${found_path}" ]]; then
cp "${found_path}" S1API/ScheduleOneAssemblies/MelonLoader/
else
echo "Missing required IL2CPP dependency: ${assembly}"
exit 1
fi
fi
done
for assembly in \
Unity.TextMeshPro.dll \
UnityEngine.AIModule.dll; do
if [[ ! -f "S1API/ScheduleOneAssemblies/Il2CppAssemblies/${assembly}" ]]; then
found_path="$(find il2cpp-scheduleone-assemblies -name "${assembly}" -type f | head -1)"
if [[ -n "${found_path}" ]]; then
cp "${found_path}" S1API/ScheduleOneAssemblies/Il2CppAssemblies/
fi
fi
done
cat > ci.build.props << 'EOF'
<Project>
<PropertyGroup>
<AutomateLocalDeployment>false</AutomateLocalDeployment>
<MelonLoaderAssembliesPath>$(MSBuildThisFileDirectory)S1API/ScheduleOneAssemblies/MelonLoader</MelonLoaderAssembliesPath>
<LocalIl2CppDeploymentPath>null</LocalIl2CppDeploymentPath>
<Il2CppAssembliesPath Condition="'$(Configuration)' == 'Il2CppMelon'">$(MSBuildThisFileDirectory)S1API/ScheduleOneAssemblies/Il2CppAssemblies</Il2CppAssembliesPath>
</PropertyGroup>
</Project>
EOF
- name: Build IL2CPP release DLL
shell: bash
run: |
set -euo pipefail
dotnet restore S1API/S1API.csproj -p:Configuration=Il2CppMelon
dotnet build S1API/S1API.csproj --no-restore -c Il2CppMelon -v minimal /p:AutomateLocalDeployment=false
cp S1API/bin/Il2CppMelon/net6.0/S1API.dll artifacts/build/S1API.Il2Cpp.MelonLoader.dll
- name: Package release assets
id: package
shell: bash
run: |
set -euo pipefail
package_dir="artifacts/S1API-Forked-${{ steps.metadata.outputs.release_version }}"
zip_path="artifacts/S1API-Forked-${{ steps.metadata.outputs.release_version }}.zip"
thunderstore_dir="artifacts/S1API-TS-${{ steps.metadata.outputs.release_version }}"
thunderstore_zip_path="artifacts/S1API-TS-${{ steps.metadata.outputs.release_version }}.zip"
mkdir -p "${package_dir}/Mods" "${package_dir}/Plugins"
cp artifacts/build/S1API.Mono.MelonLoader.dll "${package_dir}/Mods/S1API.Mono.MelonLoader.dll"
cp artifacts/build/S1API.Il2Cpp.MelonLoader.dll "${package_dir}/Mods/S1API.Il2Cpp.MelonLoader.dll"
cp artifacts/build/S1APILoader.MelonLoader.dll "${package_dir}/Plugins/S1APILoader.MelonLoader.dll"
(
cd "${package_dir}"
zip -r "../$(basename "${zip_path}")" Mods Plugins
)
unzip -l "${zip_path}"
for entry in \
"Mods/S1API.Mono.MelonLoader.dll" \
"Mods/S1API.Il2Cpp.MelonLoader.dll" \
"Plugins/S1APILoader.MelonLoader.dll"; do
unzip -Z1 "${zip_path}" | grep -Fx "${entry}" >/dev/null
done
mkdir -p "${thunderstore_dir}/Mods" "${thunderstore_dir}/Plugins"
cp artifacts/build/S1API.Mono.MelonLoader.dll "${thunderstore_dir}/Mods/S1API.Mono.MelonLoader.dll"
cp artifacts/build/S1API.Il2Cpp.MelonLoader.dll "${thunderstore_dir}/Mods/S1API.Il2Cpp.MelonLoader.dll"
cp artifacts/build/S1APILoader.MelonLoader.dll "${thunderstore_dir}/Plugins/S1APILoader.MelonLoader.dll"
cp Public/logo_compressed.png "${thunderstore_dir}/icon.png"
cp Public/README.md "${thunderstore_dir}/README.md"
jq -n \
--arg name "${THUNDERSTORE_PACKAGE_NAME}" \
--arg version "${{ steps.metadata.outputs.release_version }}" \
--arg website "${THUNDERSTORE_WEBSITE}" \
--arg description "${RELEASE_DESCRIPTION}" \
'{
name: $name,
version_number: $version,
website_url: $website,
description: $description,
dependencies: ["LavaGang-MelonLoader-0.7.2"]
}' > "${thunderstore_dir}/manifest.json"
(
cd "${thunderstore_dir}"
zip -r "../$(basename "${thunderstore_zip_path}")" icon.png README.md manifest.json Mods Plugins
)
unzip -l "${thunderstore_zip_path}"
for entry in \
"icon.png" \
"README.md" \
"manifest.json" \
"Mods/S1API.Mono.MelonLoader.dll" \
"Mods/S1API.Il2Cpp.MelonLoader.dll" \
"Plugins/S1APILoader.MelonLoader.dll"; do
unzip -Z1 "${thunderstore_zip_path}" | grep -Fx "${entry}" >/dev/null
done
jq -e --arg version "${{ steps.metadata.outputs.release_version }}" '.version_number == $version' "${thunderstore_dir}/manifest.json" >/dev/null
echo "zip_path=${zip_path}" >> "$GITHUB_OUTPUT"
echo "thunderstore_zip_path=${thunderstore_zip_path}" >> "$GITHUB_OUTPUT"
- name: Publish or update GitHub release
id: github-release
uses: actions/github-script@v7
env:
RELEASE_TAG: ${{ steps.metadata.outputs.tag }}
RELEASE_NAME: ${{ steps.metadata.outputs.release_name }}
RELEASE_VERSION: ${{ steps.metadata.outputs.release_version }}
RELEASE_BRANCH: ${{ steps.metadata.outputs.release_branch }}
PRERELEASE: ${{ steps.metadata.outputs.prerelease }}
with:
script: |
const fs = require("fs");
const path = require("path");
const owner = context.repo.owner;
const repo = context.repo.repo;
const tag = process.env.RELEASE_TAG;
const releaseName = process.env.RELEASE_NAME;
const releaseVersion = process.env.RELEASE_VERSION;
const releaseBranch = process.env.RELEASE_BRANCH;
const prerelease = process.env.PRERELEASE === "true";
const resolveTargetCommitish = async () => {
try {
await github.rest.repos.getBranch({
owner,
repo,
branch: releaseBranch
});
return releaseBranch;
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
const ref = await github.rest.git.getRef({
owner,
repo,
ref: `tags/${tag}`
});
let target = ref.data.object;
if (target.type === "tag") {
const annotated = await github.rest.git.getTag({
owner,
repo,
tag_sha: target.sha
});
target = annotated.data.object;
}
return target.sha;
};
const targetCommitish = await resolveTargetCommitish();
const curatedNotesPath = path.join(
process.env.GITHUB_WORKSPACE,
".github",
"release-notes",
`${releaseVersion}.md`
);
let releaseNotes;
if (fs.existsSync(curatedNotesPath)) {
releaseNotes = fs.readFileSync(curatedNotesPath, "utf8").trim();
if (!releaseNotes) {
throw new Error(`Curated release notes are empty: ${curatedNotesPath}`);
}
core.notice(`Using curated release notes from ${curatedNotesPath}`);
} else {
core.warning(
`No curated release notes found for ${releaseVersion}; using GitHub-generated notes.`
);
const generatedNotes = await github.rest.repos.generateReleaseNotes({
owner,
repo,
tag_name: tag,
target_commitish: targetCommitish
});
releaseNotes = generatedNotes.data.body;
}
core.setOutput("release_notes", releaseNotes);
const payload = {
owner,
repo,
tag_name: tag,
target_commitish: targetCommitish,
name: releaseName,
body: releaseNotes,
draft: false,
prerelease,
make_latest: prerelease ? "false" : "legacy"
};
let existingRelease = null;
try {
existingRelease = await github.rest.repos.getReleaseByTag({
owner,
repo,
tag
});
} catch (error) {
if (error.status !== 404) {
throw error;
}
}
if (existingRelease) {
const updated = await github.rest.repos.updateRelease({
...payload,
release_id: existingRelease.data.id
});
core.notice(`Updated GitHub release ${updated.data.html_url}`);
return;
}
const created = await github.rest.repos.createRelease(payload);
core.notice(`Published GitHub release ${created.data.html_url}`);
- name: Upload GitHub release assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release delete-asset "${{ steps.metadata.outputs.tag }}" "$(basename "${{ steps.package.outputs.thunderstore_zip_path }}")" --yes || true
gh release upload "${{ steps.metadata.outputs.tag }}" "${{ steps.package.outputs.zip_path }}" --clobber
- name: Upload Nexus Mods release
if: ${{ steps.metadata.outputs.prerelease != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_nexus) && env.NEXUSMODS_API_KEY != '' && env.NEXUSMODS_FILE_GROUP_ID != '' && env.NEXUSMODS_MOD_ID != '' }}
uses: Nexus-Mods/upload-action@v1.0.0-beta.10
with:
api_key: ${{ env.NEXUSMODS_API_KEY }}
file_id: ${{ env.NEXUSMODS_FILE_GROUP_ID }}
mod_id: ${{ env.NEXUSMODS_MOD_ID }}
filename: ${{ steps.package.outputs.zip_path }}
version: ${{ steps.metadata.outputs.release_version }}
display_name: S1API Forked ${{ steps.metadata.outputs.release_version }}
description: ${{ env.RELEASE_DESCRIPTION }}
# Nexus file publishing and changelog publishing use separate API resources.
# The changelog endpoint currently rejects this mod even after creating the
# file version, which turns a successful upload into a failed release run.
# Keep release notes on GitHub and omit the optional Nexus changelog input.
category: main
archive_existing_version: true
update_mod_version: true
primary_mod_manager_download: true
allow_mod_manager_download: true
show_requirements_pop_up: false
- name: Skip Nexus Mods upload
if: ${{ steps.metadata.outputs.prerelease != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_nexus) && (env.NEXUSMODS_API_KEY == '' || env.NEXUSMODS_FILE_GROUP_ID == '' || env.NEXUSMODS_MOD_ID == '') }}
shell: bash
run: echo "::notice::Skipping Nexus Mods upload because NEXUSMODS_API_KEY, NEXUSMODS_FILE_GROUP_ID, or NEXUSMODS_MOD_ID is not configured."
- name: Upload Thunderstore release
if: ${{ steps.metadata.outputs.prerelease != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_thunderstore) && env.THUNDERSTORE_TOKEN != '' }}
uses: GreenTF/upload-thunderstore-package@v4.3
with:
token: ${{ env.THUNDERSTORE_TOKEN }}
community: ${{ env.THUNDERSTORE_COMMUNITY }}
namespace: ${{ env.THUNDERSTORE_NAMESPACE }}
name: ${{ env.THUNDERSTORE_PACKAGE_NAME }}
description: ${{ env.RELEASE_DESCRIPTION }}
version: ${{ steps.metadata.outputs.release_version }}
file: ${{ steps.package.outputs.thunderstore_zip_path }}
repo: https://thunderstore.io
website: ${{ env.THUNDERSTORE_WEBSITE }}
categories: ${{ env.THUNDERSTORE_CATEGORIES }}
- name: Skip Thunderstore upload
if: ${{ steps.metadata.outputs.prerelease != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_thunderstore) && env.THUNDERSTORE_TOKEN == '' }}
shell: bash
run: echo "::notice::Skipping Thunderstore upload because THUNDERSTORE_TOKEN is not configured."
- name: Skip external prerelease uploads
if: ${{ steps.metadata.outputs.prerelease == 'true' }}
run: echo "::notice::Experimental releases are published to GitHub only; Nexus Mods and Thunderstore uploads are disabled."