diff --git a/README.md b/README.md index f472494..9460e7f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ exclude_private | Boolean value on whether to exclude private repositories from exclude_forked | Boolean value on whether to exclude forked repositories from this action. | false | false branches | By default, action creates branch from default branch and opens PR only against default branch. With this property you can override this behaviour. You can provide a comma-separated list of branches this action shoudl work against. You can also provide regex, but without comma as list of branches is split in code by comma. | false | default branch is used destination | Name of the directory where all files matching "patterns_to_include" will be copied. It doesn't work with "patterns_to_remove". In the format `.github/workflows`. | false | - +bot_branch_name | Use it if you do not want this action to create a new branch and new pull request with every run. By default branch names are generated. This means every single change is a separate commit. Such a static hardcoded branch name has an advantage that if you make a lot of changes, instead of having 5 PRs merged with 5 commits, you get one PR that is updated with new changes as long as the PR is not yet merged. If you use static name, and by mistake someone closed a PR, without merging and removing branch, this action will not fail but update the branch and open a new PR. Example value that you could provide: `bot_branch_name: bot/update-files-from-global-repo` | false | - ## Examples diff --git a/action.yml b/action.yml index 2d0adca..f88975d 100644 --- a/action.yml +++ b/action.yml @@ -75,6 +75,10 @@ inputs: description: > Name of the directory where all files matching "patterns_to_include" will be copied. It doesn't work with "patterns_to_remove". In the format `.github/workflows`. required: false + bot_branch_name: + description: > + Use it if you do not want this action to create a new branch and new pull request with every run. By default branch names are generated. This means every single change is a separate commit. Such a static hardcoded branch name has an advantage that if you make a lot of changes, instead of having 5 PRs merged with 5 commits, you get one PR that is updated with new changes as long as the PR is not yet merged. If you use static name, and by mistake someone closed a PR, without merging and removing branch, this action will not fail but update the branch and open a new PR. Example value that you could provide: `bot_branch_name: bot/update-files-from-global-repo`. + required: false runs: using: node16 main: dist/index.js diff --git a/dist/index.js b/dist/index.js index d71084f..1f5f863 100644 --- a/dist/index.js +++ b/dist/index.js @@ -8028,11 +8028,25 @@ async function getBranchesLocal(git) { async function push(branchName, message, committerUsername, committerEmail, git) { if (core.isDebug()) __webpack_require__(231).enable('simple-git'); core.info('Pushing changes to remote'); - await git.addConfig('user.name', committerUsername); await git.addConfig('user.email', committerEmail); await git.commit(message); - await git.push(['-u', REMOTE, branchName]); + try { + await git.push(['-u', REMOTE, branchName]); + } catch (error) { + core.info('Not able to push:', error); + try { + await git.pull([REMOTE, branchName]); + } catch (error) { + core.info('Not able to pull:', error); + await git.merge(['-X', 'ours', branchName]); + core.debug('DEBUG: Git status after merge'); + core.debug(JSON.stringify(await git.status(), null, 2)); + await git.add('./*'); + await git.commit(message); + await git.push(['-u', REMOTE, branchName]); + } + } } async function areFilesChanged(git) { @@ -14299,6 +14313,7 @@ async function run() { const commitMessage = core.getInput('commit_message'); const branches = core.getInput('branches'); const destination = core.getInput('destination'); + const customBranchName = core.getInput('bot_branch_name'); const repoNameManual = eventPayload.inputs && eventPayload.inputs.repo_name; const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); @@ -14373,7 +14388,7 @@ async function run() { /* * 4a. Creating folder where repo will be cloned and initializing git client */ - const dir = path.join(process.cwd(), './clones', repo.name); + const dir = path.join(process.cwd(), './clones', `${repo.name }-${ Math.random().toString(36).substring(7)}`); await mkdir(dir, {recursive: true}); const git = simpleGit({baseDir: dir}); @@ -14391,7 +14406,7 @@ async function run() { * Should it be just default one or the ones provided by the user */ const branchesToOperateOn = await getBranchesList(myOctokit, owner, repo.name, branches, defaultBranch); - if (!branchesToOperateOn.length) { + if (!branchesToOperateOn[0].length) { core.info('Repo has no branches that the action could operate on'); continue; } @@ -14399,7 +14414,7 @@ async function run() { /* * 4d. Per branch operation starts */ - for (const branch of branchesToOperateOn) { + for (const branch of branchesToOperateOn[0]) { /* * 4da. Checkout branch in cloned repo */ @@ -14409,8 +14424,15 @@ async function run() { /* * 4db. Creating new branch in cloned repo */ - const newBranchName = getBranchName(commitId, branchName); - await createBranch(newBranchName, git); + const newBranchName = customBranchName || getBranchName(commitId, branchName); + const wasBranchThereAlready = branchesToOperateOn[1].some(branch => branch.name === newBranchName); + core.debug(`DEBUG: was branch ${newBranchName} there already in the repository? - ${wasBranchThereAlready}`); + core.debug(JSON.stringify(branchesToOperateOn, null, 2)); + if (wasBranchThereAlready) { + await checkoutBranch(newBranchName, git); + } else { + await createBranch(newBranchName, git); + } /* * 4dc. Files replication/update or deletion @@ -14429,14 +14451,23 @@ async function run() { await push(newBranchName, commitMessage, committerUsername, committerEmail, git); /* - * 4fe. Opening a PR - */ - const pullRequestUrl = await createPr(myOctokit, newBranchName, repo.id, commitMessage, branchName); - + * 4fe. Opening a PR. Doing in try/catch as it is not always failing because of timeouts, maybe branch already has a PR + * we need to try to create a PR cause there can be branch but someone closed PR, so branch is there but PR not + */ + let pullRequestUrl; + try { + pullRequestUrl = await createPr(myOctokit, newBranchName, repo.id, commitMessage, branchName); + } catch (error) { + if (wasBranchThereAlready) + core.info(`PR creation for ${repo.name} failed as the branch was there already. Insted only push was performed to existing ${newBranchName} branch`, error); + } + core.endGroup(); if (pullRequestUrl) { core.info(`Workflow finished with success and PR for ${repo.name} is created -> ${pullRequestUrl}`); + } else if (!pullRequestUrl && wasBranchThereAlready) { + core.info(`Workflow finished without PR creation for ${repo.name}. Insted push was performed to existing ${newBranchName} branch`); } else { core.info(`Unable to create a PR because of timeouts. Create PR manually from the branch ${newBranchName} that was already created in the upstream`); } @@ -15806,7 +15837,7 @@ function getBranchName(commitId, branchName) { * @param {String} repo repo name * @param {String} branchesString comma-separated list of branches * @param {String} defaultBranch name of the repo default branch - * @returns {String} + * @returns {Array} first index is object with branches that user wants to operate on and that are in remote, next index has all remote branches */ async function getBranchesList(octokit, owner, repo, branchesString, defaultBranch) { core.info('Getting list of branches the action should operate on'); @@ -15816,9 +15847,9 @@ async function getBranchesList(octokit, owner, repo, branchesString, defaultBran //branches not available an remote will not be included const filteredBranches = filterOutMissingBranches(branchesString, branchesFromRemote, defaultBranch); - core.info(`These is a final list of branches action will operate on: ${JSON.stringify(filteredBranches, null, 2)}`); + core.info(`This is a final list of branches action will operate on: ${JSON.stringify(filteredBranches, null, 2)}`); - return filteredBranches; + return [filteredBranches, branchesFromRemote]; } /** diff --git a/lib/git.js b/lib/git.js index 0126757..2a1fa13 100644 --- a/lib/git.js +++ b/lib/git.js @@ -30,11 +30,25 @@ async function getBranchesLocal(git) { async function push(branchName, message, committerUsername, committerEmail, git) { if (core.isDebug()) require('debug').enable('simple-git'); core.info('Pushing changes to remote'); - await git.addConfig('user.name', committerUsername); await git.addConfig('user.email', committerEmail); await git.commit(message); - await git.push(['-u', REMOTE, branchName]); + try { + await git.push(['-u', REMOTE, branchName]); + } catch (error) { + core.info('Not able to push:', error); + try { + await git.pull([REMOTE, branchName]); + } catch (error) { + core.info('Not able to pull:', error); + await git.merge(['-X', 'ours', branchName]); + core.debug('DEBUG: Git status after merge'); + core.debug(JSON.stringify(await git.status(), null, 2)); + await git.add('./*'); + await git.commit(message); + await git.push(['-u', REMOTE, branchName]); + } + } } async function areFilesChanged(git) { diff --git a/lib/index.js b/lib/index.js index 7f65225..40194d8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -37,6 +37,7 @@ async function run() { const commitMessage = core.getInput('commit_message'); const branches = core.getInput('branches'); const destination = core.getInput('destination'); + const customBranchName = core.getInput('bot_branch_name'); const repoNameManual = eventPayload.inputs && eventPayload.inputs.repo_name; const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); @@ -111,7 +112,7 @@ async function run() { /* * 4a. Creating folder where repo will be cloned and initializing git client */ - const dir = path.join(process.cwd(), './clones', repo.name); + const dir = path.join(process.cwd(), './clones', `${repo.name }-${ Math.random().toString(36).substring(7)}`); await mkdir(dir, {recursive: true}); const git = simpleGit({baseDir: dir}); @@ -129,7 +130,7 @@ async function run() { * Should it be just default one or the ones provided by the user */ const branchesToOperateOn = await getBranchesList(myOctokit, owner, repo.name, branches, defaultBranch); - if (!branchesToOperateOn.length) { + if (!branchesToOperateOn[0].length) { core.info('Repo has no branches that the action could operate on'); continue; } @@ -137,7 +138,7 @@ async function run() { /* * 4d. Per branch operation starts */ - for (const branch of branchesToOperateOn) { + for (const branch of branchesToOperateOn[0]) { /* * 4da. Checkout branch in cloned repo */ @@ -147,8 +148,15 @@ async function run() { /* * 4db. Creating new branch in cloned repo */ - const newBranchName = getBranchName(commitId, branchName); - await createBranch(newBranchName, git); + const newBranchName = customBranchName || getBranchName(commitId, branchName); + const wasBranchThereAlready = branchesToOperateOn[1].some(branch => branch.name === newBranchName); + core.debug(`DEBUG: was branch ${newBranchName} there already in the repository? - ${wasBranchThereAlready}`); + core.debug(JSON.stringify(branchesToOperateOn, null, 2)); + if (wasBranchThereAlready) { + await checkoutBranch(newBranchName, git); + } else { + await createBranch(newBranchName, git); + } /* * 4dc. Files replication/update or deletion @@ -167,14 +175,23 @@ async function run() { await push(newBranchName, commitMessage, committerUsername, committerEmail, git); /* - * 4fe. Opening a PR - */ - const pullRequestUrl = await createPr(myOctokit, newBranchName, repo.id, commitMessage, branchName); - + * 4fe. Opening a PR. Doing in try/catch as it is not always failing because of timeouts, maybe branch already has a PR + * we need to try to create a PR cause there can be branch but someone closed PR, so branch is there but PR not + */ + let pullRequestUrl; + try { + pullRequestUrl = await createPr(myOctokit, newBranchName, repo.id, commitMessage, branchName); + } catch (error) { + if (wasBranchThereAlready) + core.info(`PR creation for ${repo.name} failed as the branch was there already. Insted only push was performed to existing ${newBranchName} branch`, error); + } + core.endGroup(); if (pullRequestUrl) { core.info(`Workflow finished with success and PR for ${repo.name} is created -> ${pullRequestUrl}`); + } else if (!pullRequestUrl && wasBranchThereAlready) { + core.info(`Workflow finished without PR creation for ${repo.name}. Insted push was performed to existing ${newBranchName} branch`); } else { core.info(`Unable to create a PR because of timeouts. Create PR manually from the branch ${newBranchName} that was already created in the upstream`); } diff --git a/lib/utils.js b/lib/utils.js index 99de189..76da8f4 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -259,7 +259,7 @@ function getBranchName(commitId, branchName) { * @param {String} repo repo name * @param {String} branchesString comma-separated list of branches * @param {String} defaultBranch name of the repo default branch - * @returns {String} + * @returns {Array} first index is object with branches that user wants to operate on and that are in remote, next index has all remote branches */ async function getBranchesList(octokit, owner, repo, branchesString, defaultBranch) { core.info('Getting list of branches the action should operate on'); @@ -269,9 +269,9 @@ async function getBranchesList(octokit, owner, repo, branchesString, defaultBran //branches not available an remote will not be included const filteredBranches = filterOutMissingBranches(branchesString, branchesFromRemote, defaultBranch); - core.info(`These is a final list of branches action will operate on: ${JSON.stringify(filteredBranches, null, 2)}`); + core.info(`This is a final list of branches action will operate on: ${JSON.stringify(filteredBranches, null, 2)}`); - return filteredBranches; + return [filteredBranches, branchesFromRemote]; } /**