Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion .github/scripts/backport.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,41 @@ for (const targetBranch of branches) {
// Push the backport branch (force to handle updates)
core.info(`Pushing ${backportBranch} to origin`);
execSync(`git push --force-with-lease origin ${backportBranch}`, { stdio: 'inherit' });


// Re-create each new commit through the Git Data API so the resulting chain shows as "Verified"
core.info(`Re-creating commits via the Git Data API to get verified signatures`);
const newCommitShas = execSync(`git log --format=%H ${targetBranch}..${backportBranch}`, { encoding: 'utf-8' })
Comment on lines +104 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Unsigned push blocks protected backports

When a backport branch requires signed commits, git push rejects the unsigned cherry-picks before createCommit runs. No backport PR is created.

Learn more

The workflow creates local cherry-picks and pushes the local branch before the Git Data API signs replacement commits. A signed-commit rule on a backport branch rejects that first push, so execution never reaches commit recreation. The catch handler reports failure instead of creating a backport PR.

Example: A repository applies a signed-commit rule to backport-*. Cherry-picking PR #42 creates unsigned commits locally. The push of backport-42-to-release-25.1 is rejected, even though the following API calls would have produced signed commits.

Recommended fix: Create the Git Data API commit chain before publishing the branch. Push only a signed ref, or create/update the ref through the API after uploading any missing tree and blob objects; ensure the API can resolve each tree before calling createCommit.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

.trim().split('\n').filter(Boolean).reverse(); // oldest -> newest

// Use the same base that produced the cherry-picked trees. The remote
// target branch may have advanced since the initial fetch.
let parentSha = execSync(`git rev-parse ${targetBranch}`, {
encoding: 'utf-8'
}).trim();

for (const sha of newCommitShas) {
const treeSha = execSync(`git rev-parse ${sha}^{tree}`, { encoding: 'utf-8' }).trim();
const message = execSync(`git log -1 --format=%B ${sha}`, { encoding: 'utf-8' });

const { data: newCommit } = await github.rest.git.createCommit({
owner: context.repo.owner,
repo: context.repo.repo,
message,
tree: treeSha,
parents: [parentSha]
});
Comment on lines +119 to +125

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Backport commits lose contributor attribution

For contributor-authored changes, createCommit omits the cherry-picked author's identity. The API-created commits credit the bot instead of the contributor.

Learn more

A Git commit stores an author independently of its message and tree. Cherry-picking normally preserves the original author's name and email. The new API requests send only the message, tree, and parent; the API therefore uses the authenticated identity for author metadata, replacing contributor attribution on every recreated commit.

Example: Alice authors a PR commit. The local cherry-pick retains Alice as author, but the recreated backport commit credits the GitHub Actions bot.

Recommended fix: Read each cherry-picked commit's author name, email, and date from Git, and pass them in the author object to github.rest.git.createCommit. Leave the committer as the signing bot if needed for GitHub's verified signature; verify the resulting author and signature together.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

parentSha = newCommit.sha;
}

core.info(`Repointing ${backportBranch} at signed commit ${parentSha}`);
await github.rest.git.updateRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${backportBranch}`,
sha: parentSha,
force: true
});

// Check if a PR already exists for this backport branch
const { data: existingPRs } = await github.rest.pulls.list({
owner: context.repo.owner,
Expand Down