Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/backfill-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,21 @@ jobs:
exit 1
fi

- name: Render CurseForge changelog
if: ${{ inputs.publish_curseforge }}
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
jq -Rs '{ text: ., mode: "gfm" }' release-notes.md \
| gh api --method POST markdown --input - \
> curseforge-release-notes.html

if [[ ! -s curseforge-release-notes.html ]]; then
echo "Rendered CurseForge changelog is empty" >&2
exit 1
fi

- name: Publish existing release to Modrinth
if: ${{ inputs.publish_modrinth }}
uses: Kira-NT/mc-publish@v3.3.1
Expand All @@ -176,6 +191,7 @@ jobs:

- name: Publish existing release to CurseForge
if: ${{ inputs.publish_curseforge }}
id: publish_curseforge
uses: Kira-NT/mc-publish@v3.3.1
with:
curseforge-id: ${{ vars.CURSEFORGE_PROJECT_ID }}
Expand All @@ -194,3 +210,34 @@ jobs:
retry-attempts: 3
retry-delay: 15000
fail-mode: fail

- name: Apply formatted CurseForge changelog
if: ${{ inputs.publish_curseforge }}
env:
CURSEFORGE_PROJECT_ID: ${{ vars.CURSEFORGE_PROJECT_ID }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
CURSEFORGE_FILE_ID: ${{ steps.publish_curseforge.outputs.curseforge-version }}
shell: bash
run: |
if [[ ! "$CURSEFORGE_FILE_ID" =~ ^[0-9]+$ ]]; then
echo "Publisher did not return a valid CurseForge file ID: $CURSEFORGE_FILE_ID" >&2
exit 1
fi

jq -cn \
--argjson fileID "$CURSEFORGE_FILE_ID" \
--rawfile changelog curseforge-release-notes.html \
'{ fileID: $fileID, changelog: $changelog, changelogType: "html" }' \
> curseforge-update.json

response="$(curl --fail-with-body --silent --show-error \
--request POST \
--header "X-Api-Token: $CURSEFORGE_TOKEN" \
--form "metadata=<curseforge-update.json;type=application/json" \
"https://minecraft.curseforge.com/api/projects/$CURSEFORGE_PROJECT_ID/update-file")"

updated_id="$(jq -r '.id // empty' <<< "$response")"
if [[ "$updated_id" != "$CURSEFORGE_FILE_ID" ]]; then
echo "CurseForge returned an unexpected file ID: $updated_id" >&2
exit 1
fi
45 changes: 45 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,20 @@ jobs:
exit 1
fi

- name: Render CurseForge changelog
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
jq -Rs '{ text: ., mode: "gfm" }' release-notes.md \
| gh api --method POST markdown --input - \
> curseforge-release-notes.html

if [[ ! -s curseforge-release-notes.html ]]; then
echo "Rendered CurseForge changelog is empty" >&2
exit 1
fi

- name: Prepare release artifact
id: artifact
env:
Expand All @@ -158,6 +172,7 @@ jobs:
echo "checksum=$artifact.sha256" >> "$GITHUB_OUTPUT"

- name: Publish to GitHub, Modrinth, and CurseForge
id: publish
uses: Kira-NT/mc-publish@v3.3.1
with:
modrinth-id: ${{ vars.MODRINTH_PROJECT_ID }}
Expand Down Expand Up @@ -192,3 +207,33 @@ jobs:
retry-attempts: 3
retry-delay: 15000
fail-mode: fail

- name: Apply formatted CurseForge changelog
env:
CURSEFORGE_PROJECT_ID: ${{ vars.CURSEFORGE_PROJECT_ID }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
CURSEFORGE_FILE_ID: ${{ steps.publish.outputs.curseforge-version }}
shell: bash
run: |
if [[ ! "$CURSEFORGE_FILE_ID" =~ ^[0-9]+$ ]]; then
echo "Publisher did not return a valid CurseForge file ID: $CURSEFORGE_FILE_ID" >&2
exit 1
fi

jq -cn \
--argjson fileID "$CURSEFORGE_FILE_ID" \
--rawfile changelog curseforge-release-notes.html \
'{ fileID: $fileID, changelog: $changelog, changelogType: "html" }' \
> curseforge-update.json

response="$(curl --fail-with-body --silent --show-error \
--request POST \
--header "X-Api-Token: $CURSEFORGE_TOKEN" \
--form "metadata=<curseforge-update.json;type=application/json" \
"https://minecraft.curseforge.com/api/projects/$CURSEFORGE_PROJECT_ID/update-file")"

updated_id="$(jq -r '.id // empty' <<< "$response")"
if [[ "$updated_id" != "$CURSEFORGE_FILE_ID" ]]; then
echo "CurseForge returned an unexpected file ID: $updated_id" >&2
exit 1
fi
117 changes: 117 additions & 0 deletions .github/workflows/repair-curseforge-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: Repair CurseForge Changelog

on:
workflow_dispatch:
inputs:
version:
description: 'Version whose changelog should be repaired, without the v prefix'
required: true
type: string
curseforge_file_id:
description: 'Numeric CurseForge file ID from the file page URL'
required: true
type: string

permissions:
contents: read

concurrency:
group: repair-curseforge-changelog-${{ inputs.curseforge_file_id }}
cancel-in-progress: false

jobs:
repair:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Validate request
id: version
env:
VERSION: ${{ inputs.version }}
CURSEFORGE_FILE_ID: ${{ inputs.curseforge_file_id }}
CURSEFORGE_PROJECT_ID: ${{ vars.CURSEFORGE_PROJECT_ID }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
shell: bash
run: |
if [[ -z "$VERSION" ]]; then
echo "A version is required" >&2
exit 1
fi

if [[ ! "$CURSEFORGE_FILE_ID" =~ ^[0-9]+$ ]]; then
echo "CurseForge file ID must be numeric" >&2
exit 1
fi

[[ -n "$CURSEFORGE_PROJECT_ID" ]] || { echo "CURSEFORGE_PROJECT_ID is missing" >&2; exit 1; }
[[ -n "$CURSEFORGE_TOKEN" ]] || { echo "CURSEFORGE_TOKEN is missing" >&2; exit 1; }

tag="v$VERSION"
if ! git rev-parse --verify --quiet "refs/tags/$tag" >/dev/null; then
echo "Git tag $tag does not exist" >&2
exit 1
fi

echo "tag=$tag" >> "$GITHUB_OUTPUT"

- name: Extract release notes from the tagged source
env:
TAG: ${{ steps.version.outputs.tag }}
VERSION: ${{ inputs.version }}
shell: bash
run: |
git show "$TAG:CHANGELOG.md" > tagged-changelog.md

awk -v version="$VERSION" '
index($0, "## " version " - ") == 1 { capture = 1; next }
capture && /^## / { exit }
capture { print }
' tagged-changelog.md > release-notes.md

if [[ ! -s release-notes.md ]]; then
echo "Release notes for $VERSION are missing or empty in $TAG" >&2
exit 1
fi

- name: Render changelog as HTML
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
jq -Rs '{ text: ., mode: "gfm" }' release-notes.md \
| gh api --method POST markdown --input - \
> curseforge-release-notes.html

if [[ ! -s curseforge-release-notes.html ]]; then
echo "Rendered CurseForge changelog is empty" >&2
exit 1
fi

- name: Update CurseForge changelog
env:
CURSEFORGE_PROJECT_ID: ${{ vars.CURSEFORGE_PROJECT_ID }}
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
CURSEFORGE_FILE_ID: ${{ inputs.curseforge_file_id }}
shell: bash
run: |
jq -cn \
--argjson fileID "$CURSEFORGE_FILE_ID" \
--rawfile changelog curseforge-release-notes.html \
'{ fileID: $fileID, changelog: $changelog, changelogType: "html" }' \
> curseforge-update.json

response="$(curl --fail-with-body --silent --show-error \
--request POST \
--header "X-Api-Token: $CURSEFORGE_TOKEN" \
--form "metadata=<curseforge-update.json;type=application/json" \
"https://minecraft.curseforge.com/api/projects/$CURSEFORGE_PROJECT_ID/update-file")"

updated_id="$(jq -r '.id // empty' <<< "$response")"
if [[ "$updated_id" != "$CURSEFORGE_FILE_ID" ]]; then
echo "CurseForge returned an unexpected file ID: $updated_id" >&2
exit 1
fi
16 changes: 16 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ The backfill workflow:
- verifies the GitHub checksum when a checksum asset exists
- reads release notes from the tagged version of `CHANGELOG.md`
- publishes independently to Modrinth and CurseForge
- renders the CurseForge changelog as HTML after upload so headings, lists, and inline code remain intact
- never creates or modifies a GitHub release

If one destination succeeds and the other fails, rerun the workflow with only the failed destination enabled. This avoids attempting to create a duplicate version on the successful platform.
Expand Down Expand Up @@ -89,6 +90,7 @@ When all checks pass, the workflow publishes:
- `retold-<version>.jar` to Modrinth and CurseForge
- the same JAR plus its `.sha256` checksum to GitHub Releases
- the matching `CHANGELOG.md` section as the changelog on every platform
- an HTML-rendered copy of those notes on CurseForge to preserve headings, lists, and inline code
- NeoForge, Minecraft 26.2, Java 25, and client-and-server metadata

Versions containing `-alpha` or `-snapshot` publish as alpha. Versions containing `-beta` or `-rc` publish as beta. Other versions publish as releases.
Expand All @@ -98,3 +100,17 @@ Versions containing `-alpha` or `-snapshot` publish as alpha. Versions containin
The workflow can also be started from **Actions → Release → Run workflow**. Enter the version without the `v` prefix. The entered version must still match `mod_version`, and every normal validation and publishing step still runs.

Use manual publishing only when the release tag workflow did not start. If a run partially publishes, first inspect GitHub, Modrinth, and CurseForge. Either upload the missing platform manually, or delete every version and GitHub release created by the failed run before retrying. The existing Git tag can remain in place.

## Repair A CurseForge Changelog

Use **Actions → Repair CurseForge Changelog → Run workflow** when a CurseForge file already exists but its changelog was flattened or otherwise formatted incorrectly.

1. Open the affected file in the CurseForge author dashboard.
2. Copy the numeric file ID from the file page URL.
3. Open **Actions → Repair CurseForge Changelog → Run workflow** in GitHub.
4. Enter the version without the `v` prefix and the numeric CurseForge file ID.
5. Run the workflow.

The repair workflow reads the matching release section from the tagged `CHANGELOG.md`, renders it to HTML with GitHub's Markdown renderer, and updates only that existing CurseForge file. It does not upload another JAR or modify GitHub or Modrinth.

New normal and backfilled releases perform this HTML update automatically after the CurseForge upload.
Loading