Add Github workflow to automatically create uplift PRs#792
Add Github workflow to automatically create uplift PRs#792jonathanmendez wants to merge 22 commits intomozilla:enterprise-mainfrom
Conversation
There was a problem hiding this comment.
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.ymltriggered on merged PRs toenterprise-main. - Detects
uplift-to-beta/uplift-to-releaselabels and cherry-picks merged changes onto the corresponding branch. - Opens uplift PRs via
peter-evans/create-pull-request@v6with a generated commit summary.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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=$? |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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?
There was a problem hiding this comment.
rebase and merge is mostly what i always do manually :'( :'(
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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 }} |
There was a problem hiding this comment.
I am not sure it is worth duplicating those
| 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 |
There was a problem hiding this comment.
This should be a file in the repo, and not on the action, would make it easier to review as well
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I intentionally avoided making a script because it is generally seen as a security vulnerability when using
on: pull_request_targetthat 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.
There was a problem hiding this comment.
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.
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
…icts Co-authored-by: Copilot <copilot@github.com>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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"} | ||
| ]' |
There was a problem hiding this comment.
All in bash, that's lovely :p
| - name: Set matrix | ||
| id: set-matrix |
There was a problem hiding this comment.
Please use more descriptive name and ID
| # 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))]') |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Why not using tags? Or even branches? What does security github-admin thinks about this?
| git config user.name "github-actions" | ||
| git config user.email "github-actions@github.com" |
There was a problem hiding this comment.
Shouldn't we have a valid user here, with properly limited tokens/credentials ?
| # rev-list --parents outputs: | ||
| # <commit> <parent1> [parent2 ...] | ||
| # So: | ||
| # 2 fields → normal commit (1 parent) | ||
| # >2 fields → merge commit (2+ parents) |
| - name: Uplift | ||
| id: uplift |
There was a problem hiding this comment.
Please a more descriptive name and id
| - 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" |
There was a problem hiding this comment.
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 }}" |
There was a problem hiding this comment.
Should we also set assigned field?
Description
Bugzilla: Bug-TODO
Adds a workflow that:
uplift-to-betaoruplift-to-releaseare present in the PR and if the PR's target branch is one we would uplift from (enterprise-mainfor beta or release, andenterprise-betafor release)enterprise-beta/enterprise-releasebranch(es) and creates uplift PR(s)Testing
TODO??