mirror of
https://github.com/Kir-Antipov/mc-publish.git
synced 2024-11-22 00:11:02 -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
|
||||
16
|
||||
|
||||
retry-attempts: 2
|
||||
rettry-delay: 10000
|
||||
```
|
||||
|
||||
### 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` |
|
||||
| [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` |
|
||||
| [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
|
||||
|
||||
|
@ -501,4 +505,20 @@ java: |
|
|||
8
|
||||
16
|
||||
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
|
||||
required: false
|
||||
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:
|
||||
using: node12
|
||||
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 { getInputAsObject } from "./utils/input-utils";
|
||||
import { getDefaultLogger } from "./utils/logger-utils";
|
||||
import { retry } from "./utils/function-utils";
|
||||
|
||||
async function main() {
|
||||
const commonOptions = getInputAsObject();
|
||||
|
@ -20,12 +21,25 @@ async function main() {
|
|||
const options = { ...commonOptions, ...publisherOptions };
|
||||
const fileSelector = options.files && (typeof(options.files) === "string" || options.files.primary) ? options.files : gradleOutputSelector;
|
||||
const files = await getRequiredFiles(fileSelector);
|
||||
const retryAttempts = +options.retry?.["attempts"] || 0;
|
||||
const retryDelay = +options.retry?.["delay"] || 0;
|
||||
|
||||
const publisher = publisherFactory.create(target, logger);
|
||||
logger.info(`Publishing assets to ${targetName}...`);
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -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 { File } from "./file";
|
||||
import { findVersionByName } from "./minecraft-utils";
|
||||
import SoftError from "./soft-error";
|
||||
|
||||
const baseUrl = "https://minecraft.curseforge.com/api";
|
||||
|
||||
|
@ -24,11 +25,11 @@ interface CurseForgeUploadErrorInfo {
|
|||
errorMessage: string;
|
||||
}
|
||||
|
||||
class CurseForgeUploadError extends Error {
|
||||
class CurseForgeUploadError extends SoftError {
|
||||
public readonly info?: CurseForgeUploadErrorInfo;
|
||||
|
||||
constructor(message: string, info?: CurseForgeUploadErrorInfo) {
|
||||
super(message);
|
||||
constructor(soft: boolean, message?: string, info?: CurseForgeUploadErrorInfo) {
|
||||
super(soft, message);
|
||||
this.info = info;
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +118,8 @@ export async function uploadFile(id: string, data: Record<string, any>, file: Fi
|
|||
info = <CurseForgeUploadErrorInfo>await response.json();
|
||||
errorText += `, ${JSON.stringify(info)}`;
|
||||
} 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;
|
||||
|
|
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 { File } from "./file";
|
||||
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> {
|
||||
data = {
|
||||
|
@ -30,7 +31,8 @@ export async function createVersion(modId: string, data: Record<string, any>, fi
|
|||
try {
|
||||
errorText += `, ${await response.text()}`;
|
||||
} 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;
|
||||
|
|
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