Skip to content

Update release-bot.yaml #86

Update release-bot.yaml

Update release-bot.yaml #86

Workflow file for this run

name: Cherry-pick to Release Branch (light checkout + GitHub App, resilient)
on:
# Direct usage in this repo: comment "/release to <branch>" on a PR
issue_comment:
types: [created]
# Reusable usage from another workflow / repo
workflow_call:
secrets:
APP_ID:
description: "GitHub App ID"
required: false
APP_PRIVATE_KEY:
description: "GitHub App private key (PEM)"
required: false
# Kept for compatibility, but we do not pass it to the action (avoids warnings)
APP_INSTALLATION_ID:
description: "GitHub App installation ID (optional, unused)"
required: false
permissions:
contents: write
pull-requests: write
issues: write
jobs:
cherry_pick:
runs-on: ubuntu-latest
env:
# Depths to try (in order) when we need to scan the base branch to find a squash "merge"
BASE_FETCH_DEPTHS: "100 500 2000"
# Depth for the target release branch
TARGET_FETCH_DEPTH: "50"
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."

Check failure on line 61 in .github/workflows/release-bot.yaml

View workflow run for this annotation

GitHub Actions / .github/workflows/release-bot.yaml

Invalid workflow file

You have an error in your yaml syntax on line 61
# 2) Minimal checkout (shallow + partial clone)
- name: Checkout repository (shallow, partial)
if: steps.check_command.outputs.release_valid == 'true'
uses: actions/checkout@v4
with:
fetch-depth: 1
filter: blob:none
persist-credentials: false
# 3) Resolve GitHub App credentials (supports workflow_call APP_* and repo PROBE_APP_*)
- name: Resolve GitHub App credentials
id: resolve_app
if: steps.check_command.outputs.release_valid == 'true'
env:
CALL_APP_ID: ${{ secrets.APP_ID }}
CALL_APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
CALL_APP_INSTALLATION_ID: ${{ secrets.APP_INSTALLATION_ID }}
PROBE_APP_ID: ${{ secrets.PROBE_APP_ID }}
PROBE_APP_PRIVATE_KEY: ${{ secrets.PROBE_APP_PRIVATE_KEY }}
PROBE_APP_INSTALLATION_ID: ${{ secrets.PROBE_APP_INSTALLATION_ID }}
run: |
set -euo pipefail
APP_ID="${CALL_APP_ID:-}"
APP_PRIVATE_KEY="${CALL_APP_PRIVATE_KEY:-}"
# Fallbacks for direct runs in this repo
if [ -z "${APP_ID}" ] && [ -n "${PROBE_APP_ID:-}" ]; then APP_ID="${PROBE_APP_ID}"; fi
if [ -z "${APP_PRIVATE_KEY}" ] && [ -n "${PROBE_APP_PRIVATE_KEY:-}" ]; then APP_PRIVATE_KEY="${PROBE_APP_PRIVATE_KEY}"; fi
HAS_APP=false
if [ -n "${APP_ID}" ] && [ -n "${APP_PRIVATE_KEY}" ]; then HAS_APP=true; fi
echo "HAS_APP=${HAS_APP}" >> "$GITHUB_ENV"
if [ "${HAS_APP}" = "true" ]; then
{
echo "APP_ID=${APP_ID}"
echo "APP_PRIVATE_KEY<<EOF"
echo "${APP_PRIVATE_KEY}"
echo "EOF"
} >> "$GITHUB_ENV"
fi
# Create GitHub App token (owner-based; avoids 'installation-id' warning)
- name: Create GitHub App token
id: app_token
if: env.HAS_APP == 'true'
uses: actions/create-github-app-token@v1
with:
app-id: ${{ env.APP_ID }}
private-key: ${{ env.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.outputs.token }}"
if [ -z "$TOKEN" ]; then TOKEN="${{ secrets.GITHUB_TOKEN }}"; fi
echo "GITHUB_TOKEN=$TOKEN" >> "$GITHUB_ENV"
echo "GH_TOKEN=$TOKEN" >> "$GITHUB_ENV"
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
- 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"
git config --global advice.submoduleMergeConflict false
# 4) Find the merge (or squash) commit that landed on the base branch, using REST only
- name: Find merge commit on base branch (shallow fetch)
id: pr_details
if: steps.check_command.outputs.release_valid == 'true'
env:
GH_TOKEN: ${{ env.GH_TOKEN }}
BASE_FETCH_DEPTHS: ${{ env.BASE_FETCH_DEPTHS }}
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
if [ -n "${MERGE_SHA}" ] && [ "${MERGE_SHA}" != "null" ]; then
for D in 50 200 1000; do
git fetch --no-tags --filter=blob:none --depth="$D" origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
if git cat-file -e "${MERGE_SHA}^{commit}" 2>/dev/null; then
break
fi
done
else
MERGE_SHA=""
for D in ${BASE_FETCH_DEPTHS}; do
git fetch --no-tags --filter=blob:none --depth="$D" origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}"
MERGE_SHA="$(git log "origin/${BASE_REF}" --grep="(#${PR_NUMBER})" -n 1 --pretty=format:%H || true)"
if [ -n "${MERGE_SHA}" ]; then break; fi
done
if [ -z "${MERGE_SHA}" ]; then
echo "Could not locate the squash commit for PR #${PR_NUMBER} on ${BASE_REF} within depth(s): ${BASE_FETCH_DEPTHS}" >&2
exit 1
fi
fi
{
echo "COMMIT_SHA=${MERGE_SHA}"
echo "PR_TITLE=${PR_TITLE}"
echo "BASE_REF=${BASE_REF}"
echo "PR_NUMBER=${PR_NUMBER}"
} >> "$GITHUB_OUTPUT"
# 5) Cherry-pick to the target branch, with robust conflict handling + guaranteed PR
- name: Cherry-pick merge commit to target release (shallow + resilient)
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 }}
TARGET_FETCH_DEPTH: ${{ env.TARGET_FETCH_DEPTH }}
run: |
set -euo pipefail
# Shallow fetch only target & ensure base exists
git fetch --no-tags --filter=blob:none --depth="${TARGET_FETCH_DEPTH}" origin "+refs/heads/${GITHUB_BRANCH}:refs/remotes/origin/${GITHUB_BRANCH}" || {
echo "Target branch '${GITHUB_BRANCH}' not found on remote." >&2
exit 1
}
git fetch --no-tags --filter=blob:none --depth=10 origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" || true
# Ensure we have the commit object
git fetch --no-tags --filter=blob:none origin "${GITHUB_CHERRY_PICK_COMMIT}" || true
# Work on the target branch (shallow)
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}"
[ -n "${JIRA_ID}" ] && BRANCH_NAME="${BRANCH_NAME}/${JIRA_ID}"
# Clean 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 target
git switch -c "${BRANCH_NAME}"
# If the commit touches submodules and .gitmodules exists, shallow-init submodules
if [ -f .gitmodules ]; then
SUB_PATHS="$(git config -f .gitmodules --name-only --get-regexp '^submodule\..*\.path' | sed -E 's/^submodule\.[^.]+\.path=//')"
NEED_SUBS=0
for P in $SUB_PATHS; do
if git diff-tree --no-commit-id --name-only -r "${GITHUB_CHERRY_PICK_COMMIT}" | grep -Fx "$P" >/dev/null 2>&1; then
NEED_SUBS=1; break
fi
done
if [ "$NEED_SUBS" -eq 1 ]; then
git submodule sync --recursive
git submodule update --init --depth=1 --recursive --recommend-shallow
fi
fi
# 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
# Try to apply; if conflicts arise, we won't force-continue here
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
CONFLICT_COUNT=0
CONFLICT_LIST=""
if [ "${MERGE_FAILED}" -ne 0 ]; then
# Gather a short conflicts summary BEFORE aborting
CONFLICT_COUNT="$(git ls-files -u | wc -l || true)"
CONFLICT_LIST="$(git diff --name-only --diff-filter=U | head -n 40 | sed 's/^/ - /' || true)"
# Abort to get the branch back to a clean state
git cherry-pick --abort || true
fi
# If branch has no commits compared to target (e.g., due to abort), add an empty commit
AHEAD_COUNT="$(git rev-list --count "origin/${GITHUB_BRANCH}..HEAD" || echo 0)"
if [ "${AHEAD_COUNT}" -eq 0 ]; then
git commit --allow-empty -m "chore: prepare PR for cherry-pick ${GITHUB_CHERRY_PICK_COMMIT} → ${GITHUB_BRANCH}
Cherry-pick failed with conflicts and requires manual resolution.
This empty commit exists to allow opening a draft PR.
"
fi
# Push branch
git push origin "${BRANCH_NAME}" --force
# Compose title/body
MESSAGE="$(git show -s --format=%B "${GITHUB_CHERRY_PICK_COMMIT}" 2>/dev/null || echo "")"
[ -z "${MESSAGE}" ] && MESSAGE="Cherry-pick ${GITHUB_CHERRY_PICK_COMMIT} to ${GITHUB_BRANCH}"
TITLE="$(echo "${MESSAGE}" | head -n 1)"
if [ "${MERGE_FAILED}" -ne 0 ]; then
BODY="Cherry-pick of \`${GITHUB_CHERRY_PICK_COMMIT}\` from \`${BASE_REF}\` to \`${GITHUB_BRANCH}\` requires manual resolution.
**Conflicts detected:** ${CONFLICT_COUNT}
${CONFLICT_LIST}
Tips:
- Check out this branch locally and run: \`git cherry-pick -x ${GITHUB_CHERRY_PICK_COMMIT}\`
- Resolve conflicts (including submodules if any), then push back to this branch.
Original commit: https://github.com/${GITHUB_REPO}/commit/${GITHUB_CHERRY_PICK_COMMIT}
"
else
BODY="${MESSAGE}"
fi
# Create PR (draft if 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 "${BODY}")"
else
PR_URL="$(gh pr create \
--repo "${GITHUB_REPO}" \
--base "${GITHUB_BRANCH}" \
--head "${BRANCH_NAME}" \
--title "Merging to ${GITHUB_BRANCH}: ${TITLE}" \
--body "${BODY}")"
fi
fi
# Optionally label the PR if conflicts
if [ "${MERGE_FAILED}" -ne 0 ]; then
gh label create "needs-manual-cherry-pick" --color FF8700 --description "Cherry-pick has conflicts" --repo "${GITHUB_REPO}" 2>/dev/null || true
gh pr edit "${PR_URL}" --add-label "needs-manual-cherry-pick" --repo "${GITHUB_REPO}" || true
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 encountered 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
});