-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Automerge #1247
base: main
Are you sure you want to change the base?
feat: Automerge #1247
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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-22.04 | ||
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: "20" | ||
|
||
- name: Install Dependencies | ||
run: | | ||
npm install simple-git@^3.21.0 | ||
|
||
- name: Run Auto Rebase Script | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
run: node scripts/git/auto_rebase.js |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle the case when Currently, if Apply this diff to fix the issue: if (nextBranch) {
// 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}`);
}
+ } else {
+ console.log('No subsequent release branches to rebase.');
}
|
||
|
||
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}`); | ||
} | ||
alexrisch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} 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) { | ||
const status = await git.status(); | ||
if (status.conflicts.length > 0) { | ||
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(); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
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"]); | ||
|
||
alexrisch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// 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, | ||
}); | ||
|
||
if (!selectedCommits || selectedCommits.length === 0) { | ||
console.log("No commits selected. Exiting."); | ||
return; | ||
} | ||
|
||
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, options = { captureOutput: false }) { | ||
return new Promise((resolve, reject) => { | ||
let output = ""; | ||
const spawnOptions = options.captureOutput | ||
? { shell: true } | ||
: { stdio: "inherit", shell: true }; | ||
const cmd = spawn(command, args, spawnOptions); | ||
|
||
if (options.captureOutput) { | ||
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( | ||
`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 executeCommand( | ||
"git", | ||
["log", `${targetBranch}..${sourceBranch}`, "--pretty=format:%H %s"], | ||
{ captureOutput: true } | ||
); | ||
|
||
const commits = commitsOutput | ||
.split("\n") | ||
.filter((line) => line.trim() !== "") | ||
.map((line) => { | ||
const [hash, ...messageParts] = line.split(" "); | ||
return { hash, message: messageParts.join(" ") }; | ||
}); | ||
|
||
return commits; | ||
} | ||
|
||
cherryPickCommits(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add workflow permissions and trigger restrictions
The current trigger configuration could pose security risks:
permissions
configuration to limit the workflow's access scopeApply these changes to improve security: