-
Notifications
You must be signed in to change notification settings - Fork 2
feat: add backport ci #285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7d5372f
832ba8b
c8c87bb
235683b
4ddde82
34bcc4e
e431736
055adc1
be7f08d
54327c5
18230b4
2255f54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,214 @@ | ||||||
| #!/usr/bin/env bash | ||||||
|
|
||||||
| # Safe backport helper. Creates a PR in the current repository that cherry-picks a commit from upstream. | ||||||
|
|
||||||
| set -euo pipefail | ||||||
|
|
||||||
| # ANSI colors for readability | ||||||
| RED='\033[0;31m' | ||||||
| GREEN='\033[0;32m' | ||||||
| YELLOW='\033[1;33m' | ||||||
| NC='\033[0m' | ||||||
|
|
||||||
| die() { | ||||||
| echo -e "${RED}$1${NC}" >&2 | ||||||
| exit "${2:-1}" | ||||||
| } | ||||||
|
|
||||||
| require_env() { | ||||||
| local name="$1" | ||||||
| local value="${!name:-}" | ||||||
| if [[ -z "$value" ]]; then | ||||||
| die "Environment variable $name is required" | ||||||
| fi | ||||||
| } | ||||||
|
|
||||||
| if [[ $# -ne 1 ]]; then | ||||||
| die "Usage: $0 <commit-sha>" | ||||||
| fi | ||||||
|
|
||||||
| COMMIT_SHA="$1" | ||||||
|
|
||||||
| if ! [[ "$COMMIT_SHA" =~ ^[0-9a-f]{40}$ ]]; then | ||||||
| die "Invalid commit SHA: $COMMIT_SHA" | ||||||
| fi | ||||||
|
|
||||||
| SOURCE_REPO="${SOURCE_REPO:-apache/apisix-ingress-controller}" | ||||||
| TARGET_BRANCH="${TARGET_BRANCH:-master}" | ||||||
| GITHUB_REPO="${GITHUB_REPOSITORY:-}" | ||||||
|
|
||||||
| require_env SOURCE_REPO | ||||||
| require_env TARGET_BRANCH | ||||||
| require_env GH_TOKEN | ||||||
|
|
||||||
| [[ "$SOURCE_REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || die "Invalid SOURCE_REPO: $SOURCE_REPO" | ||||||
| [[ "$TARGET_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Invalid TARGET_BRANCH: $TARGET_BRANCH" | ||||||
|
|
||||||
| if [[ -z "$GITHUB_REPO" ]]; then | ||||||
| GITHUB_REPO="$(gh repo view --json nameWithOwner -q '.nameWithOwner')" | ||||||
| fi | ||||||
|
|
||||||
| [[ "$GITHUB_REPO" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]] || die "Invalid target repo: $GITHUB_REPO" | ||||||
|
|
||||||
| echo -e "${YELLOW}Backporting commit ${COMMIT_SHA} from ${SOURCE_REPO}${NC}" | ||||||
|
|
||||||
| ORIGINAL_REF="" | ||||||
| ORIGINAL_COMMIT="" | ||||||
| if ORIGINAL_REF=$(git symbolic-ref --quiet HEAD 2>/dev/null); then | ||||||
| ORIGINAL_REF=${ORIGINAL_REF#refs/heads/} | ||||||
| else | ||||||
| ORIGINAL_COMMIT=$(git rev-parse HEAD) | ||||||
| fi | ||||||
|
|
||||||
| restore_original_ref() { | ||||||
| if [[ -n "$ORIGINAL_REF" ]]; then | ||||||
| git checkout "$ORIGINAL_REF" >/dev/null 2>&1 || true | ||||||
| elif [[ -n "$ORIGINAL_COMMIT" ]]; then | ||||||
| git checkout --detach "$ORIGINAL_COMMIT" >/dev/null 2>&1 || true | ||||||
| fi | ||||||
| } | ||||||
|
|
||||||
| if ! git cat-file -e "${COMMIT_SHA}^{commit}" 2>/dev/null; then | ||||||
| die "Commit $COMMIT_SHA is not available locally - fetch upstream before running this script" | ||||||
| fi | ||||||
|
|
||||||
| COMMIT_TITLE="$(git log --format='%s' -n 1 "$COMMIT_SHA")" | ||||||
| COMMIT_AUTHOR="$(git log --format='%an <%ae>' -n 1 "$COMMIT_SHA")" | ||||||
| COMMIT_URL="https://github.com/${SOURCE_REPO}/commit/${COMMIT_SHA}" | ||||||
| SHORT_SHA="${COMMIT_SHA:0:7}" | ||||||
| if [[ -z "$COMMIT_TITLE" ]]; then | ||||||
| COMMIT_TITLE="Backport ${SHORT_SHA} from ${SOURCE_REPO}" | ||||||
| fi | ||||||
| TITLE_SUFFIX=" (${SHORT_SHA})" | ||||||
| if [[ "$COMMIT_TITLE" == *"$SHORT_SHA"* ]]; then | ||||||
| TITLE_SUFFIX="" | ||||||
| fi | ||||||
| BRANCH_NAME="backport/${SHORT_SHA}-to-${TARGET_BRANCH}" | ||||||
|
|
||||||
| [[ "$BRANCH_NAME" =~ ^[A-Za-z0-9._/-]+$ ]] || die "Generated branch name is unsafe: $BRANCH_NAME" | ||||||
|
|
||||||
| echo -e "${YELLOW}Generated branch name: ${BRANCH_NAME}${NC}" | ||||||
|
|
||||||
| EXISTING_PR="$(gh pr list --state all --head "$BRANCH_NAME" --json url --jq '.[0].url' 2>/dev/null || true)" | ||||||
| if [[ -n "$EXISTING_PR" ]]; then | ||||||
| echo -e "${GREEN}PR already exists: ${EXISTING_PR}. Skipping duplicate.${NC}" | ||||||
| exit 0 | ||||||
| fi | ||||||
|
|
||||||
| git fetch origin "$TARGET_BRANCH" --quiet | ||||||
| git checkout -B "$TARGET_BRANCH" "origin/$TARGET_BRANCH" | ||||||
|
|
||||||
| if git rev-parse --verify "$BRANCH_NAME" >/dev/null 2>&1; then | ||||||
| git checkout "$BRANCH_NAME" | ||||||
| git reset --hard "origin/$TARGET_BRANCH" | ||||||
| else | ||||||
| git checkout -b "$BRANCH_NAME" | ||||||
| fi | ||||||
|
|
||||||
| PARENT_COUNT="$(git rev-list --parents -n 1 "$COMMIT_SHA" | awk '{print NF-1}')" | ||||||
| HAS_CONFLICTS=false | ||||||
|
|
||||||
| echo -e "${YELLOW}Running cherry-pick...${NC}" | ||||||
|
|
||||||
| cherry_pick() { | ||||||
| if [[ "$PARENT_COUNT" -gt 1 ]]; then | ||||||
| git cherry-pick -x -m 1 "$COMMIT_SHA" | ||||||
| else | ||||||
| git cherry-pick -x "$COMMIT_SHA" | ||||||
| fi | ||||||
| } | ||||||
|
Comment on lines
+113
to
+119
|
||||||
|
|
||||||
| if ! cherry_pick; then | ||||||
| echo -e "${YELLOW}Cherry-pick reported conflicts; leaving markers for manual resolution.${NC}" | ||||||
| HAS_CONFLICTS=true | ||||||
| git add . | ||||||
|
||||||
| git add . | |
| git add -u |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| name: Auto Backport from Upstream | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "*/30 * * * *" | ||
| workflow_dispatch: | ||
AlinsRan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| inputs: | ||
| force_sync: | ||
| description: "Force sync all recent commits (ignores watermark)" | ||
| required: false | ||
| default: "false" | ||
|
|
||
| concurrency: | ||
| group: auto-backport | ||
| cancel-in-progress: false | ||
|
|
||
| env: | ||
| SOURCE_REPO: apache/apisix-ingress-controller | ||
| SOURCE_BRANCH: master | ||
| TARGET_BRANCH: ${{ github.event.repository.default_branch || 'master' }} | ||
| MAX_COMMITS_PER_RUN: 5 | ||
|
|
||
| jobs: | ||
| auto-backport: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 20 | ||
| permissions: | ||
| actions: write | ||
| contents: write | ||
| pull-requests: write | ||
| issues: write | ||
| repository-projects: write | ||
| env: | ||
| GH_TOKEN: ${{ secrets.BACKPORT_PAT }} | ||
| GITHUB_REPOSITORY: ${{ github.repository }} | ||
| FORCE_SYNC: ${{ github.event.inputs.force_sync || 'false' }} | ||
|
|
||
| steps: | ||
| - name: Checkout target repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ secrets.BACKPORT_PAT }} | ||
|
|
||
| - name: Show run configuration | ||
| run: | | ||
| echo "Force sync: $FORCE_SYNC" | ||
| echo "Source repo: $SOURCE_REPO" | ||
| echo "Source branch: $SOURCE_BRANCH" | ||
| echo "Target branch: $TARGET_BRANCH" | ||
|
|
||
| - name: Configure git identity | ||
| run: | | ||
| git config --global user.name "backport-bot[bot]" | ||
| git config --global user.email "backport-bot[bot]@users.noreply.github.com" | ||
|
|
||
| - name: Add upstream remote | ||
| run: | | ||
| git remote add upstream "https://github.com/${SOURCE_REPO}.git" 2>/dev/null || true | ||
| git remote set-url upstream "https://github.com/${SOURCE_REPO}.git" | ||
|
|
||
| - name: Fetch upstream branch | ||
| run: | | ||
| git fetch --prune --no-tags upstream "${SOURCE_BRANCH}" | ||
|
|
||
| - name: Read last processed commit watermark | ||
| id: watermark | ||
| run: | | ||
| LAST_SHA=$(gh variable get LAST_BACKPORT_SHA -R "${GITHUB_REPOSITORY}" --json value --jq '.value' 2>/dev/null || echo "") | ||
| if [[ -z "$LAST_SHA" || "$FORCE_SYNC" == "true" ]]; then | ||
| LAST_SHA=$(git log "upstream/${SOURCE_BRANCH}" --since="7 days ago" --format="%H" | tail -n 1) | ||
| fi | ||
| echo "last_sha=${LAST_SHA}" >> "$GITHUB_OUTPUT" | ||
| echo "Last processed SHA: ${LAST_SHA:-<none>}" | ||
|
|
||
| - name: Collect new commits | ||
| id: collect_commits | ||
| run: | | ||
| LAST_SHA="${{ steps.watermark.outputs.last_sha }}" | ||
| if [[ -n "$LAST_SHA" ]]; then | ||
| COMMITS=$(git log "upstream/${SOURCE_BRANCH}" ^"$LAST_SHA" --format="%H" --reverse | head -"${MAX_COMMITS_PER_RUN}") | ||
| else | ||
| COMMITS=$(git log "upstream/${SOURCE_BRANCH}" -1 --format="%H") | ||
| fi | ||
| { | ||
| echo "commits<<EOF" | ||
| printf '%s\n' "$COMMITS" | ||
| echo "EOF" | ||
| } >> "$GITHUB_OUTPUT" | ||
| if [[ -z "$COMMITS" ]]; then | ||
| COUNT=0 | ||
| else | ||
| COUNT=$(printf '%s\n' "$COMMITS" | grep -c '[0-9a-f]') | ||
| fi | ||
| echo "count=${COUNT}" >> "$GITHUB_OUTPUT" | ||
| echo "Commits to process: ${COUNT}" | ||
|
|
||
| - name: Ensure labels exist | ||
| run: | | ||
| gh label create backport --color EDEDED --description "Automated backport" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true | ||
| gh label create automated --color EDEDED --description "Created by automation" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true | ||
| gh label create backport-failed --color D73A4A --description "Backport failed" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true | ||
| gh label create needs-manual-action --color FBCA04 --description "Manual intervention required" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true | ||
| gh label create conflicts --color D93F0B --description "Contains merge conflicts" -R "${GITHUB_REPOSITORY}" 2>/dev/null || true | ||
|
|
||
| - name: Process commits | ||
| if: steps.collect_commits.outputs.count != '0' | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| chmod +x .github/scripts/backport-commit.sh | ||
| SUCCESS=0 | ||
| FAILURE=0 | ||
| LAST_PROCESSED="" | ||
| while IFS= read -r COMMIT; do | ||
| [[ -z "$COMMIT" ]] && continue | ||
| if .github/scripts/backport-commit.sh "$COMMIT"; then | ||
| SUCCESS=$((SUCCESS + 1)) | ||
| LAST_PROCESSED="$COMMIT" | ||
| else | ||
| echo "Commit ${COMMIT} failed to backport" | ||
| FAILURE=$((FAILURE + 1)) | ||
| fi | ||
| done <<< "${{ steps.collect_commits.outputs.commits }}" | ||
| echo "SUCCESS_COUNT=$SUCCESS" >> "$GITHUB_ENV" | ||
| echo "FAILURE_COUNT=$FAILURE" >> "$GITHUB_ENV" | ||
| echo "LAST_PROCESSED_SHA=$LAST_PROCESSED" >> "$GITHUB_ENV" | ||
|
|
||
| - name: Update watermark | ||
| if: env.LAST_PROCESSED_SHA != '' | ||
| run: | | ||
| if [[ "${FAILURE_COUNT:-0}" == "0" ]]; then | ||
| gh variable set LAST_BACKPORT_SHA -b "${LAST_PROCESSED_SHA}" -R "${GITHUB_REPOSITORY}" | ||
| else | ||
| echo "Failures detected; watermark will not be updated." | ||
| fi | ||
|
|
||
| - name: Summary | ||
| run: | | ||
| echo "Successful cherry-picks: ${SUCCESS_COUNT:-0}" | ||
| echo "Failed cherry-picks: ${FAILURE_COUNT:-0}" | ||
| echo "Last processed SHA: ${LAST_PROCESSED_SHA:-none}" | ||
| { | ||
| echo "# Backport Summary" | ||
| echo | ||
| echo "- Successful: ${SUCCESS_COUNT:-0}" | ||
| echo "- Failed: ${FAILURE_COUNT:-0}" | ||
| echo "- Last processed: ${LAST_PROCESSED_SHA:-none}" | ||
| echo "- Force sync: ${FORCE_SYNC}" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regex only accepts lowercase hex characters but Git commit SHAs can contain uppercase letters A-F. The regex should be ^[0-9a-fA-F]{40}$ to accept both cases.