191 lines
8.1 KiB
YAML
191 lines
8.1 KiB
YAML
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
#
|
|
# FILE INFORMATION
|
|
# DEFGROUP: MokoGitea.Workflow
|
|
# INGROUP: MokoCLI.Cascade
|
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
# PATH: /.mokogitea/workflows/cascade-dev.yml
|
|
# VERSION: 02.01.00
|
|
# BRIEF: Cascade main -> dev; auto-merge clean, auto-resolve VERSION-stamp-only conflicts, else notify
|
|
|
|
name: "Cascade Main -> Dev"
|
|
|
|
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:
|
|
contents: write
|
|
pull-requests: write
|
|
|
|
env:
|
|
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
# ntfy destination is configured via repo or org variables (org vars are inherited).
|
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'gitea-releases' }}
|
|
|
|
jobs:
|
|
cascade:
|
|
name: Cascade main -> dev
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- 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 }}
|
|
run: |
|
|
set -uo pipefail
|
|
API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
|
|
AUTH="Authorization: token ${TOKEN}"
|
|
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
|
|
echo "No dev branch - nothing to cascade."; exit 0
|
|
fi
|
|
|
|
# 1. is main ahead of dev?
|
|
AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/dev...main" \
|
|
| python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0)
|
|
if [ "${AHEAD:-0}" -eq 0 ]; then
|
|
echo "dev already up to date with main."; exit 0
|
|
fi
|
|
echo "main is ${AHEAD} commit(s) ahead of dev."
|
|
|
|
# 2. reuse an open main->dev PR, else create one
|
|
PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=dev" \
|
|
| 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 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
|
|
echo "Opened cascade PR #${PR}"
|
|
else
|
|
echo "Reusing open cascade PR #${PR}"
|
|
fi
|
|
|
|
notify() {
|
|
curl -sS \
|
|
-H "Title: ${REPO}: dev cascade needs manual merge" \
|
|
-H "Tags: warning,twisted_rightwards_arrows" \
|
|
-H "Priority: high" \
|
|
-H "Click: ${MOKOGITEA_URL}/${REPO}/pulls/${PR}" \
|
|
-d "main -> dev cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \
|
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
}
|
|
|
|
# 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)."
|
|
exit 0
|
|
fi
|
|
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, <version> element, or CHANGELOG title).
|
|
is_stamp_only() {
|
|
awk '
|
|
/^<<<<<<< / { inc=1; next }
|
|
inc && /^=======$/ { next }
|
|
/^>>>>>>> / { inc=0; next }
|
|
inc { if ($0 !~ /(VERSION:|<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
|