diff --git a/.github/workflows/_cherry-pick.yml b/.github/workflows/_cherry-pick.yml index f1e28051a..bed8a350b 100644 --- a/.github/workflows/_cherry-pick.yml +++ b/.github/workflows/_cherry-pick.yml @@ -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 @@ -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 @@ -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" @@ -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 @@ -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 @@ -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" @@ -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 diff --git a/.github/workflows/test-cherry-pick.yml b/.github/workflows/test-cherry-pick.yml index f91b030df..64a6c99fd 100644 --- a/.github/workflows/test-cherry-pick.yml +++ b/.github/workflows/test-cherry-pick.yml @@ -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 @@ -71,12 +71,12 @@ 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 @@ -84,15 +84,31 @@ jobs: 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 }} @@ -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 @@ -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 @@ -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." @@ -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." diff --git a/cherry-pick/cherry-pick.yml b/cherry-pick/cherry-pick.yml index 6d705081d..be07ba030 100644 --- a/cherry-pick/cherry-pick.yml +++ b/cherry-pick/cherry-pick.yml @@ -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: @@ -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