UNPKG

@fancode/react-native-codepush-joystick

Version:
348 lines 14.3 kB
import CodePush from "react-native-code-push"; import { CodePushStatus, } from "./types"; import { GitHubProvider } from "./providers/github-provider"; import { formatDateTime } from "./utils"; import { BackHandler, Platform } from "react-native"; export class CodePushManager { stateMap = new Map(); config; callbacks; githubSourceControlProvider; cicdProvider; constructor(config) { this.config = config; this.callbacks = config.callbacks || {}; this.githubSourceControlProvider = new GitHubProvider(config.sourceControl.config); this.cicdProvider = config.cicdProvider; } async fetchPullRequests(options) { try { const pullRequests = await this.githubSourceControlProvider.fetchPullRequests(options); pullRequests.forEach((pr) => { if (!this.stateMap.has(pr.id)) { this.setStateAndNotify(pr, { status: CodePushStatus.NOT_CHECKED, loading: false, progress: null, message: null, buildInfo: null, remotePackage: null, branchName: pr.head?.ref || null, targetVersion: this.calculateTargetVersion(pr), }); } }); this.callbacks?.onPullRequestsFetched?.(pullRequests); return pullRequests; } catch (error) { this.handleError(error, "fetchPullRequests"); throw error; } } async checkCodePushUpdate(pullRequest) { const state = this.getState(pullRequest.id); try { this.setStateAndNotify(pullRequest, { loading: true, message: null }); if (!state.targetVersion) throw new Error("Unable to proceed, please restart app!"); CodePush.overrideAppVersion(state.targetVersion); const remotePackage = await CodePush.checkForUpdate(this.config.codepush.deploymentKey, undefined, state.targetVersion); if (!remotePackage) { this.setStateAndNotify(pullRequest, { status: CodePushStatus.NOT_RUNNING, message: "CodePush not found for this branch, checking for running GitHub builds!", }); await this.checkGitHubWorkflows(pullRequest); } else if (remotePackage.appVersion === state.targetVersion) { this.setStateAndNotify(pullRequest, { status: CodePushStatus.AVAILABLE, remotePackage, }); this.callbacks?.onCodePushAvailable?.(pullRequest, remotePackage); } else { this.setStateAndNotify(pullRequest, { status: CodePushStatus.ERROR, message: "There is mismatch, please re-launch the app!", }); } this.setStateAndNotify(pullRequest, { loading: false }); return this.getState(pullRequest.id); } catch (error) { this.setStateAndNotify(pullRequest, { status: CodePushStatus.ERROR, loading: false, message: error.message || "Error checking for updates", }); this.handleError(error, "checkCodePushUpdate", { pullRequest }); throw error; } } async checkGitHubWorkflows(pullRequest) { if (!pullRequest.head?.ref) { this.setStateAndNotify(pullRequest, { loading: false, message: "No branch reference found for this PR.", }); return; } try { const workflowRuns = await this.cicdProvider.getWorkflowRuns(pullRequest.head.ref); const { message, newCPState, buildInfo } = this.findWorkflowStatus(workflowRuns); const dataToBeUpdated = { status: newCPState, buildInfo, loading: false, }; if (message) dataToBeUpdated.message = message; this.setStateAndNotify(pullRequest, dataToBeUpdated); } catch (error) { this.setStateAndNotify(pullRequest, { loading: false, message: "Error checking GitHub workflows.", }); this.handleError(error, "checkGitHubWorkflows", { pullRequest }); } } findWorkflowStatus(workflowRuns) { let newCPState = CodePushStatus.NOT_RUNNING; let message = "No active running build found. Please trigger a new build."; const workflowRun = workflowRuns[0] || null; const { buildInfo, workflowStatus } = this.cicdProvider.findWorkflowStatus(workflowRun) || { buildInfo: null, workflowStatus: null, }; if (workflowStatus) { const formattedTime = formatDateTime(workflowStatus.startedAt); if (workflowStatus.isRunning) { newCPState = CodePushStatus.ALREADY_RUNNING; message = `Build #${workflowStatus.id} triggered on ${formattedTime} is running.`; } else if (workflowStatus.isFailed) { newCPState = CodePushStatus.NOT_RUNNING; message = `Build #${workflowStatus.id} failed. Triggered on ${formattedTime}`; } else if (workflowStatus.isCancelled) { newCPState = CodePushStatus.NOT_RUNNING; message = `Build #${workflowStatus.id} was cancelled. Triggered on ${formattedTime}`; } else if (workflowStatus.isCompleted) { newCPState = CodePushStatus.NOT_RUNNING; message = `Build #${workflowStatus.id} completed. Triggered on ${formattedTime}`; } } return { message, newCPState, buildInfo, }; } toASCII(str) { return str.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0); } async downloadCodePushUpdate(pullRequest) { const state = this.getState(pullRequest.id); try { this.setStateAndNotify(pullRequest, { loading: true, progress: 0 }); if (!state.remotePackage) throw new Error("No remote package available"); this.callbacks?.onDownloadStarted?.(pullRequest, state.remotePackage); const localPackage = await state.remotePackage.download((progress) => { const percent = Math.round((progress.receivedBytes / progress.totalBytes) * 100); this.setStateAndNotify(pullRequest, { progress: percent }); this.callbacks?.onDownloadProgress?.(pullRequest, percent); }); await localPackage.install(CodePush.InstallMode.ON_NEXT_RESTART); this.callbacks?.onDownloadComplete?.(pullRequest, localPackage); this.setStateAndNotify(pullRequest, { status: CodePushStatus.DOWNLOADED, progress: null, loading: false, }); return this.getState(pullRequest.id); } catch (error) { this.setStateAndNotify(pullRequest, { status: CodePushStatus.ERROR, progress: null, loading: false, message: error.message || "Error downloading update", }); this.handleError(error, "downloadCodePushUpdate", { pullRequest }); throw error; } } async triggerBuild(pullRequest) { const state = this.getState(pullRequest.id); try { this.setStateAndNotify(pullRequest, { loading: true }); if (!state.branchName || !state.targetVersion) throw new Error("Missing branch name or target version"); const params = { branch: state.branchName, version: state.targetVersion, }; const buildInfo = await this.cicdProvider.triggerBuild(params); await new Promise((resolve) => setTimeout(resolve, 5000)); this.setStateAndNotify(pullRequest, { buildInfo, status: CodePushStatus.NOT_CHECKED, message: "CodePush has been triggered", loading: false, }); this.callbacks?.onBuildTriggered?.(pullRequest, buildInfo); return buildInfo; } catch (error) { this.setStateAndNotify(pullRequest, { loading: false }); this.handleError(error, "triggerBuild", { pullRequest }); throw error; } } async cancelBuild(pullRequest) { const state = this.getState(pullRequest.id); try { this.setStateAndNotify(pullRequest, { loading: true }); if (!state.buildInfo?.buildId) throw new Error("No build to cancel"); await this.cicdProvider.cancelBuild(state.buildInfo.buildId); this.setStateAndNotify(pullRequest, { status: CodePushStatus.NOT_CHECKED, message: "CodePush is stopped now!", loading: false, }); } catch (error) { this.setStateAndNotify(pullRequest, { status: CodePushStatus.ERROR, message: "Unable to stop the CodePush, please retry!", loading: false, }); this.handleError(error, "cancelBuild", { pullRequest }); throw error; } } async restartApp() { try { if (Platform.OS === "android") { BackHandler.exitApp(); } else { // On iOS, closing the app is not allowed. console.warn("Programmatic app exit is not supported on iOS."); } } catch (error) { this.handleError(error, "restartApp"); throw error; } } async processWorkflow(pullRequest) { const state = this.getState(pullRequest.id); try { switch (state.status) { case CodePushStatus.NOT_CHECKED: case CodePushStatus.UN_AVAILABLE: case CodePushStatus.ERROR: return await this.checkCodePushUpdate(pullRequest); case CodePushStatus.AVAILABLE: return await this.downloadCodePushUpdate(pullRequest); case CodePushStatus.DOWNLOADED: await this.restartApp(); return this.getState(pullRequest.id); case CodePushStatus.ALREADY_RUNNING: await this.cancelBuild(pullRequest); return this.getState(pullRequest.id); case CodePushStatus.NOT_RUNNING: await this.triggerBuild(pullRequest); return this.getState(pullRequest.id); default: this.setStateAndNotify(pullRequest, { loading: false, message: "Unknown state, please try again", }); return this.getState(pullRequest.id); } } catch (error) { this.setStateAndNotify(pullRequest, { status: CodePushStatus.ERROR, loading: false, message: error.message || "An error occurred", }); this.handleError(error, "processWorkflow", { pullRequest }); throw error; } } getState(pullRequestId) { return (this.stateMap.get(pullRequestId) || { status: CodePushStatus.NOT_CHECKED, loading: false, progress: null, message: null, buildInfo: null, remotePackage: null, branchName: null, targetVersion: null, }); } setStateAndNotify(pullRequest, updates) { const prevState = this.getState(pullRequest.id); const newState = { ...prevState, ...updates }; this.stateMap.set(pullRequest.id, newState); this.notifyStateChange(pullRequest, newState, prevState); } calculateTargetVersion(pullRequest) { if (this.config.versioning?.strategy === "custom" && this.config.versioning.customCalculator) { return this.config.versioning.customCalculator(pullRequest, this.config.appVersion); } const versionParts = this.config.appVersion.split("."); const lastPartIndex = versionParts.length - 1; if (lastPartIndex >= 0 && pullRequest.head?.ref) { const commitId = pullRequest.statuses_url.split("/").pop()?.substring(0, 5) || ""; const commitNumber = this.toASCII(commitId); versionParts[lastPartIndex] = String(Number(versionParts[lastPartIndex]) + pullRequest.number + commitNumber); return versionParts.join("."); } return null; } formatVersionForStorage(localPackage) { let version = localPackage.label.replace("v", ""); if (localPackage.description) { version += "/" + localPackage.description; } return version; } notifyStateChange(pullRequest, newState, oldState) { this.callbacks?.onStateChange?.(pullRequest, newState, oldState); } handleError(error, context, metadata) { this.callbacks?.onError?.(error, context, metadata); } async getAppUpdateMetadata() { let updateMetadata = null; try { updateMetadata = await CodePush.getUpdateMetadata(); } catch (e) { } return { updateMetadata }; } setCallbacks(callbacks) { this.callbacks = callbacks || {}; } resetState(pullRequestId) { if (pullRequestId !== undefined) { this.stateMap.delete(pullRequestId); } else { this.stateMap.clear(); } } } //# sourceMappingURL=codepush-manager.js.map