@voxelio/deploy 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,456 +1,456 @@
1
- name: Deploy
2
-
3
- on:
4
- push:
5
- paths:
6
- - '.changeset/*.md'
7
-
8
- jobs:
9
- deploy:
10
- runs-on: ubuntu-latest
11
- permissions:
12
- contents: write
13
-
14
- steps:
15
- - name: Checkout
16
- uses: actions/checkout@v4
17
- with:
18
- token: ${{ secrets.GITHUB_TOKEN }}
19
- fetch-depth: 0
20
-
21
- - name: Setup Node.js
22
- uses: actions/setup-node@v4
23
- with:
24
- node-version: '20'
25
-
26
- - name: Setup Python
27
- uses: actions/setup-python@v5
28
- with:
29
- python-version: '3.11'
30
-
31
- - name: Install dependencies
32
- run: pip install pyyaml
33
-
34
- - name: Detect changeset
35
- id: changeset
36
- run: |
37
- CHANGESET_FILE=$(find .changeset -name "*.md" -type f | head -n 1)
38
- if [ -z "$CHANGESET_FILE" ]; then
39
- echo "found=false" >> $GITHUB_OUTPUT
40
- exit 0
41
- fi
42
- echo "found=true" >> $GITHUB_OUTPUT
43
- echo "file=$CHANGESET_FILE" >> $GITHUB_OUTPUT
44
-
45
- - name: Parse changeset
46
- id: parse
47
- if: steps.changeset.outputs.found == 'true'
48
- run: |
49
- python - <<'EOF'
50
- import re, json, os, yaml
51
-
52
- with open("${{ steps.changeset.outputs.file }}", 'r') as f:
53
- content = f.read()
54
-
55
- match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
56
- if not match:
57
- exit(1)
58
-
59
- frontmatter = yaml.safe_load(match.group(1))
60
- changelog = match.group(2).strip()
61
-
62
- with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
63
- f.write(f"game_versions={json.dumps(frontmatter.get('game_versions', []))}\n")
64
- f.write(f"loaders={json.dumps(frontmatter.get('loaders', None)) if frontmatter.get('loaders') else 'null'}\n")
65
- f.write(f"version_type={frontmatter.get('version_type', 'release')}\n")
66
- f.write(f"version_bump={frontmatter.get('version_bump', 'patch')}\n")
67
- f.write(f"changelog<<EOF\n{changelog}\nEOF\n")
68
- EOF
69
-
70
- - name: Read config
71
- id: config
72
- if: steps.changeset.outputs.found == 'true'
73
- run: |
74
- python - <<'EOF'
75
- import yaml, json, os
76
-
77
- with open('deploy.yaml', 'r') as f:
78
- config = yaml.safe_load(f)
79
-
80
- with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
81
- f.write(f"current_version={config['project']['version']}\n")
82
- f.write(f"project_name={config['project']['name']}\n")
83
- f.write(f"project_filename={config['project']['filename']}\n")
84
- f.write(f"modrinth_enabled={str(config['modrinth']['enabled']).lower()}\n")
85
- f.write(f"modrinth_project_id={config['modrinth']['project_id']}\n")
86
- f.write(f"modrinth_featured={str(config['modrinth'].get('featured', False)).lower()}\n")
87
- f.write(f"curseforge_datapack_enabled={str(config['curseforge']['datapack']['enabled']).lower()}\n")
88
- f.write(f"curseforge_datapack_id={config['curseforge']['datapack'].get('project_id', '')}\n")
89
- f.write(f"curseforge_mod_enabled={str(config['curseforge']['mod']['enabled']).lower()}\n")
90
- f.write(f"curseforge_mod_id={config['curseforge']['mod'].get('project_id', '')}\n")
91
- f.write(f"curseforge_java_versions={json.dumps(config['curseforge']['mod'].get('java_versions', []))}\n")
92
- f.write(f"curseforge_environments={json.dumps(config['curseforge']['mod'].get('environments', []))}\n")
93
- f.write(f"package_as_mod_enabled={str(config['package_as_mod']['enabled']).lower()}\n")
94
- f.write(f"package_as_mod_loaders={json.dumps(config['package_as_mod']['loaders'])}\n")
95
- f.write(f"package_as_mod_id={config['package_as_mod']['id']}\n")
96
- f.write(f"package_as_mod_filename={config['package_as_mod'].get('filename', config['package_as_mod']['id'])}\n")
97
- f.write(f"package_as_mod_authors={json.dumps(config['package_as_mod']['authors'])}\n")
98
- f.write(f"exclude_patterns={json.dumps(config.get('build', {}).get('exclude', []))}\n")
99
- EOF
100
-
101
- - name: Increment version
102
- id: version
103
- if: steps.changeset.outputs.found == 'true'
104
- run: |
105
- python - <<'EOF'
106
- import os
107
-
108
- bump = "${{ steps.parse.outputs.version_bump }}"
109
- current = "${{ steps.config.outputs.current_version }}"
110
- major, minor, patch = map(int, current.split('.'))
111
-
112
- if bump == "major":
113
- major += 1
114
- minor = 0
115
- patch = 0
116
- elif bump == "minor":
117
- minor += 1
118
- patch = 0
119
- elif bump == "patch":
120
- patch += 1
121
-
122
- new_version = f"{major}.{minor}.{patch}"
123
-
124
- with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
125
- f.write(f"version={new_version}\n")
126
- EOF
127
-
128
- - name: Resolve loaders
129
- id: loaders
130
- if: steps.changeset.outputs.found == 'true'
131
- run: |
132
- python - <<'EOF'
133
- import json, os
134
-
135
- changeset_loaders = '${{ steps.parse.outputs.loaders }}'
136
- default_loaders = '${{ steps.config.outputs.package_as_mod_loaders }}'
137
-
138
- if changeset_loaders != 'null':
139
- loaders = json.loads(changeset_loaders)
140
- else:
141
- loaders = json.loads(default_loaders)
142
-
143
- with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
144
- f.write(f"csv={','.join(loaders)}\n")
145
- f.write(f"json={json.dumps(loaders)}\n")
146
- EOF
147
-
148
- - name: Format game versions
149
- id: game_versions
150
- if: steps.changeset.outputs.found == 'true'
151
- run: |
152
- python - <<'EOF'
153
- import json, os
154
-
155
- game_versions = json.loads('${{ steps.parse.outputs.game_versions }}')
156
-
157
- with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
158
- f.write(f"csv={','.join(game_versions)}\n")
159
- f.write(f"json={json.dumps(game_versions)}\n")
160
- EOF
161
-
162
- - name: Create directories
163
- if: steps.changeset.outputs.found == 'true'
164
- run: mkdir -p dist build-temp
165
-
166
- - name: Copy files
167
- if: steps.changeset.outputs.found == 'true'
168
- run: |
169
- python - <<'EOF'
170
- import shutil, json
171
- from pathlib import Path
172
- from fnmatch import fnmatch
173
-
174
- exclude = json.loads('${{ steps.config.outputs.exclude_patterns }}') + ['build-temp', 'dist']
175
-
176
- def should_exclude(path):
177
- for pattern in exclude:
178
- if fnmatch(str(path.name), pattern) or any(fnmatch(str(p), pattern) for p in path.parents):
179
- return True
180
- return False
181
-
182
- src, dst = Path('.'), Path('build-temp')
183
- for item in src.rglob('*'):
184
- rel = item.relative_to(src)
185
- if not should_exclude(rel):
186
- dest = dst / rel
187
- if item.is_dir():
188
- dest.mkdir(parents=True, exist_ok=True)
189
- else:
190
- dest.parent.mkdir(parents=True, exist_ok=True)
191
- shutil.copy2(item, dest)
192
- EOF
193
-
194
- - name: Build datapack
195
- if: steps.changeset.outputs.found == 'true'
196
- run: |
197
- cd build-temp
198
- FILENAME="${{ steps.config.outputs.project_filename }}-${{ steps.version.outputs.version }}.zip"
199
- zip -r ../dist/${FILENAME} . -x "*.git*" "*.DS_Store"
200
- cd ..
201
- [ -f "dist/${FILENAME}" ] || exit 1
202
-
203
- - name: Package as mod
204
- if: steps.changeset.outputs.found == 'true' && steps.config.outputs.package_as_mod_enabled == 'true'
205
- run: |
206
- npm install @voxelio/converter @voxelio/breeze @voxelio/zip
207
-
208
- cat > convert.js <<'SCRIPT'
209
- import { convertDatapack, ModPlatforms } from "@voxelio/converter";
210
- import { readFile, writeFile } from "fs/promises";
211
-
212
- const args = JSON.parse(process.argv[2]);
213
- const buffer = await readFile(args.input);
214
- const file = new File([buffer], "datapack.zip");
215
- const platforms = args.loaders.map(l => ModPlatforms[l.toUpperCase()]).filter(Boolean);
216
-
217
- const response = await convertDatapack(file, platforms, {
218
- id: args.id,
219
- version: args.version,
220
- name: args.name,
221
- description: "",
222
- authors: args.authors
223
- });
224
-
225
- await writeFile(args.output, Buffer.from(await response.arrayBuffer()));
226
- SCRIPT
227
-
228
- MOD_NAME="${{ steps.config.outputs.project_name }}"
229
-
230
- node convert.js "$(jq -n \
231
- --arg input "dist/${{ steps.config.outputs.project_filename }}-${{ steps.version.outputs.version }}.zip" \
232
- --arg output "dist/${{ steps.config.outputs.package_as_mod_filename }}-${{ steps.version.outputs.version }}.jar" \
233
- --arg id "${{ steps.config.outputs.package_as_mod_id }}" \
234
- --arg version "${{ steps.version.outputs.version }}" \
235
- --arg name "$MOD_NAME" \
236
- --argjson authors '${{ steps.config.outputs.package_as_mod_authors }}' \
237
- --argjson loaders '${{ steps.loaders.outputs.json }}' \
238
- '{input:$input, output:$output, id:$id, version:$version, name:$name, authors:$authors, loaders:$loaders}')"
239
-
240
- - name: Fetch CurseForge game versions
241
- id: cf_versions
242
- if: steps.changeset.outputs.found == 'true' && (steps.config.outputs.curseforge_datapack_enabled == 'true' || steps.config.outputs.curseforge_mod_enabled == 'true')
243
- run: |
244
- curl -fsSL "https://minecraft.curseforge.com/api/game/version-types" \
245
- -H "Accept: application/json" \
246
- -H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
247
- -o cf_version_types.json
248
-
249
- curl -fsSL "https://minecraft.curseforge.com/api/game/versions" \
250
- -H "Accept: application/json" \
251
- -H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
252
- -o cf_versions.json
253
-
254
- python - <<'EOF'
255
- import json, os
256
-
257
- with open('cf_version_types.json','r') as f:
258
- version_types = json.load(f)
259
-
260
- with open('cf_versions.json','r') as f:
261
- versions = json.load(f)
262
-
263
- type_by_slug = {vt['slug']: vt['id'] for vt in version_types}
264
-
265
- minecraft_types = [
266
- tid for slug, tid in type_by_slug.items()
267
- if slug.startswith('minecraft-') and 'beta' not in slug.lower()
268
- ]
269
-
270
- modloader_type = type_by_slug.get('modloader')
271
- environment_type = type_by_slug.get('environment')
272
- java_type = type_by_slug.get('java')
273
-
274
- print(f"Minecraft types: {minecraft_types}")
275
- print(f"Modloader type: {modloader_type}")
276
- print(f"Environment type: {environment_type}")
277
- print(f"Java type: {java_type}")
278
-
279
- game_versions = json.loads('${{ steps.game_versions.outputs.json }}')
280
- loaders = json.loads('${{ steps.loaders.outputs.json }}')
281
- java_versions = json.loads('${{ steps.config.outputs.curseforge_java_versions }}')
282
- environments = json.loads('${{ steps.config.outputs.curseforge_environments }}')
283
-
284
- by_type = {}
285
- for v in versions:
286
- type_id = v.get('gameVersionTypeID')
287
- name = v.get('name')
288
- if type_id and name:
289
- if type_id not in by_type:
290
- by_type[type_id] = {}
291
- by_type[type_id][name] = v['id']
292
-
293
- ids = []
294
-
295
- for version in game_versions:
296
- found = False
297
- for type_id in minecraft_types:
298
- if type_id in by_type and version in by_type[type_id]:
299
- ids.append(by_type[type_id][version])
300
- print(f"✓ Minecraft {version} -> ID {by_type[type_id][version]} (type {type_id})")
301
- found = True
302
- break
303
- if not found:
304
- print(f"✗ Minecraft version NOT FOUND: {version}")
305
-
306
- if modloader_type:
307
- for loader in [l.capitalize() for l in loaders]:
308
- if modloader_type in by_type and loader in by_type[modloader_type]:
309
- ids.append(by_type[modloader_type][loader])
310
- print(f"✓ Loader {loader} -> ID {by_type[modloader_type][loader]}")
311
-
312
- if java_type:
313
- for java in java_versions:
314
- if java_type in by_type and java in by_type[java_type]:
315
- ids.append(by_type[java_type][java])
316
- print(f"✓ Java {java} -> ID {by_type[java_type][java]}")
317
-
318
- if environment_type:
319
- for env in [e.capitalize() for e in environments]:
320
- if environment_type in by_type and env in by_type[environment_type]:
321
- ids.append(by_type[environment_type][env])
322
- print(f"✓ Environment {env} -> ID {by_type[environment_type][env]}")
323
-
324
- ids = list(dict.fromkeys(ids))
325
- print(f"\n✅ Final IDs to send: {ids}")
326
-
327
- with open(os.environ['GITHUB_OUTPUT'], 'a') as out:
328
- out.write(f"ids={json.dumps(ids)}\n")
329
- EOF
330
-
331
- - name: Upload to Modrinth (Datapack)
332
- if: steps.changeset.outputs.found == 'true' && steps.config.outputs.modrinth_enabled == 'true'
333
- run: |
334
- FILENAME="${{ steps.config.outputs.project_filename }}-${{ steps.version.outputs.version }}.zip"
335
-
336
- DATA=$(jq -n \
337
- --arg pid "${{ steps.config.outputs.modrinth_project_id }}" \
338
- --arg name "v${{ steps.version.outputs.version }} (Datapack)" \
339
- --arg ver "${{ steps.version.outputs.version }}" \
340
- --arg type "${{ steps.parse.outputs.version_type }}" \
341
- --arg log "${{ steps.parse.outputs.changelog }}" \
342
- --argjson gv '${{ steps.game_versions.outputs.json }}' \
343
- --argjson loaders '["datapack"]' \
344
- --argjson featured ${{ steps.config.outputs.modrinth_featured || 'false' }} \
345
- '{name:$name, version_number:$ver, changelog:$log, game_versions:$gv, loaders:$loaders, project_id:$pid, version_type:$type, dependencies:[], featured:$featured, file_parts:["file"], primary_file:"file"}'
346
- )
347
-
348
- curl -sS -o resp.json -w "%{http_code}" \
349
- -X POST "https://api.modrinth.com/v2/version" \
350
- -H "Authorization: ${{ secrets.MODRINTH_TOKEN }}" \
351
- -H "User-Agent: ${{ github.repository }}" \
352
- -F "data=${DATA};type=application/json" \
353
- -F "file=@dist/${FILENAME};filename=${FILENAME}" | tee code.txt
354
-
355
- cat resp.json | jq .
356
- [ $(cat code.txt) -ge 200 ] && [ $(cat code.txt) -lt 300 ] || exit 1
357
-
358
- - name: Upload to Modrinth (Mod)
359
- if: steps.changeset.outputs.found == 'true' && steps.config.outputs.modrinth_enabled == 'true' && steps.config.outputs.package_as_mod_enabled == 'true'
360
- run: |
361
- FILENAME="${{ steps.config.outputs.package_as_mod_filename }}-${{ steps.version.outputs.version }}.jar"
362
- DATA=$(jq -n \
363
- --arg pid "${{ steps.config.outputs.modrinth_project_id }}" \
364
- --arg name "v${{ steps.version.outputs.version }} (Mod)" \
365
- --arg ver "${{ steps.version.outputs.version }}+mod" \
366
- --arg type "${{ steps.parse.outputs.version_type }}" \
367
- --arg log "${{ steps.parse.outputs.changelog }}" \
368
- --argjson gv '${{ steps.game_versions.outputs.json }}' \
369
- --argjson loaders '${{ steps.config.outputs.package_as_mod_loaders }}' \
370
- --argjson featured ${{ steps.config.outputs.modrinth_featured || 'false' }} \
371
- '{name:$name, version_number:$ver, changelog:$log, game_versions:$gv, loaders:$loaders, project_id:$pid, version_type:$type, dependencies:[], featured:$featured, file_parts:["file"], primary_file:"file"}'
372
- )
373
-
374
- curl -sS -o resp.json -w "%{http_code}" \
375
- -X POST "https://api.modrinth.com/v2/version" \
376
- -H "Authorization: ${{ secrets.MODRINTH_TOKEN }}" \
377
- -H "User-Agent: ${{ github.repository }}" \
378
- -F "data=${DATA};type=application/json" \
379
- -F "file=@dist/${FILENAME};filename=${FILENAME}" | tee code.txt
380
-
381
- cat resp.json | jq .
382
- [ $(cat code.txt) -ge 200 ] && [ $(cat code.txt) -lt 300 ] || exit 1
383
-
384
- - name: Upload to CurseForge (Datapack)
385
- if: steps.changeset.outputs.found == 'true' && steps.config.outputs.curseforge_datapack_enabled == 'true'
386
- run: |
387
- FILENAME="${{ steps.config.outputs.project_filename }}-${{ steps.version.outputs.version }}.zip"
388
-
389
- DATA=$(jq -n \
390
- --arg name "${{ steps.config.outputs.project_name }} - v${{ steps.version.outputs.version }}" \
391
- --arg log "${{ steps.parse.outputs.changelog }}" \
392
- --arg type "${{ steps.parse.outputs.version_type }}" \
393
- --argjson ids '${{ steps.cf_versions.outputs.game_ids }}' \
394
- '{displayName:$name, changelog:$log, changelogType:"markdown", releaseType:$type, gameVersions:$ids}')
395
-
396
- curl -sS -o resp.json -w "%{http_code}" \
397
- -X POST "https://minecraft.curseforge.com/api/projects/${{ steps.config.outputs.curseforge_datapack_id }}/upload-file" \
398
- -H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
399
- -H "User-Agent: ${{ github.repository }}" \
400
- -F "metadata=${DATA};type=application/json" \
401
- -F "file=@dist/${FILENAME}" | tee code.txt
402
-
403
- cat resp.json | jq .
404
- [ $(cat code.txt) -ge 200 ] && [ $(cat code.txt) -lt 300 ] || exit 1
405
-
406
- - name: Upload to CurseForge (Mod)
407
- if: steps.changeset.outputs.found == 'true' && steps.config.outputs.curseforge_mod_enabled == 'true'
408
- run: |
409
- FILENAME="${{ steps.config.outputs.package_as_mod_filename }}-${{ steps.version.outputs.version }}.jar"
410
-
411
- DATA=$(jq -n \
412
- --arg name "${{ steps.config.outputs.project_name }} - v${{ steps.version.outputs.version }}" \
413
- --arg log "${{ steps.parse.outputs.changelog }}" \
414
- --arg type "${{ steps.parse.outputs.version_type }}" \
415
- --argjson ids '${{ steps.cf_versions.outputs.ids }}' \
416
- '{displayName:$name, changelog:$log, changelogType:"markdown", releaseType:$type, gameVersions:$ids}')
417
-
418
- curl -sS -o resp.json -w "%{http_code}" \
419
- -X POST "https://minecraft.curseforge.com/api/projects/${{ steps.config.outputs.curseforge_mod_id }}/upload-file" \
420
- -H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
421
- -H "User-Agent: ${{ github.repository }}" \
422
- -F "metadata=${DATA};type=application/json" \
423
- -F "file=@dist/${FILENAME}" | tee code.txt
424
-
425
- cat resp.json | jq .
426
- [ $(cat code.txt) -ge 200 ] && [ $(cat code.txt) -lt 300 ] || exit 1
427
-
428
- - name: Update config
429
- if: steps.changeset.outputs.found == 'true'
430
- run: |
431
- python - <<'EOF'
432
- import re
433
-
434
- with open('deploy.yaml', 'r') as f:
435
- content = f.read()
436
-
437
- content = re.sub(
438
- r'(version:\s*["\']?)[0-9.]+(["\']?)',
439
- r'\g<1>${{ steps.version.outputs.version }}\g<2>',
440
- content
441
- )
442
-
443
- with open('deploy.yaml', 'w') as f:
444
- f.write(content)
445
- EOF
446
-
447
- - name: Commit
448
- if: steps.changeset.outputs.found == 'true'
449
- run: |
450
- rm ${{ steps.changeset.outputs.file }}
451
- rm -f .changeset/*.md
452
- git config user.name "github-actions[bot]"
453
- git config user.email "github-actions[bot]@users.noreply.github.com"
454
- git add deploy.yaml .changeset
455
- git commit -m "chore: release v${{ steps.version.outputs.version }}"
456
- git push
1
+ name: Deploy
2
+
3
+ on:
4
+ push:
5
+ paths:
6
+ - '.changeset/*.md'
7
+
8
+ jobs:
9
+ deploy:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+
14
+ steps:
15
+ - name: Checkout
16
+ uses: actions/checkout@v4
17
+ with:
18
+ token: ${{ secrets.GITHUB_TOKEN }}
19
+ fetch-depth: 0
20
+
21
+ - name: Setup Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: '20'
25
+
26
+ - name: Setup Python
27
+ uses: actions/setup-python@v5
28
+ with:
29
+ python-version: '3.11'
30
+
31
+ - name: Install dependencies
32
+ run: pip install pyyaml
33
+
34
+ - name: Detect changeset
35
+ id: changeset
36
+ run: |
37
+ CHANGESET_FILE=$(find .changeset -name "*.md" -type f | head -n 1)
38
+ if [ -z "$CHANGESET_FILE" ]; then
39
+ echo "found=false" >> $GITHUB_OUTPUT
40
+ exit 0
41
+ fi
42
+ echo "found=true" >> $GITHUB_OUTPUT
43
+ echo "file=$CHANGESET_FILE" >> $GITHUB_OUTPUT
44
+
45
+ - name: Parse changeset
46
+ id: parse
47
+ if: steps.changeset.outputs.found == 'true'
48
+ run: |
49
+ python - <<'EOF'
50
+ import re, json, os, yaml
51
+
52
+ with open("${{ steps.changeset.outputs.file }}", 'r') as f:
53
+ content = f.read()
54
+
55
+ match = re.match(r'^---\n(.*?)\n---\n(.*)$', content, re.DOTALL)
56
+ if not match:
57
+ exit(1)
58
+
59
+ frontmatter = yaml.safe_load(match.group(1))
60
+ changelog = match.group(2).strip()
61
+
62
+ with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
63
+ f.write(f"game_versions={json.dumps(frontmatter.get('game_versions', []))}\n")
64
+ f.write(f"loaders={json.dumps(frontmatter.get('loaders', None)) if frontmatter.get('loaders') else 'null'}\n")
65
+ f.write(f"version_type={frontmatter.get('version_type', 'release')}\n")
66
+ f.write(f"version_bump={frontmatter.get('version_bump', 'patch')}\n")
67
+ f.write(f"changelog<<EOF\n{changelog}\nEOF\n")
68
+ EOF
69
+
70
+ - name: Read config
71
+ id: config
72
+ if: steps.changeset.outputs.found == 'true'
73
+ run: |
74
+ python - <<'EOF'
75
+ import yaml, json, os
76
+
77
+ with open('deploy.yaml', 'r') as f:
78
+ config = yaml.safe_load(f)
79
+
80
+ with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
81
+ f.write(f"current_version={config['project']['version']}\n")
82
+ f.write(f"project_name={config['project']['name']}\n")
83
+ f.write(f"project_filename={config['project']['filename']}\n")
84
+ f.write(f"modrinth_enabled={str(config['modrinth']['enabled']).lower()}\n")
85
+ f.write(f"modrinth_project_id={config['modrinth']['project_id']}\n")
86
+ f.write(f"modrinth_featured={str(config['modrinth'].get('featured', False)).lower()}\n")
87
+ f.write(f"curseforge_datapack_enabled={str(config['curseforge']['datapack']['enabled']).lower()}\n")
88
+ f.write(f"curseforge_datapack_id={config['curseforge']['datapack'].get('project_id', '')}\n")
89
+ f.write(f"curseforge_mod_enabled={str(config['curseforge']['mod']['enabled']).lower()}\n")
90
+ f.write(f"curseforge_mod_id={config['curseforge']['mod'].get('project_id', '')}\n")
91
+ f.write(f"curseforge_java_versions={json.dumps(config['curseforge']['mod'].get('java_versions', []))}\n")
92
+ f.write(f"curseforge_environments={json.dumps(config['curseforge']['mod'].get('environments', []))}\n")
93
+ f.write(f"package_as_mod_enabled={str(config['package_as_mod']['enabled']).lower()}\n")
94
+ f.write(f"package_as_mod_loaders={json.dumps(config['package_as_mod']['loaders'])}\n")
95
+ f.write(f"package_as_mod_id={config['package_as_mod']['id']}\n")
96
+ f.write(f"package_as_mod_filename={config['package_as_mod'].get('filename', config['package_as_mod']['id'])}\n")
97
+ f.write(f"package_as_mod_authors={json.dumps(config['package_as_mod']['authors'])}\n")
98
+ f.write(f"exclude_patterns={json.dumps(config.get('build', {}).get('exclude', []))}\n")
99
+ EOF
100
+
101
+ - name: Increment version
102
+ id: version
103
+ if: steps.changeset.outputs.found == 'true'
104
+ run: |
105
+ python - <<'EOF'
106
+ import os
107
+
108
+ bump = "${{ steps.parse.outputs.version_bump }}"
109
+ current = "${{ steps.config.outputs.current_version }}"
110
+ major, minor, patch = map(int, current.split('.'))
111
+
112
+ if bump == "major":
113
+ major += 1
114
+ minor = 0
115
+ patch = 0
116
+ elif bump == "minor":
117
+ minor += 1
118
+ patch = 0
119
+ elif bump == "patch":
120
+ patch += 1
121
+
122
+ new_version = f"{major}.{minor}.{patch}"
123
+
124
+ with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
125
+ f.write(f"version={new_version}\n")
126
+ EOF
127
+
128
+ - name: Resolve loaders
129
+ id: loaders
130
+ if: steps.changeset.outputs.found == 'true'
131
+ run: |
132
+ python - <<'EOF'
133
+ import json, os
134
+
135
+ changeset_loaders = '${{ steps.parse.outputs.loaders }}'
136
+ default_loaders = '${{ steps.config.outputs.package_as_mod_loaders }}'
137
+
138
+ if changeset_loaders != 'null':
139
+ loaders = json.loads(changeset_loaders)
140
+ else:
141
+ loaders = json.loads(default_loaders)
142
+
143
+ with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
144
+ f.write(f"csv={','.join(loaders)}\n")
145
+ f.write(f"json={json.dumps(loaders)}\n")
146
+ EOF
147
+
148
+ - name: Format game versions
149
+ id: game_versions
150
+ if: steps.changeset.outputs.found == 'true'
151
+ run: |
152
+ python - <<'EOF'
153
+ import json, os
154
+
155
+ game_versions = json.loads('${{ steps.parse.outputs.game_versions }}')
156
+
157
+ with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
158
+ f.write(f"csv={','.join(game_versions)}\n")
159
+ f.write(f"json={json.dumps(game_versions)}\n")
160
+ EOF
161
+
162
+ - name: Create directories
163
+ if: steps.changeset.outputs.found == 'true'
164
+ run: mkdir -p dist build-temp
165
+
166
+ - name: Copy files
167
+ if: steps.changeset.outputs.found == 'true'
168
+ run: |
169
+ python - <<'EOF'
170
+ import shutil, json
171
+ from pathlib import Path
172
+ from fnmatch import fnmatch
173
+
174
+ exclude = json.loads('${{ steps.config.outputs.exclude_patterns }}') + ['build-temp', 'dist']
175
+
176
+ def should_exclude(path):
177
+ for pattern in exclude:
178
+ if fnmatch(str(path.name), pattern) or any(fnmatch(str(p), pattern) for p in path.parents):
179
+ return True
180
+ return False
181
+
182
+ src, dst = Path('.'), Path('build-temp')
183
+ for item in src.rglob('*'):
184
+ rel = item.relative_to(src)
185
+ if not should_exclude(rel):
186
+ dest = dst / rel
187
+ if item.is_dir():
188
+ dest.mkdir(parents=True, exist_ok=True)
189
+ else:
190
+ dest.parent.mkdir(parents=True, exist_ok=True)
191
+ shutil.copy2(item, dest)
192
+ EOF
193
+
194
+ - name: Build datapack
195
+ if: steps.changeset.outputs.found == 'true'
196
+ run: |
197
+ cd build-temp
198
+ FILENAME="${{ steps.config.outputs.project_filename }}-${{ steps.version.outputs.version }}.zip"
199
+ zip -r ../dist/${FILENAME} . -x "*.git*" "*.DS_Store"
200
+ cd ..
201
+ [ -f "dist/${FILENAME}" ] || exit 1
202
+
203
+ - name: Package as mod
204
+ if: steps.changeset.outputs.found == 'true' && steps.config.outputs.package_as_mod_enabled == 'true'
205
+ run: |
206
+ npm install @voxelio/converter @voxelio/breeze @voxelio/zip
207
+
208
+ cat > convert.js <<'SCRIPT'
209
+ import { convertDatapack, ModPlatforms } from "@voxelio/converter";
210
+ import { readFile, writeFile } from "fs/promises";
211
+
212
+ const args = JSON.parse(process.argv[2]);
213
+ const buffer = await readFile(args.input);
214
+ const file = new File([buffer], "datapack.zip");
215
+ const platforms = args.loaders.map(l => ModPlatforms[l.toUpperCase()]).filter(Boolean);
216
+
217
+ const response = await convertDatapack(file, platforms, {
218
+ id: args.id,
219
+ version: args.version,
220
+ name: args.name,
221
+ description: "",
222
+ authors: args.authors
223
+ });
224
+
225
+ await writeFile(args.output, Buffer.from(await response.arrayBuffer()));
226
+ SCRIPT
227
+
228
+ MOD_NAME="${{ steps.config.outputs.project_name }}"
229
+
230
+ node convert.js "$(jq -n \
231
+ --arg input "dist/${{ steps.config.outputs.project_filename }}-${{ steps.version.outputs.version }}.zip" \
232
+ --arg output "dist/${{ steps.config.outputs.package_as_mod_filename }}-${{ steps.version.outputs.version }}.jar" \
233
+ --arg id "${{ steps.config.outputs.package_as_mod_id }}" \
234
+ --arg version "${{ steps.version.outputs.version }}" \
235
+ --arg name "$MOD_NAME" \
236
+ --argjson authors '${{ steps.config.outputs.package_as_mod_authors }}' \
237
+ --argjson loaders '${{ steps.loaders.outputs.json }}' \
238
+ '{input:$input, output:$output, id:$id, version:$version, name:$name, authors:$authors, loaders:$loaders}')"
239
+
240
+ - name: Fetch CurseForge game versions
241
+ id: cf_versions
242
+ if: steps.changeset.outputs.found == 'true' && (steps.config.outputs.curseforge_datapack_enabled == 'true' || steps.config.outputs.curseforge_mod_enabled == 'true')
243
+ run: |
244
+ curl -fsSL "https://minecraft.curseforge.com/api/game/version-types" \
245
+ -H "Accept: application/json" \
246
+ -H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
247
+ -o cf_version_types.json
248
+
249
+ curl -fsSL "https://minecraft.curseforge.com/api/game/versions" \
250
+ -H "Accept: application/json" \
251
+ -H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
252
+ -o cf_versions.json
253
+
254
+ python - <<'EOF'
255
+ import json, os
256
+
257
+ with open('cf_version_types.json','r') as f:
258
+ version_types = json.load(f)
259
+
260
+ with open('cf_versions.json','r') as f:
261
+ versions = json.load(f)
262
+
263
+ type_by_slug = {vt['slug']: vt['id'] for vt in version_types}
264
+
265
+ minecraft_types = [
266
+ tid for slug, tid in type_by_slug.items()
267
+ if slug.startswith('minecraft-') and 'beta' not in slug.lower()
268
+ ]
269
+
270
+ modloader_type = type_by_slug.get('modloader')
271
+ environment_type = type_by_slug.get('environment')
272
+ java_type = type_by_slug.get('java')
273
+
274
+ print(f"Minecraft types: {minecraft_types}")
275
+ print(f"Modloader type: {modloader_type}")
276
+ print(f"Environment type: {environment_type}")
277
+ print(f"Java type: {java_type}")
278
+
279
+ game_versions = json.loads('${{ steps.game_versions.outputs.json }}')
280
+ loaders = json.loads('${{ steps.loaders.outputs.json }}')
281
+ java_versions = json.loads('${{ steps.config.outputs.curseforge_java_versions }}')
282
+ environments = json.loads('${{ steps.config.outputs.curseforge_environments }}')
283
+
284
+ by_type = {}
285
+ for v in versions:
286
+ type_id = v.get('gameVersionTypeID')
287
+ name = v.get('name')
288
+ if type_id and name:
289
+ if type_id not in by_type:
290
+ by_type[type_id] = {}
291
+ by_type[type_id][name] = v['id']
292
+
293
+ ids = []
294
+
295
+ for version in game_versions:
296
+ found = False
297
+ for type_id in minecraft_types:
298
+ if type_id in by_type and version in by_type[type_id]:
299
+ ids.append(by_type[type_id][version])
300
+ print(f"✓ Minecraft {version} -> ID {by_type[type_id][version]} (type {type_id})")
301
+ found = True
302
+ break
303
+ if not found:
304
+ print(f"✗ Minecraft version NOT FOUND: {version}")
305
+
306
+ if modloader_type:
307
+ for loader in [l.capitalize() for l in loaders]:
308
+ if modloader_type in by_type and loader in by_type[modloader_type]:
309
+ ids.append(by_type[modloader_type][loader])
310
+ print(f"✓ Loader {loader} -> ID {by_type[modloader_type][loader]}")
311
+
312
+ if java_type:
313
+ for java in java_versions:
314
+ if java_type in by_type and java in by_type[java_type]:
315
+ ids.append(by_type[java_type][java])
316
+ print(f"✓ Java {java} -> ID {by_type[java_type][java]}")
317
+
318
+ if environment_type:
319
+ for env in [e.capitalize() for e in environments]:
320
+ if environment_type in by_type and env in by_type[environment_type]:
321
+ ids.append(by_type[environment_type][env])
322
+ print(f"✓ Environment {env} -> ID {by_type[environment_type][env]}")
323
+
324
+ ids = list(dict.fromkeys(ids))
325
+ print(f"\n✅ Final IDs to send: {ids}")
326
+
327
+ with open(os.environ['GITHUB_OUTPUT'], 'a') as out:
328
+ out.write(f"ids={json.dumps(ids)}\n")
329
+ EOF
330
+
331
+ - name: Upload to Modrinth (Datapack)
332
+ if: steps.changeset.outputs.found == 'true' && steps.config.outputs.modrinth_enabled == 'true'
333
+ run: |
334
+ FILENAME="${{ steps.config.outputs.project_filename }}-${{ steps.version.outputs.version }}.zip"
335
+
336
+ DATA=$(jq -n \
337
+ --arg pid "${{ steps.config.outputs.modrinth_project_id }}" \
338
+ --arg name "v${{ steps.version.outputs.version }} (Datapack)" \
339
+ --arg ver "${{ steps.version.outputs.version }}" \
340
+ --arg type "${{ steps.parse.outputs.version_type }}" \
341
+ --arg log "${{ steps.parse.outputs.changelog }}" \
342
+ --argjson gv '${{ steps.game_versions.outputs.json }}' \
343
+ --argjson loaders '["datapack"]' \
344
+ --argjson featured ${{ steps.config.outputs.modrinth_featured || 'false' }} \
345
+ '{name:$name, version_number:$ver, changelog:$log, game_versions:$gv, loaders:$loaders, project_id:$pid, version_type:$type, dependencies:[], featured:$featured, file_parts:["file"], primary_file:"file"}'
346
+ )
347
+
348
+ curl -sS -o resp.json -w "%{http_code}" \
349
+ -X POST "https://api.modrinth.com/v2/version" \
350
+ -H "Authorization: ${{ secrets.MODRINTH_TOKEN }}" \
351
+ -H "User-Agent: ${{ github.repository }}" \
352
+ -F "data=${DATA};type=application/json" \
353
+ -F "file=@dist/${FILENAME};filename=${FILENAME}" | tee code.txt
354
+
355
+ cat resp.json | jq .
356
+ [ $(cat code.txt) -ge 200 ] && [ $(cat code.txt) -lt 300 ] || exit 1
357
+
358
+ - name: Upload to Modrinth (Mod)
359
+ if: steps.changeset.outputs.found == 'true' && steps.config.outputs.modrinth_enabled == 'true' && steps.config.outputs.package_as_mod_enabled == 'true'
360
+ run: |
361
+ FILENAME="${{ steps.config.outputs.package_as_mod_filename }}-${{ steps.version.outputs.version }}.jar"
362
+ DATA=$(jq -n \
363
+ --arg pid "${{ steps.config.outputs.modrinth_project_id }}" \
364
+ --arg name "v${{ steps.version.outputs.version }} (Mod)" \
365
+ --arg ver "${{ steps.version.outputs.version }}+mod" \
366
+ --arg type "${{ steps.parse.outputs.version_type }}" \
367
+ --arg log "${{ steps.parse.outputs.changelog }}" \
368
+ --argjson gv '${{ steps.game_versions.outputs.json }}' \
369
+ --argjson loaders '${{ steps.config.outputs.package_as_mod_loaders }}' \
370
+ --argjson featured ${{ steps.config.outputs.modrinth_featured || 'false' }} \
371
+ '{name:$name, version_number:$ver, changelog:$log, game_versions:$gv, loaders:$loaders, project_id:$pid, version_type:$type, dependencies:[], featured:$featured, file_parts:["file"], primary_file:"file"}'
372
+ )
373
+
374
+ curl -sS -o resp.json -w "%{http_code}" \
375
+ -X POST "https://api.modrinth.com/v2/version" \
376
+ -H "Authorization: ${{ secrets.MODRINTH_TOKEN }}" \
377
+ -H "User-Agent: ${{ github.repository }}" \
378
+ -F "data=${DATA};type=application/json" \
379
+ -F "file=@dist/${FILENAME};filename=${FILENAME}" | tee code.txt
380
+
381
+ cat resp.json | jq .
382
+ [ $(cat code.txt) -ge 200 ] && [ $(cat code.txt) -lt 300 ] || exit 1
383
+
384
+ - name: Upload to CurseForge (Datapack)
385
+ if: steps.changeset.outputs.found == 'true' && steps.config.outputs.curseforge_datapack_enabled == 'true'
386
+ run: |
387
+ FILENAME="${{ steps.config.outputs.project_filename }}-${{ steps.version.outputs.version }}.zip"
388
+
389
+ DATA=$(jq -n \
390
+ --arg name "${{ steps.config.outputs.project_name }} - v${{ steps.version.outputs.version }}" \
391
+ --arg log "${{ steps.parse.outputs.changelog }}" \
392
+ --arg type "${{ steps.parse.outputs.version_type }}" \
393
+ --argjson ids '${{ steps.cf_versions.outputs.game_ids }}' \
394
+ '{displayName:$name, changelog:$log, changelogType:"markdown", releaseType:$type, gameVersions:$ids}')
395
+
396
+ curl -sS -o resp.json -w "%{http_code}" \
397
+ -X POST "https://minecraft.curseforge.com/api/projects/${{ steps.config.outputs.curseforge_datapack_id }}/upload-file" \
398
+ -H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
399
+ -H "User-Agent: ${{ github.repository }}" \
400
+ -F "metadata=${DATA};type=application/json" \
401
+ -F "file=@dist/${FILENAME}" | tee code.txt
402
+
403
+ cat resp.json | jq .
404
+ [ $(cat code.txt) -ge 200 ] && [ $(cat code.txt) -lt 300 ] || exit 1
405
+
406
+ - name: Upload to CurseForge (Mod)
407
+ if: steps.changeset.outputs.found == 'true' && steps.config.outputs.curseforge_mod_enabled == 'true'
408
+ run: |
409
+ FILENAME="${{ steps.config.outputs.package_as_mod_filename }}-${{ steps.version.outputs.version }}.jar"
410
+
411
+ DATA=$(jq -n \
412
+ --arg name "${{ steps.config.outputs.project_name }} - v${{ steps.version.outputs.version }}" \
413
+ --arg log "${{ steps.parse.outputs.changelog }}" \
414
+ --arg type "${{ steps.parse.outputs.version_type }}" \
415
+ --argjson ids '${{ steps.cf_versions.outputs.ids }}' \
416
+ '{displayName:$name, changelog:$log, changelogType:"markdown", releaseType:$type, gameVersions:$ids}')
417
+
418
+ curl -sS -o resp.json -w "%{http_code}" \
419
+ -X POST "https://minecraft.curseforge.com/api/projects/${{ steps.config.outputs.curseforge_mod_id }}/upload-file" \
420
+ -H "X-Api-Token: ${{ secrets.CURSEFORGE_TOKEN }}" \
421
+ -H "User-Agent: ${{ github.repository }}" \
422
+ -F "metadata=${DATA};type=application/json" \
423
+ -F "file=@dist/${FILENAME}" | tee code.txt
424
+
425
+ cat resp.json | jq .
426
+ [ $(cat code.txt) -ge 200 ] && [ $(cat code.txt) -lt 300 ] || exit 1
427
+
428
+ - name: Update config
429
+ if: steps.changeset.outputs.found == 'true'
430
+ run: |
431
+ python - <<'EOF'
432
+ import re
433
+
434
+ with open('deploy.yaml', 'r') as f:
435
+ content = f.read()
436
+
437
+ content = re.sub(
438
+ r'(version:\s*["\']?)[0-9.]+(["\']?)',
439
+ r'\g<1>${{ steps.version.outputs.version }}\g<2>',
440
+ content
441
+ )
442
+
443
+ with open('deploy.yaml', 'w') as f:
444
+ f.write(content)
445
+ EOF
446
+
447
+ - name: Commit
448
+ if: steps.changeset.outputs.found == 'true'
449
+ run: |
450
+ rm ${{ steps.changeset.outputs.file }}
451
+ rm -f .changeset/*.md
452
+ git config user.name "github-actions[bot]"
453
+ git config user.email "github-actions[bot]@users.noreply.github.com"
454
+ git add deploy.yaml .changeset
455
+ git commit -m "chore: release v${{ steps.version.outputs.version }}"
456
+ git push