diff --git a/.gitea/workflows/cascade-dev.yml b/.gitea/workflows/cascade-dev.yml index 03295f2..d4780b1 100644 --- a/.gitea/workflows/cascade-dev.yml +++ b/.gitea/workflows/cascade-dev.yml @@ -7,18 +7,18 @@ # INGROUP: MokoStandards.Maintenance # REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API # PATH: /templates/workflows/cascade-dev.yml.template -# VERSION: 01.00.00 -# BRIEF: Forward-merge main → dev after every push to main +# VERSION: 02.00.00 +# BRIEF: Forward-merge main → all open branches after every push to main # # +========================================================================+ -# | CASCADE MAIN → DEV | +# | CASCADE MAIN → ALL BRANCHES | # +========================================================================+ # | | # | Triggers on every push to main (PR merges, bot commits, etc.) | # | | -# | 1. Check if a 'dev' branch exists | -# | 2. Create a PR (main → dev) via Gitea API | -# | 3. Auto-merge if clean; leave open for manual resolution on conflict | +# | 1. List all branches matching: dev, rc/*, beta/*, alpha/* | +# | 2. For each: create PR (main → branch), auto-merge if clean | +# | 3. On conflict: leave PR open for manual resolution | # | | # +========================================================================+ @@ -42,143 +42,172 @@ permissions: jobs: cascade: - name: Merge main → dev + name: Cascade main → branches runs-on: ubuntu-latest if: >- !contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip cascade]') steps: - - name: Check dev branch exists - id: check + - name: Discover target branches + id: branches env: GA_TOKEN: ${{ secrets.GA_TOKEN }} run: | API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - STATUS=$(curl -sS -o /dev/null -w "%{http_code}" \ - -H "Authorization: token ${GA_TOKEN}" \ - "${API}/branches/dev") + # Fetch all branches (paginated) + PAGE=1 + ALL_BRANCHES="" + while true; do + BATCH=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/branches?page=${PAGE}&limit=50" \ + | jq -r '.[].name // empty') + [ -z "$BATCH" ] && break + ALL_BRANCHES="$ALL_BRANCHES $BATCH" + PAGE=$((PAGE + 1)) + done - if [ "$STATUS" = "200" ]; then - echo "exists=true" >> "$GITHUB_OUTPUT" - echo "✅ dev branch exists" + # Filter to cascade targets: dev, dev/*, rc/*, beta/*, alpha/* + TARGETS="" + for BRANCH in $ALL_BRANCHES; do + case "$BRANCH" in + dev|dev/*|rc/*|beta/*|alpha/*) + TARGETS="$TARGETS $BRANCH" + ;; + esac + done + + TARGETS=$(echo "$TARGETS" | xargs) # trim whitespace + + if [ -z "$TARGETS" ]; then + echo "targets=" >> "$GITHUB_OUTPUT" + echo "ℹ️ No cascade target branches found" else - echo "exists=false" >> "$GITHUB_OUTPUT" - echo "ℹ️ No dev branch found (HTTP ${STATUS}) — skipping cascade" + echo "targets=$TARGETS" >> "$GITHUB_OUTPUT" + COUNT=$(echo "$TARGETS" | wc -w) + echo "📋 Found ${COUNT} target branch(es): ${TARGETS}" fi - - name: Check if dev is already up to date - if: steps.check.outputs.exists == 'true' - id: diff - env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} - run: | - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - - # Compare main..dev — if ahead_by is 0 there's nothing to cascade - RESPONSE=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ - "${API}/compare/dev...main") - - AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') - - if [ "$AHEAD" -eq 0 ]; then - echo "needs_merge=false" >> "$GITHUB_OUTPUT" - echo "✅ dev is already up to date with main" - else - echo "needs_merge=true" >> "$GITHUB_OUTPUT" - echo "ℹ️ main is ${AHEAD} commit(s) ahead of dev" - fi - - - name: Create cascade PR - if: steps.diff.outputs.needs_merge == 'true' - id: pr + - name: Cascade to all target branches + if: steps.branches.outputs.targets != '' env: GA_TOKEN: ${{ secrets.GA_TOKEN }} run: | API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" SHORT_SHA="${GITHUB_SHA:0:7}" + TARGETS="${{ steps.branches.outputs.targets }}" - # Check if a cascade PR already exists (main → dev) - EXISTING=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ - "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=dev&limit=1") + SUCCESS=0 + CONFLICTS=0 + SKIPPED=0 + FAILED=0 - EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') + for BRANCH in $TARGETS; do + echo "" + echo "═══ main → ${BRANCH} ═══" - if [ "$EXISTING_COUNT" -gt 0 ]; then - PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number') - PR_URL=$(echo "$EXISTING" | jq -r '.[0].html_url') - echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" - echo "pr_exists=true" >> "$GITHUB_OUTPUT" - echo "ℹ️ Cascade PR already exists: ${PR_URL}" - else - RESPONSE=$(curl -sS -w "\n%{http_code}" \ + # Check if branch is already up to date + ENCODED_BRANCH=$(echo "$BRANCH" | sed 's|/|%2F|g') + RESPONSE=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/compare/${ENCODED_BRANCH}...main") + + AHEAD=$(echo "$RESPONSE" | jq '.total_commits // 0') + + if [ "$AHEAD" -eq 0 ]; then + echo " ✅ Already up to date" + SKIPPED=$((SKIPPED + 1)) + continue + fi + + echo " ℹ️ main is ${AHEAD} commit(s) ahead" + + # Check for existing cascade PR + EXISTING=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls?state=open&head=${GITEA_ORG}:main&base=${ENCODED_BRANCH}&limit=1") + + EXISTING_COUNT=$(echo "$EXISTING" | jq 'length') + PR_NUMBER="" + + if [ "$EXISTING_COUNT" -gt 0 ]; then + PR_NUMBER=$(echo "$EXISTING" | jq -r '.[0].number') + echo " ℹ️ Reusing existing PR #${PR_NUMBER}" + else + # Create cascade PR + PR_RESPONSE=$(curl -sS -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: token ${GA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{ + \"title\": \"chore: cascade main → ${BRANCH} (${SHORT_SHA}) [skip ci]\", + \"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`${BRANCH}\`.\\n\\nIf conflicts exist, resolve manually and merge.\\n\\n> Auto-created by **Cascade Main → Dev**.\", + \"head\": \"main\", + \"base\": \"${BRANCH}\" + }" \ + "${API}/pulls") + + HTTP_CODE=$(echo "$PR_RESPONSE" | tail -1) + BODY=$(echo "$PR_RESPONSE" | sed '$d') + PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty') + + if [ "$HTTP_CODE" != "201" ] || [ -z "$PR_NUMBER" ]; then + MSG=$(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1) + echo " ❌ Failed to create PR (HTTP ${HTTP_CODE}): ${MSG}" + FAILED=$((FAILED + 1)) + continue + fi + + echo " ✅ Created PR #${PR_NUMBER}" + fi + + # Try auto-merge + PR_DATA=$(curl -sS \ + -H "Authorization: token ${GA_TOKEN}" \ + "${API}/pulls/${PR_NUMBER}") + + MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') + + if [ "$MERGEABLE" != "true" ]; then + echo " ⚠️ Conflicts — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) + continue + fi + + MERGE_RESPONSE=$(curl -sS -w "\n%{http_code}" \ -X POST \ -H "Authorization: token ${GA_TOKEN}" \ -H "Content-Type: application/json" \ -d "{ - \"title\": \"chore: cascade main → dev (${SHORT_SHA}) [skip ci]\", - \"body\": \"## Automatic cascade\\n\\nForward-merging \`main\` (${SHORT_SHA}) into \`dev\` to keep branches in sync.\\n\\nIf this PR has conflicts, please resolve them manually and merge.\\n\\n> Auto-created by the **Cascade Main → Dev** workflow.\", - \"head\": \"main\", - \"base\": \"dev\" + \"Do\": \"merge\", + \"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\", + \"delete_branch_after_merge\": false }" \ - "${API}/pulls") + "${API}/pulls/${PR_NUMBER}/merge") - HTTP_CODE=$(echo "$RESPONSE" | tail -1) - BODY=$(echo "$RESPONSE" | sed '$d') - PR_NUMBER=$(echo "$BODY" | jq -r '.number // empty') - PR_URL=$(echo "$BODY" | jq -r '.html_url // empty') + MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1) - if [ "$HTTP_CODE" = "201" ] && [ -n "$PR_NUMBER" ]; then - echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT" - echo "pr_exists=false" >> "$GITHUB_OUTPUT" - echo "✅ Created cascade PR #${PR_NUMBER}: ${PR_URL}" + if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then + echo " ✅ Merged — ${BRANCH} is in sync" + SUCCESS=$((SUCCESS + 1)) else - echo "❌ Failed to create PR (HTTP ${HTTP_CODE}): ${BODY}" - exit 1 + MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d') + echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open" + CONFLICTS=$((CONFLICTS + 1)) fi - fi - - - name: Auto-merge cascade PR - if: steps.pr.outputs.pr_number != '' - env: - GA_TOKEN: ${{ secrets.GA_TOKEN }} - run: | - API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" - PR_NUMBER="${{ steps.pr.outputs.pr_number }}" - - # Check if PR is mergeable - PR_DATA=$(curl -sS \ - -H "Authorization: token ${GA_TOKEN}" \ - "${API}/pulls/${PR_NUMBER}") - - MERGEABLE=$(echo "$PR_DATA" | jq -r '.mergeable // false') - - if [ "$MERGEABLE" != "true" ]; then - echo "⚠️ PR #${PR_NUMBER} has conflicts — leaving open for manual resolution" - exit 0 - fi - - # Merge the PR - RESPONSE=$(curl -sS -w "\n%{http_code}" \ - -X POST \ - -H "Authorization: token ${GA_TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{ - \"Do\": \"merge\", - \"merge_message_field\": \"chore: cascade main → dev [skip ci]\", - \"delete_branch_after_merge\": false - }" \ - "${API}/pulls/${PR_NUMBER}/merge") - - HTTP_CODE=$(echo "$RESPONSE" | tail -1) - - if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "204" ]; then - echo "✅ Cascade PR #${PR_NUMBER} merged — dev is now in sync with main" - else - BODY=$(echo "$RESPONSE" | sed '$d') - echo "⚠️ Merge failed (HTTP ${HTTP_CODE}): ${BODY}" - echo "PR #${PR_NUMBER} left open for manual resolution" + done + + # Summary + echo "" + echo "════════════════════════════════════════" + echo " ✅ Merged: ${SUCCESS}" + echo " ⚠️ Conflicts: ${CONFLICTS}" + echo " ⏭️ Up to date: ${SKIPPED}" + echo " ❌ Failed: ${FAILED}" + echo "════════════════════════════════════════" + + if [ "$FAILED" -gt 0 ]; then + exit 1 fi