Skip to content

Commit

Permalink
Merge pull request #12 from hrvey/feature/single-step
Browse files Browse the repository at this point in the history
Reduce workflow to a single step
  • Loading branch information
martingjaldbaek authored Aug 4, 2022
2 parents 5532171 + 049136c commit 0ef9821
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 69 deletions.
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,17 @@ Then on GitHub, click the 'Actions' tab at the top of your repository, then unde
![Run workflow](/images/run.png?raw=true "Run workflow")

# Customization
The workflow uses only the [actions/github-script](https://github.com/actions/github-script/) and [actions/checkout](https://github.com/actions/checkout/) actions published by GitHub.
The workflow uses only the [actions/github-script](https://github.com/actions/github-script/) action published by GitHub.
As you can see from the image you can easily set another branch prefix for matching, and choose whether to only include branches that are green (have the 'success' status on GitHub, e.g. from successful CI) - this is the default. If you don't run checks, then set this setting to false, since a PR with no checks will have a status of 'pending' rather than 'success'.

Finally, you can set a label on PRs that you want to exclude when combining PRs - by default this label is 'nocombine'. For example this can be handy if you have a single PR causing merge conflicts (see Limitations below). In that case you can exclude that PR by adding the label, and still combine the rest of the PRs.
Finally, you can set a label on PRs that you want to exclude when combining PRs - by default this label is 'nocombine'.

You are also welcome to modify the code to your needs if you need more customization than that (for example it currently doesn't work with forks, only branches within the same repo).

# Limitations
This workflow merges the branches of the PRs together into a single branch using git's simple merge and automatic merge strategies. Unfortunately that means it has the same limitations as these merge strategies, so it will fail if two or more PRs have branches that cannot be auto-merged due to a merge conflict - it will say something along the lines of "Auto-merge failed" or "Should not be doing an octopus" (see an example [here](https://github.com/hrvey/combine-prs-workflow/issues/2)).
The way this will typically happen when updating dependencies is that different dependency updates end up modifying the same line in the `.lock` file (the file that ensures stability in exactly which versions of depencies are currently being used, when dependencies are defined in a broad enough way that multiple different versions could satisfy the constraints).
This workflow merges the branches of the PRs together into a single branch using git's simple merge and automatic merge strategies. Unfortunately that means it has the same limitations as these merge strategies, so it will only work on PRs with branches that can be auto-merged without running into a merge conflict. In case any merge conflicts happen, the created combined PR will just include as many branches as could be merged without conflict, and it will list which PRs were left out due to merge conflicts.
The way merge conflicts will typically happen when updating dependencies is that different dependency updates end up modifying the same line in the `.lock` file (the file that ensures stability in exactly which versions of depencies are currently being used, when dependencies are defined in a broad enough way that multiple different versions could satisfy the constraints).
This typically happens if they share some common third dependency - for example if some modular framework has components A and B, that both depend on C, then updating A and B independently might lead to also indirectly updating C to two different versions in the two branches, and that will prevent them from being mergable.

The "correct" solution here is to add both dependencies together, then let the package manager resolve the dependencies to hopefully find a version of C to put in the `.lock` file that will satisfy both A and B.
If you find yourself in need of this, then this workflow won't help you, and you should consider switching to a dependency update service that will update dependencies together like that (at the moment *Dependabot* does not, but *Depfu* and *Renovate* do).

If it's only a single PR out of several that is causing merge issues, you can use the label 'nocombine' (see above) to merge the others together, then merge the last one alone.
119 changes: 56 additions & 63 deletions combine-prs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,36 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/github-script@v6
id: fetch-branch-names
name: Fetch branch names
id: create-combined-pr
name: Create Combined PR
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', {
owner: context.repo.owner,
repo: context.repo.repo
});
branches = [];
prs = [];
base_branch = null;
let branchesAndPRStrings = [];
let baseBranch = null;
let baseBranchSHA = null;
for (const pull of pulls) {
const branch = pull['head']['ref'];
console.log('Pull for branch: ' + branch);
if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) {
console.log('Branch matched: ' + branch);
statusOK = true;
console.log('Branch matched prefix: ' + branch);
let statusOK = true;
if(${{ github.event.inputs.mustBeGreen }}) {
console.log('Checking green status: ' + branch);
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{ref}/status', {
const statusResponse = await github.rest.repos.getCombinedStatusForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: branch
});
if(statuses.length > 0) {
const latest_status = statuses[0]['state'];
console.log('Validating status: ' + latest_status);
if(latest_status != 'success') {
console.log('Discarding ' + branch + ' with status ' + latest_status);
statusOK = false;
}
const state = statusResponse['data']['state'];
console.log('Validating status: ' + state);
if(state != 'success') {
console.log('Discarding ' + branch + ' with status ' + state);
statusOK = false;
}
}
console.log('Checking labels: ' + branch);
Expand All @@ -77,65 +75,60 @@ jobs:
}
if (statusOK) {
console.log('Adding branch to array: ' + branch);
branches.push(branch);
prs.push('#' + pull['number'] + ' ' + pull['title']);
base_branch = pull['base']['ref'];
const prString = '#' + pull['number'] + ' ' + pull['title'];
branchesAndPRStrings.push({ branch, prString });
baseBranch = pull['base']['ref'];
baseBranchSHA = pull['base']['sha'];
}
}
}
if (branches.length == 0) {
if (branchesAndPRStrings.length == 0) {
core.setFailed('No PRs/branches matched criteria');
return;
}
core.setOutput('base-branch', base_branch);
core.setOutput('prs-string', prs.join('\n'));
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: 'refs/heads/' + '${{ github.event.inputs.combineBranchName }}',
sha: baseBranchSHA
});
} catch (error) {
console.log(error);
core.setFailed('Failed to create combined branch - maybe a branch by that name already exists?');
return;
}
combined = branches.join(' ')
console.log('Combined: ' + combined);
return combined
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Creates a branch with other PR branches merged together
- name: Created combined branch
env:
BASE_BRANCH: ${{ steps.fetch-branch-names.outputs.base-branch }}
BRANCHES_TO_COMBINE: ${{ steps.fetch-branch-names.outputs.result }}
COMBINE_BRANCH_NAME: ${{ github.event.inputs.combineBranchName }}
run: |
echo "$BRANCHES_TO_COMBINE"
sourcebranches="${BRANCHES_TO_COMBINE%\"}"
sourcebranches="${sourcebranches#\"}"
basebranch="${BASE_BRANCH%\"}"
basebranch="${basebranch#\"}"
git config pull.rebase false
git config user.name github-actions
git config user.email [email protected]
git branch $COMBINE_BRANCH_NAME $basebranch
git checkout $COMBINE_BRANCH_NAME
git pull origin $sourcebranches --no-edit
git push origin $COMBINE_BRANCH_NAME
# Creates a PR with the new combined branch
- uses: actions/github-script@v6
name: Create Combined Pull Request
env:
PRS_STRING: ${{ steps.fetch-branch-names.outputs.prs-string }}
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const prString = process.env.PRS_STRING;
const body = 'This PR was created by the Combine PRs action by combining the following PRs:\n' + prString;
let combinedPRs = [];
let mergeFailedPRs = [];
for(const { branch, prString } of branchesAndPRStrings) {
try {
await github.rest.repos.merge({
owner: context.repo.owner,
repo: context.repo.repo,
base: '${{ github.event.inputs.combineBranchName }}',
head: branch,
});
console.log('Merged branch ' + branch);
combinedPRs.push(prString);
} catch (error) {
console.log('Failed to merge branch ' + branch);
mergeFailedPRs.push(prString);
}
}
console.log('Creating combined PR');
const combinedPRsString = combinedPRs.join('\n');
let body = '✅ This PR was created by the Combine PRs action by combining the following PRs:\n' + combinedPRsString;
if(mergeFailedPRs.length > 0) {
const mergeFailedPRsString = mergeFailedPRs.join('\n');
body += '\n\n⚠️ The following PRs were left out due to merge conflicts:\n' + mergeFailedPRsString
}
await github.rest.pulls.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'Combined PR',
head: '${{ github.event.inputs.combineBranchName }}',
base: '${{ steps.fetch-branch-names.outputs.base-branch }}',
base: baseBranch,
body: body
});
Binary file modified images/combined-pr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 0ef9821

Please sign in to comment.