214 lines
7.9 KiB
YAML
214 lines
7.9 KiB
YAML
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||
#
|
||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||
#
|
||
# FILE INFORMATION
|
||
# DEFGROUP: Gitea.Workflow
|
||
# INGROUP: MokoStandards.Maintenance
|
||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||
# PATH: /templates/workflows/cascade-dev.yml.template
|
||
# VERSION: 02.00.00
|
||
# BRIEF: Forward-merge main → all open branches after every push to main
|
||
#
|
||
# +========================================================================+
|
||
# | CASCADE MAIN → ALL BRANCHES |
|
||
# +========================================================================+
|
||
# | |
|
||
# | Triggers on every push to main (PR merges, bot commits, etc.) |
|
||
# | |
|
||
# | 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 |
|
||
# | |
|
||
# +========================================================================+
|
||
|
||
name: "Universal: Cascade Main → Dev"
|
||
|
||
on:
|
||
push:
|
||
branches:
|
||
- main
|
||
workflow_dispatch:
|
||
|
||
env:
|
||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||
|
||
permissions:
|
||
contents: write
|
||
pull-requests: write
|
||
|
||
jobs:
|
||
cascade:
|
||
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: Discover target branches
|
||
id: branches
|
||
env:
|
||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||
run: |
|
||
API="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||
|
||
# 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
|
||
|
||
# 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 "targets=$TARGETS" >> "$GITHUB_OUTPUT"
|
||
COUNT=$(echo "$TARGETS" | wc -w)
|
||
echo "📋 Found ${COUNT} target branch(es): ${TARGETS}"
|
||
fi
|
||
|
||
- 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 }}"
|
||
|
||
SUCCESS=0
|
||
CONFLICTS=0
|
||
SKIPPED=0
|
||
FAILED=0
|
||
|
||
for BRANCH in $TARGETS; do
|
||
echo ""
|
||
echo "═══ main → ${BRANCH} ═══"
|
||
|
||
# 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 "{
|
||
\"Do\": \"merge\",
|
||
\"merge_message_field\": \"chore: cascade main → ${BRANCH} [skip ci]\",
|
||
\"delete_branch_after_merge\": false
|
||
}" \
|
||
"${API}/pulls/${PR_NUMBER}/merge")
|
||
|
||
MERGE_HTTP=$(echo "$MERGE_RESPONSE" | tail -1)
|
||
|
||
if [ "$MERGE_HTTP" = "200" ] || [ "$MERGE_HTTP" = "204" ]; then
|
||
echo " ✅ Merged — ${BRANCH} is in sync"
|
||
SUCCESS=$((SUCCESS + 1))
|
||
else
|
||
MERGE_BODY=$(echo "$MERGE_RESPONSE" | sed '$d')
|
||
echo " ⚠️ Merge failed (HTTP ${MERGE_HTTP}) — PR #${PR_NUMBER} left open"
|
||
CONFLICTS=$((CONFLICTS + 1))
|
||
fi
|
||
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
|