-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds helper scripts Adds hopefully good action to manage automerges
- Loading branch information
Showing
4 changed files
with
455 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
name: Auto Rebase Branches | ||
|
||
on: | ||
push: | ||
branches: | ||
- "release/*" | ||
|
||
jobs: | ||
auto-rebase: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout Repository | ||
uses: actions/checkout@v3 | ||
with: | ||
fetch-depth: 0 # Fetch all history so we can work with all branches | ||
|
||
- name: Set up Node.js | ||
uses: actions/setup-node@v3 | ||
with: | ||
node-version: "16" | ||
|
||
- name: Install Dependencies | ||
run: | | ||
npm install [email protected] | ||
- name: Run Auto Rebase Script | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
run: node scripts/git/auto_rebase.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
const simpleGit = require("simple-git"); | ||
const { Octokit } = require("@octokit/rest"); | ||
const git = simpleGit(); | ||
|
||
async function main() { | ||
try { | ||
const { GITHUB_REF, GITHUB_REPOSITORY, GITHUB_TOKEN } = process.env; | ||
const octokit = new Octokit({ auth: GITHUB_TOKEN }); | ||
const [owner, repo] = GITHUB_REPOSITORY.split("/"); | ||
|
||
// Extract the branch that was just pushed | ||
const updatedBranch = GITHUB_REF.replace("refs/heads/", ""); | ||
console.log(`Updated branch: ${updatedBranch}`); | ||
|
||
// Determine the list of release branches | ||
const releaseBranches = await getBranchesMatchingPattern("release/"); | ||
|
||
// Sort release branches (assuming semantic versioning) | ||
releaseBranches.sort(compareReleaseBranches); | ||
|
||
// Find the next release branch | ||
const currentIndex = releaseBranches.indexOf(updatedBranch); | ||
let nextBranch = null; | ||
|
||
if (currentIndex !== -1 && currentIndex < releaseBranches.length - 1) { | ||
nextBranch = releaseBranches[currentIndex + 1]; | ||
console.log(`Next release branch: ${nextBranch}`); | ||
} else { | ||
// No more release branches, proceed to feature branches | ||
const featureBranches = await getBranchesMatchingPattern("feature/"); | ||
|
||
for (const featureBranch of featureBranches) { | ||
const rebaseResult = await rebaseBranch(updatedBranch, featureBranch); | ||
|
||
if (!rebaseResult) { | ||
await createIssue(octokit, owner, repo, updatedBranch, featureBranch); | ||
} else { | ||
console.log( | ||
`Successfully rebased ${featureBranch} onto ${updatedBranch}` | ||
); | ||
} | ||
} | ||
} | ||
|
||
// Attempt to rebase nextBranch onto updatedBranch | ||
const rebaseResult = await rebaseBranch(updatedBranch, nextBranch); | ||
|
||
if (!rebaseResult) { | ||
// Rebase failed due to conflicts, create an issue | ||
await createIssue(octokit, owner, repo, updatedBranch, nextBranch); | ||
} else { | ||
console.log(`Successfully rebased ${nextBranch} onto ${updatedBranch}`); | ||
} | ||
} catch (error) { | ||
console.error("An error occurred:", error.message); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
async function getBranchesMatchingPattern(pattern) { | ||
const branches = await git.branch(["-r"]); | ||
return branches.all | ||
.map((branch) => branch.replace("origin/", "")) | ||
.filter((branch) => branch.startsWith(pattern)); | ||
} | ||
|
||
function compareReleaseBranches(a, b) { | ||
// Extract version numbers and compare | ||
const versionA = a.replace("release/", "").split(".").map(Number); | ||
const versionB = b.replace("release/", "").split(".").map(Number); | ||
|
||
for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) { | ||
const numA = versionA[i] || 0; | ||
const numB = versionB[i] || 0; | ||
if (numA !== numB) { | ||
return numA - numB; | ||
} | ||
} | ||
return 0; | ||
} | ||
|
||
async function rebaseBranch(baseBranch, targetBranch) { | ||
try { | ||
await git.fetch(); | ||
|
||
// Checkout target branch | ||
await git.checkout(targetBranch); | ||
|
||
// Pull latest changes | ||
await git.pull("origin", targetBranch, { "--ff-only": null }); | ||
|
||
// Attempt rebase | ||
await git.rebase([baseBranch]); | ||
|
||
// Push rebased branch | ||
await git.push("origin", targetBranch, { "--force-with-lease": null }); | ||
|
||
return true; | ||
} catch (error) { | ||
if (error.message.includes("CONFLICT")) { | ||
console.error( | ||
`Conflicts occurred while rebasing ${targetBranch} onto ${baseBranch}` | ||
); | ||
await git.rebase(["--abort"]); | ||
return false; | ||
} else { | ||
throw error; | ||
} | ||
} | ||
} | ||
|
||
async function createIssue(octokit, owner, repo, baseBranch, targetBranch) { | ||
const issueTitle = `Conflict: Rebase ${targetBranch} onto ${baseBranch}`; | ||
const issueBody = `An automatic rebase of **${targetBranch}** onto **${baseBranch}** failed due to conflicts. Please resolve the conflicts manually. | ||
**Base Branch**: \`${baseBranch}\` | ||
**Target Branch**: \`${targetBranch}\` | ||
<details> | ||
<summary>Rebase attempt output</summary> | ||
\`\`\` | ||
[Rebase output and conflict details can be included here] | ||
\`\`\` | ||
</details> | ||
--- | ||
*This issue was generated by an automated workflow.* | ||
`; | ||
|
||
await octokit.rest.issues.create({ | ||
owner, | ||
repo, | ||
title: issueTitle, | ||
body: issueBody, | ||
labels: ["rebase-conflict"], | ||
}); | ||
|
||
console.log( | ||
`Created issue for conflict between ${baseBranch} and ${targetBranch}` | ||
); | ||
} | ||
|
||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
const { spawn } = require("child_process"); | ||
const prompts = require("prompts"); | ||
|
||
async function cherryPickCommits() { | ||
const response = await prompts([ | ||
{ | ||
type: "text", | ||
name: "sourceBranch", | ||
message: "Enter the source branch (containing commits to cherry-pick)", | ||
}, | ||
{ | ||
type: "text", | ||
name: "targetBranch", | ||
message: "Enter the target branch (branch to apply commits to)", | ||
}, | ||
{ | ||
type: "confirm", | ||
name: "selectCommits", | ||
message: "Do you want to select specific commits to cherry-pick?", | ||
initial: false, | ||
}, | ||
]); | ||
|
||
const { sourceBranch, targetBranch, selectCommits } = response; | ||
|
||
try { | ||
// Fetch and checkout target branch | ||
await executeCommand("git", ["fetch", "origin"]); | ||
await executeCommand("git", ["checkout", targetBranch]); | ||
await executeCommand("git", ["pull", "origin", targetBranch, "--ff-only"]); | ||
|
||
// Update source branch | ||
await executeCommand("git", ["fetch", "origin"]); | ||
await executeCommand("git", ["checkout", sourceBranch]); | ||
await executeCommand("git", ["pull", "origin", sourceBranch, "--ff-only"]); | ||
|
||
// Get list of commits from source branch not in target branch | ||
const commitList = await getCommitsNotInTarget(sourceBranch, targetBranch); | ||
|
||
if (commitList.length === 0) { | ||
console.log("No new commits to cherry-pick."); | ||
process.exit(0); | ||
} | ||
|
||
let commitsToCherryPick = commitList; | ||
|
||
if (selectCommits) { | ||
// Prompt user to select commits | ||
const commitChoices = commitList.map((commit) => ({ | ||
title: `${commit.hash} ${commit.message}`, | ||
value: commit.hash, | ||
})); | ||
|
||
const { selectedCommits } = await prompts({ | ||
type: "multiselect", | ||
name: "selectedCommits", | ||
message: "Select commits to cherry-pick", | ||
choices: commitChoices, | ||
min: 1, | ||
}); | ||
|
||
commitsToCherryPick = commitList.filter((commit) => | ||
selectedCommits.includes(commit.hash) | ||
); | ||
} | ||
|
||
// Switch back to target branch | ||
await executeCommand("git", ["checkout", targetBranch]); | ||
|
||
// Cherry-pick the commits | ||
for (const commit of commitsToCherryPick) { | ||
console.log(`Cherry-picking commit ${commit.hash}: ${commit.message}`); | ||
try { | ||
await executeCommand("git", ["cherry-pick", commit.hash]); | ||
} catch (error) { | ||
console.error( | ||
`Conflict occurred while cherry-picking commit ${commit.hash}.` | ||
); | ||
// Prompt user to resolve conflicts | ||
const { resolveNow } = await prompts({ | ||
type: "confirm", | ||
name: "resolveNow", | ||
message: | ||
"Do you want to resolve the conflicts now? (If not, the cherry-pick will be aborted.)", | ||
initial: true, | ||
}); | ||
|
||
if (resolveNow) { | ||
console.log( | ||
"Please resolve the conflicts manually, then continue the cherry-pick." | ||
); | ||
console.log("After resolving, run: git cherry-pick --continue"); | ||
console.log( | ||
"If you want to abort the cherry-pick, run: git cherry-pick --abort" | ||
); | ||
process.exit(1); | ||
} else { | ||
await executeCommand("git", ["cherry-pick", "--abort"]); | ||
throw new Error( | ||
`Cherry-pick aborted due to conflicts in commit ${commit.hash}.` | ||
); | ||
} | ||
} | ||
} | ||
|
||
// Push the updated target branch to origin | ||
await executeCommand("git", ["push", "origin", targetBranch]); | ||
|
||
console.log( | ||
`Successfully cherry-picked ${commitsToCherryPick.length} commits onto '${targetBranch}' and pushed to origin.` | ||
); | ||
} catch (error) { | ||
console.error("An error occurred:", error.message); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
function executeCommand(command, args) { | ||
return new Promise((resolve, reject) => { | ||
const cmd = spawn(command, args, { stdio: "inherit", shell: true }); | ||
|
||
cmd.on("close", (code) => { | ||
if (code === 0) { | ||
resolve(); | ||
} else { | ||
reject( | ||
new Error( | ||
`Command "${command} ${args.join(" ")}" exited with code ${code}` | ||
) | ||
); | ||
} | ||
}); | ||
|
||
cmd.on("error", (error) => { | ||
reject(error); | ||
}); | ||
}); | ||
} | ||
|
||
async function getCommitsNotInTarget(sourceBranch, targetBranch) { | ||
// Get commits in sourceBranch not in targetBranch | ||
const commitsOutput = await executeGitCommand([ | ||
"log", | ||
`${targetBranch}..${sourceBranch}`, | ||
"--pretty=format:%H %s", | ||
]); | ||
|
||
const commits = commitsOutput | ||
.split("\n") | ||
.filter((line) => line.trim() !== "") | ||
.map((line) => { | ||
const [hash, ...messageParts] = line.split(" "); | ||
return { hash, message: messageParts.join(" ") }; | ||
}); | ||
|
||
return commits; | ||
} | ||
|
||
function executeGitCommand(args) { | ||
return new Promise((resolve, reject) => { | ||
let output = ""; | ||
const cmd = spawn("git", args, { shell: true }); | ||
|
||
cmd.stdout.on("data", (data) => { | ||
output += data.toString(); | ||
}); | ||
|
||
cmd.stderr.on("data", (data) => { | ||
output += data.toString(); | ||
}); | ||
|
||
cmd.on("close", (code) => { | ||
if (code === 0) { | ||
resolve(output.trim()); | ||
} else { | ||
reject( | ||
new Error( | ||
`Git command "git ${args.join(" ")}" exited with code ${code}` | ||
) | ||
); | ||
} | ||
}); | ||
|
||
cmd.on("error", (error) => { | ||
reject(error); | ||
}); | ||
}); | ||
} | ||
|
||
cherryPickCommits(); |
Oops, something went wrong.