2021-01-18 16:37:27 +02:00
import * as fs from 'fs-extra' ;
2021-05-21 15:17:21 +02:00
import { readCredentialFile } from '@joplin/lib/utils/credentialFiles' ;
2021-01-18 16:37:27 +02:00
2020-01-22 22:33:43 +02:00
const fetch = require ( 'node-fetch' ) ;
2020-11-06 20:45:45 +02:00
const execa = require ( 'execa' ) ;
2021-01-18 12:13:26 +02:00
const { splitCommandString } = require ( '@joplin/lib/string-utils' ) ;
2021-01-24 21:25:32 +02:00
const moment = require ( 'moment' ) ;
2020-01-22 22:33:43 +02:00
2021-01-18 16:37:27 +02:00
function quotePath ( path : string ) {
if ( ! path ) return '' ;
if ( path . indexOf ( '"' ) < 0 && path . indexOf ( ' ' ) < 0 ) return path ;
path = path . replace ( /"/ , '\\"' ) ;
return ` " ${ path } " ` ;
}
function commandToString ( commandName : string , args : string [ ] = [ ] ) {
const output = [ quotePath ( commandName ) ] ;
for ( const arg of args ) {
output . push ( quotePath ( arg ) ) ;
}
return output . join ( ' ' ) ;
}
2021-05-14 17:17:02 +02:00
async function insertChangelog ( tag : string , changelogPath : string , changelog : string , isPrerelease : boolean ) {
2021-01-24 21:25:32 +02:00
const currentText = await fs . readFile ( changelogPath , 'UTF-8' ) ;
const lines = currentText . split ( '\n' ) ;
const beforeLines = [ ] ;
const afterLines = [ ] ;
for ( const line of lines ) {
if ( afterLines . length ) {
afterLines . push ( line ) ;
continue ;
}
if ( line . indexOf ( '##' ) === 0 ) {
afterLines . push ( line ) ;
continue ;
}
beforeLines . push ( line ) ;
}
const header = [
'##' ,
` [ ${ tag } ](https://github.com/laurent22/joplin/releases/tag/ ${ tag } ) ` ,
] ;
2021-05-14 17:17:02 +02:00
if ( isPrerelease ) header . push ( '(Pre-release)' ) ;
header . push ( '-' ) ;
// eslint-disable-next-line no-useless-escape
header . push ( ` ${ moment . utc ( ) . format ( 'YYYY-MM-DD\THH:mm:ss' ) } Z ` ) ;
2021-01-24 21:25:32 +02:00
let newLines = [ ] ;
newLines . push ( header . join ( ' ' ) ) ;
newLines . push ( '' ) ;
newLines = newLines . concat ( changelog . split ( '\n' ) ) ;
newLines . push ( '' ) ;
const output = beforeLines . concat ( newLines ) . concat ( afterLines ) ;
return output . join ( '\n' ) ;
}
2021-05-14 17:17:02 +02:00
export async function completeReleaseWithChangelog ( changelogPath : string , newVersion : string , newTag : string , appName : string , isPreRelease : boolean ) {
2021-06-15 22:08:55 +02:00
const changelog = ( await execCommand2 ( ` node ${ rootDir } /packages/tools/git-changelog ${ newTag } --publish-format full ` , { } ) ) . trim ( ) ;
2021-01-24 21:25:32 +02:00
2021-05-14 17:17:02 +02:00
const newChangelog = await insertChangelog ( newTag , changelogPath , changelog , isPreRelease ) ;
2021-01-24 21:25:32 +02:00
await fs . writeFile ( changelogPath , newChangelog ) ;
const finalCmds = [
'git pull' ,
'git add -A' ,
` git commit -m " ${ appName } ${ newVersion } " ` ,
` git tag " ${ newTag } " ` ,
'git push' ,
'git push --tags' ,
] ;
console . info ( '' ) ;
console . info ( 'Verify that the changelog is correct:' ) ;
console . info ( '' ) ;
console . info ( ` ${ process . env . EDITOR } " ${ changelogPath } " ` ) ;
console . info ( '' ) ;
console . info ( 'Then run these commands:' ) ;
console . info ( '' ) ;
console . info ( finalCmds . join ( ' && ' ) ) ;
}
2021-01-18 16:37:27 +02:00
async function loadGitHubUsernameCache() {
const path = ` ${ __dirname } /github_username_cache.json ` ;
if ( await fs . pathExists ( path ) ) {
const jsonString = await fs . readFile ( path , 'utf8' ) ;
return JSON . parse ( jsonString ) ;
}
return { } ;
}
async function saveGitHubUsernameCache ( cache : any ) {
const path = ` ${ __dirname } /github_username_cache.json ` ;
await fs . writeFile ( path , JSON . stringify ( cache ) ) ;
}
// Returns the project root dir
export const rootDir = require ( 'path' ) . dirname ( require ( 'path' ) . dirname ( __dirname ) ) ;
2017-12-04 20:16:14 +02:00
2021-08-28 11:49:31 +02:00
export function execCommand ( command : string , options : any = null ) : Promise < string > {
2021-05-15 15:13:08 +02:00
options = options || { } ;
2019-07-30 09:35:42 +02:00
const exec = require ( 'child_process' ) . exec ;
2017-12-04 20:16:14 +02:00
return new Promise ( ( resolve , reject ) = > {
2021-05-15 15:13:08 +02:00
exec ( command , options , ( error : any , stdout : any , stderr : any ) = > {
2017-12-04 20:16:14 +02:00
if ( error ) {
if ( error . signal == 'SIGTERM' ) {
resolve ( 'Process was killed' ) ;
} else {
reject ( error ) ;
}
} else {
2020-11-08 19:41:56 +02:00
resolve ( [ stdout . trim ( ) , stderr . trim ( ) ] . join ( '\n' ) ) ;
2017-12-04 20:16:14 +02:00
}
} ) ;
2019-10-13 01:21:56 +02:00
} ) ;
2020-11-06 20:45:45 +02:00
}
2021-01-18 16:37:27 +02:00
export function resolveRelativePathWithinDir ( baseDir : string , . . . relativePath : string [ ] ) {
2021-01-05 17:25:15 +02:00
const path = require ( 'path' ) ;
const resolvedBaseDir = path . resolve ( baseDir ) ;
const resolvedPath = path . resolve ( baseDir , . . . relativePath ) ;
if ( resolvedPath . indexOf ( resolvedBaseDir ) !== 0 ) throw new Error ( ` Resolved path for relative path " ${ JSON . stringify ( relativePath ) } " is not within base directory " ${ baseDir } " (Was resolved to ${ resolvedPath } ) ` ) ;
return resolvedPath ;
2021-01-18 16:37:27 +02:00
}
2021-01-05 17:25:15 +02:00
2021-01-18 16:37:27 +02:00
export function execCommandVerbose ( commandName : string , args : string [ ] = [ ] ) {
2020-11-06 20:45:45 +02:00
console . info ( ` > ${ commandToString ( commandName , args ) } ` ) ;
const promise = execa ( commandName , args ) ;
promise . stdout . pipe ( process . stdout ) ;
return promise ;
2021-01-18 16:37:27 +02:00
}
interface ExecCommandOptions {
showInput? : boolean ;
showOutput? : boolean ;
quiet? : boolean ;
}
2020-11-06 20:45:45 +02:00
2021-01-18 12:13:26 +02:00
// There's lot of execCommandXXX functions, but eventually all scripts should
// use the one below, which supports:
//
// - Printing the command being executed
// - Printing the output in real time (piping to stdout)
// - Returning the command result as string
2021-01-18 16:37:27 +02:00
export async function execCommand2 ( command : string | string [ ] , options : ExecCommandOptions = null ) : Promise < string > {
2021-01-18 12:13:26 +02:00
options = {
showInput : true ,
showOutput : true ,
2021-01-18 16:37:27 +02:00
quiet : false ,
2021-01-18 12:13:26 +02:00
. . . options ,
} ;
2021-01-18 16:37:27 +02:00
if ( options . quiet ) {
options . showInput = false ;
options . showOutput = false ;
}
if ( options . showInput ) {
if ( typeof command === 'string' ) {
console . info ( ` > ${ command } ` ) ;
} else {
console . info ( ` > ${ commandToString ( command [ 0 ] , command . slice ( 1 ) ) } ` ) ;
}
}
const args : string [ ] = typeof command === 'string' ? splitCommandString ( command ) : command as string [ ] ;
2021-01-18 12:13:26 +02:00
const executableName = args [ 0 ] ;
args . splice ( 0 , 1 ) ;
const promise = execa ( executableName , args ) ;
if ( options . showOutput ) promise . stdout . pipe ( process . stdout ) ;
const result = await promise ;
2021-01-18 16:37:27 +02:00
return result . stdout . trim ( ) ;
}
2021-01-18 12:13:26 +02:00
2021-01-18 16:37:27 +02:00
export function execCommandWithPipes ( executable : string , args : string [ ] ) {
2020-03-14 01:46:14 +02:00
const spawn = require ( 'child_process' ) . spawn ;
2019-10-13 01:21:56 +02:00
return new Promise ( ( resolve , reject ) = > {
2020-02-05 00:09:34 +02:00
const child = spawn ( executable , args , { stdio : 'inherit' } ) ;
2019-10-13 01:21:56 +02:00
2021-01-18 16:37:27 +02:00
child . on ( 'error' , ( error : any ) = > {
2019-10-13 01:21:56 +02:00
reject ( error ) ;
} ) ;
2021-01-18 16:37:27 +02:00
child . on ( 'close' , ( code : any ) = > {
2019-10-13 01:21:56 +02:00
if ( code !== 0 ) {
reject ( ` Ended with code ${ code } ` ) ;
} else {
2021-01-18 16:37:27 +02:00
resolve ( null ) ;
2019-10-13 01:21:56 +02:00
}
} ) ;
2017-12-04 20:16:14 +02:00
} ) ;
2021-01-18 16:37:27 +02:00
}
2017-12-04 20:16:14 +02:00
2021-01-18 16:37:27 +02:00
export function toSystemSlashes ( path : string ) {
2020-11-06 20:45:45 +02:00
const os = process . platform ;
if ( os === 'win32' ) return path . replace ( /\//g , '\\' ) ;
return path . replace ( /\\/g , '/' ) ;
2021-01-18 16:37:27 +02:00
}
2020-11-06 20:45:45 +02:00
2021-01-18 16:37:27 +02:00
export async function setPackagePrivateField ( filePath : string , value : any ) {
2020-12-28 00:22:29 +02:00
const text = await fs . readFile ( filePath , 'utf8' ) ;
const obj = JSON . parse ( text ) ;
if ( ! value ) {
delete obj . private ;
} else {
obj . private = true ;
}
await fs . writeFile ( filePath , JSON . stringify ( obj , null , 2 ) , 'utf8' ) ;
2021-01-18 16:37:27 +02:00
}
2020-12-28 00:22:29 +02:00
2021-01-18 16:37:27 +02:00
export async function downloadFile ( url : string , targetPath : string ) {
2017-12-04 20:16:14 +02:00
const https = require ( 'https' ) ;
const fs = require ( 'fs' ) ;
return new Promise ( ( resolve , reject ) = > {
const file = fs . createWriteStream ( targetPath ) ;
2021-01-18 16:37:27 +02:00
https . get ( url , function ( response : any ) {
2019-09-19 23:51:18 +02:00
if ( response . statusCode !== 200 ) reject ( new Error ( ` HTTP error ${ response . statusCode } ` ) ) ;
2017-12-04 20:16:14 +02:00
response . pipe ( file ) ;
file . on ( 'finish' , function ( ) {
2019-10-09 21:35:13 +02:00
// file.close();
2021-01-18 16:37:27 +02:00
resolve ( null ) ;
2017-12-04 20:16:14 +02:00
} ) ;
2021-01-18 16:37:27 +02:00
} ) . on ( 'error' , ( error : any ) = > {
2017-12-04 20:16:14 +02:00
reject ( error ) ;
} ) ;
} ) ;
2021-01-18 16:37:27 +02:00
}
2017-12-04 20:16:14 +02:00
2021-01-18 16:37:27 +02:00
export function fileSha256 ( filePath : string ) {
2017-12-04 20:16:14 +02:00
return new Promise ( ( resolve , reject ) = > {
const crypto = require ( 'crypto' ) ;
const fs = require ( 'fs' ) ;
const algo = 'sha256' ;
const shasum = crypto . createHash ( algo ) ;
const s = fs . ReadStream ( filePath ) ;
2021-01-18 16:37:27 +02:00
s . on ( 'data' , function ( d : any ) { shasum . update ( d ) ; } ) ;
2017-12-04 20:16:14 +02:00
s . on ( 'end' , function ( ) {
const d = shasum . digest ( 'hex' ) ;
resolve ( d ) ;
} ) ;
2021-01-18 16:37:27 +02:00
s . on ( 'error' , function ( error : any ) {
2017-12-04 20:16:14 +02:00
reject ( error ) ;
} ) ;
} ) ;
2021-01-18 16:37:27 +02:00
}
2017-12-04 20:16:14 +02:00
2021-01-18 16:37:27 +02:00
export async function unlinkForce ( filePath : string ) {
2017-12-04 20:16:14 +02:00
const fs = require ( 'fs-extra' ) ;
try {
await fs . unlink ( filePath ) ;
} catch ( error ) {
if ( error . code === 'ENOENT' ) return ;
throw error ;
}
2021-01-18 16:37:27 +02:00
}
2017-12-04 20:16:14 +02:00
2021-01-18 16:37:27 +02:00
export function fileExists ( filePath : string ) {
2018-10-13 12:09:03 +02:00
const fs = require ( 'fs-extra' ) ;
2019-07-30 09:35:42 +02:00
2017-12-04 20:16:14 +02:00
return new Promise ( ( resolve , reject ) = > {
2021-01-18 16:37:27 +02:00
fs . stat ( filePath , function ( err : any ) {
2017-12-04 20:16:14 +02:00
if ( err == null ) {
resolve ( true ) ;
2019-10-09 21:35:13 +02:00
} else if ( err . code == 'ENOENT' ) {
2017-12-04 20:16:14 +02:00
resolve ( false ) ;
} else {
reject ( err ) ;
}
} ) ;
} ) ;
2021-01-18 16:37:27 +02:00
}
2020-01-22 22:33:43 +02:00
2021-01-18 16:37:27 +02:00
export async function gitRepoClean ( ) : Promise < boolean > {
const output = await execCommand2 ( 'git status --porcelain' , { quiet : true } ) ;
return ! output . trim ( ) ;
2020-01-22 22:33:43 +02:00
}
2021-01-18 16:37:27 +02:00
export async function gitRepoCleanTry() {
if ( ! ( await gitRepoClean ( ) ) ) throw new Error ( ` There are pending changes in the repository: ${ process . cwd ( ) } ` ) ;
2020-01-22 22:33:43 +02:00
}
2021-01-20 01:02:58 +02:00
export async function gitPullTry ( ignoreIfNotBranch = true ) {
2020-11-18 12:36:19 +02:00
try {
2021-01-18 16:37:27 +02:00
await execCommand ( 'git pull' ) ;
2020-11-18 12:36:19 +02:00
} catch ( error ) {
2021-01-20 01:02:58 +02:00
if ( ignoreIfNotBranch && error . message . includes ( 'no tracking information for the current branch' ) ) {
2020-11-18 12:36:19 +02:00
console . info ( 'Skipping git pull because no tracking information on current branch' ) ;
} else {
throw error ;
}
}
2021-01-18 16:37:27 +02:00
}
2020-11-18 12:36:19 +02:00
2021-01-18 16:37:27 +02:00
export async function githubUsername ( email : string , name : string ) {
2020-01-22 22:33:43 +02:00
const cache = await loadGitHubUsernameCache ( ) ;
2020-01-25 00:43:55 +02:00
const cacheKey = ` ${ email } : ${ name } ` ;
if ( cacheKey in cache ) return cache [ cacheKey ] ;
2020-01-22 22:33:43 +02:00
let output = null ;
2021-01-18 16:37:27 +02:00
const oauthToken = await githubOauthToken ( ) ;
2020-01-22 22:33:43 +02:00
2020-01-25 00:43:55 +02:00
const urlsToTry = [
` https://api.github.com/search/users?q= ${ encodeURI ( email ) } +in:email ` ,
2021-08-28 11:49:31 +02:00
// Note that this can fail if the email could not be found and the user
// shares a name with someone else. It's rare enough that we can leave
// it for now.
// https://github.com/laurent22/joplin/pull/5390
2020-01-25 00:43:55 +02:00
` https://api.github.com/search/users?q=user: ${ encodeURI ( name ) } ` ,
] ;
for ( const url of urlsToTry ) {
const response = await fetch ( url , {
method : 'GET' ,
headers : {
'Content-Type' : 'application/json' ,
'Authorization' : ` token ${ oauthToken } ` ,
} ,
} ) ;
const responseText = await response . text ( ) ;
2020-01-22 22:33:43 +02:00
2020-01-25 00:43:55 +02:00
if ( ! response . ok ) continue ;
2020-01-22 22:33:43 +02:00
const responseJson = JSON . parse ( responseText ) ;
2020-01-25 00:43:55 +02:00
if ( ! responseJson || ! responseJson . items || responseJson . items . length !== 1 ) continue ;
output = responseJson . items [ 0 ] . login ;
break ;
2020-01-22 22:33:43 +02:00
}
2020-01-25 00:43:55 +02:00
cache [ cacheKey ] = output ;
2020-01-22 22:33:43 +02:00
await saveGitHubUsernameCache ( cache ) ;
2020-01-25 00:43:55 +02:00
2020-01-22 22:33:43 +02:00
return output ;
2021-01-18 16:37:27 +02:00
}
2020-01-22 22:33:43 +02:00
2021-01-18 16:37:27 +02:00
export function patreonOauthToken() {
return readCredentialFile ( 'patreon_oauth_token.txt' ) ;
}
2020-04-16 00:11:42 +02:00
2021-01-18 16:37:27 +02:00
export function githubOauthToken() {
return readCredentialFile ( 'github_oauth_token.txt' ) ;
}
2018-02-04 19:42:33 +02:00
2021-01-18 16:37:27 +02:00
export async function githubRelease ( project : string , tagName : string , options : any = null ) {
2019-01-12 00:07:23 +02:00
options = Object . assign ( { } , {
isDraft : false ,
isPreRelease : false ,
} , options ) ;
2021-01-18 16:37:27 +02:00
const oauthToken = await githubOauthToken ( ) ;
2019-07-30 09:35:42 +02:00
2019-09-19 23:51:18 +02:00
const response = await fetch ( ` https://api.github.com/repos/laurent22/ ${ project } /releases ` , {
2019-07-30 09:35:42 +02:00
method : 'POST' ,
2018-02-04 19:42:33 +02:00
body : JSON.stringify ( {
tag_name : tagName ,
name : tagName ,
2019-01-12 00:07:23 +02:00
draft : options.isDraft ,
prerelease : options.isPreRelease ,
2018-02-04 19:42:33 +02:00
} ) ,
headers : {
'Content-Type' : 'application/json' ,
2019-09-19 23:51:18 +02:00
'Authorization' : ` token ${ oauthToken } ` ,
2018-02-04 19:42:33 +02:00
} ,
} ) ;
const responseText = await response . text ( ) ;
2019-07-30 09:35:42 +02:00
2019-09-19 23:51:18 +02:00
if ( ! response . ok ) throw new Error ( ` Cannot create GitHub release: ${ responseText } ` ) ;
2018-02-04 19:42:33 +02:00
const responseJson = JSON . parse ( responseText ) ;
2019-09-19 23:51:18 +02:00
if ( ! responseJson . url ) throw new Error ( ` No URL for release: ${ responseText } ` ) ;
2018-02-04 19:42:33 +02:00
return responseJson ;
2021-01-18 16:37:27 +02:00
}
2018-02-04 19:42:33 +02:00
2021-01-18 16:37:27 +02:00
export function readline ( question : string ) {
2019-09-13 00:16:42 +02:00
return new Promise ( ( resolve ) = > {
2019-06-15 19:58:09 +02:00
const readline = require ( 'readline' ) ;
const rl = readline . createInterface ( {
input : process.stdin ,
2019-07-30 09:35:42 +02:00
output : process.stdout ,
2019-06-15 19:58:09 +02:00
} ) ;
2021-01-18 16:37:27 +02:00
rl . question ( ` ${ question } ` , ( answer : string ) = > {
2019-06-15 19:58:09 +02:00
resolve ( answer ) ;
rl . close ( ) ;
} ) ;
} ) ;
2021-01-18 16:37:27 +02:00
}
2019-06-15 19:58:09 +02:00
2021-01-18 16:37:27 +02:00
export function isLinux() {
2018-10-13 00:25:11 +02:00
return process && process . platform === 'linux' ;
2021-01-18 16:37:27 +02:00
}
2018-10-13 00:25:11 +02:00
2021-01-18 16:37:27 +02:00
export function isWindows() {
2018-10-13 00:25:11 +02:00
return process && process . platform === 'win32' ;
2021-01-18 16:37:27 +02:00
}
2018-10-13 00:25:11 +02:00
2021-01-18 16:37:27 +02:00
export function isMac() {
2018-10-13 00:25:11 +02:00
return process && process . platform === 'darwin' ;
2021-01-18 16:37:27 +02:00
}
2018-10-13 00:25:11 +02:00
2021-01-18 16:37:27 +02:00
export async function insertContentIntoFile ( filePath : string , markerOpen : string , markerClose : string , contentToInsert : string ) {
2019-07-18 19:36:29 +02:00
const fs = require ( 'fs-extra' ) ;
let content = await fs . readFile ( filePath , 'utf-8' ) ;
// [^]* matches any character including new lines
2019-09-19 23:51:18 +02:00
const regex = new RegExp ( ` ${ markerOpen } [^]*? ${ markerClose } ` ) ;
2019-07-18 19:36:29 +02:00
content = content . replace ( regex , markerOpen + contentToInsert + markerClose ) ;
await fs . writeFile ( filePath , content ) ;
2021-01-18 16:37:27 +02:00
}
2019-07-18 19:36:29 +02:00
2021-01-18 16:37:27 +02:00
export function dirname ( path : string ) {
2020-11-05 18:58:23 +02:00
if ( ! path ) throw new Error ( 'Path is empty' ) ;
const s = path . split ( /\/|\\/ ) ;
s . pop ( ) ;
return s . join ( '/' ) ;
2021-01-18 16:37:27 +02:00
}
2020-11-05 18:58:23 +02:00
2021-01-18 16:37:27 +02:00
export function basename ( path : string ) {
2020-11-05 18:58:23 +02:00
if ( ! path ) throw new Error ( 'Path is empty' ) ;
const s = path . split ( /\/|\\/ ) ;
return s [ s . length - 1 ] ;
2021-01-18 16:37:27 +02:00
}
2020-11-05 18:58:23 +02:00
2021-01-18 16:37:27 +02:00
export function filename ( path : string , includeDir = false ) {
2020-11-05 18:58:23 +02:00
if ( ! path ) throw new Error ( 'Path is empty' ) ;
2021-01-18 16:37:27 +02:00
const output = includeDir ? path : basename ( path ) ;
2020-11-05 18:58:23 +02:00
if ( output . indexOf ( '.' ) < 0 ) return output ;
const splitted = output . split ( '.' ) ;
splitted . pop ( ) ;
return splitted . join ( '.' ) ;
2021-01-18 16:37:27 +02:00
}
2020-11-05 18:58:23 +02:00
2021-01-18 16:37:27 +02:00
export function fileExtension ( path : string ) {
2020-11-05 18:58:23 +02:00
if ( ! path ) throw new Error ( 'Path is empty' ) ;
const output = path . split ( '.' ) ;
if ( output . length <= 1 ) return '' ;
return output [ output . length - 1 ] ;
2021-01-18 16:37:27 +02:00
}