diff --git a/.github/actions/cherry-pick/action.yml b/.github/actions/cherry-pick/action.yml new file mode 100644 index 0000000000..fd57944342 --- /dev/null +++ b/.github/actions/cherry-pick/action.yml @@ -0,0 +1,206 @@ +name: "Cherry-picker" +description: "Cherry-pick PRs based on their labels" + +inputs: + token: + description: "GitHub Token" + required: true + +runs: + using: "composite" + steps: + - name: Check if workflow should run + id: should_run + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.token }} + run: | + # Case 1: PR was just merged (closed + merged = true) + if [ "${{ github.event.action }}" = "closed" ] && [ "${{ github.event.pull_request.merged }}" = "true" ]; then + echo "should_run=true" >> $GITHUB_OUTPUT + echo "reason=pr_merged" >> $GITHUB_OUTPUT + exit 0 + fi + + # Case 2: Label was added + if [ "${{ github.event.action }}" = "labeled" ]; then + LABEL_NAME="${{ github.event.label.name }}" + # Check if it's a backport label + if [[ "$LABEL_NAME" =~ ^backport/(.+)$ ]]; then + # Check if PR is already merged + if [ "${{ github.event.pull_request.merged }}" = "true" ]; then + echo "should_run=true" >> $GITHUB_OUTPUT + echo "reason=label_added_to_merged_pr" >> $GITHUB_OUTPUT + exit 0 + else + echo "should_run=false" >> $GITHUB_OUTPUT + echo "reason=label_added_to_open_pr" >> $GITHUB_OUTPUT + echo "Backport label added to open PR. Will run after PR is merged." + exit 0 + fi + else + echo "should_run=false" >> $GITHUB_OUTPUT + echo "reason=non_backport_label" >> $GITHUB_OUTPUT + exit 0 + fi + fi + + echo "should_run=false" >> $GITHUB_OUTPUT + echo "reason=unknown" >> $GITHUB_OUTPUT + - name: Configure Git + if: steps.should_run.outputs.should_run == 'true' + shell: bash + run: | + git config --global user.name "authentik-automation[bot]" + git config --global user.email "135050075+authentik-automation[bot]@users.noreply.github.com" + - name: Extract backport labels and cherry-pick + if: steps.should_run.outputs.should_run == 'true' + shell: bash + env: + GITHUB_TOKEN: ${{ inputs.token }} + run: | + # Get PR number and commit SHA + PR_NUMBER="${{ github.event.pull_request.number }}" + COMMIT_SHA="${{ github.event.pull_request.merge_commit_sha }}" + PR_TITLE="${{ github.event.pull_request.title }}" + PR_AUTHOR="${{ github.event.pull_request.user.login }}" + + echo "Processing PR #$PR_NUMBER (reason: ${{ steps.should_run.outputs.reason }})" + + # Determine which labels to process + if [ "${{ steps.should_run.outputs.reason }}" = "label_added_to_merged_pr" ]; then + # Only process the specific label that was just added + LABEL_NAME="${{ github.event.label.name }}" + if [[ "$LABEL_NAME" =~ ^backport/(.+)$ ]]; then + LABELS="$LABEL_NAME" + else + echo "Label $LABEL_NAME does not match backport pattern" + exit 0 + fi + else + # PR was just merged, process all backport labels + LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name' | grep '^backport/' || true) + fi + + if [ -z "$LABELS" ]; then + echo "No backport labels found" + exit 0 + fi + + echo "Found backport labels: $LABELS" + + # Process each backport label + echo "$LABELS" | while read -r label; do + if [[ "$label" =~ ^backport/(.+)$ ]]; then + TARGET_BRANCH="${BASH_REMATCH[1]}" + echo "Processing backport to branch: $TARGET_BRANCH" + + # Check if target branch exists + if ! git ls-remote --heads origin "$TARGET_BRANCH" | grep -q "$TARGET_BRANCH"; then + echo "❌ Target branch $TARGET_BRANCH does not exist, skipping" + + # Comment on the original PR about the missing branch + gh pr comment $PR_NUMBER --body "⚠️ Cannot backport to \`$TARGET_BRANCH\`: branch does not exist." + continue + fi + + # Create a unique branch name for the cherry-pick + CHERRY_PICK_BRANCH="cherry-pick-${PR_NUMBER}-to-${TARGET_BRANCH}" + + # Check if a cherry-pick PR already exists + EXISTING_PR=$(gh pr list --head "$CHERRY_PICK_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "") + if [ -n "$EXISTING_PR" ]; then + echo "⚠️ Cherry-pick PR already exists: #$EXISTING_PR" + gh pr comment $PR_NUMBER --body "Cherry-pick to \`$TARGET_BRANCH\` already exists: #$EXISTING_PR" + continue + fi + + # Fetch and checkout target branch + git fetch origin "$TARGET_BRANCH" + git checkout -b "$CHERRY_PICK_BRANCH" "origin/$TARGET_BRANCH" + + # Attempt cherry-pick + if git cherry-pick "$COMMIT_SHA"; then + echo "✅ Cherry-pick successful for $TARGET_BRANCH" + + # Push the cherry-pick branch + git push origin "$CHERRY_PICK_BRANCH" + + # Create PR for the cherry-pick + CHERRY_PICK_TITLE="$PR_TITLE (cherry-pick #$PR_NUMBER)" + CHERRY_PICK_BODY="Cherry-pick of #$PR_NUMBER to \`$TARGET_BRANCH\` branch. + + **Original PR:** #$PR_NUMBER + **Original Author:** @$PR_AUTHOR + **Cherry-picked commit:** $COMMIT_SHA + + --- + + ${{ github.event.pull_request.body }}" + + NEW_PR=$(gh pr create \ + --title "$CHERRY_PICK_TITLE" \ + --body "$CHERRY_PICK_BODY" \ + --base "$TARGET_BRANCH" \ + --head "$CHERRY_PICK_BRANCH" \ + --label "cherry-pick" \ + --label "backport" \ + --json number --jq '.number') + + echo "✅ Created cherry-pick PR #$NEW_PR for $TARGET_BRANCH" + + # Comment on original PR + gh pr comment $PR_NUMBER --body "🍒 Cherry-pick to \`$TARGET_BRANCH\` created: #$NEW_PR" + + else + echo "⚠️ Cherry-pick failed for $TARGET_BRANCH, creating conflict resolution PR" + + # Add conflicted files and commit + git add . + git commit -m "Cherry-pick #$PR_NUMBER to $TARGET_BRANCH (with conflicts) + + This cherry-pick has conflicts that need manual resolution. + + Original PR: #$PR_NUMBER + Original commit: $COMMIT_SHA" + + # Push the branch with conflicts + git push origin "$CHERRY_PICK_BRANCH" + + # Create PR with conflict notice + CONFLICT_TITLE="$PR_TITLE (backport of #$PR_NUMBER)" + CONFLICT_BODY="⚠️ **This cherry-pick has conflicts that require manual resolution.** + + Cherry-pick of #$PR_NUMBER to \`$TARGET_BRANCH\` branch. + + **Original PR:** #$PR_NUMBER + **Original Author:** @$PR_AUTHOR + **Cherry-picked commit:** $COMMIT_SHA + + **Please resolve the conflicts in this PR before merging.** + + --- + + ${{ github.event.pull_request.body }}" + + NEW_PR=$(gh pr create \ + --title "$CONFLICT_TITLE" \ + --body "$CONFLICT_BODY" \ + --base "$TARGET_BRANCH" \ + --head "$CHERRY_PICK_BRANCH" \ + --label "cherry-pick" \ + --label "backport" \ + --label "conflicts" \ + --json number --jq '.number') + + echo "⚠️ Created conflict resolution PR #$NEW_PR for $TARGET_BRANCH" + + # Comment on original PR + gh pr comment $PR_NUMBER --body "⚠️ Cherry-pick to \`$TARGET_BRANCH\` has conflicts: #$NEW_PR" + fi + + # Clean up - go back to main branch + git checkout main + git branch -D "$CHERRY_PICK_BRANCH" 2>/dev/null || true + fi + done diff --git a/.github/workflows/_reusable-docker-build-single.yaml b/.github/workflows/_reusable-docker-build-single.yml similarity index 100% rename from .github/workflows/_reusable-docker-build-single.yaml rename to .github/workflows/_reusable-docker-build-single.yml diff --git a/.github/workflows/_reusable-docker-build.yaml b/.github/workflows/_reusable-docker-build.yml similarity index 96% rename from .github/workflows/_reusable-docker-build.yaml rename to .github/workflows/_reusable-docker-build.yml index 47bc042bce..961b985cf2 100644 --- a/.github/workflows/_reusable-docker-build.yaml +++ b/.github/workflows/_reusable-docker-build.yml @@ -21,7 +21,7 @@ on: jobs: build-server-amd64: - uses: ./.github/workflows/_reusable-docker-build-single.yaml + uses: ./.github/workflows/_reusable-docker-build-single.yml secrets: inherit with: image_name: ${{ inputs.image_name }} @@ -31,7 +31,7 @@ jobs: registry_ghcr: ${{ inputs.registry_ghcr }} release: ${{ inputs.release }} build-server-arm64: - uses: ./.github/workflows/_reusable-docker-build-single.yaml + uses: ./.github/workflows/_reusable-docker-build-single.yml secrets: inherit with: image_name: ${{ inputs.image_name }} diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml index 394af09fe6..b6f37ebbfe 100644 --- a/.github/workflows/ci-main.yml +++ b/.github/workflows/ci-main.yml @@ -256,7 +256,7 @@ jobs: # Needed for checkout contents: read needs: ci-core-mark - uses: ./.github/workflows/_reusable-docker-build.yaml + uses: ./.github/workflows/_reusable-docker-build.yml secrets: inherit with: image_name: ${{ github.repository == 'goauthentik/authentik-internal' && 'ghcr.io/goauthentik/internal-server' || 'ghcr.io/goauthentik/dev-server' }} diff --git a/.github/workflows/gh-cherry-pick.yml b/.github/workflows/gh-cherry-pick.yml new file mode 100644 index 0000000000..27b6a83aee --- /dev/null +++ b/.github/workflows/gh-cherry-pick.yml @@ -0,0 +1,23 @@ +name: GH - Cherry-pick + +on: + pull_request: + types: [closed, labeled] + +jobs: + cherry-pick: + runs-on: ubuntu-latest + steps: + - id: app-token + name: Generate app token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.GH_APP_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + token: "${{ steps.app-token.outputs.token }}" + - uses: ./.github/actions/cherry-pick + with: + token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/release-branch-off.yml b/.github/workflows/release-branch-off.yml index 1e60fd0064..1c4ed77eb8 100644 --- a/.github/workflows/release-branch-off.yml +++ b/.github/workflows/release-branch-off.yml @@ -43,10 +43,13 @@ jobs: with: dependencies: python - name: Create version branch + env: + GH_TOKEN: "${{ steps.app-token.outputs.token }}" run: | current_major_version="$(uv version --short | grep -oE "^[0-9]{4}\.[0-9]{1,2}")" git checkout -b "version-${current_major_version}" git push origin "version-${current_major_version}" + gh label create "backport/version-${current_major_version}" --description "Add this label to PRs to backport changes to version-${current_major_version}" --color "fbca04" bump-version-pr: name: Open version bump PR needs: diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 7459a117f6..869e05a767 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -7,7 +7,7 @@ on: jobs: build-server: - uses: ./.github/workflows/_reusable-docker-build.yaml + uses: ./.github/workflows/_reusable-docker-build.yml secrets: inherit permissions: contents: read