Skip to content

Commit 5ab9915

Browse files
authored
Merge pull request atom#20999 from atom/dependency-automation
Automate dependency bumps
2 parents 0fe5533 + 9140f65 commit 5ab9915

18 files changed

+2652
-13
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
const fetch = require('node-fetch');
2+
const npmCheck = require('npm-check');
3+
4+
// this may be updated to use github releases instead
5+
const apm = async function({ dependencies, packageDependencies }) {
6+
try {
7+
console.log('Checking apm registry...');
8+
const coreDependencies = Object.keys(dependencies).filter(dependency => {
9+
// all core packages point to a remote url
10+
return dependencies[dependency].match(new RegExp('^https?://'));
11+
});
12+
13+
const promises = coreDependencies.map(async dependency => {
14+
return fetch(`https://atom.io/api/packages/${dependency}`)
15+
.then(res => res.json())
16+
.then(res => res)
17+
.catch(ex => console.log(ex.message));
18+
});
19+
20+
const packages = await Promise.all(promises);
21+
const outdatedPackages = [];
22+
packages.map(dependency => {
23+
if (dependency.hasOwnProperty('name')) {
24+
const latestVersion = dependency.releases.latest;
25+
const installed = packageDependencies[dependency.name];
26+
if (latestVersion > installed) {
27+
outdatedPackages.push({
28+
moduleName: dependency.name,
29+
latest: dependency.releases.latest,
30+
isCorePackage: true,
31+
installed
32+
});
33+
}
34+
}
35+
});
36+
37+
console.log(`${outdatedPackages.length} outdated package(s) found`);
38+
39+
return outdatedPackages;
40+
} catch (ex) {
41+
console.error(`An error occured: ${ex.message}`);
42+
}
43+
};
44+
45+
const npm = async function(cwd) {
46+
try {
47+
console.log('Checking npm registry...');
48+
49+
const currentState = await npmCheck({
50+
cwd,
51+
ignoreDev: true,
52+
skipUnused: true
53+
});
54+
const outdatedPackages = currentState
55+
.get('packages')
56+
.filter(p => {
57+
if (p.packageJson && p.latest && p.installed) {
58+
return p.latest > p.installed;
59+
}
60+
})
61+
.map(({ packageJson, installed, moduleName, latest }) => ({
62+
packageJson,
63+
installed,
64+
moduleName,
65+
latest,
66+
isCorePackage: false
67+
}));
68+
69+
console.log(`${outdatedPackages.length} outdated package(s) found`);
70+
71+
return outdatedPackages;
72+
} catch (ex) {
73+
console.error(`An error occured: ${ex.message}`);
74+
}
75+
};
76+
77+
module.exports = {
78+
apm,
79+
npm
80+
};

script/lib/update-dependency/git.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
const git = (git, repositoryRootPath) => {
2+
const path = require('path');
3+
const packageJsonFilePath = path.join(repositoryRootPath, 'package.json');
4+
const packageLockFilePath = path.join(
5+
repositoryRootPath,
6+
'package-lock.json'
7+
);
8+
try {
9+
git.getRemotes((err, remotes) => {
10+
if (!err && !remotes.map(({ name }) => name).includes('ATOM')) {
11+
git.addRemote(
12+
'ATOM',
13+
`https://atom:${process.env.AUTH_TOKEN}@github.com/atom/atom.git/`
14+
);
15+
}
16+
});
17+
} catch (ex) {
18+
console.log(ex.message);
19+
}
20+
return {
21+
switchToMaster: async function() {
22+
const { current } = await git.branch();
23+
if (current !== 'master') {
24+
await git.checkout('master');
25+
}
26+
},
27+
makeBranch: async function(dependency) {
28+
const newBranch = `${dependency.moduleName}-${dependency.latest}`;
29+
const { branches } = await git.branch();
30+
const { files } = await git.status();
31+
if (files.length > 0) {
32+
await git.reset('hard');
33+
}
34+
const found = Object.keys(branches).find(
35+
branch => branch.indexOf(newBranch) > -1
36+
);
37+
found
38+
? await git.checkout(found)
39+
: await git.checkoutLocalBranch(newBranch);
40+
return { found, newBranch };
41+
},
42+
createCommit: async function({ moduleName, latest }) {
43+
try {
44+
const commitMessage = `:arrow_up: ${moduleName}@${latest}`;
45+
await git.add([packageJsonFilePath, packageLockFilePath]);
46+
await git.commit(commitMessage);
47+
} catch (ex) {
48+
throw Error(ex.message);
49+
}
50+
},
51+
publishBranch: async function(branch) {
52+
try {
53+
await git.push('ATOM', branch);
54+
} catch (ex) {
55+
throw Error(ex.message);
56+
}
57+
},
58+
deleteBranch: async function(branch) {
59+
try {
60+
await git.deleteLocalBranch(branch, true);
61+
} catch (ex) {
62+
throw Error(ex.message);
63+
}
64+
}
65+
};
66+
};
67+
module.exports = git;

script/lib/update-dependency/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const run = require('./main');
2+
3+
run();

script/lib/update-dependency/main.js

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/* eslint-disable camelcase */
2+
const simpleGit = require('simple-git');
3+
const path = require('path');
4+
5+
const { repositoryRootPath } = require('../../config');
6+
const packageJSON = require(path.join(repositoryRootPath, 'package.json'));
7+
const git = simpleGit(repositoryRootPath);
8+
const { createPR, findPR, addLabel } = require('./pull-request');
9+
const runApmInstall = require('../run-apm-install');
10+
const {
11+
makeBranch,
12+
createCommit,
13+
switchToMaster,
14+
publishBranch,
15+
deleteBranch
16+
} = require('./git')(git, repositoryRootPath);
17+
const { updatePackageJson, sleep } = require('./util')(repositoryRootPath);
18+
const fetchOutdatedDependencies = require('./fetch-outdated-dependencies');
19+
20+
module.exports = async function() {
21+
try {
22+
// ensure we are on master
23+
await switchToMaster();
24+
const failedBumps = [];
25+
const successfullBumps = [];
26+
const outdateDependencies = [
27+
...(await fetchOutdatedDependencies.npm(repositoryRootPath)),
28+
...(await fetchOutdatedDependencies.apm(packageJSON))
29+
];
30+
const totalDependencies = outdateDependencies.length;
31+
const pendingPRs = [];
32+
for (const dependency of outdateDependencies) {
33+
const { found, newBranch } = await makeBranch(dependency);
34+
if (found) {
35+
console.log(`Branch was found ${found}`);
36+
console.log('checking if a PR already exists');
37+
const {
38+
data: { total_count }
39+
} = await findPR(dependency, newBranch);
40+
if (total_count > 0) {
41+
console.log(`pull request found!`);
42+
} else {
43+
console.log(`pull request not found!`);
44+
const pr = { dependency, branch: newBranch, branchIsRemote: false };
45+
// confirm if branch found is a local branch
46+
if (found.indexOf('remotes') === -1) {
47+
await publishBranch(found);
48+
} else {
49+
pr.branchIsRemote = true;
50+
}
51+
pendingPRs.push(pr);
52+
}
53+
} else {
54+
await updatePackageJson(dependency);
55+
runApmInstall(repositoryRootPath, false);
56+
await createCommit(dependency);
57+
await publishBranch(newBranch);
58+
pendingPRs.push({
59+
dependency,
60+
branch: newBranch,
61+
branchIsRemote: false
62+
});
63+
}
64+
65+
await switchToMaster();
66+
}
67+
// create PRs here
68+
for (const { dependency, branch, branchIsRemote } of pendingPRs) {
69+
const { status, data = {} } = await createPR(dependency, branch);
70+
if (status === 201) {
71+
successfullBumps.push(dependency);
72+
await addLabel(data.number);
73+
} else {
74+
failedBumps.push(dependency);
75+
}
76+
77+
if (!branchIsRemote) {
78+
await deleteBranch(branch);
79+
}
80+
// https://developer.github.com/v3/guides/best-practices-for-integrators/#dealing-with-abuse-rate-limits
81+
await sleep(2000);
82+
}
83+
console.table([
84+
{
85+
totalDependencies,
86+
totalSuccessfullBumps: successfullBumps.length,
87+
totalFailedBumps: failedBumps.length
88+
}
89+
]);
90+
console.log('Successfull bumps');
91+
console.table(successfullBumps);
92+
console.log('Failed bumps');
93+
console.table(failedBumps);
94+
} catch (ex) {
95+
console.log(ex.message);
96+
}
97+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const { request } = require('@octokit/request');
2+
3+
const requestWithAuth = request.defaults({
4+
baseUrl: 'https://api.github.com',
5+
headers: {
6+
'user-agent': 'atom',
7+
authorization: `token ${process.env.AUTH_TOKEN}`
8+
},
9+
owner: 'atom',
10+
repo: 'atom'
11+
});
12+
13+
module.exports = {
14+
createPR: async (
15+
{ moduleName, isCorePackage, latest, installed },
16+
branch
17+
) => {
18+
let description = `Bumps ${moduleName} from ${installed} to ${latest}`;
19+
if (isCorePackage) {
20+
description = `*List of changes between ${moduleName}@${installed} and ${moduleName}@${latest}: https://github.com/atom/${moduleName}/compare/v${installed}...v${latest}*`;
21+
}
22+
return requestWithAuth('POST /repos/:owner/:repo/pulls', {
23+
title: `⬆️ ${moduleName}@${latest}`,
24+
body: description,
25+
base: 'master',
26+
head: branch
27+
});
28+
},
29+
findPR: async ({ moduleName, latest }, branch) => {
30+
return requestWithAuth('GET /search/issues', {
31+
q: `${moduleName} type:pr ${moduleName}@${latest} in:title repo:atom/atom head:${branch} state:open`
32+
});
33+
},
34+
addLabel: async pullRequestNumber => {
35+
return requestWithAuth('PATCH /repos/:owner/:repo/issues/:issue_number', {
36+
labels: ['depency ⬆️'],
37+
issue_number: pullRequestNumber
38+
});
39+
}
40+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const path = require('path');
2+
const fetchOutdatedDependencies = require('../fetch-outdated-dependencies');
3+
const { nativeDependencies } = require('./helpers');
4+
const repositoryRootPath = path.resolve('.', 'fixtures', 'dummy');
5+
const packageJSON = require(path.join(repositoryRootPath, 'package.json'));
6+
7+
describe('Fetch outdated dependencies', function() {
8+
it('should fetch outdated native dependencies', async () => {
9+
spyOn(fetchOutdatedDependencies, 'npm').andReturn(
10+
Promise.resolve(nativeDependencies)
11+
);
12+
13+
expect(await fetchOutdatedDependencies.npm(repositoryRootPath)).toEqual(
14+
nativeDependencies
15+
);
16+
});
17+
18+
it('should fetch outdated core dependencies', async () => {
19+
spyOn(fetchOutdatedDependencies, 'apm').andReturn(
20+
Promise.resolve(nativeDependencies)
21+
);
22+
23+
expect(await fetchOutdatedDependencies.apm(packageJSON)).toEqual(
24+
nativeDependencies
25+
);
26+
});
27+
});

0 commit comments

Comments
 (0)