From ae4fb4d80c43a25420239dcafe133e7d4d56a3b2 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Mon, 13 Jul 2026 16:21:20 +0000 Subject: [PATCH] chore: sync cascade-dev.yml from Template-Generic [skip ci] --- .mokogitea/workflows/cascade-dev.yml | 128 ++++++++++++++++++++++----- 1 file changed, 106 insertions(+), 22 deletions(-) diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index 31c35ad..8ae17c4 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -7,8 +7,8 @@ # INGROUP: MokoCLI.Cascade # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic # PATH: /.mokogitea/workflows/cascade-dev.yml -# VERSION: 02.00.00 -# BRIEF: Cascade main -> dev via PR; auto-merge only if conflict-free, else notify +# VERSION: 02.01.00 +# BRIEF: Cascade main -> dev; auto-merge clean, auto-resolve VERSION-stamp-only conflicts, else notify name: "Cascade Main -> Dev" @@ -16,6 +16,10 @@ on: push: branches: - main + # Daily safety net: catches drift even when main only received [skip ci] pushes + # (which never fire the push trigger above). Off-round minute to avoid a fleet-wide spike. + schedule: + - cron: '23 7 * * *' workflow_dispatch: permissions: @@ -33,7 +37,13 @@ jobs: name: Cascade main -> dev runs-on: ubuntu-latest steps: - - name: Open main -> dev PR (auto-merge if clean, else notify) + - name: Checkout (full history for merge/resolve) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.MOKOGITEA_TOKEN }} + + - name: Cascade main -> dev (auto-resolve version stamps, else notify) env: TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} REPO: ${{ github.repository }} @@ -41,7 +51,7 @@ jobs: set -uo pipefail API="${MOKOGITEA_URL}/api/v1/repos/${REPO}" AUTH="Authorization: token ${TOKEN}" - jqnum() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; } + jqget() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; } # 0. dev must exist if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then @@ -61,8 +71,8 @@ jobs: | python3 -c "import sys,json; d=json.load(sys.stdin); print(next((str(p['number']) for p in d if p.get('head',{}).get('ref')=='main'), ''))" 2>/dev/null || echo "") if [ -z "$PR" ]; then RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \ - -d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges only if conflict-free; otherwise left open for manual resolution."}') - PR=$(printf '%s' "$RESP" | jqnum number) + -d '{"head":"main","base":"dev","title":"chore(sync): cascade main -> dev","body":"Automated cascade of main into dev. Auto-merges when conflict-free, auto-resolves VERSION-stamp-only conflicts, otherwise left open for manual resolution."}') + PR=$(printf '%s' "$RESP" | jqget number) if [ -z "$PR" ]; then echo "::warning::Could not open cascade PR: $RESP"; exit 0 fi @@ -71,15 +81,6 @@ jobs: echo "Reusing open cascade PR #${PR}" fi - # 3. wait for MokoGitea to compute mergeability (conflict detection) - MERGEABLE="" - for _ in 1 2 3 4 5 6; do - MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqnum mergeable) - case "$MERGEABLE" in True|False) break ;; esac - sleep 3 - done - echo "mergeable=${MERGEABLE}" - notify() { curl -sS \ -H "Title: ${REPO}: dev cascade needs manual merge" \ @@ -90,17 +91,100 @@ jobs: "${NTFY_URL}/${NTFY_TOPIC}" || true } - # 4. auto-merge only if conflict-free; otherwise notify + # 3. wait for MokoGitea to compute mergeability (conflict detection) + MERGEABLE="" + for _ in 1 2 3 4 5 6; do + MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqget mergeable) + case "$MERGEABLE" in True|False) break ;; esac + sleep 3 + done + echo "mergeable=${MERGEABLE}" + + # 4a. conflict-free -> merge via API (existing behaviour) if [ "$MERGEABLE" = "True" ]; then CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \ -X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}') if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)." - else - echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)" - notify "could not be auto-merged (HTTP ${CODE})." + exit 0 fi - else - echo "::warning::Cascade PR #${PR} has conflicts (mergeable=${MERGEABLE}); sending notification." - notify "has conflicts and cannot be merged automatically." + echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)" + notify "could not be auto-merged (HTTP ${CODE})." + exit 0 fi + + # 4b. conflicts -> try to auto-resolve if they are ONLY VERSION-stamp lines. + echo "PR not cleanly mergeable; checking whether conflicts are VERSION-stamp-only..." + git config user.name "MokoGitea Cascade" + git config user.email "actions@mokoconsulting.tech" + git fetch --quiet origin main dev + git checkout -B dev origin/dev + + if git merge --no-ff --no-commit origin/main >/dev/null 2>&1; then + # Became clean at git level (e.g. mergeability was still computing) -> commit + push. + git commit -m "chore(sync): cascade main -> dev [skip ci]" >/dev/null + git push origin dev + echo "Cascade merged cleanly at git level and pushed to dev." + exit 0 + fi + + CONFLICTS=$(git diff --name-only --diff-filter=U) + echo "Conflicted files:"; echo "${CONFLICTS}" + + # A conflict is "stamp-only" when every line inside every conflict block matches + # a version-stamp pattern (VERSION: header, element, or CHANGELOG title). + is_stamp_only() { + awk ' + /^<<<<<<< / { inc=1; next } + inc && /^=======$/ { next } + /^>>>>>>> / { inc=0; next } + inc { if ($0 !~ /(VERSION:||# Changelog)/) { bad=1 } } + END { exit(bad ? 1 : 0) } + ' "$1" + } + # Resolve a stamp-only file by keeping dev (ours) for the conflicting lines, + # preserving all auto-merged content around them. + keep_ours() { + awk ' + /^<<<<<<< / { inc=1; side="ours"; next } + inc && /^=======$/ { side="theirs"; next } + /^>>>>>>> / { inc=0; next } + { if (!inc) { print; next } if (side=="ours") print } + ' "$1" > "$1.resolved" && mv "$1.resolved" "$1" + } + + ALL_STAMP=1 + for f in ${CONFLICTS}; do + if ! is_stamp_only "$f"; then + echo "::notice::$f has non-stamp conflicts -> manual resolution required." + ALL_STAMP=0; break + fi + done + + if [ "$ALL_STAMP" != "1" ]; then + git merge --abort || true + notify "has non-version-stamp conflicts and cannot be auto-resolved." + exit 0 + fi + + echo "All conflicts are VERSION-stamp-only; resolving in favour of dev." + for f in ${CONFLICTS}; do + keep_ours "$f" + git add "$f" + done + + # Best-effort: normalise stamps to dev's version if mokocli is available. + if [ -f /opt/mokocli/cli/version_check.php ]; then + php /opt/mokocli/cli/version_check.php --fix || true + git add -A + fi + + git commit -m "chore(sync): cascade main -> dev (auto-resolved version stamps) [skip ci]" >/dev/null + git push origin dev + echo "Cascade auto-resolved and pushed to dev." + + # Close the now-redundant PR (its changes are in dev) with an explanatory comment. + curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/issues/${PR}/comments" \ + -d '{"body":"Auto-resolved VERSION-stamp-only conflicts and pushed the merge to dev. Closing."}' >/dev/null || true + curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH "${API}/pulls/${PR}" \ + -d '{"state":"closed"}' >/dev/null || true