Added modrinth-unfeature-mode input

Closes #9
This commit is contained in:
Kir_Antipov 2022-06-07 16:08:55 +03:00
parent 1aaf662cc2
commit 38cc76ee30
5 changed files with 168 additions and 6 deletions

View file

@ -16,6 +16,8 @@ jobs:
- uses: Kir-Antipov/mc-publish@v3.0 - uses: Kir-Antipov/mc-publish@v3.0
with: with:
modrinth-id: AANobbMI modrinth-id: AANobbMI
modrinth-featured: true
modrinth-unfeature-mode: subset
modrinth-token: ${{ secrets.MODRINTH_TOKEN }} modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
curseforge-id: 394468 curseforge-id: 394468
@ -91,6 +93,7 @@ jobs:
| [modrinth-id](#user-content-modrinth-id) | The ID of the Modrinth project to upload to | A value specified in the config file | `AANobbMI` | | [modrinth-id](#user-content-modrinth-id) | The ID of the Modrinth project to upload to | A value specified in the config file | `AANobbMI` |
| [modrinth-token](#user-content-modrinth-token) | A valid token for the Modrinth API | ❌ | `${{ secrets.MODRINTH_TOKEN }}` | | [modrinth-token](#user-content-modrinth-token) | A valid token for the Modrinth API | ❌ | `${{ secrets.MODRINTH_TOKEN }}` |
| [modrinth-featured](#user-content-modrinth-featured) | Indicates whether the version should be featured on Modrinth or not | `true` | `true` <br> `false` | | [modrinth-featured](#user-content-modrinth-featured) | Indicates whether the version should be featured on Modrinth or not | `true` | `true` <br> `false` |
| [modrinth-unfeature-mode](#user-content-modrinth-unfeature-mode) | Determines the way automatic unfeaturing of older Modrinth versions works | If [`modrinth-featured`](#user-content-modrinth-featured) is set to true, `subset`; otherwise, `none` | `none` <br> `subset` <br> `intersection` <br> `any` |
| [curseforge-id](#user-content-curseforge-id) | The ID of the CurseForge project to upload to | A value specified in the config file | `394468` | | [curseforge-id](#user-content-curseforge-id) | The ID of the CurseForge project to upload to | A value specified in the config file | `394468` |
| [curseforge-token](#user-content-curseforge-token) | A valid token for the CurseForge API | ❌ | `${{ secrets.CURSEFORGE_TOKEN }}` | | [curseforge-token](#user-content-curseforge-token) | A valid token for the CurseForge API | ❌ | `${{ secrets.CURSEFORGE_TOKEN }}` |
| [github-tag](#user-content-github-tag) | The tag name of the release to upload assets to | A tag of the release that triggered the action | `mc1.17.1-0.3.2` | | [github-tag](#user-content-github-tag) | The tag name of the release to upload assets to | A tag of the release that triggered the action | `mc1.17.1-0.3.2` |
@ -227,6 +230,30 @@ Indicates whether the version should be featured on Modrinth or not.
modrinth-featured: true modrinth-featured: true
``` ```
#### modrinth-unfeature-mode
Determines the way automatic unfeaturing of older Modrinth versions works. Default value is `subset`, if [`modrinth-featured`](#user-content-modrinth-featured) is set to true; otherwise, `none`.
```yaml
modrinth-unfeature-mode: version-intersection | loader-subset
```
Available presets:
- `none` - no Modrinth versions will be unfeatured
- `subset` - only those Modrinth versions which are considered a subset of the new one *(i.e., new release suports all of the version's mod loaders **and** game versions)* will be unfeatured
- `intersection` - only those Modrinth versions which intersects with the new one *(i.e., support at least one of the mod loaders and one of the game versions supported by the new release)* will be unfeatured
- `any` - all Modrinth versions will be unfeatured
If none of the given presets suits your needs, you can construct a new one from the following values via bitwise `OR`, like so - `version-intersection | loaders-subset`:
- `version-subset`
- `version-intersection`
- `version-any`
- `loader-subset`
- `loader-intersection`
- `loader-any`
#### curseforge-id #### curseforge-id
The ID of the CurseForge project to upload to. The ID of the CurseForge project to upload to.

View file

@ -17,6 +17,10 @@ inputs:
description: Indicates whether the version should be featured on Modrinth or not description: Indicates whether the version should be featured on Modrinth or not
required: false required: false
default: ${undefined} default: ${undefined}
modrinth-unfeature-mode: Determines the way automatic unfeaturing of older Modrinth versions works
description:
required: false
default: ${undefined}
curseforge-id: curseforge-id:
description: The ID of the CurseForge project to upload to description: The ID of the CurseForge project to upload to

View file

@ -1,10 +1,30 @@
import { createVersion, getProject } from "../../utils/modrinth-utils"; import { createVersion, getProject, getVersions, modifyVersion } from "../../utils/modrinth-utils";
import { File } from "../../utils/file"; import { File } from "../../utils/file";
import ModPublisher from "../mod-publisher"; import ModPublisher from "../mod-publisher";
import PublisherTarget from "../publisher-target"; import PublisherTarget from "../publisher-target";
import Dependency from "../../metadata/dependency"; import Dependency from "../../metadata/dependency";
import DependencyKind from "../../metadata/dependency-kind"; import DependencyKind from "../../metadata/dependency-kind";
import { mapBooleanInput } from "../../utils/input-utils"; import { mapBooleanInput, mapEnumInput } from "../../utils/input-utils";
enum UnfeatureMode {
None = 0,
VersionSubset = 1,
VersionIntersection = 2,
VersionAny = 4,
LoaderSubset = 8,
LoaderIntersection = 16,
LoaderAny = 32,
Subset = VersionSubset | LoaderSubset,
Intersection = VersionIntersection | LoaderIntersection,
Any = VersionAny | LoaderAny,
}
function hasFlag(unfeatureMode: UnfeatureMode, flag: UnfeatureMode): boolean {
return (unfeatureMode & flag) === flag;
}
const modrinthDependencyKinds = new Map([ const modrinthDependencyKinds = new Map([
[DependencyKind.Depends, "required"], [DependencyKind.Depends, "required"],
@ -21,6 +41,7 @@ export default class ModrinthPublisher extends ModPublisher {
protected async publishMod(id: string, token: string, name: string, version: string, channel: string, loaders: string[], gameVersions: string[], _java: string[], changelog: string, files: File[], dependencies: Dependency[], options: Record<string, unknown>): Promise<void> { protected async publishMod(id: string, token: string, name: string, version: string, channel: string, loaders: string[], gameVersions: string[], _java: string[], changelog: string, files: File[], dependencies: Dependency[], options: Record<string, unknown>): Promise<void> {
const featured = mapBooleanInput(options.featured, true); const featured = mapBooleanInput(options.featured, true);
const unfeatureMode = mapEnumInput(options.unfeatureMode, UnfeatureMode, featured ? UnfeatureMode.Subset : UnfeatureMode.None);
const projects = (await Promise.all(dependencies const projects = (await Promise.all(dependencies
.filter((x, _, self) => (x.kind !== DependencyKind.Suggests && x.kind !== DependencyKind.Includes) || !self.find(y => y.id === x.id && y.kind !== DependencyKind.Suggests && y.kind !== DependencyKind.Includes)) .filter((x, _, self) => (x.kind !== DependencyKind.Suggests && x.kind !== DependencyKind.Includes) || !self.find(y => y.id === x.id && y.kind !== DependencyKind.Suggests && y.kind !== DependencyKind.Includes))
.map(async x => ({ .map(async x => ({
@ -29,6 +50,10 @@ export default class ModrinthPublisher extends ModPublisher {
})))) }))))
.filter(x => x.project_id && x.dependency_type); .filter(x => x.project_id && x.dependency_type);
if (unfeatureMode !== UnfeatureMode.None) {
await this.unfeatureOlderVersions(id, token, unfeatureMode, loaders, gameVersions);
}
const data = { const data = {
name: name || version, name: name || version,
version_number: version, version_number: version,
@ -41,4 +66,36 @@ export default class ModrinthPublisher extends ModPublisher {
}; };
await createVersion(id, data, files, token); await createVersion(id, data, files, token);
} }
private async unfeatureOlderVersions(id: string, token: string, unfeatureMode: UnfeatureMode, loaders: string[], gameVersions: string[]): Promise<void> {
this.logger.info("Unfeaturing older Modrinth versions...");
const start = new Date();
const unfeaturedVersions = <string[]>[];
const versionSubset = hasFlag(unfeatureMode, UnfeatureMode.VersionSubset);
const loaderSubset = hasFlag(unfeatureMode, UnfeatureMode.LoaderSubset);
const olderVersions = await getVersions(id, hasFlag(unfeatureMode, UnfeatureMode.LoaderAny) ? null : loaders, hasFlag(unfeatureMode, UnfeatureMode.VersionAny) ? null : gameVersions, true, token);
for (const olderVersion of olderVersions) {
if (loaderSubset && !olderVersion.loaders.every(x => loaders.includes(x))) {
continue;
}
if (versionSubset && !olderVersion.game_versions.every(x => gameVersions.includes(x))) {
continue;
}
if (await modifyVersion(olderVersion.id, { featured: false }, token)) {
unfeaturedVersions.push(olderVersion.id);
} else {
this.logger.warn(`Cannot unfeature version ${olderVersion.id}`);
}
}
if (unfeaturedVersions.length) {
const end = new Date();
this.logger.info(`Successfully unfeatured versions ${unfeaturedVersions.join(", ")} (in ${end.getTime() - start.getTime()} ms)`);
} else {
this.logger.info("No versions to unfeature were found");
}
}
} }

View file

@ -1,5 +1,6 @@
import FormData from "form-data"; import FormData from "form-data";
import fetch, { Response } from "node-fetch"; import fetch, { Response } from "node-fetch";
import { URLSearchParams } from "url";
import { File } from "./file"; import { File } from "./file";
import SoftError from "./soft-error"; import SoftError from "./soft-error";
@ -12,6 +13,9 @@ interface ModrinthProject {
interface ModrinthVersion { interface ModrinthVersion {
id: string; id: string;
loaders: string[];
game_versions: string[];
featured: boolean;
} }
export function createVersion(modId: string, data: Record<string, any>, files: File[], token: string): Promise<ModrinthVersion> { export function createVersion(modId: string, data: Record<string, any>, files: File[], token: string): Promise<ModrinthVersion> {
@ -44,7 +48,37 @@ export function createVersion(modId: string, data: Record<string, any>, files: F
export function getProject(idOrSlug: string): Promise<ModrinthProject> { export function getProject(idOrSlug: string): Promise<ModrinthProject> {
return processResponse(fetch(`${baseUrl}/project/${idOrSlug}`), { 404: () => <ModrinthProject>null }); return processResponse(fetch(`${baseUrl}/project/${idOrSlug}`), { 404: () => <ModrinthProject>null });
return await response.json(); }
export function getVersions(idOrSlug: string, loaders?: string[], gameVersions?: string[], featured?: boolean, token?: string): Promise<ModrinthVersion[]> {
const urlParams = new URLSearchParams();
if (loaders) {
urlParams.append("loaders", JSON.stringify(loaders));
}
if (gameVersions) {
urlParams.append("game_versions", JSON.stringify(gameVersions));
}
if (typeof featured === "boolean") {
urlParams.append("featured", String(featured));
}
const response = fetch(`${baseUrl}/project/${idOrSlug}/version?${urlParams}`, token ? {
headers: { "Authorization": token }
} : undefined);
return processResponse(response, { 404: () => <ModrinthVersion[]>[] });
}
export async function modifyVersion(id: string, version: Partial<ModrinthVersion>, token: string): Promise<boolean> {
const response = await fetch(`${baseUrl}/version/${id}`, {
method: "PATCH",
headers: {
"Authorization": token,
"Content-Type": "application/json",
},
body: JSON.stringify(version)
});
return response.ok;
} }
async function processResponse<T>(response: Response | Promise<Response>, mappers?: Record<number, (response: Response) => T | Promise<T>>, errorFactory?: (isServerError: boolean, message: string, response: Response) => Error | Promise<Error>): Promise<T | never> { async function processResponse<T>(response: Response | Promise<Response>, mappers?: Record<number, (response: Response) => T | Promise<T>>, errorFactory?: (isServerError: boolean, message: string, response: Response) => Error | Promise<Error>): Promise<T | never> {

View file

@ -1,5 +1,7 @@
import { describe, test, expect } from "@jest/globals"; import { describe, test, expect } from "@jest/globals";
import { getProject } from "../src/utils/modrinth-utils"; import { getProject, getVersions } from "../src/utils/modrinth-utils";
const timeout = 15000;
describe("getProject", () => { describe("getProject", () => {
test("returned versions have expected ids", async () => { test("returned versions have expected ids", async () => {
@ -15,7 +17,7 @@ describe("getProject", () => {
const project = await getProject(slug); const project = await getProject(slug);
expect(project).toHaveProperty("id", id); expect(project).toHaveProperty("id", id);
} }
}, 15000); }, timeout);
test("the method returns null if project with the given slug does not exist", async () => { test("the method returns null if project with the given slug does not exist", async () => {
const nonExistentProjects = [ const nonExistentProjects = [
@ -31,5 +33,43 @@ describe("getProject", () => {
const project = await getProject(slug); const project = await getProject(slug);
expect(project).toBeNull(); expect(project).toBeNull();
} }
}, 15000); }, timeout);
});
describe("getVersions", () => {
test("returns unfiltered versions if no parameters were passed", async () => {
const versions = await getVersions("terra");
expect(versions.find(x => x.featured)).toBeTruthy();
expect(versions.find(x => !x.featured)).toBeTruthy();
expect(versions.find(x => x.loaders.includes("fabric"))).toBeTruthy();
expect(versions.find(x => x.loaders.includes("forge"))).toBeTruthy();
expect(versions.find(x => x.game_versions.includes("1.18.2"))).toBeTruthy();
expect(versions.find(x => x.game_versions.includes("1.16.5"))).toBeTruthy();
}, timeout);
test("returns only featured versions with featured === true", async () => {
const versions = await getVersions("terra", null, null, true);
expect(versions.every(x => x.featured)).toBe(true);
}, timeout);
test("returns only unfeatured versions with featured === false", async () => {
const versions = await getVersions("terra", null, null, false);
expect(versions.every(x => !x.featured)).toBe(true);
}, timeout);
test("returns only versions that support given modloaders", async () => {
const fabricVersions = await getVersions("terra", ["fabric"]);
expect(fabricVersions.every(x => x.loaders.includes("fabric"))).toBe(true);
const forgeVersions = await getVersions("terra", ["forge"]);
expect(forgeVersions.every(x => x.loaders.includes("forge"))).toBe(true);
}, timeout);
test("returns only versions that support given mc versions", async () => {
const versions_1_18_2 = await getVersions("terra", null, ["1.18.2"]);
expect(versions_1_18_2.every(x => x.game_versions.includes("1.18.2"))).toBe(true);
const versions_1_16_5 = await getVersions("terra", null, ["1.16.5"]);
expect(versions_1_16_5.every(x => x.game_versions.includes("1.16.5"))).toBe(true);
}, timeout);
}); });