From 362e75a9a845f7e4e62c9984fa34f3f97acf52a7 Mon Sep 17 00:00:00 2001 From: Jonathan Miller <1+jmiller@noreply.git.mokoconsulting.tech> Date: Tue, 14 Jul 2026 04:45:34 +0000 Subject: [PATCH] chore: sync cascade-dev.yml from Template-Joomla [skip ci] --- .mokogitea/workflows/cascade-dev.yml | 207 ++++++--------------------- 1 file changed, 41 insertions(+), 166 deletions(-) diff --git a/.mokogitea/workflows/cascade-dev.yml b/.mokogitea/workflows/cascade-dev.yml index 8ae17c44..1b9f8a6b 100644 --- a/.mokogitea/workflows/cascade-dev.yml +++ b/.mokogitea/workflows/cascade-dev.yml @@ -1,190 +1,65 @@ # Copyright (C) 2026 Moko Consulting -# # 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 +# INGROUP: MokoCLI.Release +# BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11. +# +# +========================================================================+ +# | CASCADE MAIN -> DEV | +# +========================================================================+ +# | dev mirrors main and is reset on every release. | +# | delete+recreate cannot run against a protected branch, so this | +# | force-pushes main -> dev. The automation identity is force-push | +# | allowlisted on dev via branch-protection.yml. | +# | Runs AFTER the release workflow completes, so dev picks up the | +# | fully version-bumped + changelog-promoted main (not the pre-bump | +# | state). Force-push (not merge) => no version-file conflicts. | +# +========================================================================+ 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_run: + workflows: ["Universal: Build & Release"] + types: [completed] workflow_dispatch: +concurrency: + group: cascade-dev-${{ github.repository }} + cancel-in-progress: true + 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 + name: Reset dev to main + if: >- + !startsWith(github.event.repository.name, 'Template-') && + (github.event_name == 'workflow_dispatch' || + github.event.workflow_run.conclusion == 'success') 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) + - name: Force dev to main env: TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} + SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} 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; } + set -euo pipefail + git init -q sync && cd sync + git config user.email "mokogitea-actions[bot]@mokoconsulting.tech" + git config user.name "mokogitea-actions[bot]" + git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git" + git fetch -q --depth=1 origin main - # 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}" + if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then + # dev exists -> force it to match main (dev is a mirror of main) + git push --force origin FETCH_HEAD:refs/heads/dev + echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY" else - echo "Reusing open cascade PR #${PR}" + # dev missing (e.g. renamed to rc mid-cycle) -> recreate from main + git push origin FETCH_HEAD:refs/heads/dev + echo "dev created from main" >> "$GITHUB_STEP_SUMMARY" 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, 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