diff --git a/.github/actions/composite/updateProtectedBranch/action.yml b/.github/actions/composite/updateProtectedBranch/action.yml new file mode 100644 index 000000000000..7f067ec0c89b --- /dev/null +++ b/.github/actions/composite/updateProtectedBranch/action.yml @@ -0,0 +1,140 @@ +name: Update Protected Branch +description: Create, approve, and merge a pull request against a protected branch + +inputs: + TARGET_BRANCH: + description: The target branch to update. This becomes the base branch of the pull request. + required: true + SOURCE_BRANCH: + description: If updating main, you must also provide a head branch to update main with. + required: false + default: '' + OS_BOTIFY_TOKEN: + description: GitHub token for OSBotify + required: true + GPG_PASSPHRASE: + description: Passphrase used to decrypt GPG key for OSBotify + required: true + SLACK_WEBHOOK: + description: URL of the slack webhook + required: true + +runs: + using: composite + steps: + - name: Validate target branch + if: ${{ !contains(fromJSON('["main", "staging", "production"]'), github.event.inputs.TARGET_BRANCH) }} + shell: bash + run: | + echo "Target branch must be one of ['main', 'staging', 'production]" + exit 1 + + # If updating main, SOURCE_BRANCH must not be empty + - name: Validate source branch + if: github.event.inputs.TARGET_BRANCH == 'main' && github.event.inputs.SOURCE_BRANCH == '' + shell: bash + run: | + echo "Cannot update main branch without specifying a source branch" + exit 1 + + # If updating staging, the source branch will always be main + # If updating production, the source branch will always be staging + - name: Set source branch + shell: bash + run: | + if [[ ${{ github.event.inputs.TARGET_BRANCH }} == 'staging' ]]; then + echo "SOURCE_BRANCH=main" >> "$GITHUB_ENV" + elif [[ ${{ github.event.inputs.TARGET_BRANCH }} == 'production' ]]; then + echo "SOURCE_BRANCH=staging" >> "$GITHUB_ENV" + else + echo "SOURCE_BRANCH=${{ github.event.inputs.SOURCE_BRANCH }}" >> "$GITHUB_ENV" + fi + + - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main + with: + GPG_PASSPHRASE: ${{ inputs.GPG_PASSPHRASE }} + + - name: Checkout source branch + shell: bash + run: git checkout ${{ env.SOURCE_BRANCH }} + + - name: Set New Version + shell: bash + run: echo "NEW_VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" + + - name: Create temporary branch to resolve conflicts + if: ${{ contains(fromJSON('["staging", "production"]'), github.event.inputs.TARGET_BRANCH) }} + shell: bash + run: | + git checkout ${{ github.event.inputs.TARGET_BRANCH }} + BRANCH_NAME=update-${{ github.event.inputs.TARGET_BRANCH }}-from-${{ env.SOURCE_BRANCH }} + git checkout -b "$BRANCH_NAME" + git merge -Xtheirs ${{ env.SOURCE_BRANCH }} + git push --set-upstream origin "$BRANCH_NAME" + + - name: Create Pull Request + id: createPullRequest + shell: bash + run: | + gh pr create \ + --title "Update version to ${{ env.NEW_VERSION }} on ${{ github.event.inputs.TARGET_BRANCH }}" \ + --body "Update version to ${{ env.NEW_VERSION }}" \ + --label "automerge" \ + --base ${{ github.event.inputs.TARGET_BRANCH }} + sleep 5 + echo "::set-output name=PR_NUMBER::$(gh pr view --json 'number' --jq '.number')" + env: + GITHUB_TOKEN: ${{ inputs.OS_BOTIFY_TOKEN }} + + - name: Check changed files + if: ${{ github.event.inputs.TARGET_BRANCH == 'main' }} + id: changedFiles + # Version: 3.3.0 + uses: umani/changed-files@1d252c611c64289d35243fc37ece7323ea5e93e1 + with: + repo-token: ${{ github.token }} + pr-number: ${{ steps.createPullRequest.outputs.PR_NUMBER }} + + - name: Validate changed files + if: ${{ github.event.inputs.TARGET_BRANCH == 'main' && (steps.changedFiles.outputs.files_updated != 'android/app/build.gradle ios/NewExpensify/Info.plist ios/NewExpensifyTests/Info.plist package-lock.json package.json' || steps.changedFiles.outputs.files_created != '' || steps.changedFiles.outputs.files_deleted != '') }} + shell: bash + run: exit 1 + + - name: Auto-approve the PR + shell: bash + run: gh pr review --approve + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Check if pull request is mergeable + id: isPullRequestMergeable + uses: Expensify/App/.github/actions/javascript/isPullRequestMergeable@main + with: + GITHUB_TOKEN: ${{ github.token }} + PULL_REQUEST_NUMBER: ${{ steps.createPullRequest.outputs.PR_NUMBER }} + + - name: Leave comment if PR is not mergeable + if: ${{ !fromJSON(steps.isPullRequestMergeable.outputs.IS_MERGEABLE) }} + shell: bash + run: | + gh pr comment --body \ + ":bell: @Expensify/mobile-deployers :bell: - The Update Protected Branch workflow has failed because this PR was not mergable. + If you are the deployer this week, please resolve the error and merge this PR to continue the deploy process." + env: + GITHUB_TOKEN: ${{ inputs.OS_BOTIFY_TOKEN }} + + - name: Fail workflow if PR is not mergeable + if: ${{ steps.isPullRequestMergeable.outputs.IS_MERGEABLE == 'false' }} + shell: bash + run: exit 1 + + - name: Auto-merge the PR + shell: bash + run: gh pr merge ${{ steps.createPullRequest.outputs.PR_NUMBER }} --merge --delete-branch + env: + GITHUB_TOKEN: ${{ inputs.OS_BOTIFY_TOKEN }} + + - if: ${{ failure() }} + uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main + with: + SLACK_WEBHOOK: ${{ inputs.SLACK_WEBHOOK }} diff --git a/.github/workflows/createNewVersion.yml b/.github/workflows/createNewVersion.yml index 02522c9d77ad..25da68be78c7 100644 --- a/.github/workflows/createNewVersion.yml +++ b/.github/workflows/createNewVersion.yml @@ -55,11 +55,13 @@ jobs: git push origin ${{ env.VERSION_BRANCH }} - name: Update main branch - uses: Expensify/App/.github/actions/javascript/triggerWorkflowAndWait@main + uses: Expensify/App/.github/actions/composite/updateProtectedBranch@main with: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - WORKFLOW: updateProtectedBranch.yml - INPUTS: '{ "TARGET_BRANCH": "main", "SOURCE_BRANCH": "${{ env.VERSION_BRANCH }}" }' + TARGET_BRANCH: main + SOURCE_BRANCH: ${{ env.VERSION_BRANCH }} + OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - if: ${{ failure() }} uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main diff --git a/.github/workflows/finishReleaseCycle.yml b/.github/workflows/finishReleaseCycle.yml index c5364d16242e..38adc56189ab 100644 --- a/.github/workflows/finishReleaseCycle.yml +++ b/.github/workflows/finishReleaseCycle.yml @@ -62,11 +62,12 @@ jobs: token: ${{ secrets.OS_BOTIFY_TOKEN }} - name: Update production branch - uses: Expensify/App/.github/actions/javascript/triggerWorkflowAndWait@main + uses: Expensify/App/.github/actions/composite/updateProtectedBranch@main with: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - WORKFLOW: updateProtectedBranch.yml - INPUTS: '{ "TARGET_BRANCH": "production" }' + TARGET_BRANCH: production + OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} # Deploy deferred PRs to staging and create a new StagingDeployCash for the next release cycle. createNewStagingDeployCash: @@ -95,11 +96,12 @@ jobs: INPUTS: '{ "SEMVER_LEVEL": "PATCH" }' - name: Update staging branch to trigger staging deploy - uses: Expensify/App/.github/actions/javascript/triggerWorkflowAndWait@main + uses: Expensify/App/.github/actions/composite/updateProtectedBranch@main with: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - WORKFLOW: updateProtectedBranch.yml - INPUTS: '{ "TARGET_BRANCH": "staging" }' + TARGET_BRANCH: staging + OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - name: Pull staging to get the new version run: | diff --git a/.github/workflows/preDeploy.yml b/.github/workflows/preDeploy.yml index e8d133a7a033..114d231c1cd1 100644 --- a/.github/workflows/preDeploy.yml +++ b/.github/workflows/preDeploy.yml @@ -108,11 +108,12 @@ jobs: - name: Update staging branch from main if: ${{ !fromJSON(needs.chooseDeployActions.outputs.isStagingDeployLocked) }} - uses: Expensify/App/.github/actions/javascript/triggerWorkflowAndWait@main + uses: Expensify/App/.github/actions/composite/updateProtectedBranch@main with: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - WORKFLOW: updateProtectedBranch.yml - INPUTS: '{ "TARGET_BRANCH": "staging" }' + TARGET_BRANCH: staging + OS_BOTIFY_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} + GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} - name: Determine if this pull request will be cherry-picked run: echo "DO_CHERRY_PICK=${{ fromJSON(needs.chooseDeployActions.outputs.isStagingDeployLocked) && fromJSON(needs.chooseDeployActions.outputs.shouldCherryPick) }}" >> "$GITHUB_ENV" diff --git a/.github/workflows/updateProtectedBranch.yml b/.github/workflows/updateProtectedBranch.yml deleted file mode 100644 index 1e905d6e2d8b..000000000000 --- a/.github/workflows/updateProtectedBranch.yml +++ /dev/null @@ -1,129 +0,0 @@ -# This is a utility workflow to create, approve, and merge a pull request against a protected branch. -name: Update Protected Branch - -on: - workflow_dispatch: - inputs: - TARGET_BRANCH: - description: The target branch to update. This becomes the base branch of the pull request. - required: true - SOURCE_BRANCH: - description: If updating main, you must also provide a head branch to update main with. - required: false - default: '' - -jobs: - updateBranch: - if: github.actor == 'OSBotify' - runs-on: ubuntu-latest - steps: - - name: Validate target branch - if: ${{ !contains(fromJSON('["main", "staging", "production"]'), github.event.inputs.TARGET_BRANCH) }} - run: | - echo "Target branch must be one of ['main', 'staging', 'production']" - exit 1 - - # If updating main, SOURCE_BRANCH must not be empty - - name: Validate source branch - if: github.event.inputs.TARGET_BRANCH == 'main' && github.event.inputs.SOURCE_BRANCH == '' - run: | - echo "Cannot update main branch without specifying a source branch" - exit 1 - - # If updating staging, the source branch will always be main - # If updating production, the source branch will always be staging - - name: Set source branch - run: | - if [[ ${{ github.event.inputs.TARGET_BRANCH }} == 'staging' ]]; then - echo "SOURCE_BRANCH=main" >> "$GITHUB_ENV" - elif [[ ${{ github.event.inputs.TARGET_BRANCH }} == 'production' ]]; then - echo "SOURCE_BRANCH=staging" >> "$GITHUB_ENV" - else - echo "SOURCE_BRANCH=${{ github.event.inputs.SOURCE_BRANCH }}" >> "$GITHUB_ENV" - fi - - # Version: 2.3.4 - - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f - with: - fetch-depth: 0 - token: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Checkout source branch - run: git checkout ${{ env.SOURCE_BRANCH }} - - - name: Set New Version - run: echo "NEW_VERSION=$(npm run print-version --silent)" >> "$GITHUB_ENV" - - - uses: Expensify/App/.github/actions/composite/setupGitForOSBotify@main - with: - GPG_PASSPHRASE: ${{ secrets.LARGE_SECRET_PASSPHRASE }} - - - name: Create temporary branch to resolve conflicts - if: ${{ contains(fromJSON('["staging", "production"]'), github.event.inputs.TARGET_BRANCH) }} - run: | - git config user.name ${{ github.actor }} - git checkout ${{ github.event.inputs.TARGET_BRANCH }} - git checkout -b update-${{ github.event.inputs.TARGET_BRANCH }}-from-${{ env.SOURCE_BRANCH }} - git merge -Xtheirs ${{ env.SOURCE_BRANCH }} - git push --set-upstream origin update-${{ github.event.inputs.TARGET_BRANCH }}-from-${{ env.SOURCE_BRANCH }} - - - name: Create Pull Request - id: createPullRequest - run: | - gh pr create \ - --title "Update version to ${{ env.NEW_VERSION }} on ${{ github.event.inputs.TARGET_BRANCH }}" \ - --body "Update version to ${{ env.NEW_VERSION }}" \ - --label "automerge" \ - --base ${{ github.event.inputs.TARGET_BRANCH }} - sleep 5 - echo "::set-output name=PR_NUMBER::$(gh pr view --json 'number' --jq '.number')" - env: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Check changed files - if: ${{ github.event.inputs.TARGET_BRANCH == 'main' }} - id: changedFiles - # Version: 3.3.0 - uses: umani/changed-files@1d252c611c64289d35243fc37ece7323ea5e93e1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - pr-number: ${{ steps.createPullRequest.outputs.PR_NUMBER }} - - - name: Validate changed files - if: ${{ github.event.inputs.TARGET_BRANCH == 'main' && (steps.changedFiles.outputs.files_updated != 'android/app/build.gradle ios/NewExpensify/Info.plist ios/NewExpensifyTests/Info.plist package-lock.json package.json' || steps.changedFiles.outputs.files_created != '' || steps.changedFiles.outputs.files_deleted != '') }} - run: exit 1 - - - name: Auto-approve the PR - run: gh pr review --approve - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Check if pull request is mergeable - id: isPullRequestMergeable - uses: Expensify/App/.github/actions/javascript/isPullRequestMergeable@main - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PULL_REQUEST_NUMBER: ${{ steps.createPullRequest.outputs.PR_NUMBER }} - - - name: Leave comment if PR is not mergeable - if: ${{ !fromJSON(steps.isPullRequestMergeable.outputs.IS_MERGEABLE) }} - run: | - gh pr comment --body \ - ":bell: @Expensify/mobile-deployers :bell: - The Update Protected Branch workflow has failed because this PR was not mergable. - If you are the deployer this week, please resolve the error and merge this PR to continue the deploy process." - env: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - - - name: Fail workflow if PR is not mergeable - if: ${{ steps.isPullRequestMergeable.outputs.IS_MERGEABLE == 'false' }} - run: exit 1 - - - name: Auto-merge the PR - run: gh pr merge ${{ steps.createPullRequest.outputs.PR_NUMBER }} --merge --delete-branch - env: - GITHUB_TOKEN: ${{ secrets.OS_BOTIFY_TOKEN }} - - - if: ${{ failure() }} - uses: Expensify/App/.github/actions/composite/announceFailedWorkflowInSlack@main - with: - SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}