Skip to content

Commit

Permalink
feat!: add base_branch support and improve error handling (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
derberg authored Feb 5, 2022
1 parent 2a92ca6 commit e3baee7
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 124 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Organization Projects' Dependency Manager
GitHub Action that handles automated update of dependencies in package.json between projects from the same GitHub organization. You run this workflow after npm package release. It searches for libraries in your GitHub organization that depend on this package and bump version through a PR flow.

While updating multiple repositories, if there are issues with one of them, the action doesn't fail but continues bumping deps in next repo from the list.

<!-- toc -->

- [Why I Created This Action?](#why-i-created-this-action)
Expand Down Expand Up @@ -57,6 +59,7 @@ committer_email | The committer's email that will be used in the commit of chang
commit_message_prod | It is used as a commit message when bumping dependency from "dependencies" section in package.json. In case dependency is located in both dependencies and devDependencies of dependant, then prod commit message is used. It is also used as a title of the pull request that is created by this action. | false | `fix: update ${dependencyName} to ${dependencyVersion} version`
commit_message_dev | It is used as a commit message when bumping dependency from "devDependencies" section in package.json. It is also used as a title of the pull request that is created by this action. | false | `chore: update ${dependencyName} to ${dependencyVersion} version`
repos_to_ignore | Comma-separated list of repositories that should not get updates from this action. Action already ignores the repo in which the action is triggered so you do not need to add it explicitly. In the format `repo1,repo2`. | false | -
base_branch | Name of the base branch, where changes in package.json must be applied. It is used in PR creation. Branch where changes are introduced is cut from this base branch. If not provided, default branch is used. In the format: `next-major`. | false | -

## Example

Expand All @@ -77,10 +80,11 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Bumping
uses: derberg/npm-dependency-manager-for-your-github-org@v3
uses: derberg/npm-dependency-manager-for-your-github-org@v4
with:
github_token: ${{ secrets.CUSTOM_TOKEN }}
repos_to_ignore: repo1,repo2
base_branch: next-major
packagejson_path: ./custom/path
committer_username: pomidor
committer_email: pomidor@pomidor.com
Expand Down
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ inputs:
Action already ignores the repo in which the action is triggered so you do not need to add it explicitly.
In the format: `repo1,repo2`.
required: false
base_branch:
description: >
Name of the base branch, where changes in package.json must be applied. It is used in PR creation. If not provided, default branch is used
In the format: `next-major`.
required: false
runs:
using: node12
main: dist/index.js
Expand Down
179 changes: 117 additions & 62 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5654,16 +5654,18 @@ exports.parseStringResponse = parseStringResponse;
/***/ 374:
/***/ (function(module) {

module.exports = {createBranch, clone, push};
module.exports = {createBranch, clone, push, removeRemoteBranch};

const remoteName = 'auth';

async function createBranch(branchName, git) {
return await git
.checkout(`-b${branchName}`);
}

async function clone(remote, dir, git) {
async function clone(remote, dir, branchName, git) {
return await git
.clone(remote, dir, {'--depth': 1});
.clone(remote, dir, {'--depth': 1, '--branch': branchName});
}

async function push(token, url, branchName, message, committerUsername, committerEmail, git) {
Expand All @@ -5677,8 +5679,12 @@ async function push(token, url, branchName, message, committerUsername, committe
.addConfig('user.name', committerUsername)
.addConfig('user.email', committerEmail)
.commit(message)
.addRemote('auth', authanticatedUrl(token, url, committerUsername))
.push(['-u', 'auth', branchName]);
.addRemote(remoteName, authanticatedUrl(token, url, committerUsername))
.push(['-u', remoteName, branchName]);
}

async function removeRemoteBranch(branchName, git) {
return await git.push(['-u', remoteName, branchName, '--delete', '--force']);
}


Expand Down Expand Up @@ -11421,7 +11427,7 @@ const simpleGit = __webpack_require__(477);
const path = __webpack_require__(622);
const {mkdir} = __webpack_require__(747).promises;

const { createBranch, clone, push } = __webpack_require__(374);
const { createBranch, clone, push, removeRemoteBranch } = __webpack_require__(374);
const { getReposList, createPr, getRepoDefaultBranch } = __webpack_require__(119);
const { readPackageJson, parseCommaList, verifyDependencyType, installDependency } = __webpack_require__(918);

Expand All @@ -11431,79 +11437,128 @@ const { readPackageJson, parseCommaList, verifyDependencyType, installDependency
* It looks complex because of extensive usage of core package to log as much as possible
*/
async function run() {
const gitHubKey = process.env.GITHUB_TOKEN || core.getInput('github_token', { required: true });
const committerUsername = core.getInput('committer_username') || 'web-flow';
const committerEmail = core.getInput('committer_email') || 'noreply@github.com';
const packageJsonPath = process.env.PACKAGE_JSON_LOC || core.getInput('packagejson_path') || './';
const { name: dependencyName, version: dependencyVersion} = await readPackageJson(path.join(packageJsonPath, 'package.json'));
core.info(`Identified dependency name as ${dependencyName} with version ${dependencyVersion}. Now it will be bumped in dependent projects.`);
const commitMessageProd = core.getInput('commit_message_prod') || `fix: update ${dependencyName} to ${dependencyVersion} version`;
const commitMessageDev = core.getInput('commit_message_dev') || `chore: update ${dependencyName} to ${dependencyVersion} version`;
const reposToIgnore = core.getInput('repos_to_ignore');
const baseBranchName = core.getInput('base_branch');

const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
const octokit = github.getOctokit(gitHubKey);
const ignoredRepositories = reposToIgnore ? parseCommaList(reposToIgnore) : [];
//by default repo where workflow runs should always be ignored
let reposList;

try {
const gitHubKey = process.env.GITHUB_TOKEN || core.getInput('github_token', { required: true });
const committerUsername = core.getInput('committer_username') || 'web-flow';
const committerEmail = core.getInput('committer_email') || 'noreply@github.com';
const packageJsonPath = process.env.PACKAGE_JSON_LOC || core.getInput('packagejson_path') || './';
const { name: dependencyName, version: dependencyVersion} = await readPackageJson(path.join(packageJsonPath, 'package.json'));
core.info(`Identified dependency name as ${dependencyName} with version ${dependencyVersion}. Now it will be bumped in dependent projects.`);
const commitMessageProd = core.getInput('commit_message_prod') || `fix: update ${dependencyName} to ${dependencyVersion} version`;
const commitMessageDev = core.getInput('commit_message_dev') || `chore: update ${dependencyName} to ${dependencyVersion} version`;
const reposToIgnore = core.getInput('repos_to_ignore');

const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
const octokit = github.getOctokit(gitHubKey);
const ignoredRepositories = reposToIgnore ? parseCommaList(reposToIgnore) : [];
//by default repo where workflow runs should always be ignored
ignoredRepositories.push(repo);

const reposList = await getReposList(octokit, dependencyName, owner);
core.debug('DEBUG: List of all repost returned by search without duplicates:');
core.debug(JSON.stringify(reposList, null, 2));
reposList = await getReposList(octokit, dependencyName, owner);
} catch (error) {
core.setFailed(`Action failed while getting list of repos to process: ${ error}`);
}

core.debug('DEBUG: List of all repost returned by search without duplicates:');
core.debug(JSON.stringify(reposList, null, 2));

const foundReposAmount = reposList.length;
if (!foundReposAmount) return core.info(`No dependants found. No version bump performed. Looks like you do not use ${dependencyName} in your organization :man_shrugging:`);
const foundReposAmount = reposList.length;
if (!foundReposAmount) return core.info(`No dependants found. No version bump performed. Looks like you do not use ${dependencyName} in your organization :man_shrugging:`);

core.startGroup(`Iterating over ${foundReposAmount} repos from ${owner} that have ${dependencyName} in their package.json. The following repos will be later ignored: ${ignoredRepositories}`);
core.startGroup(`Iterating over ${foundReposAmount} repos from ${owner} that have ${dependencyName} in their package.json. The following repos will be later ignored: ${ignoredRepositories}`);

for (const {path: filepath, repository: { name, html_url, node_id }} of reposList) {
if (ignoredRepositories.includes(name)) continue;
//Sometimes there might be files like package.json.js or similar as the repository might contain some templated package.json files that cannot be parsed from string to JSON
//Such files must be ignored
if (filepath.substring(filepath.lastIndexOf('/') + 1) !== 'package.json') {
core.info(`Ignoring ${filepath} from ${name} repo as only package.json files are supported`);
continue;
}
for (const {path: filepath, repository: { name, html_url, node_id }} of reposList) {
if (ignoredRepositories.includes(name)) continue;
//Sometimes there might be files like package.json.js or similar as the repository might contain some templated package.json files that cannot be parsed from string to JSON
//Such files must be ignored
if (filepath.substring(filepath.lastIndexOf('/') + 1) !== 'package.json') {
core.info(`Ignoring ${filepath} from ${name} repo as only package.json files are supported`);
continue;
}

const cloneDir = __webpack_require__.ab + "clones/" + name;
const baseBranchWhereApplyChanges = baseBranchName || await getRepoDefaultBranch(octokit, name, owner);
const branchName = `bot/bump-${dependencyName}-${dependencyVersion}`;
const cloneDir = __webpack_require__.ab + "clones/" + name;

try {
await mkdir(cloneDir, {recursive: true});
} catch (error) {
core.warning(`Unable to create directory where close should end up: ${ error}`);
}

const branchName = `bot/bump-${dependencyName}-${dependencyVersion}`;
const git = simpleGit({baseDir: cloneDir});
const git = simpleGit({baseDir: cloneDir});

core.info(`Clonning ${name} with branch ${baseBranchWhereApplyChanges}.`);
try {
await clone(html_url, cloneDir, baseBranchWhereApplyChanges, git);
} catch (error) {
core.warning(`Cloning failed: ${ error}`);
continue;
}

core.info(`Clonning ${name}.`);
await clone(html_url, cloneDir, git);

core.info(`Creating branch ${branchName}.`);
core.info(`Creating branch ${branchName}.`);
try {
await createBranch(branchName, git);
} catch (error) {
core.warning(`Branch creation failes: ${ error}`);
continue;
}

core.info('Checking if dependency is prod, dev or both');
const packageJsonLocation = path.join(cloneDir, filepath);
const packageJson = await readPackageJson(packageJsonLocation);
const dependencyType = await verifyDependencyType(packageJson, dependencyName);
if (dependencyType === 'NONE') {
core.info(`We could not find ${dependencyName} neither in dependencies property nor in the devDependencies property. No further steps will be performed. It was found as GitHub search is not perfect and you probably use a package with similar name.`);
continue;
}
core.info('Checking if dependency is prod, dev or both');
const packageJsonLocation = path.join(cloneDir, filepath);
let packageJson;
let dependencyType;
try {
packageJson = await readPackageJson(packageJsonLocation);
dependencyType = await verifyDependencyType(packageJson, dependencyName);
} catch (error) {
core.warning(`Verification of dependency failed: ${ error}`);
continue;
}

if (dependencyType === 'NONE') {
core.info(`We could not find ${dependencyName} neither in dependencies property nor in the devDependencies property. No further steps will be performed. It was found as GitHub search is not perfect and you probably use a package with similar name.`);
continue;
}

core.info('Bumping version');
core.info('Bumping version');
try {
await installDependency(dependencyName, dependencyVersion, packageJsonLocation);
const commitMessage = dependencyType === 'PROD' ? commitMessageProd : commitMessageDev;
} catch (error) {
core.warning(`Dependency installation failed: ${ error}`);
continue;
}
const commitMessage = dependencyType === 'PROD' ? commitMessageProd : commitMessageDev;

core.info('Pushing changes to remote');
core.info('Pushing changes to remote');
try {
await push(gitHubKey, html_url, branchName, commitMessage, committerUsername, committerEmail, git);
} catch (error) {
core.warning(`Pushing changes failed: ${ error}`);
continue;
}

core.info('Creating PR');
const pullRequestUrl = await createPr(octokit, branchName, node_id, commitMessage, await getRepoDefaultBranch(octokit, name, owner));

core.info(`Finished with success and PR for ${name} is created -> ${pullRequestUrl}`);
let pullRequestUrl;
core.info('Creating PR');
try {
pullRequestUrl = await createPr(octokit, branchName, node_id, commitMessage, baseBranchWhereApplyChanges);
} catch (error) {
core.warning(`Opening PR failed: ${ error}`);
core.info('Attempting to remove branch that was initially pushed to remote');
try {
//we should cleanup dead branch from remote if PR creation is not possible
await removeRemoteBranch(branchName, git);
} catch (error) {
core.warning(`Could not remove branch in remote after failed PR creation: ${ error}`);
}
continue;
}

core.endGroup();
} catch (error) {
core.setFailed(`Action failed because of: ${ error}`);

core.info(`Finished with success and PR for ${name} is created -> ${pullRequestUrl}`);
}

core.endGroup();
}

run();
Expand Down
16 changes: 11 additions & 5 deletions lib/git.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
module.exports = {createBranch, clone, push};
module.exports = {createBranch, clone, push, removeRemoteBranch};

const remoteName = 'auth';

async function createBranch(branchName, git) {
return await git
.checkout(`-b${branchName}`);
}

async function clone(remote, dir, git) {
async function clone(remote, dir, branchName, git) {
return await git
.clone(remote, dir, {'--depth': 1});
.clone(remote, dir, {'--depth': 1, '--branch': branchName});
}

async function push(token, url, branchName, message, committerUsername, committerEmail, git) {
Expand All @@ -21,7 +23,11 @@ async function push(token, url, branchName, message, committerUsername, committe
.addConfig('user.name', committerUsername)
.addConfig('user.email', committerEmail)
.commit(message)
.addRemote('auth', authanticatedUrl(token, url, committerUsername))
.push(['-u', 'auth', branchName]);
.addRemote(remoteName, authanticatedUrl(token, url, committerUsername))
.push(['-u', remoteName, branchName]);
}

async function removeRemoteBranch(branchName, git) {
return await git.push(['-u', remoteName, branchName, '--delete', '--force']);
}

Loading

0 comments on commit e3baee7

Please sign in to comment.