@wmfs/lerna-sync 1.37.1 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +21 -0
- package/CHANGELOG.md +29 -0
- package/README.md +147 -59
- package/lib/discover.js +52 -0
- package/lib/execute-plan.js +141 -0
- package/lib/github-graphql-query.js +39 -36
- package/lib/index.js +78 -345
- package/lib/inspect.js +99 -0
- package/lib/plan-sync.js +182 -0
- package/lib/report.js +195 -0
- package/lib/utils/git.js +59 -0
- package/lib/utils/map-limit.js +17 -0
- package/lib/utils/package-folders-from-patterns.js +25 -0
- package/package.json +8 -8
- package/renovate.json +4 -0
- package/test/discover-spec.js +128 -0
- package/test/execute-plan-spec.js +233 -0
- package/test/helpers/fixtures.js +64 -0
- package/test/helpers/git-sandbox.js +87 -0
- package/test/inspect-spec.js +148 -0
- package/test/plan-sync-spec.js +247 -0
- package/test/report-spec.js +178 -0
- package/test/sync-spec.js +130 -8
- package/test/utils-spec.js +136 -0
- package/lib/actions/clone.js +0 -49
- package/lib/actions/index.js +0 -4
- package/lib/actions/pull.js +0 -52
- package/lib/utils/Messages.js +0 -69
- package/lib/utils/run.js +0 -11
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* eslint-env mocha */
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai')
|
|
4
|
+
const { captureConsole } = require('./helpers/fixtures')
|
|
5
|
+
const packageFoldersFromPatterns = require('../lib/utils/package-folders-from-patterns')
|
|
6
|
+
const mapLimit = require('../lib/utils/map-limit')
|
|
7
|
+
|
|
8
|
+
describe('packageFoldersFromPatterns', () => {
|
|
9
|
+
let output
|
|
10
|
+
beforeEach(() => { output = captureConsole() })
|
|
11
|
+
afterEach(() => output.restore())
|
|
12
|
+
|
|
13
|
+
it('turns "<folder>/*" patterns into folder names', () => {
|
|
14
|
+
expect(packageFoldersFromPatterns(['packages/*', 'plugins/*', 'blueprints/*']))
|
|
15
|
+
.to.deep.equal(['packages', 'plugins', 'blueprints'])
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('normalises leading ./, trailing slashes and Windows separators', () => {
|
|
19
|
+
expect(packageFoldersFromPatterns(['./plugins/*', 'blueprints\\*', 'apps/*/']))
|
|
20
|
+
.to.deep.equal(['plugins', 'blueprints', 'apps'])
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('keeps nested folders', () => {
|
|
24
|
+
expect(packageFoldersFromPatterns(['plugins/core/*'])).to.deep.equal(['plugins/core'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('skips negations silently', () => {
|
|
28
|
+
expect(packageFoldersFromPatterns(['packages/*', '!packages/legacy'])).to.deep.equal(['packages'])
|
|
29
|
+
expect(output.lines).to.have.length(0)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('warns about and skips patterns that cannot be a clone target', () => {
|
|
33
|
+
expect(packageFoldersFromPatterns(['apps/**', 'apps/*/packages/*', 'packages'])).to.deep.equal([])
|
|
34
|
+
expect(output.lines).to.have.length(3)
|
|
35
|
+
expect(output.text()).to.include('apps/**')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('de-duplicates', () => {
|
|
39
|
+
expect(packageFoldersFromPatterns(['plugins/*', './plugins/*'])).to.deep.equal(['plugins'])
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('mapLimit', () => {
|
|
44
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
45
|
+
|
|
46
|
+
it('returns results in input order, not completion order', async () => {
|
|
47
|
+
const result = await mapLimit([30, 10, 20], 3, async ms => { await delay(ms); return ms })
|
|
48
|
+
expect(result).to.deep.equal([30, 10, 20])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('never runs more than the limit at once', async () => {
|
|
52
|
+
let running = 0
|
|
53
|
+
let peak = 0
|
|
54
|
+
await mapLimit(Array.from({ length: 10 }, (_, i) => i), 3, async () => {
|
|
55
|
+
running++
|
|
56
|
+
peak = Math.max(peak, running)
|
|
57
|
+
await delay(5)
|
|
58
|
+
running--
|
|
59
|
+
})
|
|
60
|
+
expect(peak).to.equal(3)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('handles an empty list', async () => {
|
|
64
|
+
expect(await mapLimit([], 4, async x => x)).to.deep.equal([])
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('passes the index to the callback', async () => {
|
|
68
|
+
expect(await mapLimit(['a', 'b'], 2, async (x, i) => `${x}${i}`)).to.deep.equal(['a0', 'b1'])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
for (const bad of [0, -1, 1.5, NaN, undefined, '4']) {
|
|
72
|
+
it(`rejects a limit of ${String(bad)}`, async () => {
|
|
73
|
+
let error
|
|
74
|
+
try { await mapLimit([1, 2], bad, async x => x) } catch (e) { error = e }
|
|
75
|
+
expect(error).to.be.instanceOf(RangeError)
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('git helper', () => {
|
|
81
|
+
const { git, tryGit } = require('../lib/utils/git')
|
|
82
|
+
const os = require('os')
|
|
83
|
+
|
|
84
|
+
// A git alias runs through sh, which Git for Windows also ships
|
|
85
|
+
const alias = command => ['-c', `alias.probe=!${command}`, 'probe']
|
|
86
|
+
|
|
87
|
+
it('resolves with trimmed stdout', async () => {
|
|
88
|
+
expect(await git(alias('printf "hello\\n\\n"'), os.tmpdir())).to.equal('hello')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('kills a command that runs past the timeout', async () => {
|
|
92
|
+
let error
|
|
93
|
+
try {
|
|
94
|
+
await git(alias('sleep 5'), os.tmpdir(), { timeout: 300 })
|
|
95
|
+
} catch (e) {
|
|
96
|
+
error = e
|
|
97
|
+
}
|
|
98
|
+
expect(error.killed).to.equal(true)
|
|
99
|
+
expect(error.message).to.match(/timed out after 300ms/)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('returns null from tryGit instead of throwing', async () => {
|
|
103
|
+
expect(await tryGit(alias('exit 1'), os.tmpdir())).to.equal(null)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('environment', () => {
|
|
107
|
+
const saved = {}
|
|
108
|
+
const keys = ['GIT_TERMINAL_PROMPT', 'GIT_SSH_COMMAND', 'GIT_SSH']
|
|
109
|
+
|
|
110
|
+
beforeEach(() => keys.forEach(k => { saved[k] = process.env[k]; delete process.env[k] }))
|
|
111
|
+
afterEach(() => keys.forEach(k => {
|
|
112
|
+
if (saved[k] === undefined) delete process.env[k]
|
|
113
|
+
else process.env[k] = saved[k]
|
|
114
|
+
}))
|
|
115
|
+
|
|
116
|
+
const envVar = name => git(alias(`printf %s "$${name}"`), os.tmpdir())
|
|
117
|
+
|
|
118
|
+
it('disables terminal credential prompts', async () => {
|
|
119
|
+
expect(await envVar('GIT_TERMINAL_PROMPT')).to.equal('0')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('stops ssh prompting for passphrases or host keys', async () => {
|
|
123
|
+
expect(await envVar('GIT_SSH_COMMAND')).to.equal('ssh -o BatchMode=yes')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('keeps a GIT_SSH_COMMAND the user already set', async () => {
|
|
127
|
+
process.env.GIT_SSH_COMMAND = 'ssh -i ~/.ssh/work_key'
|
|
128
|
+
expect(await envVar('GIT_SSH_COMMAND')).to.equal('ssh -i ~/.ssh/work_key')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('does not change process.env itself', async () => {
|
|
132
|
+
await envVar('GIT_TERMINAL_PROMPT')
|
|
133
|
+
expect(process.env.GIT_TERMINAL_PROMPT).to.equal(undefined)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
package/lib/actions/clone.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
const chalk = require('chalk')
|
|
2
|
-
const path = require('path')
|
|
3
|
-
const run = require('./../utils/run')
|
|
4
|
-
|
|
5
|
-
module.exports = async function cloneAction (action, options, messages) {
|
|
6
|
-
const repoName = action.config.repoName
|
|
7
|
-
const destPath = path.join(
|
|
8
|
-
options.monorepoPath,
|
|
9
|
-
action.config.packageName,
|
|
10
|
-
repoName
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
const creds = `${options.gitHubUser}:${options.gitHubToken}@github.com`
|
|
14
|
-
|
|
15
|
-
// eg.g. cloneUrl = 'https://github.com/octocat/Hello-World.git'
|
|
16
|
-
const cloneUrl = `https://github.com/${options.gitHubOrgName}/${repoName}.git`
|
|
17
|
-
const cloneCmd = `git clone ${cloneUrl.replace('github.com', creds)} ${destPath}`
|
|
18
|
-
|
|
19
|
-
console.log(`${chalk.green('CLONE')} ${chalk.gray(`/${action.config.packageName}/`)}${repoName}`)
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
await run(cloneCmd)
|
|
23
|
-
} catch (err) {
|
|
24
|
-
console.log(`${chalk.inverse(chalk.red('FAILED'))} - ${chalk.red(err.message)}`)
|
|
25
|
-
messages.addMessage(
|
|
26
|
-
repoName,
|
|
27
|
-
'Cloning',
|
|
28
|
-
'ERROR',
|
|
29
|
-
err
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/*
|
|
35
|
-
async function cloneRepo (cloneUrl, creds, destPath) {
|
|
36
|
-
const cloneCmd = `git clone ${cloneUrl.replace('github.com', creds)} ${destPath}`
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
await run(cloneCmd)
|
|
40
|
-
return [true, 'cloned']
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (!err.message.includes('already exists and is not an empty directory')) {
|
|
43
|
-
throw err
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return [false, 'cloned']
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
*/
|
package/lib/actions/index.js
DELETED
package/lib/actions/pull.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
const path = require('path')
|
|
2
|
-
const STATUS_CMD = 'git -c core.quotepath=false status'
|
|
3
|
-
const MERGE_CMD = 'git -c core.quotepath=false merge origin/master --no-stat -v'
|
|
4
|
-
const COMMIT_OR_STASH = /commit your changes or stash them /
|
|
5
|
-
const ALREADY_UP_TO_DATE = /Already up to date./
|
|
6
|
-
const UNMERGED_CHANGES = /not possible because you have unmerged files/
|
|
7
|
-
const FETCH_CMD = 'git -c core.quotepath=false fetch origin --progress --prune'
|
|
8
|
-
const run = require('./../utils/run')
|
|
9
|
-
const chalk = require('chalk')
|
|
10
|
-
module.exports = async function pullAction (action, options, messages) {
|
|
11
|
-
const repoName = action.config.repoName
|
|
12
|
-
const destPath = path.join(
|
|
13
|
-
options.monorepoPath,
|
|
14
|
-
action.config.packageName,
|
|
15
|
-
repoName
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
await run(FETCH_CMD, destPath)
|
|
20
|
-
const mergeResult = await run(MERGE_CMD, destPath)
|
|
21
|
-
if (ALREADY_UP_TO_DATE.test(mergeResult)) {
|
|
22
|
-
await run(STATUS_CMD, destPath)
|
|
23
|
-
action.type = 'SKIP'
|
|
24
|
-
// TODO: Study what's returned and regex to victory, if such a thing is possible.
|
|
25
|
-
messages.addMessage(
|
|
26
|
-
repoName,
|
|
27
|
-
'Pulling',
|
|
28
|
-
'INFO',
|
|
29
|
-
new Error('You may have untracked files that need adding.')
|
|
30
|
-
)
|
|
31
|
-
console.log(`${chalk.blue(' INFO')} ${chalk.gray(`/${action.config.packageName}/`)}${repoName}`)
|
|
32
|
-
} else {
|
|
33
|
-
console.log(`${chalk.cyan(' PULL')} ${chalk.gray(`/${action.config.packageName}/`)}${repoName}`)
|
|
34
|
-
}
|
|
35
|
-
} catch (err) {
|
|
36
|
-
if (COMMIT_OR_STASH.test(err.message) || UNMERGED_CHANGES.test(err.message)) {
|
|
37
|
-
messages.addMessage(
|
|
38
|
-
repoName,
|
|
39
|
-
'Pulling',
|
|
40
|
-
'WARNING',
|
|
41
|
-
new Error('Failed to fetch/pull because there are local changes afoot.\nYou\'ll need to push/stash them before this repo can be synced.')
|
|
42
|
-
)
|
|
43
|
-
} else {
|
|
44
|
-
messages.addMessage(
|
|
45
|
-
repoName,
|
|
46
|
-
'Pulling',
|
|
47
|
-
'ERROR',
|
|
48
|
-
err
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
package/lib/utils/Messages.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
const chalk = require('chalk')
|
|
2
|
-
|
|
3
|
-
module.exports = class Messages {
|
|
4
|
-
constructor () {
|
|
5
|
-
this.messages = {}
|
|
6
|
-
this.errorCount = 0
|
|
7
|
-
this.messageCount = 0
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
addMessage (repoName, domain, level, err) {
|
|
11
|
-
this.messageCount++
|
|
12
|
-
if (!this.messages[domain]) {
|
|
13
|
-
this.messages[domain] = []
|
|
14
|
-
}
|
|
15
|
-
this.messages[domain].push(
|
|
16
|
-
{
|
|
17
|
-
repoName,
|
|
18
|
-
level,
|
|
19
|
-
err
|
|
20
|
-
}
|
|
21
|
-
)
|
|
22
|
-
if (level === 'ERROR') {
|
|
23
|
-
this.errorCount++
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
output () {
|
|
28
|
-
if (this.messageCount > 0) {
|
|
29
|
-
console.log('')
|
|
30
|
-
console.log(`Messages ${chalk.blue(`(${this.messageCount})`)}`)
|
|
31
|
-
|
|
32
|
-
Object.entries(this.messages).forEach(([domain, messages]) => {
|
|
33
|
-
console.log('')
|
|
34
|
-
console.log(` ${chalk.underline(domain)}`)
|
|
35
|
-
console.log('')
|
|
36
|
-
messages.forEach(
|
|
37
|
-
message => {
|
|
38
|
-
let title = message.level
|
|
39
|
-
title = `[${title}]`
|
|
40
|
-
switch (message.level) {
|
|
41
|
-
case 'ERROR':
|
|
42
|
-
title = chalk.red(`${title} ${message.repoName}`)
|
|
43
|
-
break
|
|
44
|
-
case 'WARNING':
|
|
45
|
-
title = chalk.yellow(`${title} ${message.repoName}`)
|
|
46
|
-
break
|
|
47
|
-
case 'INFO':
|
|
48
|
-
title = chalk.blue(`${title} ${message.repoName}`)
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
title = chalk.bold(title)
|
|
52
|
-
let messageBody = message.err.toString().replace(/\n/g, '\n ')
|
|
53
|
-
if (messageBody.slice(0, 7) === 'Error: ') {
|
|
54
|
-
messageBody = messageBody.slice(7)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
console.log(` ${title}`)
|
|
58
|
-
console.log(` ${messageBody}`)
|
|
59
|
-
console.log('')
|
|
60
|
-
}
|
|
61
|
-
)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
if (this.errorCount > 0) {
|
|
65
|
-
console.log(chalk.inverse(chalk.red(`SYNCING FINISHED WITH ${this.errorCount} ERROR${this.errorCount === 1 ? '' : 'S'}`)))
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
package/lib/utils/run.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
const { exec } = require('child_process')
|
|
2
|
-
|
|
3
|
-
module.exports = function run (cmd, cwd = null) {
|
|
4
|
-
return new Promise((resolve, reject) => {
|
|
5
|
-
exec(cmd, { cwd }, (err, stdout, stderr) => {
|
|
6
|
-
// console.log(stdout)
|
|
7
|
-
if (err) reject(err)
|
|
8
|
-
else resolve(stdout)
|
|
9
|
-
})
|
|
10
|
-
})
|
|
11
|
-
}
|