Skip to content
90 changes: 74 additions & 16 deletions .github/workflows/_cherry-pick.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ on:
required: false
type: string
default: ''
create_pr:
description: 'Create a PR to dest_branch instead of pushing directly'
required: false
type: boolean
default: false
test_mode:
description: 'Run in test mode (skips Azure auth and GPG signing)'
description: 'Run in test mode (skips Azure auth and uses local token)'
required: false
type: boolean
default: false
outputs:
cherry_pick_branch:
description: 'The cherry-pick branch created when create_pr is true, empty otherwise'
value: ${{ jobs.cherry-pick.outputs.cherry_pick_branch }}
secrets:
AZURE_SUBSCRIPTION_ID:
required: false
Expand All @@ -34,10 +43,13 @@ permissions: {}
jobs:
cherry-pick:
name: Cherry Pick Commit
outputs:
cherry_pick_branch: ${{ steps.create-cherry-pick-branch.outputs.cherry_pick_branch }}
env:
_SOURCE_PR: ${{ inputs.source_pr }}
_DEST_BRANCH: ${{ inputs.dest_branch }}
_DEST_BRANCH_ALLOWED: ${{ inputs.dest_branch_allowed }}
_CHERRY_PICK_BRANCH: cherry-pick/${{ inputs.source_pr }}-to-${{ inputs.dest_branch }}
runs-on: ubuntu-24.04
permissions:
contents: write
Expand All @@ -48,7 +60,7 @@ jobs:
if: inputs.dest_branch_allowed != ''
run: |
if ! echo "$_DEST_BRANCH" | grep -Eq "$_DEST_BRANCH_ALLOWED"; then
echo "ERROR: Destination branch [$_DEST_BRANCH] does not match allowed pattern [$_DEST_BRANCH_ALLOWED]"
echo "::error::Destination branch [$_DEST_BRANCH] does not match allowed pattern [$_DEST_BRANCH_ALLOWED]"
exit 1
fi
echo "INFO: Destination branch matches allowed pattern"
Expand Down Expand Up @@ -94,7 +106,7 @@ jobs:
id: validate_dest_branch
run: |
if ! git rev-parse --quiet --verify origin/"$_DEST_BRANCH" >/dev/null; then
echo "ERROR: Could not find destination branch [$_DEST_BRANCH]. Please confirm the branch exists and the name is correct."
echo "::error::Could not find destination branch [$_DEST_BRANCH]. Please confirm the branch exists and the name is correct."
exit 1
fi

Expand All @@ -105,45 +117,48 @@ jobs:
WORKFLOW_REF: ${{ github.workflow_ref }}
run: |
# get PR info
if ! pr_info=$(gh pr view "$_SOURCE_PR" --json state,baseRefName,mergeCommit); then
echo "ERROR: Could not gather data for PR [#$_SOURCE_PR]. Please verify the number."
if ! pr_info=$(gh pr view "$_SOURCE_PR" --json state,baseRefName,mergeCommit,title); then
echo "::error::Could not gather data for PR [#$_SOURCE_PR]. Please verify the number."
exit 1
fi

pr_state=$(echo "$pr_info" | jq -r .state)
pr_base_ref=$(echo "$pr_info" | jq -r .baseRefName)
pr_commit=$(echo "$pr_info" | jq -r .mergeCommit.oid)
pr_title=$(echo "$pr_info" | jq -r .title)

echo "INFO: PR state: [$pr_state]"
echo "INFO: PR base ref: [$pr_base_ref]"
echo "INFO: PR commit: [$pr_commit]"
echo "INFO: PR title: [$pr_title]"
echo "INFO: Calling workflow: [$WORKFLOW_REF]"

# if this is being run by the test-cherry-pick workflow, allow picking from the simulated main branch
if [[ $WORKFLOW_REF == bitwarden/gh-actions/.github/workflows/test-cherry-pick.yml@* ]]; then
echo "INFO: This workflow is being run by the [test-cherry-pick.yml] workflow."
if [[ ${pr_base_ref:0:21} != "cherry-pick-sim-main-" ]]; then
echo "ERROR: Invalid test base ref: [$pr_base_ref]. Test ref/branch must match [cherry-pick-sim-main-*]"
echo "::error::Invalid test base ref: [$pr_base_ref]. Test ref/branch must match [cherry-pick-sim-main-*]"
exit 1
fi
else
if [[ $pr_base_ref != 'main' ]]; then
echo "ERROR: Invalid PR base ref: [$pr_base_ref]. Base ref/branch must be [main]."
echo "::error::Invalid PR base ref: [$pr_base_ref]. Base ref/branch must be [main]."
exit 1
fi
fi

if [[ $pr_state != 'MERGED' ]]; then
echo "ERROR: Invalid PR state: [$pr_state]. State must be [MERGED]."
echo "::error::Invalid PR state: [$pr_state]. State must be [MERGED]."
exit 1
fi

if [[ -z $pr_commit ]]; then
echo "ERROR: GitHub API returned empty commit for PR. Cannot continue."
echo "::error::GitHub API returned empty commit for PR. Cannot continue."
exit 1
fi

echo "pr_commit=$pr_commit" >> $GITHUB_OUTPUT
echo "pr_title=$pr_title" >> $GITHUB_OUTPUT

- name: Checkout dest branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
Expand All @@ -153,6 +168,14 @@ jobs:
persist-credentials: true
fetch-depth: 0

- name: Create cherry-pick branch
if: inputs.create_pr
id: create-cherry-pick-branch
run: |
git checkout -b "$_CHERRY_PICK_BRANCH"
echo "INFO: Created branch [$_CHERRY_PICK_BRANCH] from [$_DEST_BRANCH]"
echo "cherry_pick_branch=$_CHERRY_PICK_BRANCH" >> $GITHUB_OUTPUT

- name: Configure git
run: |
git config --local user.name "bitwarden-devops-bot"
Expand All @@ -161,31 +184,66 @@ jobs:
- name: Cherry pick
env:
PR_COMMIT: ${{ steps.validate-pr.outputs.pr_commit }}
GH_TOKEN: ${{ inputs.test_mode && github.token || steps.app-token.outputs.token }}
run: |
echo "INFO: Cherry-picking commit [$PR_COMMIT] from PR [#$_SOURCE_PR] to branch [$_DEST_BRANCH]"
echo "INFO: Cherry-picking commit [$PR_COMMIT] from PR [#$_SOURCE_PR]"
if ! git cherry-pick -x "$PR_COMMIT"; then
echo "ERROR: Cherry-pick failed for commit [$PR_COMMIT]"
echo "::error::Cherry-pick failed for commit [$PR_COMMIT]"
git cherry-pick --abort
exit 1
fi
echo 'INFO: Cherry pick successful!'

- name: Push to destination branch
if: ${{ !inputs.create_pr }}
env:
GH_TOKEN: ${{ inputs.test_mode && github.token || steps.app-token.outputs.token }}
run: |
echo "INFO: Pushing local [$_DEST_BRANCH] branch to origin"
if ! git push origin "$_DEST_BRANCH"; then
echo "ERROR: Failed to push to origin."
echo "::error::Failed to push to origin."
exit 1
fi

echo "INFO: Adding 'cherry-picked' label to PR [#$_SOURCE_PR]"
if ! gh pr edit "$_SOURCE_PR" --add-label "cherry-picked"; then
echo "WARN: Failed to add label to PR [#$_SOURCE_PR]"
echo "::warning::Failed to add label to PR [#$_SOURCE_PR]"
fi

run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo "INFO: Adding comment to PR [#$_SOURCE_PR]"
if ! gh pr comment "$_SOURCE_PR" --body "βœ… Successfully cherry-picked to \`$_DEST_BRANCH\`. [View run]($run_url)"; then
echo "WARN: Failed to add comment to PR [#$_SOURCE_PR]"
if ! gh pr comment "$_SOURCE_PR" --body "βœ… Successfully cherry-picked to [$_DEST_BRANCH]. [View run]($run_url)"; then
echo "::warning::Failed to add comment to PR [#$_SOURCE_PR]"
fi

- name: Push branch and open PR
if: inputs.create_pr
env:
GH_TOKEN: ${{ inputs.test_mode && github.token || steps.app-token.outputs.token }}
PR_TITLE: ${{ steps.validate-pr.outputs.pr_title }}
run: |
echo "INFO: Pushing branch [$_CHERRY_PICK_BRANCH] to origin"
if ! git push origin "$_CHERRY_PICK_BRANCH"; then
echo "::error::Failed to push branch [$_CHERRY_PICK_BRANCH]."
exit 1
fi

run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"

PR_BODY="πŸ’ Automated cherry-pick of PR [#$_SOURCE_PR] to [$_DEST_BRANCH]. [View run]($run_url)"

if ! new_pr_url=$(gh pr create \
--base "$_DEST_BRANCH" \
--head "$_CHERRY_PICK_BRANCH" \
--title "Cherry pick: $PR_TITLE" \
--body "$PR_BODY"); then
echo "::error::Failed to create PR."
exit 1
fi

echo "INFO: Cherry-pick PR created: [$new_pr_url]"

if ! gh pr comment "$_SOURCE_PR" --body "πŸ’ Cherry-pick PR created targeting [$_DEST_BRANCH]: $new_pr_url. [View run]($run_url)"; then
echo "::warning::Failed to add comment to PR [#$_SOURCE_PR]"
fi

echo "πŸ’ Cherry-pick PR created: [Here]($new_pr_url)." >> $GITHUB_STEP_SUMMARY
49 changes: 39 additions & 10 deletions .github/workflows/test-cherry-pick.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
git add "$sim_branch.txt"
git commit -m 'created test file'
if ! git push origin "$sim_branch"; then
echo "ERROR: Failed to push [$sim_branch] branch to origin. Exiting."
echo "::error::Failed to push [$sim_branch] branch to origin. Exiting."
exit 1
fi
git switch main
Expand All @@ -71,28 +71,44 @@ jobs:
--body "Automated test PR for cherry-pick workflow testing. This should be cleaned up automatically and can safely be deleted if cleanup has failed."

if [[ $? -ne 0 ]]; then
echo "ERROR: Failed to create PR from [$simulated_feature_branch] branch to [$simulated_main_branch]. Exiting"
echo "::error::Failed to create PR from [$simulated_feature_branch] branch to [$simulated_main_branch]. Exiting"
exit 1
fi

if ! simulated_pr_number=$(gh pr view "$simulated_feature_branch" --json number --jq '.number'); then
echo "ERROR: Failed to retrieve simulated PR number from [$simulated_feature_branch]. Exiting."
echo "::error::Failed to retrieve simulated PR number from [$simulated_feature_branch]. Exiting."
exit 1
fi
echo "simulated_pr_number=$simulated_pr_number" >> $GITHUB_OUTPUT
echo "INFO: Simulated PR is [#$simulated_pr_number]"

echo "INFO: Merging PR [#$simulated_pr_number] into branch [$simulated_main_branch]"
if ! gh pr merge "$simulated_pr_number" --squash; then
echo "ERROR: Failed to merge PR [#$simulated_pr_number]. Exiting."
echo "::error::Failed to merge PR [#$simulated_pr_number]. Exiting."
exit 1
fi


cherry-pick-pr:
# Call the cherry-pick.yml workflow in PR mode and pass in the simulated branches and PR.
name: Cherry-pick-pr
needs: setup
uses: ./.github/workflows/_cherry-pick.yml
with:
source_pr: ${{ needs.setup.outputs.simulated_pr_number }}
dest_branch: ${{ needs.setup.outputs.simulated_dest_branch }}
dest_branch_allowed: '^cherry-pick-sim-dest-[0-9]{10}$'
test_mode: true
create_pr: true
permissions:
contents: write
pull-requests: write
id-token: write

cherry-pick:
# Call the cherry-pick.yml workflow and pass in the simulated branches and PR.
name: Cherry-pick
needs: setup
needs: [setup, cherry-pick-pr]
uses: ./.github/workflows/_cherry-pick.yml
with:
source_pr: ${{ needs.setup.outputs.simulated_pr_number }}
Expand All @@ -108,13 +124,14 @@ jobs:
# Cleanup all the simulated resources
name: Cleanup Simulated Resources
runs-on: ubuntu-24.04
needs: [setup, cherry-pick]
needs: [setup, cherry-pick-pr, cherry-pick]
if: always()
env:
_SIMULATED_MAIN_BRANCH: ${{ needs.setup.outputs.simulated_main_branch }}
_SIMULATED_FEATURE_BRANCH: ${{ needs.setup.outputs.simulated_feature_branch }}
_SIMULATED_DEST_BRANCH: ${{ needs.setup.outputs.simulated_dest_branch }}
_SIMULATED_PR_NUMBER: ${{ needs.setup.outputs.simulated_pr_number }}
_CHERRY_PICK_BRANCH: ${{ needs.cherry-pick-pr.outputs.cherry_pick_branch }}
permissions:
contents: write
pull-requests: write
Expand All @@ -128,12 +145,13 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
sleep 120
if [[ -n $_SIMULATED_PR_NUMBER ]]; then
pr_state=$(gh pr view "$_SIMULATED_PR_NUMBER" --json state --jq '.state')
if [[ $pr_state == 'OPEN' ]]; then
echo "INFO: Closing simulated PR [#$_SIMULATED_PR_NUMBER]"
if ! gh pr close "$_SIMULATED_PR_NUMBER"; then
echo "WARN: Failed to close PR [#$_SIMULATED_PR_NUMBER]. Please manually delete these."
echo "::warning::Failed to close PR [#$_SIMULATED_PR_NUMBER]. Please manually delete these."
fi
fi
fi
Expand All @@ -142,7 +160,7 @@ jobs:
if git ls-remote --exit-code --heads origin "$_SIMULATED_FEATURE_BRANCH" >/dev/null 2>&1; then
echo "INFO: Deleting simulated feature branch [$_SIMULATED_FEATURE_BRANCH]"
if ! git push origin --delete "$_SIMULATED_FEATURE_BRANCH"; then
echo "WARN: Failed to delete [$_SIMULATED_FEATURE_BRANCH] branch. Please manually delete this."
echo "::warning::Failed to delete [$_SIMULATED_FEATURE_BRANCH] branch. Please manually delete this."
fi
else
echo "INFO: Branch [$_SIMULATED_FEATURE_BRANCH] does not exist or was already deleted."
Expand All @@ -153,18 +171,29 @@ jobs:
if git ls-remote --exit-code --heads origin "$_SIMULATED_MAIN_BRANCH" >/dev/null 2>&1; then
echo "INFO: Deleting simulated main branch [$_SIMULATED_MAIN_BRANCH]"
if ! git push origin --delete "$_SIMULATED_MAIN_BRANCH"; then
echo "WARN: Failed to delete [$_SIMULATED_MAIN_BRANCH] branch. Please manually delete this."
echo "::warning::Failed to delete [$_SIMULATED_MAIN_BRANCH] branch. Please manually delete this."
fi
else
echo "INFO: Branch [$_SIMULATED_MAIN_BRANCH] does not exist or was already deleted."
fi
fi

if [[ -n $_CHERRY_PICK_BRANCH ]]; then
if git ls-remote --exit-code --heads origin "$_CHERRY_PICK_BRANCH" >/dev/null 2>&1; then
echo "INFO: Deleting cherry-pick branch [$_CHERRY_PICK_BRANCH]"
if ! git push origin --delete "$_CHERRY_PICK_BRANCH"; then
echo "::warning::Failed to delete [$_CHERRY_PICK_BRANCH] branch. Please manually delete this."
fi
else
echo "INFO: Branch [$_CHERRY_PICK_BRANCH] does not exist or was already deleted."
fi
fi

if [[ -n $_SIMULATED_DEST_BRANCH ]]; then
if git ls-remote --exit-code --heads origin "$_SIMULATED_DEST_BRANCH" >/dev/null 2>&1; then
echo "INFO: Deleting simulated destination branch [$_SIMULATED_DEST_BRANCH]"
if ! git push origin --delete "$_SIMULATED_DEST_BRANCH"; then
echo "WARN: Failed to delete [$_SIMULATED_DEST_BRANCH] branch. Please manually delete this."
echo "::warning::Failed to delete [$_SIMULATED_DEST_BRANCH] branch. Please manually delete this."
fi
else
echo "INFO: Branch [$_SIMULATED_DEST_BRANCH] does not exist or was already deleted."
Expand Down
6 changes: 6 additions & 0 deletions cherry-pick/cherry-pick.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ on:
- rc
- hotfix-rc
- rc-2024-02
create_pr:
description: 'Create a PR to dest_branch instead of pushing directly'
required: false
type: boolean
default: false

# It's also possible to provide `dest_branch` as a free-form string input
# dest_branch:
Expand All @@ -35,6 +40,7 @@ jobs:
# Optionally validate `dest_branch` by providing an extended regex expression (ERE) pattern that `dest_branch` must match. Leave empty ('') to disable.
# dest_branch_allowed: '^20[2-9][0-9]\.[0-9][0-9]\-rc[0-9]$' # matches format `2025.01-rc2`
dest_branch_allowed: ''
create_pr: ${{ inputs.create_pr }}
permissions:
contents: read
id-token: write
Expand Down
Loading