semantic-release-minecraft 1.0.0

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.
Files changed (42) hide show
  1. package/.github/workflows/release.yml +43 -0
  2. package/.prettierrc +18 -0
  3. package/.releaserc.json +51 -0
  4. package/CHANGELOG.md +10 -0
  5. package/dist/curseforge.d.ts +4 -0
  6. package/dist/curseforge.d.ts.map +1 -0
  7. package/dist/curseforge.js +131 -0
  8. package/dist/curseforge.js.map +1 -0
  9. package/dist/definitions/curseforge.d.ts +40 -0
  10. package/dist/definitions/curseforge.d.ts.map +1 -0
  11. package/dist/definitions/curseforge.js +6 -0
  12. package/dist/definitions/curseforge.js.map +1 -0
  13. package/dist/definitions/plugin_config.d.ts +49 -0
  14. package/dist/definitions/plugin_config.d.ts.map +1 -0
  15. package/dist/definitions/plugin_config.js +2 -0
  16. package/dist/definitions/plugin_config.js.map +1 -0
  17. package/dist/index.d.ts +8 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +60 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/modrinth.d.ts +7 -0
  22. package/dist/modrinth.d.ts.map +1 -0
  23. package/dist/modrinth.js +238 -0
  24. package/dist/modrinth.js.map +1 -0
  25. package/dist/prepare.d.ts +11 -0
  26. package/dist/prepare.d.ts.map +1 -0
  27. package/dist/prepare.js +88 -0
  28. package/dist/prepare.js.map +1 -0
  29. package/dist/utils/utils.d.ts +29 -0
  30. package/dist/utils/utils.d.ts.map +1 -0
  31. package/dist/utils/utils.js +92 -0
  32. package/dist/utils/utils.js.map +1 -0
  33. package/eslint.config.js +22 -0
  34. package/package.json +49 -0
  35. package/src/curseforge.ts +192 -0
  36. package/src/definitions/curseforge.ts +51 -0
  37. package/src/definitions/plugin_config.ts +68 -0
  38. package/src/index.ts +98 -0
  39. package/src/modrinth.ts +316 -0
  40. package/src/prepare.ts +199 -0
  41. package/src/utils/utils.ts +120 -0
  42. package/tsconfig.json +25 -0
@@ -0,0 +1,192 @@
1
+ import axios from 'axios';
2
+ import FormData from 'form-data';
3
+ import { readFile } from 'fs/promises';
4
+ import { template } from 'lodash';
5
+ import { PublishContext } from 'semantic-release';
6
+ import { Plugin_config } from './definitions/plugin_config';
7
+ import {
8
+ findFiles,
9
+ getCurseForgeModLoaders,
10
+ resolveTemplate,
11
+ } from './utils/utils';
12
+
13
+ /**
14
+ * 为 CurseForge 查找单个文件
15
+ */
16
+ async function findFileCurseforge(
17
+ pluginConfig: Plugin_config,
18
+ context: PublishContext
19
+ ): Promise<string> {
20
+ const { logger } = context;
21
+ const version = context.nextRelease.version;
22
+
23
+ // 获取 CurseForge 的 glob 配置,如果没有则使用全局配置
24
+ const curseforgeGlob =
25
+ pluginConfig.curseforge?.glob || pluginConfig.glob || [];
26
+
27
+ // 查找文件
28
+ const files = await findFiles(curseforgeGlob, context);
29
+
30
+ // 如果只找到一个文件,直接返回
31
+ if (files.length === 1) {
32
+ logger.log(`Using the only found file for CurseForge: ${files[0]}`);
33
+ return files[0];
34
+ }
35
+
36
+ logger.log(
37
+ `Found ${files.length} files for CurseForge, trying to filter by version`
38
+ );
39
+
40
+ // 如果有版本号,尝试筛选包含版本号的文件
41
+ if (version) {
42
+ const versionedFiles = files.filter((file) => file.includes(version));
43
+ if (versionedFiles.length === 1) {
44
+ logger.log(
45
+ `Using versioned file for CurseForge: ${versionedFiles[0]}`
46
+ );
47
+ return versionedFiles[0];
48
+ } else if (versionedFiles.length > 1) {
49
+ throw new Error(
50
+ `Multiple files found containing version ${version} for CurseForge. Please specify a more specific glob pattern. Found: ${versionedFiles.join(', ')}`
51
+ );
52
+ }
53
+ }
54
+
55
+ // 如果无法筛选出单个文件,报错
56
+ throw new Error(
57
+ `Multiple files found for CurseForge but could not determine which to use. Found: ${files.join(', ')}`
58
+ );
59
+ }
60
+
61
+ export async function publishToCurseforge(
62
+ pluginConfig: Plugin_config,
63
+ context: PublishContext,
64
+ curseforgeGameVersionIds?: number[]
65
+ ): Promise<string> {
66
+ const { env, logger } = context;
67
+ const { curseforge } = pluginConfig;
68
+ const apiKey = env.CURSEFORGE_TOKEN!;
69
+ const projectId = curseforge!.project_id!;
70
+ const nextRelease = context.nextRelease;
71
+
72
+ // 查找单个文件
73
+ const filePath = await findFileCurseforge(pluginConfig, context);
74
+ logger.log(
75
+ `Publishing file ${filePath} to CurseForge project ${projectId}...`
76
+ );
77
+
78
+ const form = new FormData();
79
+ const file = await readFile(filePath);
80
+ form.append('file', file, {
81
+ filename: filePath.split('\\').pop() || 'mod.jar',
82
+ });
83
+
84
+ // 准备 metadata
85
+ // display_name 按照优先级:平台特定配置 > 全局配置 > 平台特定环境变量 > 全局环境变量
86
+ const displayName = resolveTemplate(
87
+ [
88
+ curseforge?.display_name,
89
+ pluginConfig.display_name,
90
+ env.CURSEFORGE_DISPLAY_NAME,
91
+ env.DISPLAY_NAME,
92
+ ],
93
+ nextRelease
94
+ );
95
+
96
+ // 准备基础 metadata
97
+ const metadata: any = {
98
+ gameVersions: curseforgeGameVersionIds,
99
+ releaseType: pluginConfig.release_type || 'release',
100
+ changelog: curseforge?.changelog || context.nextRelease.notes,
101
+ changelogType: curseforge?.changelog_type || 'markdown',
102
+ };
103
+
104
+ // 只有找到值时才添加 displayName
105
+ if (displayName) {
106
+ metadata.displayName = displayName;
107
+ } else {
108
+ // 如果没有找到任何配置,使用默认值
109
+ metadata.displayName = context.nextRelease.name;
110
+ }
111
+
112
+ // 添加 metadataModLoaders(如果有值)
113
+ const metadataModLoaders = getCurseForgeModLoaders(
114
+ pluginConfig,
115
+ env,
116
+ nextRelease
117
+ );
118
+ if (metadataModLoaders) {
119
+ metadata.modLoaders = metadataModLoaders;
120
+ }
121
+
122
+ // 只有当提供值时才添加这三个特定字段
123
+ if (curseforge?.parent_file_id !== undefined) {
124
+ metadata.parentFileID = curseforge.parent_file_id;
125
+ }
126
+
127
+ if (curseforge?.is_marked_for_manual_release !== undefined) {
128
+ metadata.isMarkedForManualRelease =
129
+ curseforge.is_marked_for_manual_release;
130
+ }
131
+
132
+ if (curseforge?.relations && Object.keys(curseforge.relations).length > 0) {
133
+ metadata.relations = curseforge.relations;
134
+ }
135
+
136
+ form.append('metadata', JSON.stringify(metadata));
137
+
138
+ try {
139
+ const response = await axios.post(
140
+ `https://upload.curseforge.com/api/projects/${projectId}/upload-file`,
141
+ form,
142
+ {
143
+ headers: {
144
+ ...form.getHeaders(),
145
+ 'X-API-Key': apiKey,
146
+ },
147
+ }
148
+ );
149
+
150
+ // 验证返回的数据是否包含 id 字段
151
+ if (response.data && typeof response.data.id === 'number') {
152
+ logger.log(
153
+ `Successfully published to CurseForge: ${response.data.displayName || `File ID: ${response.data.id}`}`
154
+ );
155
+ return response.data.id.toString();
156
+ } else {
157
+ throw new Error(
158
+ `CurseForge API returned unexpected response: ${JSON.stringify(response.data)}`
159
+ );
160
+ }
161
+ } catch (error: any) {
162
+ if (error.response) {
163
+ // 服务器返回了错误响应
164
+ const status = error.response.status;
165
+ const data = error.response.data;
166
+
167
+ if (data && data.error && data.description) {
168
+ logger.error(
169
+ `CurseForge API Error (${status}): ${data.error} - ${data.description}`
170
+ );
171
+ throw new Error(
172
+ `CurseForge 发布失败:${data.error} - ${data.description}`
173
+ );
174
+ } else {
175
+ logger.error(
176
+ `CurseForge API Error (${status}): ${JSON.stringify(data)}`
177
+ );
178
+ throw new Error(
179
+ `CurseForge 发布失败 (状态码:${status}): ${JSON.stringify(data)}`
180
+ );
181
+ }
182
+ } else if (error.request) {
183
+ // 请求已发送但未收到响应
184
+ logger.error('CurseForge API 请求失败,未收到响应');
185
+ throw new Error('CurseForge 发布失败:未收到服务器响应');
186
+ } else {
187
+ // 请求配置出错
188
+ logger.error('CurseForge API 请求配置错误:', error.message);
189
+ throw new Error(`CurseForge 发布失败:${error.message}`);
190
+ }
191
+ }
192
+ }
@@ -0,0 +1,51 @@
1
+ export interface CurseForgeGameVersion {
2
+ id: number;
3
+ gameVersionTypeID: number;
4
+ name: string;
5
+ slug: string;
6
+ url: string;
7
+ }
8
+
9
+ export interface CurseForgeGameVersionType {
10
+ id: number;
11
+ name: string;
12
+ slug: string;
13
+ }
14
+
15
+ export interface CurseForgeGameVersionMap {
16
+ /**
17
+ * Minecraft 游戏版本的数组。
18
+ */
19
+ game_versions: CurseForgeGameVersion[];
20
+
21
+ /**
22
+ * 主要用于 Bukkit 插件的游戏版本数组。
23
+ */
24
+ game_versions_for_plugins: CurseForgeGameVersion[];
25
+
26
+ /**
27
+ * 附加组件的游戏版本数组。
28
+ */
29
+ game_versions_for_addons: CurseForgeGameVersion[];
30
+
31
+ /**
32
+ * Java 版本的数组。
33
+ */
34
+ java_versions: CurseForgeGameVersion[];
35
+
36
+ /**
37
+ * 模组加载器的游戏版本数组。
38
+ */
39
+ loaders: CurseForgeGameVersion[];
40
+
41
+ /**
42
+ * 不同环境的游戏版本数组。
43
+ */
44
+ environments: CurseForgeGameVersion[];
45
+ }
46
+
47
+ export const BUKKIT_GAME_VERSION_TYPE: CurseForgeGameVersionType = {
48
+ id: 1,
49
+ name: 'Bukkit',
50
+ slug: 'bukkit',
51
+ };
@@ -0,0 +1,68 @@
1
+ export type Plugin_config = {
2
+ // 全局发布相关配置
3
+ release_type?: 'alpha' | 'beta' | 'release'; // 全局发布类型
4
+ game_versions?: string | string[]; // 全局游戏版本列表
5
+ mod_loaders?: string | string[]; // 全局模组加载器列表
6
+ display_name?: string;
7
+
8
+ // 全局文件查找相关配置(如果平台没有指定自己的 glob,则使用此配置)
9
+ glob?: string | string[]; // glob 模式,用于查找要发布的文件
10
+
11
+ // Minecraft 发布相关配置
12
+ curseforge?: {
13
+ project_id: string;
14
+ game_versions?: string | string[]; // CurseForge 专用的游戏版本列表
15
+ java_versions?: string | string[];
16
+ environments?: string | string[];
17
+ game_versions_for_plugins?: string | string[];
18
+ game_versions_for_addon?: string | string[];
19
+ mod_loaders?: string | string[]; // CurseForge 专用的模组加载器列表
20
+ changelog?: string;
21
+ changelog_type?: 'text' | 'html' | 'markdown';
22
+ display_name?: string; // 可选:网站上显示的友好名称
23
+ parent_file_id?: number; // 可选:此文件的父文件 ID
24
+ is_marked_for_manual_release?: boolean; // 可选:如果为 true,文件获批后不会立即发布
25
+ relations?: {
26
+ projects?: Array<{
27
+ slug: string; // 相关插件的 slug
28
+ project_id?: string; // 可选:用于精确匹配项目
29
+ type:
30
+ | 'embedded_library'
31
+ | 'incompatible'
32
+ | 'optional_dependency'
33
+ | 'required_dependency'
34
+ | 'tool';
35
+ }>;
36
+ };
37
+ glob?: string | string[]; // CurseForge 专用的 glob 模式,用于查找要发布的 JAR 文件
38
+ };
39
+ modrinth?: {
40
+ project_id: string;
41
+ version_number?: string;
42
+ display_name?: string;
43
+ game_versions?: string[]; // Modrinth 专用的游戏版本列表
44
+ mod_loaders?: string[]; // Modrinth 专用的模组加载器列表
45
+ changelog?: string;
46
+ dependencies?: Array<{
47
+ version_id?: string; // 可选:依赖版本的 ID
48
+ project_id?: string; // 可选:依赖项目的 ID
49
+ file_name?: string; // 可选:依赖的文件名
50
+ dependency_type:
51
+ | 'required'
52
+ | 'optional'
53
+ | 'incompatible'
54
+ | 'embedded'; // 依赖类型
55
+ }>;
56
+ featured?: boolean; // 是否标记为特色版本
57
+ status?:
58
+ | 'listed'
59
+ | 'archived'
60
+ | 'draft'
61
+ | 'unlisted'
62
+ | 'scheduled'
63
+ | 'unknown'; // 版本状态
64
+ requested_status?: 'listed' | 'archived' | 'draft' | 'unlisted'; // 请求的状态
65
+ glob?: string | string[]; // Modrinth 专用的 glob 模式
66
+ primary_file_glob?: string | string[]; // 用于匹配主要文件的 glob 模式
67
+ };
68
+ };
package/src/index.ts ADDED
@@ -0,0 +1,98 @@
1
+ import {
2
+ PrepareContext,
3
+ PublishContext,
4
+ VerifyConditionsContext,
5
+ } from 'semantic-release';
6
+ import { publishToCurseforge } from './curseforge';
7
+ import { Plugin_config } from './definitions/plugin_config';
8
+ import { publishToModrinth } from './modrinth';
9
+ import { getCurseForgeGameVersionIds } from './prepare';
10
+
11
+ // 模块级变量存储 CurseForge 版本映射
12
+ let curseforgeGameVersionsIds: number[] | undefined;
13
+
14
+ export async function verifyConditions(
15
+ pluginConfig: Plugin_config,
16
+ context: VerifyConditionsContext
17
+ ) {
18
+ // 验证配置:只检查 project_id,token 通过环境变量验证
19
+ if (pluginConfig.curseforge && !pluginConfig.curseforge.project_id) {
20
+ throw new Error('CurseForge project ID is required');
21
+ }
22
+
23
+ if (pluginConfig.modrinth && !pluginConfig.modrinth.project_id) {
24
+ throw new Error('Modrinth project ID is required');
25
+ }
26
+ }
27
+
28
+ export async function prepare(
29
+ pluginConfig: Plugin_config,
30
+ context: PrepareContext
31
+ ) {
32
+ const { env, logger } = context;
33
+
34
+ if (env.CURSEFORGE_TOKEN) {
35
+ const apiKey = env.CURSEFORGE_TOKEN;
36
+ try {
37
+ logger.log('Fetching CurseForge game versions and types...');
38
+
39
+ // 创建版本映射并存储在模块级变量中供后续使用
40
+ curseforgeGameVersionsIds = await getCurseForgeGameVersionIds(
41
+ apiKey,
42
+ pluginConfig.curseforge!,
43
+ env,
44
+ context.nextRelease
45
+ );
46
+
47
+ logger.log(
48
+ `Successfully transform ${Object.keys(curseforgeGameVersionsIds).length} CurseForge game versions`
49
+ );
50
+ } catch (error) {
51
+ logger.warn(
52
+ `Failed to fetch CurseForge game versions: ${error instanceof Error ? error.message : 'Unknown error'}`
53
+ );
54
+ }
55
+ }
56
+ }
57
+
58
+ export async function publish(
59
+ pluginConfig: Plugin_config,
60
+ context: PublishContext
61
+ ): Promise<{ url: string }[]> {
62
+ const { env, logger } = context;
63
+ const results: { url: string }[] = [];
64
+
65
+ try {
66
+ if (env.CURSEFORGE_TOKEN) {
67
+ const curseforgeId = await publishToCurseforge(
68
+ pluginConfig,
69
+ context,
70
+ curseforgeGameVersionsIds
71
+ );
72
+ results.push({
73
+ url: `https://www.curseforge.com/minecraft/mc-mods/${pluginConfig.curseforge!.project_id}/files/${curseforgeId}`,
74
+ });
75
+ } else {
76
+ logger.log(
77
+ 'CurseForge publishing is skipped: CURSEFORGE_TOKEN environment variable not found.'
78
+ );
79
+ }
80
+
81
+ // 发布到 Modrinth(如果配置了 Modrinth 且存在环境变量)
82
+ if (env.MODRINTH_TOKEN) {
83
+ const modrinthId = await publishToModrinth(pluginConfig, context);
84
+ results.push({
85
+ url: `https://modrinth.com/mod/${pluginConfig.modrinth!.project_id}/version/${modrinthId}`,
86
+ });
87
+ } else {
88
+ logger.log(
89
+ 'Modrinth publishing is skipped: MODRINTH_TOKEN environment variable not found.'
90
+ );
91
+ }
92
+
93
+ return results;
94
+ } catch (error) {
95
+ logger.error('Failed to publish:', error);
96
+ throw error;
97
+ }
98
+ }