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
33import hashlib
44import 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
92118def 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" )
0 commit comments