shipthis
Version:
ShipThis manages building and uploading your Godot games to the App Store and Google Play.
260 lines (254 loc) • 10.1 kB
JavaScript
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
import { Flags } from '@oclif/core';
import { Box, Text, render } from 'ink';
import { M as LogLevel, W as WEB_URL, a6 as getShortAuthRequiredUrl, c as BaseGameCommand, z as getJob, H as downloadBuildById } from '../../index-BwnzoldS.js';
import 'ink-spinner';
import 'node:crypto';
import 'node:fs';
import 'node:path';
import 'node:readline';
import 'node:url';
import 'readline-sync';
import 'luxon';
import axios from 'axios';
import 'isomorphic-git';
import '@tanstack/react-query';
import { useContext, useState, useEffect } from 'react';
import 'fast-glob';
import 'uuid';
import 'yazl';
import 'socket.io-client';
import 'fullscreen-ink';
import 'string-length';
import 'strip-ansi';
import open from 'open';
import { g as getShortUUID } from '../../index-CJWMt1s-.js';
import 'crypto-js';
import { u as useJobWatching, J as JobLogTail } from '../../JobLogTail-D35FO5v-.js';
import { J as JobStatusTable } from '../../JobStatusTable-DIJ_h-gi.js';
import '@inkjs/ui';
import { u as useShip, J as JobProgress } from '../../JobProgress-D-9KESaA.js';
import { C as CommandContext, G as GameContext, e as useSafeInput, M as Markdown } from '../../index-hoHfGrjg.js';
import 'qrcode';
import '../../ejs-DirFZbza.js';
import 'marked';
import 'marked-terminal';
import { C as CommandGame } from '../../CommandGame-D2NqytWc.js';
import '@expo/apple-utils/build/index.js';
import 'deepmerge';
import 'ini';
import '../../useAndroidServiceAccountTestResult-CwKeW0ED.js';
import '../../useWebSocket-cM5yOcDv.js';
import '../../Title-BCQtayg6.js';
import '../../StatusTable-DzRWcMr4.js';
import '../../git-BpsfNFZ_.js';
import '../../ProgressSpinner-Um6ARKlk.js';
import 'fs';
import 'path';
import '../../Command-DN1j3tjt.js';
function isNetworkError(exception) {
if (!axios.isAxiosError(exception)) return false;
return ["ECONNABORTED", "ERR_NETWORK"].includes(`${exception.code}`);
}
function getErrorMessage(error) {
try {
if (isNetworkError(error)) {
return "Please check your internet connection.";
}
const data = error?.response?.data;
const apiValidation = Array.isArray(data) ? data.map((r) => "message" in r ? `Error - ${r.message}` : r.toString()).join(" ") : "";
const apiErr = error?.response?.data?.error || "";
const apiMsg = `${apiErr}${apiValidation ? " " + apiValidation : ""}`;
if (apiMsg.length === 0) {
return "message" in error ? error.message : error.toString();
}
return apiMsg;
} catch {
return error ? error.toString() : "Error";
}
}
const JobFollow = ({ jobId, onComplete, onFailure, projectId }) => {
useJobWatching({
isWatching: true,
jobId,
onComplete,
onFailure,
onNewLogEntry(logEntry) {
if (logEntry.level === LogLevel.ERROR) console.error(logEntry.message);
else console.log(logEntry.message);
},
projectId
});
return null;
};
const Ship = ({ onComplete, onError }) => {
const { command } = useContext(CommandContext);
const flags = command && command.getFlags();
const { gameId } = useContext(GameContext);
const shipMutation = useShip();
const [jobs, setJobs] = useState(null);
const [failedJobs, setFailedJobs] = useState([]);
const [successJobs, setSuccessJobs] = useState([]);
const [shipLog, setShipLog] = useState("");
const [showLog, setShowLog] = useState(false);
const [isComplete, setIsComplete] = useState(false);
const handleStartOnMount = async () => {
if (!command) throw new Error("No command in context");
const logFn = flags?.follow ? console.log : setShipLog;
const startedJobs = await shipMutation.mutateAsync({ command, log: logFn });
setJobs(startedJobs);
};
useEffect(() => {
handleStartOnMount().catch(onError);
}, []);
useSafeInput(async (input) => {
if (!gameId) return;
const i = input.toLowerCase();
switch (i) {
case "l": {
setShowLog((prev) => !prev);
break;
}
case "b": {
const dashUrl = jobs?.length === 1 ? `/games/${gameId}/job/${jobs[0].id}` : `/games/${gameId}`;
const url = await getShortAuthRequiredUrl(dashUrl);
await open(url);
break;
}
}
});
const handleJobComplete = (job) => {
setSuccessJobs([...successJobs, job]);
const newJobs = (jobs || []).filter((prevJob) => prevJob.id !== job.id);
setJobs(newJobs);
if (newJobs.length === 0) {
setIsComplete(true);
}
};
const handleJobFailure = (job) => {
setFailedJobs([...failedJobs, job]);
handleJobComplete(job);
};
useEffect(() => {
if (!isComplete) return;
setTimeout(() => {
failedJobs.length === 0 ? onComplete(successJobs) : onError("One or more jobs failed");
}, 500);
}, [isComplete]);
if (!gameId) return /* @__PURE__ */ jsx(Fragment, {});
if (flags?.follow) {
if (jobs && jobs.length > 0) {
return /* @__PURE__ */ jsx(JobFollow, { jobId: jobs[0].id, onComplete: handleJobComplete, onFailure: handleJobFailure, projectId: gameId });
}
return /* @__PURE__ */ jsx(Fragment, {});
}
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
jobs === null && /* @__PURE__ */ jsx(Text, { children: shipLog }),
jobs && jobs.map((job) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
/* @__PURE__ */ jsx(JobStatusTable, { isWatching: true, jobId: job.id, projectId: job.project.id }),
/* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsx(JobProgress, { job, onComplete: handleJobComplete, onFailure: handleJobFailure }) }),
showLog && /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(JobLogTail, { isWatching: true, jobId: job.id, length: 10, projectId: job.project.id }) })
] }, job.id)),
jobs && !isComplete && /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(Text, { children: "Press L to show and hide the job logs." }),
/* @__PURE__ */ jsx(Text, { children: "Press B to open the ShipThis dashboard in your browser." }),
/* @__PURE__ */ jsx(Text, { bold: true, children: "Please wait while ShipThis builds your game..." })
] }),
isComplete && /* @__PURE__ */ jsxs(Fragment, { children: [
failedJobs.length === 0 && /* @__PURE__ */ jsx(
Markdown,
{
filename: "ship-success.md.ejs",
templateVars: {
gameBuildsUrl: `${WEB_URL}games/${getShortUUID(gameId)}/builds`,
wasPublished: !flags?.skipPublish
}
}
),
failedJobs.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
Markdown,
{
filename: "ship-failure.md.ejs",
templateVars: {
jobDashboardUrl: `${WEB_URL}games/${getShortUUID(gameId)}/job/${getShortUUID(failedJobs[0].id)}`
}
}
),
/* @__PURE__ */ jsx(Box, { marginTop: 1, children: failedJobs.map((fj) => /* @__PURE__ */ jsx(JobLogTail, { isWatching: false, jobId: fj.id, length: 10, projectId: fj.project.id }, fj.id)) })
] })
] })
] });
};
class GameShip extends BaseGameCommand {
static args = {};
static description = "Builds the app (for all platforms with valid credentials) and ships it to the stores.";
static examples = [
"<%= config.bin %> <%= command.id %>",
"<%= config.bin %> <%= command.id %> --platform ios",
"<%= config.bin %> <%= command.id %> --platform android --skipPublish",
"<%= config.bin %> <%= command.id %> --platform android --download game.aab",
"<%= config.bin %> <%= command.id %> --platform android --follow --downloadAPK game.apk"
];
static flags = {
...BaseGameCommand.flags,
download: Flags.string({
dependsOn: ["platform"],
description: "Download the build artifact to the specified file",
required: false
}),
downloadAPK: Flags.string({
dependsOn: ["platform"],
description: "Download the APK artifact (if available) to the specified file",
required: false
}),
follow: Flags.boolean({
dependsOn: ["platform"],
description: "Follow the job logs in real-time. Requires --platform to be specified.",
required: false
}),
platform: Flags.string({
description: 'The platform to ship the game to. This can be "android" or "ios"',
options: ["android", "ios"],
required: false
}),
skipPublish: Flags.boolean({
default: false,
description: "Skip the publish step",
required: false
})
};
async run() {
await this.ensureWeAreInAProjectDir();
const gameId = this.getGameId();
if (!gameId) {
this.error("No game ID found");
}
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 5e3;
const handleComplete = async ([originalJob]) => {
if (!this.flags.download && !this.flags.downloadAPK) return process.exit(0);
let job = null;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
job = await getJob(originalJob.id, gameId);
if (job.builds && job.builds.length > 0) break;
if (attempt < MAX_RETRIES) await new Promise((res) => setTimeout(res, RETRY_DELAY_MS));
}
if (!job?.builds || job.builds.length === 0) this.error("No builds found for this job after multiple attempts");
const { platform } = this.flags;
const type = platform === "android" ? this.flags.downloadAPK ? "APK" : "AAB" : "IPA";
const build = job.builds.find((b) => b.buildType === type);
if (!build) this.error(`No build found for type ${type}`);
const filename = this.flags.download || this.flags.downloadAPK;
await downloadBuildById(gameId, build.id, `${filename}`);
process.exit(0);
};
const handleError = (e) => {
this.error(getErrorMessage(e));
};
render(
/* @__PURE__ */ jsx(CommandGame, { command: this, children: /* @__PURE__ */ jsx(Ship, { onComplete: handleComplete, onError: handleError }) })
);
}
}
export { GameShip as default };