Update release-bot.yaml #83
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 (with GitHub App auth) | ||
| on: | ||
| issue_comment: | ||
| types: [created] | ||
| workflow_call: | ||
| inputs: | ||
| app-id: | ||
| description: "GitHub App ID (optional). If set with private-key, the workflow will auth as the App." | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| private-key: | ||
| description: "GitHub App private key (PEM, multiline)." | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| installation-id: | ||
| description: "GitHub App installation ID (optional). If omitted, installation for the repo owner is used." | ||
| required: false | ||
| type: string | ||
| default: "" | ||
| 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) Optional: Authenticate as a GitHub App (token with the app identity) | ||
| - name: Create GitHub App token (by installation id) | ||
| id: app_token_id | ||
| if: ${{ github.event_name == 'workflow_call' && inputs.app-id != '' && inputs.private-key != '' && inputs.installation-id != '' }} | ||
| uses: actions/create-github-app-token@v1 | ||
| with: | ||
| app-id: ${{ inputs['app-id'] }} | ||
| private-key: ${{ inputs['private-key'] }} | ||
| installation-id: ${{ inputs['installation-id'] }} | ||
| - name: Create GitHub App token (by owner) | ||
| id: app_token_owner | ||
| if: ${{ github.event_name == 'workflow_call' && inputs.app-id != '' && inputs.private-key != '' && inputs.installation-id == '' }} | ||
| uses: actions/create-github-app-token@v1 | ||
| with: | ||
| app-id: ${{ inputs['app-id'] }} | ||
| private-key: ${{ inputs['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_id.outputs.token }}" | ||
| if [ -z "$TOKEN" ]; then | ||
| TOKEN="${{ steps.app_token_owner.outputs.token }}" | ||
| fi | ||
| if [ -z "$TOKEN" ]; then | ||
| TOKEN="${{ secrets.GITHUB_TOKEN }}" | ||
| fi | ||
| # Export for following steps (gh CLI prefers GITHUB_TOKEN/GH_TOKEN) | ||
| echo "GITHUB_TOKEN=$TOKEN" >> "$GITHUB_ENV" | ||
| echo "GH_TOKEN=$TOKEN" >> "$GITHUB_ENV" | ||
| # Ensure pushes/pulls use our chosen token (avoid any extraheader from checkout) | ||
| 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) Locate the *merge* commit that landed on the base branch when the PR was merged | ||
| - 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 | ||
| # Ensure base branch is present locally | ||
| git fetch origin "${BASE_REF}" --quiet | ||
| # Fallbacks for different merge strategies/messages | ||
| 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 | ||
| # Squash merge default message ends with "(#<PR_NUMBER>)" | ||
| 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 | ||
| # Ensure both base and target branches are present/up-to-date | ||
| git fetch origin "${BASE_REF}" --quiet | ||
| git fetch origin "${GITHUB_BRANCH}" --quiet | ||
| git checkout -B "${GITHUB_BRANCH}" "origin/${GITHUB_BRANCH}" | ||
| # Create a consistent branch name (optionally include JIRA key) | ||
| 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 | ||
| # Remove any stale branches locally/remotely | ||
| git branch -D "${BRANCH_NAME}" 2>/dev/null || true | ||
| git push origin --delete "${BRANCH_NAME}" 2>/dev/null || true | ||
| # Create working branch from the release branch | ||
| git switch -c "${BRANCH_NAME}" | ||
| # Determine if selected commit is a merge (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 | ||
| # Merge commit -> use -m 1 | ||
| git cherry-pick -x -m 1 "${GITHUB_CHERRY_PICK_COMMIT}" || MERGE_FAILED=$? | ||
| else | ||
| # Normal / squash commit | ||
| git cherry-pick -x "${GITHUB_CHERRY_PICK_COMMIT}" || MERGE_FAILED=$? | ||
| fi | ||
| # If conflicts happened, attempt to continue so we still open a (draft) PR | ||
| 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 | ||
| # Prepare PR metadata from the cherry-picked commit | ||
| MESSAGE="$(git show -s --format=%B "${GITHUB_CHERRY_PICK_COMMIT}")" | ||
| TITLE="$(echo "${MESSAGE}" | head -n 1)" | ||
| # Reuse existing PR for this head if present; else create one (draft on conflicts) | ||
| 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 (use App token if we have it) | ||
| - 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 | ||
| }); | ||