Update release-bot.yaml #85
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Cherry-pick to Release Branch (GitHub App ready) | ||
|
Check failure on line 1 in .github/workflows/release-bot.yaml
|
||
| on: | ||
| # Direct usage in this repo: comment "/release to <branch>" on a PR | ||
| issue_comment: | ||
| types: [created] | ||
| # Reusable usage from another workflow: | ||
| workflow_call: | ||
| secrets: | ||
| APP_ID: | ||
| description: "GitHub App ID (string)." | ||
| required: false | ||
| APP_PRIVATE_KEY: | ||
| description: "GitHub App private key (PEM, multiline)." | ||
| required: false | ||
| APP_INSTALLATION_ID: | ||
| description: "GitHub App installation ID (string)." | ||
| required: false | ||
| permissions: | ||
| contents: write | ||
| pull-requests: write | ||
| issues: write | ||
| jobs: | ||
| cherry_pick: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| # 1) Only run when someone comments `/release to <branch>` on a PR | ||
| - name: Check for release command | ||
| id: check_command | ||
| uses: actions/github-script@v6 | ||
| with: | ||
| github-token: ${{ secrets.GITHUB_TOKEN }} | ||
| script: | | ||
| const { issue, comment } = context.payload || {}; | ||
| if (!issue || !issue.pull_request || !comment) { | ||
| core.setOutput('release_valid','false'); | ||
| return; | ||
| } | ||
| const m = (comment.body || '').trim().match(/^\/release to\s+([^\s]+)\s*$/i); | ||
| if (!m) { | ||
| core.setOutput('release_valid','false'); | ||
| return; | ||
| } | ||
| core.setOutput('release_valid','true'); | ||
| core.setOutput('release_branch', m[1]); | ||
| core.setOutput('pr_number', issue.number); | ||
| - name: Skip if not a valid command | ||
| if: steps.check_command.outputs.release_valid == 'false' | ||
| run: | | ||
| echo "Skipping: no '/release to <branch>' command found." | ||
| # 2) Checkout repo with full history; do NOT persist the default token | ||
| - name: Checkout repository | ||
| if: steps.check_command.outputs.release_valid == 'true' | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| persist-credentials: false | ||
| # 3) Optionally authenticate as a GitHub App | ||
| # First try secrets passed via workflow_call (APP_*), then fall back | ||
| # to repo-level secrets (PROBE_*). If none provided, use GITHUB_TOKEN. | ||
| - name: Create GitHub App token (workflow_call secrets with installation id) | ||
| id: app_token_call_id | ||
| if: ${{ secrets.APP_ID != '' && secrets.APP_PRIVATE_KEY != '' && secrets.APP_INSTALLATION_ID != '' }} | ||
| uses: actions/create-github-app-token@v1 | ||
| with: | ||
| app-id: ${{ secrets.APP_ID }} | ||
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | ||
| installation-id: ${{ secrets.APP_INSTALLATION_ID }} | ||
| - name: Create GitHub App token (workflow_call secrets by owner) | ||
| id: app_token_call_owner | ||
| if: ${{ secrets.APP_ID != '' && secrets.APP_PRIVATE_KEY != '' && (secrets.APP_INSTALLATION_ID == '') }} | ||
| uses: actions/create-github-app-token@v1 | ||
| with: | ||
| app-id: ${{ secrets.APP_ID }} | ||
| private-key: ${{ secrets.APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
| - name: Create GitHub App token (repo PROBE_* secrets with installation id) | ||
| id: app_token_repo_id | ||
| if: ${{ secrets.PROBE_APP_ID != '' && secrets.PROBE_APP_PRIVATE_KEY != '' && secrets.PROBE_APP_INSTALLATION_ID != '' }} | ||
| uses: actions/create-github-app-token@v1 | ||
| with: | ||
| app-id: ${{ secrets.PROBE_APP_ID }} | ||
| private-key: ${{ secrets.PROBE_APP_PRIVATE_KEY }} | ||
| installation-id: ${{ secrets.PROBE_APP_INSTALLATION_ID }} | ||
| - name: Create GitHub App token (repo PROBE_* secrets by owner) | ||
| id: app_token_repo_owner | ||
| if: ${{ secrets.PROBE_APP_ID != '' && secrets.PROBE_APP_PRIVATE_KEY != '' && (secrets.PROBE_APP_INSTALLATION_ID == '') }} | ||
| uses: actions/create-github-app-token@v1 | ||
| with: | ||
| app-id: ${{ secrets.PROBE_APP_ID }} | ||
| private-key: ${{ secrets.PROBE_APP_PRIVATE_KEY }} | ||
| owner: ${{ github.repository_owner }} | ||
| - name: Select auth token & prepare git remote | ||
| if: steps.check_command.outputs.release_valid == 'true' | ||
| run: | | ||
| set -euo pipefail | ||
| TOKEN="${{ steps.app_token_call_id.outputs.token }}" | ||
| if [ -z "$TOKEN" ]; then TOKEN="${{ steps.app_token_call_owner.outputs.token }}"; fi | ||
| if [ -z "$TOKEN" ]; then TOKEN="${{ steps.app_token_repo_id.outputs.token }}"; fi | ||
| if [ -z "$TOKEN" ]; then TOKEN="${{ steps.app_token_repo_owner.outputs.token }}"; fi | ||
| if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITHUB_TOKEN }}"; fi | ||
| echo "GITHUB_TOKEN=$TOKEN" >> "$GITHUB_ENV" | ||
| echo "GH_TOKEN=$TOKEN" >> "$GITHUB_ENV" | ||
| # Ensure pushes/pulls use our chosen token (and not checkout extraheader) | ||
| git config --local --unset-all http.https://github.com/.extraheader || true | ||
| git remote set-url origin "https://x-access-token:${TOKEN}@github.com/${{ github.repository }}.git" | ||
| git remote -v | ||
| shell: bash | ||
| - name: Configure git user | ||
| if: steps.check_command.outputs.release_valid == 'true' | ||
| run: | | ||
| git config --global user.email "bot@tyk.io" | ||
| git config --global user.name "Tyk Bot" | ||
| # 4) Find the merge commit created when the PR was merged into its base (e.g., master) | ||
| - name: Find merge commit on base branch | ||
| id: pr_details | ||
| if: steps.check_command.outputs.release_valid == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ env.GH_TOKEN }} | ||
| run: | | ||
| set -euo pipefail | ||
| PR_NUMBER='${{ steps.check_command.outputs.pr_number }}' | ||
| OWNER_REPO='${{ github.repository }}' | ||
| PR_JSON="$(gh api "repos/${OWNER_REPO}/pulls/${PR_NUMBER}")" | ||
| PR_TITLE="$(jq -r '.title' <<<"$PR_JSON")" | ||
| BASE_REF="$(jq -r '.base.ref' <<<"$PR_JSON")" | ||
| MERGED="$(jq -r '.merged' <<<"$PR_JSON")" | ||
| MERGE_SHA="$(jq -r '.merge_commit_sha // empty' <<<"$PR_JSON")" | ||
| if [ "${MERGED}" != "true" ]; then | ||
| echo "PR #${PR_NUMBER} is not merged; cannot use merge commit." >&2 | ||
| exit 1 | ||
| fi | ||
| git fetch origin "${BASE_REF}" --quiet | ||
| if [ -z "${MERGE_SHA}" ] || [ "${MERGE_SHA}" = "null" ]; then | ||
| MERGE_SHA="$(git log "origin/${BASE_REF}" --merges --grep="Merge pull request #${PR_NUMBER}" -n 1 --pretty=format:%H || true)" | ||
| fi | ||
| if [ -z "${MERGE_SHA}" ]; then | ||
| MERGE_SHA="$(git log "origin/${BASE_REF}" --grep="(#${PR_NUMBER})" -n 1 --pretty=format:%H || true)" | ||
| fi | ||
| if [ -z "${MERGE_SHA}" ]; then | ||
| echo "Could not find merge commit for PR #${PR_NUMBER} on ${BASE_REF}. Was it 'rebase and merge'?" >&2 | ||
| exit 1 | ||
| fi | ||
| { | ||
| echo "COMMIT_SHA=${MERGE_SHA}" | ||
| echo "PR_TITLE=${PR_TITLE}" | ||
| echo "BASE_REF=${BASE_REF}" | ||
| } >> "$GITHUB_OUTPUT" | ||
| # 5) Cherry-pick that merge commit onto the requested release branch and open a PR | ||
| - name: Cherry-pick merge commit to target release | ||
| id: cherry_pick | ||
| if: steps.check_command.outputs.release_valid == 'true' | ||
| env: | ||
| GH_TOKEN: ${{ env.GH_TOKEN }} | ||
| GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }} | ||
| GITHUB_REPO: ${{ github.repository }} | ||
| GITHUB_BRANCH: ${{ steps.check_command.outputs.release_branch }} | ||
| GITHUB_CHERRY_PICK_COMMIT: ${{ steps.pr_details.outputs.COMMIT_SHA }} | ||
| PR_TITLE: ${{ steps.pr_details.outputs.PR_TITLE }} | ||
| BASE_REF: ${{ steps.pr_details.outputs.BASE_REF }} | ||
| run: | | ||
| set -euo pipefail | ||
| git fetch origin "${BASE_REF}" --quiet | ||
| git fetch origin "${GITHUB_BRANCH}" --quiet | ||
| git checkout -B "${GITHUB_BRANCH}" "origin/${GITHUB_BRANCH}" | ||
| # Optional JIRA id in branch name | ||
| JIRA_ID="$(echo "${PR_TITLE}" | grep -oE '[A-Z]{1,10}-[0-9]{1,10}' | head -n 1 || true)" | ||
| BRANCH_NAME="merge/${GITHUB_BRANCH}/${GITHUB_CHERRY_PICK_COMMIT}" | ||
| if [ -n "${JIRA_ID}" ]; then | ||
| BRANCH_NAME="${BRANCH_NAME}/${JIRA_ID}" | ||
| fi | ||
| git branch -D "${BRANCH_NAME}" 2>/dev/null || true | ||
| git push origin --delete "${BRANCH_NAME}" 2>/dev/null || true | ||
| git switch -c "${BRANCH_NAME}" | ||
| # Is it a merge commit (has >1 parent)? | ||
| PARENTS_LINE="$(git rev-list --parents -n 1 "${GITHUB_CHERRY_PICK_COMMIT}")" | ||
| WORDS_COUNT="$(wc -w <<<"${PARENTS_LINE}")" | ||
| MERGE_FAILED=0 | ||
| if [ "${WORDS_COUNT}" -gt 2 ]; then | ||
| git cherry-pick -x -m 1 "${GITHUB_CHERRY_PICK_COMMIT}" || MERGE_FAILED=$? | ||
| else | ||
| git cherry-pick -x "${GITHUB_CHERRY_PICK_COMMIT}" || MERGE_FAILED=$? | ||
| fi | ||
| if [ "${MERGE_FAILED}" -ne 0 ]; then | ||
| git add -A || true | ||
| git -c core.editor=true cherry-pick --continue --no-edit || true | ||
| fi | ||
| git push origin "${BRANCH_NAME}" --force | ||
| MESSAGE="$(git show -s --format=%B "${GITHUB_CHERRY_PICK_COMMIT}")" | ||
| TITLE="$(echo "${MESSAGE}" | head -n 1)" | ||
| # Reuse PR if it already exists | ||
| PR_URL="$(gh pr view --repo "${GITHUB_REPO}" --head "${BRANCH_NAME}" --json url -q .url 2>/dev/null || true)" | ||
| if [ -z "${PR_URL}" ]; then | ||
| if [ "${MERGE_FAILED}" -ne 0 ]; then | ||
| PR_URL="$(gh pr create --draft \ | ||
| --repo "${GITHUB_REPO}" \ | ||
| --base "${GITHUB_BRANCH}" \ | ||
| --head "${BRANCH_NAME}" \ | ||
| --title "Merging to ${GITHUB_BRANCH}: ${TITLE}" \ | ||
| --body "${MESSAGE}")" | ||
| else | ||
| PR_URL="$(gh pr create \ | ||
| --repo "${GITHUB_REPO}" \ | ||
| --base "${GITHUB_BRANCH}" \ | ||
| --head "${BRANCH_NAME}" \ | ||
| --title "Merging to ${GITHUB_BRANCH}: ${TITLE}" \ | ||
| --body "${MESSAGE}")" | ||
| fi | ||
| fi | ||
| { | ||
| echo "PR_URL=${PR_URL}" | ||
| echo "MERGE_FAILED=${MERGE_FAILED}" | ||
| echo "BRANCH_NAME=${BRANCH_NAME}" | ||
| } >> "$GITHUB_OUTPUT" | ||
| # 6) Comment back on the original PR with the result (uses selected token) | ||
| - name: Comment back on original PR | ||
| if: always() && steps.check_command.outputs.release_valid == 'true' | ||
| uses: actions/github-script@v6 | ||
| with: | ||
| github-token: ${{ env.GITHUB_TOKEN }} | ||
| script: | | ||
| const prUrl = '${{ steps.cherry_pick.outputs.PR_URL }}'; | ||
| const mergeFailed = '${{ steps.cherry_pick.outputs.MERGE_FAILED }}' === '1'; | ||
| let body; | ||
| if ('${{ job.status }}' === 'success') { | ||
| body = mergeFailed | ||
| ? `⚠️ Cherry-pick completed with conflicts. A draft PR was created: ${prUrl}` | ||
| : `✅ Cherry-pick successful. A PR was created: ${prUrl}`; | ||
| } else { | ||
| body = '❌ Cherry-pick failed. Please check the workflow logs.'; | ||
| } | ||
| github.rest.issues.createComment({ | ||
| issue_number: ${{ steps.check_command.outputs.pr_number }}, | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| body | ||
| }); | ||