This repository was archived by the owner on Nov 12, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.js
More file actions
executable file
·371 lines (316 loc) · 12.7 KB
/
Copy pathindex.js
File metadata and controls
executable file
·371 lines (316 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
#!/usr/bin/env node
//Only designed for *nix
const pjson = require('./package.json');
const path = require('path');
const { getInstalledPath } = require('get-installed-path')
const term = require('terminal-kit').terminal;
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { hashElement } = require('folder-hash');
const { Octokit } = require("@octokit/rest");
const { execSync } = require('child_process');
//Change for your project
const IOS_CP_KEY = "LumberjackApps/Biteup-iOS"
const ANDROID_CP_KEY = "LumberjackApps/Biteup-Android"
const PROD_DEPLOYMENT_NAME = "Production"
const DEV_DEPLOYMENT_NAME = "Staging"
const HASHING_PATH = '.'
const TARGET_GIT_REMOTE = "https://github.com/emitapp/emit.git"
const MAIN_GIT_BRANCH = "master"
const IOS_CP_TAG_PREFIX = "ios-cp-"
const ANDROID_CP_TAG_PREFIX = "android-cp-"
const GIT_REPO_OWNER = "emitapp"
const GIT_REPO_NAME = "emit"
//Stuff you probably don't wanna change
const GIT_USER_AGENT = "https://github.com/emitapp/dymo"
const ANDROID_CHOICE_INDEX = 0
const IOS_CHOICE_INDEX = 1
const BOTH_CHOICE_INDEX = 2
const MENU_OPTIONS = { cancelable: true }
const ORANGE_HEX = "#FFA500"
const MODES = {
PROD: "prod",
DEV: "dev",
CLEAR_DEV: "clear"
}
const main = async () => {
try {
const args = parseArguments()
listenForCtrlC()
await checkForCorrectGitRemote()
await installEnv()
if (args.mode == MODES.PROD) {
checkForGithubKey()
await checkForMoreutils()
const platformChoice = await getPlatformChoice();
term("\n")
const lastVersions = await displayLastProdCPInfo(platformChoice)
const hash = await calculateProjectHash()
term(`Current project hash: ^g${hash.hash}^ (from ^+${recursivelyCountHashChildren(hash)}^ hashed files)`)
term("\n")("\n")
const commitHash = await chooseCommitToTag()
const extraMessage = await getExtraReleaseMessage()
if (userChoseAndroid(platformChoice))
await codepushAndTag(true, lastVersions, hash, commitHash, extraMessage, args)
if (userChoseIOS(platformChoice))
await codepushAndTag(false, lastVersions, hash, commitHash, extraMessage, args)
} else if (args.mode == MODES.DEV) {
const platformChoice = await getPlatformChoice();
term("\n")
if (userChoseAndroid(platformChoice))
await deployToCodepush(true, args)
if (userChoseIOS(platformChoice))
await deployToCodepush(false, args)
}
} catch (err) {
term.error.nextLine(1).red(err).nextLine(1)
} finally {
process.exit()
}
}
const installEnv = async () => {
const installationPath = await getInstalledPath(pjson.name)
const env = require('dotenv').config({ path: path.join(installationPath, ".env") })
if (env.error) {
throw env.error
}
}
const parseArguments = () => {
const args = process.argv.slice(2);
let mode = ""
//Ugly repitition but whatever
if (args.includes("prod")) {
if (mode) {
term.red("Choose only one mode")("\n")
process.exit()
}
mode = MODES.PROD
}
if (args.includes("dev")) {
if (mode) {
term.red("Choose only one mode")("\n")
process.exit()
}
mode = MODES.DEV
}
if (args.includes("clear")) {
term.red("Clear mode not supported yet! Generally, clearing codepush deployments is unsafe")("\n")
term.red("For now, you'll have to enter these manually:")("\n")
term.yellow(`code-push deployment clear ${ANDROID_CP_KEY} ${DEV_DEPLOYMENT_NAME}`)("\n")
term.yellow(`code-push deployment clear ${IOS_CP_KEY} ${DEV_DEPLOYMENT_NAME}`)("\n")
process.exit()
}
if (!mode) {
term.red("Usage: dymo [dev | prod | clean] [m]")("\n")
process.exit()
}
const argsObj = {
mode,
mandatory: args.includes("m"),
}
return argsObj
}
//TODO: consider switching to something like https://stackoverflow.com/questions/26350256/node-js-multiline-input
//NOTE: Vipe is not available on windows.
const checkForMoreutils = async () => {
try {
const command = "which vipe" //For some reason, `where` fails if vipe is not installed
const { stdout, stderr } = await exec(command);
if (!stdout) {
term.red("vipe (part of moreutils) not found. Installation instructions: https://rentes.github.io/unix/utilities/2015/07/27/moreutils-package/")
term("\n")
process.exit()
}
} catch {
term.red("which vipe failed")
term.red("vipe (part of moreutils) probably not found. Installation instructions: https://rentes.github.io/unix/utilities/2015/07/27/moreutils-package/")
term("\n")
process.exit()
}
}
const checkForCorrectGitRemote = async () => {
const command = `git remote get-url origin`
const { stdout, stderr } = await exec(command);
if (stderr) throw stderr
const url = stdout.trim()
if (url != TARGET_GIT_REMOTE) {
term.yellow(`Incorrect git remote. Looking for ${TARGET_GIT_REMOTE}, got ${url}`)("\n")
process.exit()
}
}
const listenForCtrlC = () => {
term.grabInput(); //More info about this here: https://blog.soulserv.net/tag/terminal/
term.on('key', function (name, matches, data) {
if (!name) return;
if (name === 'CTRL_C') {
term("\n")("\n").red("Dymo cancelled by user.")("\n")
process.exit();
}
});
}
const getPlatformChoice = async () => {
term.bgColorRgbHex(ORANGE_HEX)('Which platforms do you want to CP to?').bgDefaultColor();
const items = [
'ANDROID only',
'IOS only',
'BOTH'
];
const platformChoice = await term.singleColumnMenu(items, MENU_OPTIONS).promise;
if (platformChoice.canceled) {
term.red("Cancelled \n");
process.exit();
}
return platformChoice
}
const userChoseAndroid = (platformChoice) => {
const choiceIndex = platformChoice.selectedIndex
return choiceIndex == ANDROID_CHOICE_INDEX || choiceIndex == BOTH_CHOICE_INDEX
}
const userChoseIOS = (platformChoice) => {
const choiceIndex = platformChoice.selectedIndex
return choiceIndex == IOS_CHOICE_INDEX || choiceIndex == BOTH_CHOICE_INDEX
}
const displayLastProdCPInfo = async (platformChoice) => {
const versionInfo = {
ANDROID: undefined,
IOS: undefined
}
const fetchPromise = async (isAndroid) => {
const infoArray = await getVersionInfo(isAndroid)
const lastVersion = infoArray[0] ?? "v0"
const releaseDate = infoArray[1] ?? "-"
const binaryTarget = infoArray[2] ?? "-"
versionInfo[isAndroid ? "ANDROID" : "IOS"] = { lastVersion, releaseDate, binaryTarget }
return `^g${isAndroid ? "Android" : "iOS"}^: Last v: ^+^y${lastVersion}^ ^ released at ${releaseDate} for binary ${binaryTarget}`
}
const promises = []
if (userChoseAndroid(platformChoice)) {
promises.push(fetchPromise(true))
}
if (userChoseIOS(platformChoice)) {
promises.push(fetchPromise(false))
}
const s = await term.spinner("impulse");
term('Getting version info...');
const results = await Promise.all(promises)
s.animate(false)
term.eraseLine().column(0)
results.forEach(r => term(r)("\n"))
return versionInfo
}
const getVersionInfo = async (isAndroid) => {
const name = isAndroid ? ANDROID_CP_KEY : IOS_CP_KEY
const command = `appcenter codepush deployment history -a ${name} ${PROD_DEPLOYMENT_NAME} --output json`
const { stdout, stderr } = await exec(command);
if (stderr) throw stderr
const parsedResult = JSON.parse(stdout)
if (parsedResult.length == 0) return []
return (parsedResult[parsedResult.length - 1])
}
const calculateProjectHash = async () => {
const options = {
folders: { exclude: ['.*', 'node_modules', 'test_coverage', 'android', 'ios', "__tests__",] },
files: { exclude: ['.DS_Store'] },
};
return await hashElement(HASHING_PATH, options)
}
const recursivelyCountHashChildren = (hashObject) => {
if (!hashObject.children) return 1
const reducer = (prevValue, currentValue) => {
return prevValue + recursivelyCountHashChildren(currentValue)
}
return hashObject.children.reduce(reducer, 0)
}
const chooseCommitToTag = async () => {
//More info on decorate here: https://stackoverflow.com/q/63673227
const command = `git log -n 5 --oneline origin/${MAIN_GIT_BRANCH} --decorate=short`
const { stdout, stderr } = await exec(command);
if (stderr) throw stderr
const commits = stdout.trim().split("\n").map(c => c.trim())
term.bgColorRgbHex(ORANGE_HEX)('Which remote commit should be tagged?').bgDefaultColor();
const commitChoice = await term.singleColumnMenu(commits, MENU_OPTIONS).promise;
if (commitChoice.canceled) {
term.red("Cancelled \n");
process.exit();
}
const chosenCommitHash = commits[commitChoice.selectedIndex].split(" ")[0]
return chosenCommitHash
}
const getFullCommitHash = async (hash) => {
const command = `git rev-parse ${hash}`
const { stdout, stderr } = await exec(command);
if (stderr) throw stderr
return stdout
}
const getCommitMessage = async (fullHash) => {
const command = `git log --format=%B -n 1 ${fullHash}`
const { stdout, stderr } = await exec(command);
if (stderr) throw stderr
return stdout
}
const checkForGithubKey = () => {
if (!process.env.GITHUB_KEY) {
term.red("Missing Github Personal Access Token \n");
process.exit();
}
}
const tagGithubRepo = async (codepushVersion, isAndroid, hash, commitHash, extraMessage) => {
//https://octokit.github.io/rest.js/v18#usage
const octokit = new Octokit({
auth: process.env.GITHUB_KEY,
userAgent: GIT_USER_AGENT, //https://docs.github.com/en/rest/overview/resources-in-the-rest-api#user-agent-required
})
const fullCommitHash = await (await getFullCommitHash(commitHash)).trim()
const baseName = isAndroid ? ANDROID_CP_TAG_PREFIX : IOS_CP_TAG_PREFIX
let body = `Release made with [Dymo](https://github.com/emitapp/dymo) v${pjson.version} \n`;
body += `Project hash: \`${hash.hash}\` (from ${recursivelyCountHashChildren(hash)} hashed files).`
body += `\n\nCommit Message: \n` + await getCommitMessage(fullCommitHash)
if (extraMessage) body += `\n\n` + extraMessage
const title = `${isAndroid ? "Android" : "iOS"} Codepush v${codepushVersion}`
await octokit.rest.repos.createRelease({
owner: GIT_REPO_OWNER,
repo: GIT_REPO_NAME,
tag_name: baseName + codepushVersion,
body,
target_commitish: fullCommitHash,
name: title
});
}
const getExtraReleaseMessage = async () => {
term.bgColorRgbHex(ORANGE_HEX)('Add extra Github Release info?').bgDefaultColor();
const choice = await term.singleColumnMenu(["NO", "YES"], MENU_OPTIONS).promise;
if (choice.canceled) {
term.red("Cancelled \n");
process.exit();
}
if (choice.selectedIndex == 0) return ""
//https://unix.stackexchange.com/questions/125580/piping-commands-modify-stdin-write-to-stdout
//(I use nano because vim is evil and noone can convince me otherwise)
const command = `echo "" | EDITOR=nano vipe | cat`
const message = (await execSync(command)).toString().trim(); //Using normal exec result in bad input delay in nano
return message
}
codepushAndTag = async (isAndroid, lastVersions, projectHash, commitHash, extraMessage, processArgs) => {
//Getting the version of the next release (the one about to be made)
const newVersionString = (isAndroid ? lastVersions.ANDROID : lastVersions.IOS).lastVersion.replace(/\D/g, '')
let newVersion = parseInt(newVersionString)
if (isNaN(newVersion)) {
term.red("Version is NaN O.o \n");
process.exit();
}
newVersion += 1;
await tagGithubRepo(newVersion, isAndroid, projectHash, commitHash, extraMessage)
await deployToCodepush(isAndroid, processArgs)
}
const deployToCodepush = async (isAndroid, processArgs) => {
const name = isAndroid ? ANDROID_CP_KEY : IOS_CP_KEY
const deployment = processArgs.mode == MODES.PROD ? PROD_DEPLOYMENT_NAME : DEV_DEPLOYMENT_NAME
const mandatory = processArgs.mandatory ? "-m" : ""
const xCodeSchema = (processArgs.mode == MODES.PROD && !isAndroid) ? "-c \"Prod.Release\"" : ""
const command = `appcenter codepush release-react -a ${name} -d ${deployment} ${mandatory} ${xCodeSchema}`
const execPromise = exec(command);
execPromise.child.stdout.pipe(process.stdout)
const { stderr } = await execPromise
if (stderr) throw stderr
}
main()