Skip to content

Add Github workflow to automatically create uplift PRs#792

Open
jonathanmendez wants to merge 22 commits intomozilla:enterprise-mainfrom
jonathanmendez:auto-uplift
Open

Add Github workflow to automatically create uplift PRs#792
jonathanmendez wants to merge 22 commits intomozilla:enterprise-mainfrom
jonathanmendez:auto-uplift

Conversation

@jonathanmendez
Copy link
Copy Markdown
Contributor

@jonathanmendez jonathanmendez commented Apr 24, 2026

Description

Bugzilla: Bug-TODO
Adds a workflow that:

  • triggers when a PR is merged
  • checks whether labels uplift-to-beta or uplift-to-release are present in the PR and if the PR's target branch is one we would uplift from (enterprise-main for beta or release, and enterprise-beta for release)
  • if so, cherry-picks the commit(s) from the merge into the enterprise-beta/enterprise-release branch(es) and creates uplift PR(s)
    • if the PR was done as a merge, cherry-picks the individual commits
    • if the PR was done as a squash, cherry-picks the squash commit
  • if a conflict is encountered during a cherry-pick, the created uplift PR is changed to a Draft, and the original author is added as a reviewer with a comment for resolution instructions

Testing

TODO??

  • Added tests
  • Manual testing performed

@jonathanmendez jonathanmendez added the branch:main PR that should be merged on enterprise-main branch label Apr 24, 2026
@gcp gcp requested a review from Copilot April 25, 2026 18:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an “Auto Uplift” GitHub Actions workflow intended to automatically create uplift PRs to enterprise-beta and/or enterprise-release when PRs are merged into enterprise-main and carry uplift labels.

Changes:

  • Introduces .github/workflows/uplift.yml triggered on merged PRs to enterprise-main.
  • Detects uplift-to-beta / uplift-to-release labels and cherry-picks merged changes onto the corresponding branch.
  • Opens uplift PRs via peter-evans/create-pull-request@v6 with a generated commit summary.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/uplift.yml
Comment on lines +80 to +135
PARENTS=$(git rev-list --parents -n 1 $SHA)
COUNT=$(echo $PARENTS | wc -w)

APPLIED=0

if [ "$COUNT" -eq 3 ]; then
echo "Merge commit → multi-commit mode"

COMMITS=$(git log --reverse --no-merges --pretty=format:"%H" $SHA^1..$SHA^2)

for commit in $COMMITS; do
SHORT=$(git rev-parse --short $commit)
MSG=$(git log -1 --pretty=format:"%s" $commit)
LINK="$REPO_URL/commit/$commit"

if git branch -r --contains $commit | grep -q "origin/$TARGET"; then
echo "- [$SHORT]($LINK) $MSG (already in branch)" >> $SUMMARY_FILE
continue
fi

set +e
git cherry-pick -x -X theirs $commit
STATUS=$?
set -e

if [ $STATUS -ne 0 ]; then
# Check if it's an empty cherry-pick
if git diff --cached --quiet && git diff --quiet; then
echo "Empty cherry-pick for $commit → skipping"

git cherry-pick --skip

echo "- [$SHORT]($LINK) $MSG (already applied)" >> $SUMMARY_FILE
continue
fi

git add -A
git commit -m "Cherry-pick $commit with conflicts"
echo "- [$SHORT]($LINK) $MSG (⚠️ conflict)" >> $SUMMARY_FILE
else
echo "- [$SHORT]($LINK) $MSG" >> $SUMMARY_FILE
fi

APPLIED=1
done

else
echo "Single commit mode"

SHORT=$(git rev-parse --short $SHA)
MSG=$(git log -1 --pretty=format:"%s" $SHA)
LINK="$REPO_URL/commit/$SHA"

set +e
git cherry-pick -x -X theirs $SHA
STATUS=$?
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

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

The script assumes that any non-merge merge_commit_sha implies a single-commit uplift, but for GitHub’s “rebase and merge” (and sometimes other non-merge merge strategies), the PR may have multiple commits and merge_commit_sha will only reference the last one. In that case this workflow would uplift only one commit and silently miss the rest. Consider deriving the commit list from the PR (e.g., via gh pr view --json commits / GitHub API) and cherry-picking all PR commits (or the single squash commit when applicable) rather than branching on parent count of merge_commit_sha.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@lissyx I wonder if we should disallow the 'rebase and merge' strategy (for general reasons, not just for this). AFAIK we don't use it and wouldn't want to use it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

rebase and merge is mostly what i always do manually :'( :'(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Well I'm only talking about the PR completion action, not any manual rebasing/merging. And I haven't put a lot of thought into "we shouldn't use the rebase-and-merge PR completion", I just know that we've previously said among ourselves to use merge if our commit history is good or squash if not, so I thought we'd implicitly ruled it out.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I haven't found a way to detect when the merge was done with squash vs rebase-and-merge. If we think it makes sense to remove rebase-and-merge as a PR merge strategy, then this workflow can work as-is. Otherwise I'll just have to leave a warning comment or something to go manually check if the original PR was merged via rebase-and-merge and tell the reviewers to abandon the created uplift PR and do it manually.

Comment thread .github/workflows/uplift.yml Outdated
Comment thread .github/workflows/uplift.yml Outdated
Comment thread .github/workflows/uplift.yml Outdated
Comment thread .github/workflows/uplift.yml Outdated
Comment on lines +29 to +33
env:
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
REPO_URL: https://github.com/${{ github.repository }}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I am not sure it is worth duplicating those

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Still not convinced.

Comment on lines +58 to +174
cat << 'EOF' > uplift.sh
set -e

TARGET=$1
SHA=$2

BRANCH="uplift-${TARGET}-${SHA}"
SUMMARY_FILE=summary.txt

echo "---- Uplifting to $TARGET ----"

git fetch origin $TARGET

# Skip entire operation if merge commit already in branch
if git branch -r --contains $SHA | grep -q "origin/$TARGET"; then
echo "Already fully present in $TARGET"
echo "summary=Already present, nothing to uplift." >> $GITHUB_OUTPUT
echo "branch=" >> $GITHUB_OUTPUT
exit 0
fi

# Avoid duplicate branch
if git ls-remote --heads origin $BRANCH | grep -q $BRANCH; then
echo "Branch already exists, skipping"
echo "summary=Uplift branch already exists." >> $GITHUB_OUTPUT
echo "branch=" >> $GITHUB_OUTPUT
exit 0
fi

git checkout -b $BRANCH origin/$TARGET

touch $SUMMARY_FILE

PARENTS=$(git rev-list --parents -n 1 $SHA)
COUNT=$(echo $PARENTS | wc -w)

APPLIED=0

if [ "$COUNT" -eq 3 ]; then
echo "Merge commit → multi-commit mode"

COMMITS=$(git log --reverse --no-merges --pretty=format:"%H" $SHA^1..$SHA^2)

for commit in $COMMITS; do
SHORT=$(git rev-parse --short $commit)
MSG=$(git log -1 --pretty=format:"%s" $commit)
LINK="$REPO_URL/commit/$commit"

if git branch -r --contains $commit | grep -q "origin/$TARGET"; then
echo "- [$SHORT]($LINK) $MSG (already in branch)" >> $SUMMARY_FILE
continue
fi

set +e
git cherry-pick -x $commit
STATUS=$?
set -e

if [ $STATUS -ne 0 ]; then
# Check if it's an empty cherry-pick
if git diff --cached --quiet && git diff --quiet; then
echo "Empty cherry-pick for $commit → skipping"

git cherry-pick --skip

echo "- [$SHORT]($LINK) $MSG (already applied)" >> $SUMMARY_FILE
continue
fi

git add -A
git cherry-pick --continue
echo "- [$SHORT]($LINK) $MSG (⚠️ conflict)" >> $SUMMARY_FILE
else
echo "- [$SHORT]($LINK) $MSG" >> $SUMMARY_FILE
fi

APPLIED=1
done

else
echo "Single commit mode"

SHORT=$(git rev-parse --short $SHA)
MSG=$(git log -1 --pretty=format:"%s" $SHA)
LINK="$REPO_URL/commit/$SHA"

set +e
git cherry-pick -x $SHA
STATUS=$?
set -e

if [ $STATUS -ne 0 ]; then
git add -A
git cherry-pick --continue
echo "- [$SHORT]($LINK) $MSG (⚠️ conflict)" >> $SUMMARY_FILE
else
echo "- [$SHORT]($LINK) $MSG" >> $SUMMARY_FILE
fi

APPLIED=1
fi

if [ "$APPLIED" -eq 0 ]; then
echo "No new commits applied"
echo "summary=All commits already exist in $TARGET." >> $GITHUB_OUTPUT
echo "branch=" >> $GITHUB_OUTPUT
exit 0
fi

git push origin $BRANCH

echo "branch=$BRANCH" >> $GITHUB_OUTPUT

DELIM="GITHUB_OUTPUT_$(uuidgen)"
echo "summary<<$DELIM" >> $GITHUB_OUTPUT
cat $SUMMARY_FILE >> $GITHUB_OUTPUT
echo "$DELIM" >> $GITHUB_OUTPUT
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This should be a file in the repo, and not on the action, would make it easier to review as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I intentionally avoided making a script because it is generally seen as a security vulnerability when using on: pull_request_target that executes with privilege. The idea of the security vulnerability is that while the existing/committed version of the workflow will be what runs (even if the PR contains changes to it), having the workflow call a script could in theory run a (maliciously) modified version of the script in the PR.

It may be enough mitigation that I am only running this code when the PR is closed and merged, but I guess I'll let the github admins weigh in on that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I intentionally avoided making a script because it is generally seen as a security vulnerability when using on: pull_request_target that executes with privilege. The idea of the security vulnerability is that while the existing/committed version of the workflow will be what runs (even if the PR contains changes to it), having the workflow call a script could in theory run a (maliciously) modified version of the script in the PR.

I fail to understand the rationale here. Script is checked out, right, so it would not change.

For review purpose, it would still be much easier, can you split it during review and move it back into the payload if it's really required, before we land this PR?

It may be enough mitigation that I am only running this code when the PR is closed and merged, but I guess I'll let the github admins weigh in on that.

Comment thread .github/workflows/uplift.yml Outdated
Comment thread .github/workflows/uplift.yml Outdated
Comment thread .github/workflows/uplift.yml Outdated
Comment thread .github/workflows/uplift.yml Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/uplift.yml
Comment thread .github/workflows/uplift.yml
jonathanmendez and others added 2 commits May 1, 2026 11:15
Copilot AI review requested due to automatic review settings May 1, 2026 18:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/uplift.yml
Comment thread .github/workflows/uplift.yml
Comment thread .github/workflows/uplift.yml
Comment thread .github/workflows/uplift.yml
Comment thread .github/workflows/uplift.yml Outdated
@jonathanmendez jonathanmendez requested a review from lissyx May 1, 2026 21:08
Copilot AI review requested due to automatic review settings May 1, 2026 22:23
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/uplift.yml
Comment on lines +29 to +33
ALL_ENTRIES='[
{"source": "enterprise-main", "target": "enterprise-beta", "trigger_label": "uplift-to-beta", "target_label": "branch:beta"},
{"source": "enterprise-main", "target": "enterprise-release", "trigger_label": "uplift-to-release", "target_label": "branch:release"},
{"source": "enterprise-beta", "target": "enterprise-release", "trigger_label": "uplift-to-release", "target_label": "branch:release"}
]'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All in bash, that's lovely :p

Comment on lines +25 to +26
- name: Set matrix
id: set-matrix
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please use more descriptive name and ID

Comment on lines +39 to +40
# Use jq to filter entries where source matches base.ref AND trigger_label is in labels
FILTERED=$(echo "$ALL_ENTRIES" | jq --arg base "$BASE_REF" --argjson labels "$LABELS" '[.[] | select(.source == $base and (.trigger_label as $tl | $labels | index($tl) != null))]')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I dont get clearly what this is doing. We filter labels? You mention "entries" but the ALL_ENTRIES is just a structure we defined above, so it far from being clear.

Are we getting tricked by how jq works, and $ALL_ENTRIES being piped into stdin is just a way to pass the structure, but the real "input" is $LABELS ?

This needs to be better documented, especially since this is something running on CI so hard to debug locally when it fails. And the output is also not clear from there. What do we get, a list of sha? Or just we find the entry in ALL_ENTRIES that we care about?


steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why not using tags? Or even branches? What does security github-admin thinks about this?

Comment on lines +69 to +70
git config user.name "github-actions"
git config user.email "github-actions@github.com"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Shouldn't we have a valid user here, with properly limited tokens/credentials ?

Comment on lines +107 to +111
# rev-list --parents outputs:
# <commit> <parent1> [parent2 ...]
# So:
# 2 fields → normal commit (1 parent)
# >2 fields → merge commit (2+ parents)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's much clear!

Comment on lines +183 to +184
- name: Uplift
id: uplift
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please a more descriptive name and id

Suggested change
- name: Uplift
id: uplift
- name: Use uplift script to generate uplift branch
id: generate-uplift-branch

--label "${{ matrix.target_label }}" \
--label "uplift")

gh pr edit "$PR_URL" --add-reviewer "mozilla/enterprise-firefox-engineering"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I really dislike this catch-all reviewer but I'm not sure we can have a better option, we would have to expose our self-generated "merge rotation duty" somewhere on github? Or can we tag explicitely people? Better: configured from an env var. Then the people who review can decide if others needs to verify as well or if a single reviewer is enough?

Otherwise we spam everybody ...

gh pr comment "$PR_URL" --body-file comment_body.txt

# Add original author and mark PR as draft since manual resolution is required
gh pr edit "$PR_URL" --add-reviewer "${{ github.event.pull_request.user.login }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should we also set assigned field?

Copy link
Copy Markdown
Contributor

@lissyx lissyx left a comment

Choose a reason for hiding this comment

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

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

branch:main PR that should be merged on enterprise-main branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants