UNPKG

shipthis

Version:

ShipThis manages building and uploading your Godot games to the App Store and Google Play.

276 lines (270 loc) 10.8 kB
import { jsx, Fragment, jsxs } from 'react/jsx-runtime'; import { Flags } from '@oclif/core'; import { Box, Text, render } from 'ink'; import { a2 as LogLevel, W as WEB_URL, a3 as getShortAuthRequiredUrl, z as getJob, L as downloadBuildById } from '../../baseCommand-CTn3KGH3.js'; import { C as CommandContext, G as GameContext, w as useSafeInput, M as Markdown, g as getShortUUID, c as BaseGameCommand } from '../../baseGameCommand-8VL7xe-O.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 'crypto-js'; import { u as useJobWatching, J as JobLogTail } from '../../JobLogTail-Da8GuReK.js'; import { J as JobStatusTable } from '../../JobStatusTable-DVJjHw97.js'; import '@inkjs/ui'; import { u as useShip, J as JobProgress } from '../../JobProgress-DltCQpzA.js'; import 'qrcode'; import 'marked'; import 'marked-terminal'; import { C as CommandGame } from '../../CommandGame-CuvuH-z6.js'; import 'chalk'; import '@expo/apple-utils/build/index.js'; import 'deepmerge'; import 'ini'; import 'fs'; import 'path'; import '../../useWebSocket-MXDbQHcu.js'; import '../../Title-BCQtayg6.js'; import '../../StatusTable-DzRWcMr4.js'; import '../../git-BpsfNFZ_.js'; import '../../ProgressSpinner-Um6ARKlk.js'; import '../../Command-Cj6F5B5a.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 || flags?.useDemoCredentials), usedDemoCredentials: !!flags?.useDemoCredentials } } ), 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 and publishes your ShipThis game."; 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", "<%= config.bin %> <%= command.id %> --platform ios --follow --verbose", "<%= config.bin %> <%= command.id %> --platform ios --useDemoCredentials --download game.ipa", "<%= config.bin %> <%= command.id %> --platform android --gameEngineVersion 4.5.1 --skipPublish" ]; 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)", 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 }), verbose: Flags.boolean({ default: false, description: "Enable verbose logging", required: false }), useDemoCredentials: Flags.boolean({ dependsOn: ["platform"], description: "Use demo credentials for this build (requires --platform, implies --skipPublish)", required: false }), gameEngineVersion: Flags.string({ description: "Override the specified game engine version for this build", 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 };