Skip to content

Commit a6dcb16

Browse files
committed
Publish the same release to CurseForge and Maven
1 parent d55bf05 commit a6dcb16

3 files changed

Lines changed: 206 additions & 9 deletions

File tree

‎.github/maven-release/README.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# MMD Maven releases
22

3+
## New Forge releases
4+
5+
Use **Release basePower** on the MMD repository's default branch. After the
6+
version branch passes CI, tag the exact tested commit and add the CI run, artifact
7+
ID, archive SHA-256 and tested main-jar SHA-256 to `releases.json`. Run
8+
**check-only** first, then **publish**. The workflow uploads the checked player
9+
jar to CurseForge, verifies the public download, and publishes those same bytes
10+
to MMD Maven before creating the GitHub release.
11+
12+
If CurseForge has accepted the file but it isn't public yet, wait for approval.
13+
Start a new run with the recorded CurseForge file ID so it verifies that upload
14+
without submitting the jar again. The release workflow currently covers Java 8
15+
Forge targets only.
16+
17+
## Existing CurseForge releases
18+
319
Use **Actions > Publish to MMD Maven** on the MMD repository's default branch.
420
Select a version listed in `releases.json` and run **check-only** first. Once it
521
passes, run **publish** for that same version.

‎.github/maven-release/verify.py‎

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Check an approved release without rebuilding it or uploading to CurseForge."""
1+
"""Stage checksum-sealed CI files and verify the public release destinations."""
22

33
import hashlib
44
import json
@@ -46,14 +46,16 @@ def github(path):
4646
return subprocess.check_output(["gh", "api", path])
4747

4848

49-
def stage(manifest, version, release, work):
49+
def stage(manifest, version, release, work, candidate=False):
5050
repo = manifest["repository"]
5151
require(os.environ.get("GITHUB_REPOSITORY", repo) == repo, "Wrong source repository")
5252
artifact_id = release["artifactId"]
5353
ci = json.loads(github(f"repos/{repo}/actions/runs/{release['runId']}"))
5454
require(ci["head_sha"] == release["commit"] and ci["conclusion"] == "success"
5555
and ci["path"] == ".github/workflows/ci.yml" and ci["event"] == "push"
5656
and ci["head_branch"] == release["branch"], "CI provenance changed")
57+
if candidate:
58+
require(release.get("tag") == version, "New publication requires the exact release tag")
5759
if release.get("tag"):
5860
ref = json.loads(github(f"repos/{repo}/commits/{release['tag']}"))
5961
require(ref["sha"] == release["commit"], "Release tag moved")
@@ -75,18 +77,42 @@ def stage(manifest, version, release, work):
7577
require(name not in sums and re.fullmatch(r"[a-fA-F0-9]{64}", sha), "Malformed CI checksums")
7678
sums[name] = sha.lower()
7779
require(set(sums) == set(jars), "CI did not seal exactly the three release jars")
78-
require(sums[jars[0]] == release["mainSha256"], "CI main jar does not match the published release")
80+
require(sums[jars[0]] == release["mainSha256"], "CI main jar does not match the approved release")
7981
for name in jars:
8082
member = "build/libs/" + name
8183
require(bundle.namelist().count(member) == 1, f"Missing or duplicate CI jar: {name}")
8284
data = bundle.read(member)
8385
require(digest(data) == sums[name], f"Bad CI checksum: {name}")
8486
(work / name).write_bytes(data)
85-
file_id = release["curseFileId"]
86-
public = read_url(f"https://edge.forgecdn.net/files/{file_id // 1000}/{file_id % 1000}/{jars[0]}")
87-
require(digest(public) == release["mainSha256"], "CurseForge bytes changed")
87+
if candidate:
88+
changelog = bundle.read("CHANGELOG.txt").decode("utf-8-sig").strip()
89+
require(version in changelog.splitlines()[0], "Changelog does not start with the release version")
90+
notes = "\n\n".join(changelog.split("\n\n", 2)[:2])
91+
(work / "release-notes.txt").write_text(notes + "\n", encoding="utf-8")
8892
(work / "verified.json").write_text(json.dumps(sums), encoding="ascii")
89-
print(f"CurseForge file {file_id} matches {jars[0]}: {release['mainSha256']}")
93+
if not candidate:
94+
verify_curseforge(manifest, version, release, release["curseFileId"])
95+
96+
97+
def verify_curseforge(manifest, version, release, file_id, project_id=None):
98+
require(type(file_id) is int and file_id > 0, "Invalid CurseForge file ID")
99+
jar = jar_names(manifest, version)[0]
100+
if project_id is not None:
101+
url = f"https://www.curseforge.com/api/v1/mods/{project_id}/files/{file_id}/download"
102+
else:
103+
url = f"https://edge.forgecdn.net/files/{file_id // 1000}/{file_id % 1000}/{jar}"
104+
for delay in (0, 5, 15, 30, 60):
105+
time.sleep(delay if project_id is not None else 0)
106+
try:
107+
public = read_url(url)
108+
except HTTPError as error:
109+
if error.code == 404 and project_id is not None:
110+
continue
111+
raise
112+
require(digest(public) == release["mainSha256"], "CurseForge bytes changed")
113+
print(f"CurseForge file {file_id} matches {jar}: {release['mainSha256']}")
114+
return
115+
raise ValueError(f"CurseForge file {file_id} is not public yet; resume with that file ID")
90116

91117

92118
def jar_names(manifest, version):
@@ -135,8 +161,11 @@ def main():
135161
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
136162
require(version in manifest["releases"], f"Version is not approved: {version}")
137163
release, work = manifest["releases"][version], Path(folder)
138-
if mode == "stage":
139-
stage(manifest, version, release, work)
164+
if mode in ("stage", "stage-candidate"):
165+
stage(manifest, version, release, work, candidate=mode == "stage-candidate")
166+
elif mode == "verify-curseforge":
167+
verify_curseforge(manifest, version, release, int(os.environ["CURSEFORGE_FILE_ID"]),
168+
int(os.environ["CURSEFORGE_PROJECT_ID"]))
140169
elif mode in ("check", "require-absent"):
141170
state = destination_state(manifest, version, release, work)
142171
require(mode == "check" or state == "absent", "Destination is not empty")
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
name: Release basePower
2+
run-name: ${{ inputs.mode }} basePower ${{ inputs.version }}
3+
4+
on:
5+
workflow_dispatch:
6+
inputs:
7+
version:
8+
description: Approved version from .github/maven-release/releases.json
9+
required: true
10+
type: string
11+
mode:
12+
description: Check the exact release, or publish it
13+
required: true
14+
default: check-only
15+
type: choice
16+
options: [check-only, publish]
17+
existing_curseforge_file_id:
18+
description: Resume with an already uploaded CurseForge file (never upload it again)
19+
required: false
20+
type: string
21+
22+
permissions:
23+
actions: read
24+
contents: write
25+
26+
defaults:
27+
run:
28+
shell: bash
29+
30+
concurrency:
31+
group: mmd-maven-publication
32+
cancel-in-progress: false
33+
34+
jobs:
35+
release:
36+
name: Publish one verified release to CurseForge, Maven, and GitHub
37+
if: github.repository == 'MinecraftModDevelopmentMods/basePower' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
38+
runs-on: ubuntu-latest
39+
timeout-minutes: 20
40+
env:
41+
RELEASE_VERSION: ${{ inputs.version }}
42+
EXISTING_CF_FILE: ${{ inputs.existing_curseforge_file_id }}
43+
CURSEFORGE_PROJECT_ID: '1780140'
44+
GH_TOKEN: ${{ github.token }}
45+
steps:
46+
- name: Check out the publication workflow
47+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
48+
with:
49+
persist-credentials: false
50+
51+
- name: Install Java for the Maven publisher
52+
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
53+
with:
54+
distribution: temurin
55+
java-version: '17'
56+
57+
- name: Verify the tag, CI artifact, and Maven destination
58+
id: preflight
59+
run: |
60+
set -euo pipefail
61+
[[ "$RELEASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]
62+
[[ -z "$EXISTING_CF_FILE" || "$EXISTING_CF_FILE" =~ ^[1-9][0-9]*$ ]]
63+
release_work="$RUNNER_TEMP/release"
64+
python3 .github/maven-release/verify.py stage-candidate "$RELEASE_VERSION" "$release_work"
65+
bash gradlew -p .github/maven-release generatePomFileForMavenJavaPublication \
66+
"-PreleaseVersion=$RELEASE_VERSION" "-PreleaseWork=$release_work" --no-daemon
67+
state=$(python3 .github/maven-release/verify.py check "$RELEASE_VERSION" "$release_work")
68+
if [[ "$state" == present && -z "$EXISTING_CF_FILE" ]]; then
69+
echo "Maven already has this release. Supply its existing CurseForge file ID." >&2
70+
exit 1
71+
fi
72+
mc=$(jq -er --arg v "$RELEASE_VERSION" '.releases[$v].branch | capture("^master-(?<mc>1\\.[0-9]+\\.[0-9]+)$").mc' .github/maven-release/releases.json)
73+
case "$mc" in
74+
1.10.2|1.12.2|1.13.2|1.14.4|1.15.2|1.16.5) ;;
75+
*) echo "This publisher only handles the Java 8 Forge releases" >&2; exit 1 ;;
76+
esac
77+
echo "minecraft=$mc" >> "$GITHUB_OUTPUT"
78+
echo "state=$state" >> "$GITHUB_OUTPUT"
79+
echo "RELEASE_WORK=$release_work" >> "$GITHUB_ENV"
80+
echo "Verified $RELEASE_VERSION for Minecraft $mc. Maven: $state." >> "$GITHUB_STEP_SUMMARY"
81+
82+
- name: Check publication credentials
83+
if: inputs.mode == 'publish'
84+
env:
85+
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
86+
MAVEN_UPLOAD_URL: ${{ secrets.MAVEN_UPLOAD_URL }}
87+
MAVEN_UPLOAD_USERNAME: ${{ secrets.MAVEN_UPLOAD_USERNAME }}
88+
MAVEN_UPLOAD_PASSWORD: ${{ secrets.MAVEN_UPLOAD_PASSWORD }}
89+
run: |
90+
set -euo pipefail
91+
test -n "$EXISTING_CF_FILE" || test -n "$CURSEFORGE_TOKEN"
92+
test -n "$MAVEN_UPLOAD_USERNAME" && test -n "$MAVEN_UPLOAD_PASSWORD"
93+
[[ "$MAVEN_UPLOAD_URL" == https://maven.moddev.zone/releases || "$MAVEN_UPLOAD_URL" == https://maven.moddev.zone/releases/ ]]
94+
if [[ -z "$EXISTING_CF_FILE" && "$GITHUB_RUN_ATTEMPT" != 1 ]]; then
95+
echo "Do not retry an upload. Start a new dispatch with the existing file ID." >&2
96+
exit 1
97+
fi
98+
99+
- name: Upload the player jar to CurseForge
100+
if: inputs.mode == 'publish' && inputs.existing_curseforge_file_id == ''
101+
id: curseforge
102+
uses: Kira-NT/mc-publish@52307b03863581dec6b652b83e597aec02ebb075 # v3.3.1
103+
with:
104+
curseforge-id: '1780140'
105+
curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }}
106+
name: basePower ${{ inputs.version }} for Minecraft ${{ steps.preflight.outputs.minecraft }} (Forge)
107+
version: ${{ inputs.version }}
108+
version-type: release
109+
changelog-file: ${{ env.RELEASE_WORK }}/release-notes.txt
110+
files: ${{ env.RELEASE_WORK }}/basePower-${{ inputs.version }}.jar
111+
game-versions: ${{ steps.preflight.outputs.minecraft }}
112+
game-version-filter: none
113+
loaders: forge
114+
environment: client | server
115+
java: Java 8
116+
curseforge-dependencies: |
117+
240967(required)
118+
245586(required)
119+
243157(optional)
120+
fail-mode: fail
121+
122+
- name: Verify the public CurseForge download
123+
if: inputs.mode == 'publish' || inputs.existing_curseforge_file_id != ''
124+
env:
125+
UPLOADED_CF_FILE: ${{ steps.curseforge.outputs.curseforge-version }}
126+
run: |
127+
set -euo pipefail
128+
export CURSEFORGE_FILE_ID="${EXISTING_CF_FILE:-$UPLOADED_CF_FILE}"
129+
[[ "$CURSEFORGE_FILE_ID" =~ ^[1-9][0-9]*$ ]]
130+
echo "CurseForge file ID: $CURSEFORGE_FILE_ID. Use this ID to resume if approval is pending." >> "$GITHUB_STEP_SUMMARY"
131+
python3 .github/maven-release/verify.py verify-curseforge "$RELEASE_VERSION" "$RELEASE_WORK"
132+
133+
- name: Publish the same release to MMD Maven
134+
if: inputs.mode == 'publish' && steps.preflight.outputs.state == 'absent'
135+
env:
136+
MAVEN_UPLOAD_URL: ${{ secrets.MAVEN_UPLOAD_URL }}
137+
MAVEN_UPLOAD_USERNAME: ${{ secrets.MAVEN_UPLOAD_USERNAME }}
138+
MAVEN_UPLOAD_PASSWORD: ${{ secrets.MAVEN_UPLOAD_PASSWORD }}
139+
run: |
140+
set -euo pipefail
141+
python3 .github/maven-release/verify.py require-absent "$RELEASE_VERSION" "$RELEASE_WORK"
142+
bash gradlew -p .github/maven-release publishMavenJavaPublicationToReleaseRepository \
143+
"-PreleaseVersion=$RELEASE_VERSION" "-PreleaseWork=$RELEASE_WORK" --no-daemon
144+
145+
- name: Verify Maven and publish the GitHub release
146+
if: inputs.mode == 'publish'
147+
run: |
148+
set -euo pipefail
149+
python3 .github/maven-release/verify.py verify "$RELEASE_VERSION" "$RELEASE_WORK" | tee -a "$GITHUB_STEP_SUMMARY"
150+
gh release create "$RELEASE_VERSION" --verify-tag \
151+
--title "basePower $RELEASE_VERSION" --notes-file "$RELEASE_WORK/release-notes.txt" \
152+
"$RELEASE_WORK/"*.jar

0 commit comments

Comments
 (0)