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
18 changes: 18 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
* text eol=lf
*.bat text eol=crlf
*.patch text eol=lf
*.java text eol=lf
*.gradle text eol=crlf
*.png binary
*.pdn binary
*.gif binary
*.exe binary
*.dll binary
*.jar binary
*.lzma binary
*.zip binary
*.pyd binary
*.cfg text eol=lf
*.jks binary
*.ogg binary
*.nbt binary
51 changes: 51 additions & 0 deletions .github/scripts/curseforge-version-ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Resolve CurseForge upload IDs by category and name, not name alone."""
import json
from pathlib import Path
import sys


def entries(value):
if isinstance(value, dict):
value = value.get("data")
if not isinstance(value, list) or not value:
raise ValueError("CurseForge returned an empty or invalid catalog")
return value


def resolve(versions, types, game, loader, java):
versions, types = entries(versions), entries(types)

def find(name, category, required=True):
type_ids = {row["id"] for row in types if row.get("slug") == category}
ids = {
row["id"] for row in versions
if row.get("name") == name and row.get("gameVersionTypeID") in type_ids
}
if len(type_ids) != 1 or len(ids) != 1:
message = f"CurseForge needs one {name!r} in category {category!r}; found {len(type_ids)} categories and {len(ids)} versions"
if required:
raise ValueError(message)
print(message + "; omitting optional Java metadata", file=sys.stderr)
return None
return ids.pop()

# 1.21.1 belongs to minecraft-1-21; 26.3 belongs to minecraft-26-3.
category = "minecraft-" + "-".join(game.split(".")[:2])
result = [find(game, category), find(loader, "modloader"),
find("Client", "environment"), find("Server", "environment")]
java_id = find(java, "java", required=False)
if java_id is not None:
result.append(java_id)
return result


if __name__ == "__main__":
if len(sys.argv) != 6:
sys.exit("Usage: curseforge-version-ids.py <versions.json> <types.json> <game> <loader> <java>")
try:
versions = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
types = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
print(json.dumps(resolve(versions, types, *sys.argv[3:])))
except (OSError, ValueError, KeyError, TypeError) as error:
sys.exit(str(error))
25 changes: 25 additions & 0 deletions .github/scripts/discord-merge-message.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Prints the Discord webhook payload for a merged pull request: its title as a link, then the CHANGELOG.md bullets
# that this merge added. GitHub's own preview card is switched off (flags 4) and no one gets pinged.
# Usage: discord-merge-message.sh <merge commit> <repo name> <pr number> <title> <url>
set -euo pipefail

merge=$1 repo=$2 number=$3 title=$4 url=$5
# Discord rejects messages over 2000 characters; leave room for the "more" line.
limit=1900

# The first parent is main before the merge, so this diff is exactly what the pull request changed.
bullets=$(git diff --unified=0 "$merge^1" "$merge" -- CHANGELOG.md | sed -n 's/^+\([[:space:]]*[-*] \)/\1/p')

content="✅ **[${repo} #${number}: ${title}](${url})**"
if [ -n "$bullets" ]; then
while IFS= read -r line; do
if [ $(( ${#content} + ${#line} + 1 )) -gt "$limit" ]; then
content+=$'\n'"…and more in CHANGELOG.md"
break
fi
content+=$'\n'"$line"
done <<< "$bullets"
fi

jq -n --arg content "$content" '{content: $content, flags: 4, allowed_mentions: {parse: []}}'
43 changes: 43 additions & 0 deletions .github/scripts/release-bump.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Points main at the patch after a published tag. The published number itself is never
# committed: the tag's jars already used it, and this only writes the following version.
# Usage: release-bump.sh 0.2.0 0.2.1 CHANGELOG.md tag-changelog.md gradle.properties
# Prints a summary. Exits 1 when the change would move the version backwards.
set -euo pipefail

die() { echo "$1" >&2; exit 1; }

[[ $# -eq 5 ]] || die "Usage: release-bump.sh 0.2.0 0.2.1 CHANGELOG.md tag-changelog.md gradle.properties"
released=$1
next=$2
changelog=$3
tagged=$4
properties=$5
here=$(dirname "$0")

order=$(bash "$here/release-version.sh" cmp "$(bash "$here/release-version.sh" read "$properties")" "$next")
case "$order" in
gt) die "gradle.properties is already past ${next}, so this would move the version backwards" ;;
eq) version_note="\`gradle.properties\` is already \`${next}\`." ;;
lt)
bash "$here/release-version.sh" write "$properties" "$next"
version_note="\`gradle.properties\` now says \`${next}\`."
;;
esac

if grep -Fxq "## ${released}" "$changelog"; then
changelog_note="\`## ${released}\` is already in the changelog."
elif cmp -s \
<(bash "$here/release-changelog.sh" extract "$changelog") \
<(bash "$here/release-changelog.sh" extract "$tagged"); then
bash "$here/release-changelog.sh" bump "$changelog" "$released"
changelog_note="The new notes are under \`## ${released}\`, and \`## Unreleased\` is empty again. Earlier version sections stay in place."
else
changelog_note="\`## Unreleased\` on main does not match the notes that were published, so the heading was left as it is."
fi

cat <<EOF
\`${released}\` is published. ${version_note}

${changelog_note}
EOF
105 changes: 105 additions & 0 deletions .github/scripts/release-changelog.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
# Prereleases publish Unreleased only. Stable releases also include the prerelease
# sections since the previous stable heading. Bumping archives only Unreleased.
# Usage:
# release-changelog.sh extract CHANGELOG.md # may be empty; used to compare notes
# release-changelog.sh notes CHANGELOG.md 0.2.0
# release-changelog.sh bump CHANGELOG.md 0.2.0
set -euo pipefail

die() { echo "$1" >&2; exit 1; }

semver='^(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-alpha|-beta)?$'

extract() {
local file=$1 body
body=$(awk '
/^## Unreleased$/ { in_section = 1; found = 1; next }
/^## / && in_section { exit }
in_section { lines[++n] = $0 }
END {
if (!found) exit 1
start = 1
while (start <= n && lines[start] ~ /^[[:space:]]*$/) start++
end = n
while (end >= start && lines[end] ~ /^[[:space:]]*$/) end--
for (i = start; i <= end; i++) print lines[i]
}
' "$file") || die "${file} has no ## Unreleased section"
printf '%s\n' "$body"
}

notes() {
local file=$1 version=$2 body history
[[ "$version" =~ $semver ]] || die "Version ${version} has to look like 0.0.1 or 0.0.1-alpha"
body=$(extract "$file")
case "$version" in
*-alpha|*-beta)
printf '%s\n' "$body" | grep '^[[:space:]]*[-*] ' >/dev/null || die "${file} has no bullets under ## Unreleased"
;;
*)
# Headings are newest first. A stable heading is the exclusive cutoff; beta
# headings are included just like alpha headings. Reject unknown headings.
history=$(awk -v semver="$semver" '
/^## Unreleased$/ { found = 1; next }
/^## / && found {
version = substr($0, 4)
if (version !~ semver) exit 1
if (version !~ /-(alpha|beta)$/) exit
in_history = 1
}
in_history { lines[++n] = $0 }
END {
while (n > 0 && lines[n] ~ /^[[:space:]]*$/) n--
for (i = 1; i <= n; i++) print lines[i]
}
' "$file") || die "${file} has an invalid version heading"
if [[ -n "$body" ]]; then
body=$(printf '## %s\n\n%s' "$version" "$body")
fi
if [[ -n "$history" ]]; then
if [[ -n "$body" ]]; then
body=$(printf '%s\n\n%s' "$body" "$history")
else
body=$history
fi
fi
printf '%s\n' "$body" | grep '^[[:space:]]*[-*] ' >/dev/null || die "${file} has no changes since the previous stable release"
;;
esac
printf '%s\n' "$body"
}

bump() {
local file=$1 version=$2 tmp
[[ "$version" =~ $semver ]] || die "Version ${version} has to look like 0.0.1 or 0.0.1-alpha"
grep -Fxq "## ${version}" "$file" && die "${file} already has a ## ${version} section"
# A stable promotion can have an empty Unreleased section. Still insert its
# heading so it becomes the cutoff for the next stable release.
notes "$file" "$version" >/dev/null
tmp=$(mktemp)
awk -v version="$version" '
/^## Unreleased$/ && !done {
print "## Unreleased"
print ""
print "## " version
done = 1
next
}
{ print }
END { if (!done) exit 1 }
' "$file" > "$tmp" && mv "$tmp" "$file" || {
rm -f "$tmp"
die "Could not rename ## Unreleased in ${file}"
}
}

[[ $# -ge 1 ]] || die "Usage: release-changelog.sh extract|notes|bump ..."
cmd=$1
shift
case "$cmd" in
extract) [[ $# -eq 1 ]] || die "Usage: release-changelog.sh extract CHANGELOG.md"; extract "$1" ;;
notes) [[ $# -eq 2 ]] || die "Usage: release-changelog.sh notes CHANGELOG.md 0.2.0"; notes "$1" "$2" ;;
bump) [[ $# -eq 2 ]] || die "Usage: release-changelog.sh bump CHANGELOG.md 0.2.0"; bump "$1" "$2" ;;
*) die "Unknown command: ${cmd}" ;;
esac
51 changes: 51 additions & 0 deletions .github/scripts/release-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Commit the post-release version and changelog directly to current main.
# Usage: release-commit.sh 0.0.1-alpha 0.0.2 0.0.1-alpha
set -euo pipefail

die() { echo "$1" >&2; exit 1; }
[[ $# -eq 3 ]] || die "Usage: release-commit.sh <version> <next> <tag>"
version=$1
next=$2
tag=$3
here=$(dirname "$0")

[[ -z "$(git status --porcelain)" ]] || die "The release checkout must be clean"

# Keep using the tagged scripts even if main has changed since the release build.
work=$(mktemp -d)
trap 'rm -f "$work/"*.sh "$work/tag-changelog.md"; rmdir "$work"' EXIT
cp "$here/release-version.sh" "$here/release-changelog.sh" "$here/release-bump.sh" "$work/"
git show "${tag}:CHANGELOG.md" > "$work/tag-changelog.md"
git fetch origin main
git checkout --detach origin/main
bash "$work/release-bump.sh" "$version" "$next" CHANGELOG.md "$work/tag-changelog.md" gradle.properties
git diff --stat -- gradle.properties CHANGELOG.md
if git diff --quiet -- gradle.properties CHANGELOG.md; then
echo "main already has the next version and changelog heading."
exit 0
fi
git add gradle.properties CHANGELOG.md
git -c user.name='github-actions[bot]' -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \
commit -m "Set the development version to ${next} after publishing ${tag}."
# Main requires all-versions from GitHub Actions, even for generated commits.
# GITHUB_TOKEN pushes do not trigger push workflows, so dispatch CI explicitly.
commit=$(git rev-parse HEAD)
branch="release-next/${tag}-${commit:0:12}"
git push origin "HEAD:refs/heads/${branch}"
gh workflow run ci.yml --ref "$branch"
run_id=''
for attempt in {1..12}; do
run_id=$(gh run list --workflow ci.yml --branch "$branch" --commit "$commit" \
--event workflow_dispatch --limit 1 --json databaseId --jq '.[0].databaseId // empty')
[[ -n "$run_id" ]] && break
sleep 5
done
[[ -n "$run_id" ]] || die "CI did not start for ${branch}; main was not changed"
gh run watch "$run_id" --exit-status --interval 15 || die "CI did not pass for ${branch}; main was not changed"

# Never force-push: concurrent commits must survive. Rerunning the job fetches
# fresh main and computes the update again instead of replaying a stale edit.
git push origin HEAD:refs/heads/main || die "Could not update main. If main advanced, rerun this job; otherwise check the action's push permissions."

git push origin --delete "$branch"
Loading
Loading