mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-25 09:51:01 -05:00
Added the ability to retry publishing assets
This commit is contained in:
parent
b98fa4496b
commit
fbda2b1cfe
9 changed files with 155 additions and 8 deletions
20
README.md
20
README.md
|
@ -53,6 +53,8 @@ jobs:
|
||||||
8
|
8
|
||||||
16
|
16
|
||||||
|
|
||||||
|
retry-attempts: 2
|
||||||
|
rettry-delay: 10000
|
||||||
```
|
```
|
||||||
|
|
||||||
### Minimalistic Example
|
### Minimalistic Example
|
||||||
|
@ -104,6 +106,8 @@ jobs:
|
||||||
| [version-resolver](#user-content-version-resolver) | Determines the way automatic [`game-versions`](#user-content-game-versions) resolvement works | `releasesIfAny` | `exact` <br> `latest` <br> `all` <br> `releases` <br> `releasesIfAny` |
|
| [version-resolver](#user-content-version-resolver) | Determines the way automatic [`game-versions`](#user-content-game-versions) resolvement works | `releasesIfAny` | `exact` <br> `latest` <br> `all` <br> `releases` <br> `releasesIfAny` |
|
||||||
| [dependencies](#user-content-dependencies) | A list of dependencies | A dependency list specified in the config file | `fabric \| depends \| 0.40.0` <br> `fabric-api` |
|
| [dependencies](#user-content-dependencies) | A list of dependencies | A dependency list specified in the config file | `fabric \| depends \| 0.40.0` <br> `fabric-api` |
|
||||||
| [java](#user-content-java) | A list of supported Java versions | *empty string* | `Java 8` <br> `Java 1.8` <br> `8` |
|
| [java](#user-content-java) | A list of supported Java versions | *empty string* | `Java 8` <br> `Java 1.8` <br> `8` |
|
||||||
|
| [retry-attempts](#user-content-retry-attempts) | The maximum number of attempts to publish assets | `2` | `2` <br> `10` <br> `-1` |
|
||||||
|
| [retry-delay](#user-content-retry-delay) | Time delay between attempts to publish assets (in milliseconds) | `10000` | `10000` <br> `60000` <br> `0` |
|
||||||
|
|
||||||
#### modrinth-id
|
#### modrinth-id
|
||||||
|
|
||||||
|
@ -502,3 +506,19 @@ java: |
|
||||||
16
|
16
|
||||||
Java 17
|
Java 17
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### retry-attempts
|
||||||
|
|
||||||
|
The maximum number of attempts to publish assets.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
retry-attempts: 2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### retry-delay
|
||||||
|
|
||||||
|
Time delay between attempts to publish assets (in milliseconds).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
retry-delay: 10000
|
||||||
|
```
|
|
@ -87,6 +87,15 @@ inputs:
|
||||||
description: A list of supported Java versions
|
description: A list of supported Java versions
|
||||||
required: false
|
required: false
|
||||||
default: ${undefined}
|
default: ${undefined}
|
||||||
|
|
||||||
|
retry-attempts:
|
||||||
|
description: The maximum number of attempts to publish assets
|
||||||
|
required: false
|
||||||
|
default: 2
|
||||||
|
retry-delay:
|
||||||
|
description: Time delay between attempts to publish assets (in milliseconds)
|
||||||
|
required: false
|
||||||
|
default: 10000
|
||||||
runs:
|
runs:
|
||||||
using: node12
|
using: node12
|
||||||
main: dist/index.js
|
main: dist/index.js
|
20
src/index.ts
20
src/index.ts
|
@ -3,6 +3,7 @@ import PublisherFactory from "./publishing/publisher-factory";
|
||||||
import PublisherTarget from "./publishing/publisher-target";
|
import PublisherTarget from "./publishing/publisher-target";
|
||||||
import { getInputAsObject } from "./utils/input-utils";
|
import { getInputAsObject } from "./utils/input-utils";
|
||||||
import { getDefaultLogger } from "./utils/logger-utils";
|
import { getDefaultLogger } from "./utils/logger-utils";
|
||||||
|
import { retry } from "./utils/function-utils";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const commonOptions = getInputAsObject();
|
const commonOptions = getInputAsObject();
|
||||||
|
@ -20,12 +21,25 @@ async function main() {
|
||||||
const options = { ...commonOptions, ...publisherOptions };
|
const options = { ...commonOptions, ...publisherOptions };
|
||||||
const fileSelector = options.files && (typeof(options.files) === "string" || options.files.primary) ? options.files : gradleOutputSelector;
|
const fileSelector = options.files && (typeof(options.files) === "string" || options.files.primary) ? options.files : gradleOutputSelector;
|
||||||
const files = await getRequiredFiles(fileSelector);
|
const files = await getRequiredFiles(fileSelector);
|
||||||
|
const retryAttempts = +options.retry?.["attempts"] || 0;
|
||||||
|
const retryDelay = +options.retry?.["delay"] || 0;
|
||||||
|
|
||||||
const publisher = publisherFactory.create(target, logger);
|
const publisher = publisherFactory.create(target, logger);
|
||||||
logger.info(`Publishing assets to ${targetName}...`);
|
logger.info(`Publishing assets to ${targetName}...`);
|
||||||
const start = new Date();
|
const start = new Date();
|
||||||
await publisher.publish(files, options);
|
|
||||||
logger.info(`Successfully published assets to ${targetName} (in ${new Date().getTime() - start.getTime()}ms)`);
|
await retry({
|
||||||
|
func: () => publisher.publish(files, options),
|
||||||
|
maxAttempts: retryAttempts,
|
||||||
|
delay: retryDelay,
|
||||||
|
errorCallback: e => {
|
||||||
|
logger.error(`${e}`);
|
||||||
|
logger.info(`Retrying to publish assets to ${targetName} in ${retryDelay} ms...`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const end = new Date();
|
||||||
|
logger.info(`Successfully published assets to ${targetName} (in ${end.getTime() - start.getTime()} ms)`);
|
||||||
publishedTo.push(targetName);
|
publishedTo.push(targetName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,4 +50,4 @@ async function main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(error => getDefaultLogger().fatal(error instanceof Error ? error.message : `Something went horribly wrong: ${error}`));
|
main().catch(error => getDefaultLogger().fatal(error instanceof Error ? `${error}` : `Something went horribly wrong: ${error}`));
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { FormData } from "formdata-node";
|
||||||
import { fileFromPath } from "formdata-node/file-from-path";
|
import { fileFromPath } from "formdata-node/file-from-path";
|
||||||
import { File } from "./file";
|
import { File } from "./file";
|
||||||
import { findVersionByName } from "./minecraft-utils";
|
import { findVersionByName } from "./minecraft-utils";
|
||||||
|
import SoftError from "./soft-error";
|
||||||
|
|
||||||
const baseUrl = "https://minecraft.curseforge.com/api";
|
const baseUrl = "https://minecraft.curseforge.com/api";
|
||||||
|
|
||||||
|
@ -24,11 +25,11 @@ interface CurseForgeUploadErrorInfo {
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class CurseForgeUploadError extends Error {
|
class CurseForgeUploadError extends SoftError {
|
||||||
public readonly info?: CurseForgeUploadErrorInfo;
|
public readonly info?: CurseForgeUploadErrorInfo;
|
||||||
|
|
||||||
constructor(message: string, info?: CurseForgeUploadErrorInfo) {
|
constructor(soft: boolean, message?: string, info?: CurseForgeUploadErrorInfo) {
|
||||||
super(message);
|
super(soft, message);
|
||||||
this.info = info;
|
this.info = info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,7 +118,8 @@ export async function uploadFile(id: string, data: Record<string, any>, file: Fi
|
||||||
info = <CurseForgeUploadErrorInfo>await response.json();
|
info = <CurseForgeUploadErrorInfo>await response.json();
|
||||||
errorText += `, ${JSON.stringify(info)}`;
|
errorText += `, ${JSON.stringify(info)}`;
|
||||||
} catch { }
|
} catch { }
|
||||||
throw new CurseForgeUploadError(`Failed to upload file: ${response.status} (${errorText})`, info);
|
const isServerError = response.status >= 500;
|
||||||
|
throw new CurseForgeUploadError(isServerError, `Failed to upload file: ${response.status} (${errorText})`, info);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (<{ id: number }>await response.json()).id;
|
return (<{ id: number }>await response.json()).id;
|
||||||
|
|
19
src/utils/function-utils.ts
Normal file
19
src/utils/function-utils.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import sleep from "./sleep";
|
||||||
|
|
||||||
|
export async function retry<T>({ func, delay = 0, maxAttempts = -1, softErrorPredicate, errorCallback }: { func: () => T | Promise<T>, delay?: number, maxAttempts?: number, softErrorPredicate?: (error: unknown) => boolean, errorCallback?: (error: unknown) => void }): Promise<T> {
|
||||||
|
let attempts = 0;
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return await func();
|
||||||
|
} catch (e) {
|
||||||
|
const isSoft = softErrorPredicate ? softErrorPredicate(e) : e?.soft;
|
||||||
|
if (!isSoft || maxAttempts >= 0 && ++attempts >= maxAttempts ) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
if (errorCallback) {
|
||||||
|
errorCallback(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await sleep(delay);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { fileFromPath } from "formdata-node/file-from-path";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { File } from "./file";
|
import { File } from "./file";
|
||||||
import { computeHash } from "./hash-utils";
|
import { computeHash } from "./hash-utils";
|
||||||
|
import SoftError from "./soft-error";
|
||||||
|
|
||||||
export async function createVersion(modId: string, data: Record<string, any>, files: File[], token: string): Promise<string> {
|
export async function createVersion(modId: string, data: Record<string, any>, files: File[], token: string): Promise<string> {
|
||||||
data = {
|
data = {
|
||||||
|
@ -30,7 +31,8 @@ export async function createVersion(modId: string, data: Record<string, any>, fi
|
||||||
try {
|
try {
|
||||||
errorText += `, ${await response.text()}`;
|
errorText += `, ${await response.text()}`;
|
||||||
} catch { }
|
} catch { }
|
||||||
throw new Error(`Failed to upload file: ${response.status} (${errorText})`);
|
const isServerError = response.status >= 500;
|
||||||
|
throw new SoftError(isServerError, `Failed to upload file: ${response.status} (${errorText})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionId = (<{ id: string }>await response.json()).id;
|
const versionId = (<{ id: string }>await response.json()).id;
|
||||||
|
|
3
src/utils/sleep.ts
Normal file
3
src/utils/sleep.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export default function sleep(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
8
src/utils/soft-error.ts
Normal file
8
src/utils/soft-error.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default class SoftError extends Error {
|
||||||
|
readonly soft: boolean;
|
||||||
|
|
||||||
|
constructor(soft: boolean, message?: string) {
|
||||||
|
super(message);
|
||||||
|
this.soft = soft;
|
||||||
|
}
|
||||||
|
}
|
70
test/function-utils.test.ts
Normal file
70
test/function-utils.test.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { describe, test, expect } from "@jest/globals";
|
||||||
|
import { retry } from "../src/utils/function-utils";
|
||||||
|
import SoftError from "../src/utils/soft-error";
|
||||||
|
|
||||||
|
function createThrowingFunc(attempts: number): () => true {
|
||||||
|
let counter = 0;
|
||||||
|
return () => {
|
||||||
|
if (++counter !== attempts) {
|
||||||
|
throw new SoftError(true);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAsyncThrowingFunc(attempts: number): () => Promise<true> {
|
||||||
|
const func = createThrowingFunc(attempts);
|
||||||
|
return async () => func();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("retry", () => {
|
||||||
|
test("function resolves after several attempts", async () => {
|
||||||
|
expect(await retry({ func: createThrowingFunc(5), maxAttempts: 5 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delay is applied between the attempts", async () => {
|
||||||
|
const start = new Date();
|
||||||
|
expect(await retry({ func: createThrowingFunc(2), maxAttempts: 2, delay: 100 })).toBe(true);
|
||||||
|
const end = new Date();
|
||||||
|
const duration = end.getTime() - start.getTime();
|
||||||
|
expect(duration >= 100 && duration < 200).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the original error is thrown if retry function didn't succeed", async () => {
|
||||||
|
await expect(retry({ func: createThrowingFunc(5), maxAttempts: 1 })).rejects.toThrow(<any>SoftError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("softErrorPredicate is used to determine whether the error is soft or not", async () => {
|
||||||
|
await expect(retry({ func: createThrowingFunc(5), maxAttempts: 5, softErrorPredicate: _ => false })).rejects.toThrow(<any>SoftError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("errorCallback is called whenever an error occurs", async () => {
|
||||||
|
await expect(retry({ func: createThrowingFunc(5), maxAttempts: 5, errorCallback: e => { throw e; } })).rejects.toThrow(<any>SoftError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("retry (async)", () => {
|
||||||
|
test("function resolves after several attempts", async () => {
|
||||||
|
expect(await retry({ func: createAsyncThrowingFunc(5), maxAttempts: 5 })).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("delay is applied between the attempts", async () => {
|
||||||
|
const start = new Date();
|
||||||
|
expect(await retry({ func: createAsyncThrowingFunc(2), maxAttempts: 2, delay: 100 })).toBe(true);
|
||||||
|
const end = new Date();
|
||||||
|
const duration = end.getTime() - start.getTime();
|
||||||
|
expect(duration >= 100 && duration < 200).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the original error is thrown if retry function didn't succeed", async () => {
|
||||||
|
await expect(retry({ func: createAsyncThrowingFunc(5), maxAttempts: 1 })).rejects.toThrow(<any>SoftError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("softErrorPredicate is used to determine whether the error is soft or not", async () => {
|
||||||
|
await expect(retry({ func: createAsyncThrowingFunc(5), maxAttempts: 5, softErrorPredicate: _ => false })).rejects.toThrow(<any>SoftError);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("errorCallback is called whenever an error occurs", async () => {
|
||||||
|
await expect(retry({ func: createAsyncThrowingFunc(5), maxAttempts: 5, errorCallback: e => { throw e; } })).rejects.toThrow(<any>SoftError);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue