@mokoconsulting/mokoabs-mcp 0.0.0-stage → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +41 -0
- package/.gitattributes +94 -0
- package/.gitmessage +9 -0
- package/.mokogit/ISSUE_TEMPLATE/adr.md +110 -0
- package/.mokogit/ISSUE_TEMPLATE/bug_report.md +48 -0
- package/.mokogit/ISSUE_TEMPLATE/config.yml +18 -0
- package/.mokogit/ISSUE_TEMPLATE/documentation.md +52 -0
- package/.mokogit/ISSUE_TEMPLATE/feature_request.md +51 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_api_integration.md +48 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_connection_issue.md +69 -0
- package/.mokogit/ISSUE_TEMPLATE/mcp_tool_request.md +50 -0
- package/.mokogit/ISSUE_TEMPLATE/question.md +82 -0
- package/.mokogit/ISSUE_TEMPLATE/rfc.md +126 -0
- package/.mokogit/ISSUE_TEMPLATE/security.md +51 -0
- package/.mokogit/ISSUE_TEMPLATE/version.md +24 -0
- package/.mokogit/actions/resolve-source-dir/action.yml +72 -0
- package/.mokogit/workflows/apidocs.yml +132 -0
- package/.mokogit/workflows/auto-assign.yml +76 -0
- package/.mokogit/workflows/auto-dev-issue.yml +207 -0
- package/.mokogit/workflows/auto-release.yml +1787 -0
- package/.mokogit/workflows/branch-cleanup.yml +63 -0
- package/.mokogit/workflows/cascade.yml +645 -0
- package/.mokogit/workflows/ci-issue-reporter.yml +100 -0
- package/.mokogit/workflows/cleanup.yml +112 -0
- package/.mokogit/workflows/makefile-lint.yml +240 -0
- package/.mokogit/workflows/npm-build-test.yml +507 -0
- package/.mokogit/workflows/npm-sdk-check.yml +110 -0
- package/.mokogit/workflows/npm-tool-inventory.yml +81 -0
- package/.mokogit/workflows/pr-branch-check.yml +92 -0
- package/.mokogit/workflows/pre-release.yml +1134 -0
- package/.mokogit/workflows/rc-revert.yml +75 -0
- package/AGENTS.md +48 -0
- package/CHANGELOG.md +37 -0
- package/CODE_OF_CONDUCT.md +70 -0
- package/CONTRIBUTING.md +164 -0
- package/LICENSE +696 -0
- package/Makefile +94 -0
- package/README.md +109 -2
- package/SECURITY.md +245 -0
- package/build/README.md +7 -0
- package/dist/README.md +7 -0
- package/dist/index.js +153 -0
- package/package.json +34 -5
- package/sample/config.example.json +15 -0
- package/source/client.ts +132 -0
- package/source/config.ts +139 -0
- package/source/credentialStore.ts +216 -0
- package/source/index.ts +235 -0
- package/source/tokenReader.ts +120 -0
- package/source/types.ts +63 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
#
|
|
5
|
+
# FILE INFORMATION
|
|
6
|
+
# DEFGROUP: MokoGIT.Workflow
|
|
7
|
+
# INGROUP: MokoCLI.Cascade
|
|
8
|
+
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
9
|
+
# PATH: /.mokogit/workflows/cascade.yml
|
|
10
|
+
# VERSION: 03.07.00
|
|
11
|
+
# BRIEF: (03.07.00) TOPOLOGY + SERVER-CASCADE NOTE (MokoCLI #965/#967): (1) R4 dev -> child-branch pass. The workflow previously cascaded ONLY the production branch (SRC) into CASCADE_TARGETS; it now does an optional SECOND pass that cascades `dev` into a new DEV_CHILD_TARGETS variable (reusing cascade_to with SRC repointed to dev). DEV_CHILD_TARGETS DEFAULTS TO EMPTY, so this is a strict no-op for every existing repo and must be opted into per repo/org -- no fleet behaviour changes until a repo sets it. The pass is guarded by branch_exists dev and self-skips when dev is absent. dev -> child REPOS remains the SYNC engine's responsibility and is intentionally NOT done here. (2) R3 is satisfied PRIMARILY by the MokoGIT server-side cascade (the Gitea-fork notifier fires per-merge -- including on [skip ci] commits the platform skips for workflows -- and auto-merges/fan-outs/chains via the cascade rule `mode`); this workflow is the SECONDARY/fallback layer (cron reconcile + workflow_dispatch after sync). The header/`on:` comments now state that explicitly. The daily cron is UNCHANGED (left intact as the documented safety-net). --- (03.06.00) RC4 WEDGE RECOVERY: when a cascade PR's mergeability check wedges in Gitea's internal "checking" state so every /merge returns HTTP 405 "Please try again later" indefinitely (MokoGIT #1729; neither the 12x retry loop + prime_merge_status nor a close+reopen of the SAME PR recovers it), the conflict-free path now ABANDONS the stuck PR and recreates a fresh ${SRC}->${TGT} PR (the only recovery -- a brand-new PR gets a clean check task), then retries the immediate merge once; non-405 failures still short-circuit straight to notify. --- RECONCILED (03.05.01): carries BOTH divergent 03.05.00 improvements. (1) SRC=stable fix: SRC resolves to the PRODUCTION branch via branch_exists() -- `stable` if it exists, else `main` if it exists, else default_branch -- so a channel-named repo whose DEFAULT is `dev` still back-merges stable -> dev instead of cascading dev -> rc/alpha/beta the wrong direction; every PR title/body/commit/log is labelled by the RESOLVED source (chore(sync): cascade ${SRC} -> <TGT>), no more misleading literal "main". (2) composer.json conflict resolution: composer.json version conflicts are inline-resolved to version=max (via resolve_json_version), same as Makefile PROJECT_VERSION, package.json and XML manifests, so a fix/chore->stable patch bump can no longer cascade a LOWER composer.json version onto a dev already bumped ahead (the DEV VERSION NEVER REGRESSES ON CASCADE); previously composer.json fell through to the '*' deeper-resolver/human path, blocking the cascade (Mokonyx #320/#408 manual reconcile) and racing the version between channels. Cascade the production branch -> the full release ladder (dev + on-demand rc/alpha/beta). Rolls up the retired cascade-dev.yml: one job loops the ladder, cascading the source into each target that EXISTS. dev is permanent and self-heals (recreated from main if missing after an RC-rename); rc/alpha/beta are on-demand and are SKIPPED when absent (never conjured). Branch-motivated (real push to main cascades down immediately) PLUS a daily scheduled safety-net: workflow_sync.php commits to stable/main carry [skip ci] and fire NO push trigger, so if its post-sync workflow_dispatch is missed the sync commits never back-merge and stable silently forks from dev (the MokonyxLicensing 79-commit drift incident). The cron reconciles stable/main -> dev periodically regardless of [skip ci]; it is idempotent/no-op when already in sync. Auto-merge clean, auto-resolve routine conflicts INLINE (manifest version=max / .mokogit-.gitea-.github -> main / docs-stamps -> dev) with no mokocli dependency, fall back to cascade_resolve.php then notify. Resolved merges are LANDED VIA PR (temp branch + PR-merge API), never direct-pushed to a protected target (the runner token is refused by the pre-receive hook). Conflict-free path does an IMMEDIATE PR merge so a clean cascade self-completes on repos with no required status checks.
|
|
12
|
+
|
|
13
|
+
name: "Cascade Main -> Ladder"
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
# PRIMARY per-merge guarantee (MokoCLI #965 R3) is the MokoGIT SERVER-SIDE
|
|
17
|
+
# cascade: the Gitea-fork notifier fires on EVERY merge to the production
|
|
18
|
+
# branch -- INCLUDING [skip ci] commits, which the platform skips for
|
|
19
|
+
# workflows -- and auto-merges / fans out / chains transitively per the
|
|
20
|
+
# cascade rule `mode`. THIS WORKFLOW is the SECONDARY/fallback layer only.
|
|
21
|
+
#
|
|
22
|
+
# Branch-motivated: any real push to main/stable cascades to the ladder
|
|
23
|
+
# immediately. Sync commits from workflow_sync.php carry [skip ci] and are
|
|
24
|
+
# skipped by the platform (they fire no trigger at all), so workflow_sync.php
|
|
25
|
+
# dispatches this workflow directly (workflow_dispatch) after it changes a repo
|
|
26
|
+
# -- that is what carries .mokogit drift down the ladder.
|
|
27
|
+
push:
|
|
28
|
+
branches:
|
|
29
|
+
- main
|
|
30
|
+
- stable
|
|
31
|
+
# Safety-net: a daily schedule reconciles stable/main -> dev regardless of
|
|
32
|
+
# [skip ci]. If the post-sync workflow_dispatch is ever missed, [skip ci]-only
|
|
33
|
+
# sync commits to the production branch would otherwise never back-merge and the
|
|
34
|
+
# production branch silently forks from dev (the MokonyxLicensing 79-commit
|
|
35
|
+
# drift that blocked its stable cut, back-merged in Licensing PR #160). The cron
|
|
36
|
+
# closes that gap: it is idempotent -- cascade_to() skips any target already up
|
|
37
|
+
# to date -- so a repo already in sync is a no-op. Scheduled runs execute on the
|
|
38
|
+
# repo default (production) branch, matching the existing branch-guard convention.
|
|
39
|
+
schedule:
|
|
40
|
+
- cron: '17 6 * * *'
|
|
41
|
+
workflow_dispatch:
|
|
42
|
+
|
|
43
|
+
permissions:
|
|
44
|
+
contents: write
|
|
45
|
+
pull-requests: write
|
|
46
|
+
|
|
47
|
+
env:
|
|
48
|
+
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
49
|
+
# ntfy destination is configured via repo or org variables (org vars are inherited).
|
|
50
|
+
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
51
|
+
NTFY_TOPIC: ${{ vars.CASCADE_NTFY_TOPIC || vars.NTFY_TOPIC || 'git-releases' }}
|
|
52
|
+
# The release ladder, high -> low. main cascades into each of these that exists.
|
|
53
|
+
# `dev` is permanent (self-heals); the rest are on-demand (skipped when absent).
|
|
54
|
+
# Overridable per repo/org via the CASCADE_TARGETS variable.
|
|
55
|
+
CASCADE_TARGETS: ${{ vars.CASCADE_TARGETS || 'dev rc alpha beta' }}
|
|
56
|
+
# R4 (MokoCLI #967): optional dev -> child-BRANCH cascade targets. DEFAULTS
|
|
57
|
+
# EMPTY so this is a strict no-op unless a repo/org opts in by setting the
|
|
58
|
+
# DEV_CHILD_TARGETS variable (e.g. a repo-local integration branch that must
|
|
59
|
+
# track dev). dev -> child REPOS is the SYNC engine's job, NOT this workflow.
|
|
60
|
+
DEV_CHILD_TARGETS: ${{ vars.DEV_CHILD_TARGETS || '' }}
|
|
61
|
+
|
|
62
|
+
jobs:
|
|
63
|
+
cascade:
|
|
64
|
+
name: Cascade main -> ladder
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
# Branch-guard: only run on the production branch context. push is already
|
|
67
|
+
# filtered to main/stable above; scheduled and manual runs execute against the
|
|
68
|
+
# repo default (production) branch, so require ref_name == default_branch. This
|
|
69
|
+
# never fires the cascade from a feature/dev checkout.
|
|
70
|
+
if: >-
|
|
71
|
+
github.event_name == 'push' ||
|
|
72
|
+
github.ref_name == github.event.repository.default_branch
|
|
73
|
+
steps:
|
|
74
|
+
- name: Checkout (full history for merge/resolve)
|
|
75
|
+
uses: actions/checkout@v4
|
|
76
|
+
with:
|
|
77
|
+
fetch-depth: 0
|
|
78
|
+
token: ${{ github.token }}
|
|
79
|
+
|
|
80
|
+
- name: Cascade main -> ladder (auto-resolve routine conflicts, else notify)
|
|
81
|
+
env:
|
|
82
|
+
TOKEN: ${{ github.token }}
|
|
83
|
+
REPO: ${{ github.repository }}
|
|
84
|
+
MOKOGIT_TOKEN: ${{ github.token }}
|
|
85
|
+
run: |
|
|
86
|
+
set -uo pipefail
|
|
87
|
+
API="${MOKOGIT_URL}/api/v1/repos/${REPO}"
|
|
88
|
+
AUTH="Authorization: token ${TOKEN}"
|
|
89
|
+
jqget() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
|
|
90
|
+
|
|
91
|
+
# prime_merge_status(pr): a cascade PR carries NO CI, so its head commit
|
|
92
|
+
# has zero commit statuses -> Gitea reports the combined state as "pending"
|
|
93
|
+
# (never "success"). A Do:merge on such a PR is accepted (2xx) but SCHEDULED
|
|
94
|
+
# (merge-when-checks-succeed) and then waits forever for a success that can
|
|
95
|
+
# never arrive, so the cascade PR sits open and dev never advances past
|
|
96
|
+
# stable (fleet-wide drift; MokoCLI #813). Stamp a synthetic success status
|
|
97
|
+
# on the PR head so the (possibly deferred) merge lands immediately. Best
|
|
98
|
+
# effort; never fatal. $1 = PR number.
|
|
99
|
+
prime_merge_status() {
|
|
100
|
+
local sha
|
|
101
|
+
sha=$(curl -sf -H "$AUTH" "${API}/pulls/$1" 2>/dev/null | python3 -c "import sys,json; print(json.load(sys.stdin).get('head',{}).get('sha',''))" 2>/dev/null || true)
|
|
102
|
+
[ -n "$sha" ] && curl -s -o /dev/null -H "$AUTH" -H "Content-Type: application/json" \
|
|
103
|
+
-X POST "${API}/statuses/${sha}" \
|
|
104
|
+
-d '{"state":"success","context":"cascade/auto-merge","description":"Cascade PR: no CI required; approved for auto-merge (#813)."}' || true
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# recreate_cascade_pr(): wedge recovery for MokoGIT #1729 (root cause 4). A
|
|
108
|
+
# cascade PR can get its mergeability check stuck in Gitea's internal
|
|
109
|
+
# "checking" state, so every /merge returns HTTP 405 "Please try again later"
|
|
110
|
+
# indefinitely -- the 12x retry loop and prime_merge_status cannot recover it,
|
|
111
|
+
# and close+reopen of the SAME PR keeps the wedged check. The ONLY fix is to
|
|
112
|
+
# abandon the PR and open a BRAND-NEW one for the same SRC->TGT, which gets a
|
|
113
|
+
# clean check task. Closes the current ${PR}, opens a fresh ${SRC}->${TGT} PR,
|
|
114
|
+
# and updates ${PR} (caller's local; bash dynamic scope, same as notify) to
|
|
115
|
+
# the new number. Returns 0 if a new PR now exists, 1 otherwise.
|
|
116
|
+
recreate_cascade_pr() {
|
|
117
|
+
echo "::warning::cascade PR #${PR} merge wedged in Gitea 'checking' (persistent 405); abandoning it and recreating ${SRC} -> ${TGT} (MokoGIT #1729)."
|
|
118
|
+
curl -s -o /dev/null -H "$AUTH" -H "Content-Type: application/json" \
|
|
119
|
+
-X PATCH "${API}/pulls/${PR}" -d '{"state":"closed"}' || true
|
|
120
|
+
local RESP NEWPR
|
|
121
|
+
RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
|
|
122
|
+
-d "{\"head\":\"${SRC}\",\"base\":\"${TGT}\",\"title\":\"chore(sync): cascade ${SRC} -> ${TGT} (recreated)\",\"body\":\"Recreated after the prior cascade PR wedged in Gitea 'checking' (405 'Please try again later'); see MokoGIT #1729.\"}")
|
|
123
|
+
NEWPR=$(printf '%s' "$RESP" | jqget number)
|
|
124
|
+
if [ -z "$NEWPR" ]; then
|
|
125
|
+
echo "::warning::Could not recreate cascade PR for ${TGT}: $RESP"; return 1
|
|
126
|
+
fi
|
|
127
|
+
PR="$NEWPR"
|
|
128
|
+
echo "Recreated cascade PR #${PR} (${SRC} -> ${TGT})"
|
|
129
|
+
return 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# SRC = the production branch this cascade flows FROM. Resolve it to the
|
|
133
|
+
# PRODUCTION branch, NOT default_branch: use `stable` if it exists, else
|
|
134
|
+
# `main` if it exists, else fall back to the repo default. This is the
|
|
135
|
+
# SRC=stable fix -- a channel-named repo whose DEFAULT is `dev` must still
|
|
136
|
+
# back-merge stable -> dev (+rc/alpha/beta), never cascade dev DOWN the
|
|
137
|
+
# ladder in the wrong direction. Existence is probed with the same
|
|
138
|
+
# /branches/<name> API the target loop uses (#1294 model).
|
|
139
|
+
DEFAULT_BRANCH="${{ github.event.repository.default_branch }}"
|
|
140
|
+
branch_exists() { curl -sf -H "$AUTH" "${API}/branches/$1" >/dev/null 2>&1; }
|
|
141
|
+
if branch_exists stable; then
|
|
142
|
+
SRC="stable"
|
|
143
|
+
elif branch_exists main; then
|
|
144
|
+
SRC="main"
|
|
145
|
+
else
|
|
146
|
+
SRC="${DEFAULT_BRANCH}"
|
|
147
|
+
fi
|
|
148
|
+
echo "Cascade source (production) branch: ${SRC} (default_branch=${DEFAULT_BRANCH})"
|
|
149
|
+
|
|
150
|
+
# The source must exist. A dev-only repo (no stable/main branch) has
|
|
151
|
+
# nothing to cascade FROM, so skip the whole run immediately.
|
|
152
|
+
if ! branch_exists "${SRC}"; then
|
|
153
|
+
echo "No ${SRC} branch (dev-only repo) - nothing to cascade."; exit 0
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
# notify(msg): ntfy that the CURRENT target ($TGT / $PR, dynamic scope from
|
|
157
|
+
# cascade_to) needs a manual merge. $1 = context sentence.
|
|
158
|
+
notify() {
|
|
159
|
+
curl -sS \
|
|
160
|
+
-H "Title: ${REPO}: ${TGT} cascade needs manual merge" \
|
|
161
|
+
-H "Tags: warning,twisted_rightwards_arrows" \
|
|
162
|
+
-H "Priority: high" \
|
|
163
|
+
-H "Click: ${MOKOGIT_URL}/${REPO}/pulls/${PR}" \
|
|
164
|
+
-d "${SRC} -> ${TGT} cascade PR #${PR} $1 It was NOT auto-merged; resolve it manually." \
|
|
165
|
+
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Land the current (already-committed) resolved merge on the protected
|
|
169
|
+
# target branch WITHOUT a direct push. The runner's github.token is refused
|
|
170
|
+
# by the target's pre-receive / branch-protection hook on a direct push, but
|
|
171
|
+
# it CAN merge a PR. So publish HEAD to a throwaway branch and land it on the
|
|
172
|
+
# target through the PR API. Reads $TGT / $PR from cascade_to (dynamic scope).
|
|
173
|
+
# $1 = note for the merge comment.
|
|
174
|
+
land_on_target_via_pr() {
|
|
175
|
+
local note="$1"
|
|
176
|
+
local tmp="cascade/auto-${TGT}-$(git rev-parse --short HEAD)"
|
|
177
|
+
# Publish the resolved merge to a throwaway branch. Prefer MOKOGIT_TOKEN
|
|
178
|
+
# (guaranteed push rights); the temp branch is NON-protected, so this does
|
|
179
|
+
# not bypass the target's protection -- the target is only ever written via
|
|
180
|
+
# the PR-merge API below. Fall back to origin (github.token) if unset.
|
|
181
|
+
local remote="origin"
|
|
182
|
+
[ -n "${MOKOGIT_TOKEN:-}" ] && remote="https://x-access-token:${MOKOGIT_TOKEN}@${MOKOGIT_URL#https://}/${REPO}.git"
|
|
183
|
+
if ! git push -f "${remote}" "HEAD:refs/heads/${tmp}"; then
|
|
184
|
+
echo "::error::Could not publish resolved merge to ${tmp}"; return 1
|
|
185
|
+
fi
|
|
186
|
+
local tpr resp
|
|
187
|
+
tpr=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=${TGT}" \
|
|
188
|
+
| 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')=='${tmp}'), ''))" 2>/dev/null || echo "")
|
|
189
|
+
if [ -z "$tpr" ]; then
|
|
190
|
+
resp=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
|
|
191
|
+
-d "{\"head\":\"${tmp}\",\"base\":\"${TGT}\",\"title\":\"chore(sync): cascade ${SRC} -> ${TGT} (auto-resolved)\",\"body\":\"Auto-resolved cascade merge, landed via PR (${TGT} is protected; the runner token cannot direct-push).\"}")
|
|
192
|
+
tpr=$(printf '%s' "$resp" | jqget number)
|
|
193
|
+
if [ -z "$tpr" ]; then
|
|
194
|
+
echo "::error::Could not open temp cascade PR: $resp"
|
|
195
|
+
git push "${remote}" --delete "${tmp}" >/dev/null 2>&1 || true
|
|
196
|
+
return 1
|
|
197
|
+
fi
|
|
198
|
+
fi
|
|
199
|
+
# Any failure exit from here MUST tear down ${tmp} + PR #${tpr} so a failed
|
|
200
|
+
# run does not leave orphaned cascade/auto-* branches / PRs.
|
|
201
|
+
_cleanup_tmp() {
|
|
202
|
+
curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH "${API}/pulls/${tpr}" -d '{"state":"closed"}' >/dev/null 2>&1 || true
|
|
203
|
+
git push "${remote}" --delete "${tmp}" >/dev/null 2>&1 || true
|
|
204
|
+
}
|
|
205
|
+
local m=""
|
|
206
|
+
for _ in 1 2 3 4 5 6; do
|
|
207
|
+
m=$(curl -sf -H "$AUTH" "${API}/pulls/${tpr}" | jqget mergeable)
|
|
208
|
+
case "$m" in True|False) break ;; esac
|
|
209
|
+
sleep 3
|
|
210
|
+
done
|
|
211
|
+
if [ "$m" != "True" ]; then
|
|
212
|
+
echo "::warning::Temp cascade PR #${tpr} not mergeable (mergeable=${m}); cleaning up."
|
|
213
|
+
_cleanup_tmp
|
|
214
|
+
return 1
|
|
215
|
+
fi
|
|
216
|
+
# Prefer an IMMEDIATE merge (delete_branch_after_merge is honoured on it).
|
|
217
|
+
# Right after PR creation MokoGIT may still be computing the merge base and
|
|
218
|
+
# returns HTTP 405 "Please try again later" -- retry with backoff. Do NOT rely
|
|
219
|
+
# on merge_when_checks_succeed as the primary path: on a repo with NO required
|
|
220
|
+
# checks it waits forever for checks that never run (target never lands).
|
|
221
|
+
prime_merge_status "${tpr}"
|
|
222
|
+
local code=000 attempt
|
|
223
|
+
for attempt in $(seq 1 12); do
|
|
224
|
+
code=$(curl -s -o /tmp/tmerge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls/${tpr}/merge" -d '{"Do":"merge","delete_branch_after_merge":true}')
|
|
225
|
+
{ [ "$code" -ge 200 ] && [ "$code" -lt 300 ]; } && break
|
|
226
|
+
echo "::notice::merge attempt ${attempt} for temp PR #${tpr} got HTTP ${code}; retrying in 15s"
|
|
227
|
+
sleep 15
|
|
228
|
+
done
|
|
229
|
+
if ! { [ "$code" -ge 200 ] && [ "$code" -lt 300 ]; }; then
|
|
230
|
+
code=$(curl -s -o /tmp/tmerge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls/${tpr}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true,"delete_branch_after_merge":true}')
|
|
231
|
+
fi
|
|
232
|
+
if [ "$code" -ge 200 ] && [ "$code" -lt 300 ]; then
|
|
233
|
+
echo "Cascade landed on ${TGT} via PR #${tpr} (merged, or scheduled to merge+delete when checks pass)."
|
|
234
|
+
curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/issues/${tpr}/comments" \
|
|
235
|
+
-d "{\"body\":\"${note}\"}" >/dev/null 2>&1 || true
|
|
236
|
+
# The original main->TGT PR is superseded by this resolved PR.
|
|
237
|
+
[ -n "${PR:-}" ] && curl -s -H "$AUTH" -H "Content-Type: application/json" -X PATCH "${API}/pulls/${PR}" \
|
|
238
|
+
-d '{"state":"closed"}' >/dev/null 2>&1 || true
|
|
239
|
+
return 0
|
|
240
|
+
fi
|
|
241
|
+
echo "::warning::Temp cascade PR #${tpr} merge returned HTTP ${code}: $(cat /tmp/tmerge.json); cleaning up."
|
|
242
|
+
_cleanup_tmp
|
|
243
|
+
return 1
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
# cascade_to(target): cascade main into one ladder target. Returns 0 on
|
|
247
|
+
# success/no-op/soft-fail-notified, non-zero only on an unexpected error so
|
|
248
|
+
# the loop can flag the run. $TGT / $PR are locals visible to the helpers
|
|
249
|
+
# above via bash dynamic scoping.
|
|
250
|
+
cascade_to() {
|
|
251
|
+
local TGT="$1"
|
|
252
|
+
local PR=""
|
|
253
|
+
|
|
254
|
+
# Target existence. dev is PERMANENT and self-heals; on-demand targets
|
|
255
|
+
# (rc/alpha/beta) are SKIPPED when absent -- an on-demand branch that does
|
|
256
|
+
# not exist must not be conjured into being by the cascade.
|
|
257
|
+
if ! curl -sf -H "$AUTH" "${API}/branches/${TGT}" >/dev/null 2>&1; then
|
|
258
|
+
if [ "$TGT" = "dev" ]; then
|
|
259
|
+
# LADDER FIX (defensive): the release ladder renames `dev` -> `rc` on
|
|
260
|
+
# promote and is meant to recreate a fresh `dev` from `main`. If that
|
|
261
|
+
# recreate ever fails to land, `dev` goes missing; recreate it from
|
|
262
|
+
# `main` via the Git POST /branches CREATE endpoint (governance permits
|
|
263
|
+
# ref creation even on a protected target; NOT a protection toggle).
|
|
264
|
+
# IDEMPOTENT: only runs when `dev` is genuinely absent.
|
|
265
|
+
echo "No dev branch - recreating it from ${SRC} (ladder RC-rename recovery)."
|
|
266
|
+
local CODE
|
|
267
|
+
CODE=$(curl -s -o /tmp/cascade_recreate_dev.json -w "%{http_code}" \
|
|
268
|
+
-H "$AUTH" -H "Content-Type: application/json" \
|
|
269
|
+
-X POST "${API}/branches" \
|
|
270
|
+
-d "{\"new_branch_name\":\"dev\",\"old_branch_name\":\"${SRC}\"}")
|
|
271
|
+
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
|
|
272
|
+
echo "Recreated dev from ${SRC} (HTTP ${CODE}); continuing cascade."
|
|
273
|
+
else
|
|
274
|
+
echo "::warning::Could not recreate dev from ${SRC} (HTTP ${CODE}): $(cat /tmp/cascade_recreate_dev.json 2>/dev/null)"
|
|
275
|
+
echo "No dev branch and recreate failed - nothing to cascade to dev."; return 0
|
|
276
|
+
fi
|
|
277
|
+
else
|
|
278
|
+
echo "On-demand target '${TGT}' does not exist - skipping."; return 0
|
|
279
|
+
fi
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# 1. is main ahead of TGT? Resolve BOTH sides to their BRANCH head SHAs
|
|
283
|
+
# first: the channel-named branch model (#1294) allows a same-named TAG
|
|
284
|
+
# (alpha/beta/stable exist as both a release tag AND a branch), and Gitea's
|
|
285
|
+
# compare resolves a bare ref as branch-OR-tag — so `compare/${TGT}...main`
|
|
286
|
+
# could measure against the like-named TAG and silently skip a needed
|
|
287
|
+
# cascade. The /branches/<name> API is branch-scoped, so its .commit.id is
|
|
288
|
+
# unambiguous; comparing by SHA can't be fooled by a same-named tag.
|
|
289
|
+
local AHEAD TGT_SHA MAIN_SHA
|
|
290
|
+
TGT_SHA=$(curl -sf -H "$AUTH" "${API}/branches/${TGT}" \
|
|
291
|
+
| python3 -c "import sys,json; print(json.load(sys.stdin).get('commit',{}).get('id',''))" 2>/dev/null || echo "")
|
|
292
|
+
MAIN_SHA=$(curl -sf -H "$AUTH" "${API}/branches/${SRC}" \
|
|
293
|
+
| python3 -c "import sys,json; print(json.load(sys.stdin).get('commit',{}).get('id',''))" 2>/dev/null || echo "")
|
|
294
|
+
if [ -z "$TGT_SHA" ] || [ -z "$MAIN_SHA" ]; then
|
|
295
|
+
echo "::warning::Could not resolve branch SHAs for ${TGT}/main — skipping cascade this run."; return 0
|
|
296
|
+
fi
|
|
297
|
+
AHEAD=$(curl -sf -H "$AUTH" "${API}/compare/${TGT_SHA}...${MAIN_SHA}" \
|
|
298
|
+
| python3 -c "import sys,json; print(json.load(sys.stdin).get('total_commits',0))" 2>/dev/null || echo 0)
|
|
299
|
+
if [ "${AHEAD:-0}" -eq 0 ]; then
|
|
300
|
+
echo "${TGT} already up to date with main."; return 0
|
|
301
|
+
fi
|
|
302
|
+
echo "main is ${AHEAD} commit(s) ahead of ${TGT}."
|
|
303
|
+
|
|
304
|
+
# 2. reuse an open main->TGT PR, else create one
|
|
305
|
+
PR=$(curl -sf -H "$AUTH" "${API}/pulls?state=open&base=${TGT}" \
|
|
306
|
+
| 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')=='${SRC}'), ''))" 2>/dev/null || echo "")
|
|
307
|
+
if [ -z "$PR" ]; then
|
|
308
|
+
local RESP
|
|
309
|
+
RESP=$(curl -s -H "$AUTH" -H "Content-Type: application/json" -X POST "${API}/pulls" \
|
|
310
|
+
-d "{\"head\":\"${SRC}\",\"base\":\"${TGT}\",\"title\":\"chore(sync): cascade ${SRC} -> ${TGT}\",\"body\":\"Automated cascade of ${SRC} into ${TGT}. Auto-merges when conflict-free, auto-resolves VERSION-stamp-only conflicts, otherwise left open for manual resolution.\"}")
|
|
311
|
+
PR=$(printf '%s' "$RESP" | jqget number)
|
|
312
|
+
if [ -z "$PR" ]; then
|
|
313
|
+
echo "::warning::Could not open cascade PR for ${TGT}: $RESP"; return 0
|
|
314
|
+
fi
|
|
315
|
+
echo "Opened cascade PR #${PR} (${SRC} -> ${TGT})"
|
|
316
|
+
else
|
|
317
|
+
echo "Reusing open cascade PR #${PR} (${SRC} -> ${TGT})"
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
# 3. wait for MokoGIT to compute mergeability (conflict detection)
|
|
321
|
+
local MERGEABLE=""
|
|
322
|
+
for _ in 1 2 3 4 5 6; do
|
|
323
|
+
MERGEABLE=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqget mergeable)
|
|
324
|
+
case "$MERGEABLE" in True|False) break ;; esac
|
|
325
|
+
sleep 3
|
|
326
|
+
done
|
|
327
|
+
echo "mergeable=${MERGEABLE}"
|
|
328
|
+
|
|
329
|
+
# 4a. conflict-free -> IMMEDIATE merge via API (retry 405 "try again later";
|
|
330
|
+
# only fall back to merge_when_checks_succeed if genuinely refused).
|
|
331
|
+
if [ "$MERGEABLE" = "True" ]; then
|
|
332
|
+
# Ensure the head has a passing status so Do:merge lands now, not deferred.
|
|
333
|
+
prime_merge_status "${PR}"
|
|
334
|
+
local CODE=000 attempt
|
|
335
|
+
for attempt in $(seq 1 12); do
|
|
336
|
+
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
|
|
337
|
+
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge"}')
|
|
338
|
+
{ [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; } && break
|
|
339
|
+
echo "::notice::merge attempt ${attempt} for cascade PR #${PR} got HTTP ${CODE}; retrying in 15s"
|
|
340
|
+
sleep 15
|
|
341
|
+
done
|
|
342
|
+
# Persistent 405 "Please try again later" across the whole retry loop ==
|
|
343
|
+
# the PR's mergeability check is wedged in Gitea's "checking" state
|
|
344
|
+
# (MokoGIT #1729 RC4). Recreating the PR is the ONLY recovery: close the
|
|
345
|
+
# wedged one, open a fresh ${SRC}->${TGT} PR, re-check mergeability, and
|
|
346
|
+
# retry the immediate merge ONCE on the new PR before any other fallback.
|
|
347
|
+
# Non-405 failures short-circuit this and fall straight through to notify.
|
|
348
|
+
if ! { [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; } \
|
|
349
|
+
&& [ "$CODE" = "405" ] && grep -q "Please try again later" /tmp/merge.json 2>/dev/null \
|
|
350
|
+
&& recreate_cascade_pr; then
|
|
351
|
+
local MERGEABLE2=""
|
|
352
|
+
for _ in 1 2 3 4 5 6; do
|
|
353
|
+
MERGEABLE2=$(curl -sf -H "$AUTH" "${API}/pulls/${PR}" | jqget mergeable)
|
|
354
|
+
case "$MERGEABLE2" in True|False) break ;; esac
|
|
355
|
+
sleep 3
|
|
356
|
+
done
|
|
357
|
+
if [ "$MERGEABLE2" = "True" ]; then
|
|
358
|
+
prime_merge_status "${PR}"
|
|
359
|
+
for attempt in $(seq 1 12); do
|
|
360
|
+
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
|
|
361
|
+
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge"}')
|
|
362
|
+
{ [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; } && break
|
|
363
|
+
echo "::notice::merge attempt ${attempt} for recreated cascade PR #${PR} got HTTP ${CODE}; retrying in 15s"
|
|
364
|
+
sleep 15
|
|
365
|
+
done
|
|
366
|
+
fi
|
|
367
|
+
fi
|
|
368
|
+
# Still not merged -> last-resort deferred merge (merge-when-checks-succeed).
|
|
369
|
+
if ! { [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; }; then
|
|
370
|
+
CODE=$(curl -s -o /tmp/merge.json -w "%{http_code}" -H "$AUTH" -H "Content-Type: application/json" \
|
|
371
|
+
-X POST "${API}/pulls/${PR}/merge" -d '{"Do":"merge","merge_when_checks_succeed":true}')
|
|
372
|
+
fi
|
|
373
|
+
if [ "$CODE" -ge 200 ] && [ "$CODE" -lt 300 ]; then
|
|
374
|
+
echo "Cascade PR #${PR} merged (or scheduled to merge when checks pass)."
|
|
375
|
+
return 0
|
|
376
|
+
fi
|
|
377
|
+
echo "::warning::Auto-merge returned HTTP ${CODE}: $(cat /tmp/merge.json)"
|
|
378
|
+
notify "could not be auto-merged (HTTP ${CODE})."
|
|
379
|
+
return 0
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
# 4b. conflicts -> try to auto-resolve if they are ONLY routine classes.
|
|
383
|
+
echo "PR not cleanly mergeable; checking whether conflicts are auto-resolvable..."
|
|
384
|
+
git config user.name "MokoGIT Cascade"
|
|
385
|
+
git config user.email "actions@mokoconsulting.tech"
|
|
386
|
+
# Fetch the BRANCH refs explicitly (refs/heads/...): the channel-named
|
|
387
|
+
# branch model (#1294) allows a tag and a branch to share a name (e.g. a
|
|
388
|
+
# `stable`/`rc` release tag alongside the branch), so a bare `git fetch
|
|
389
|
+
# origin ${SRC} ${TGT}` could resolve to the same-named TAG and poison
|
|
390
|
+
# origin/${TGT} + the merge below. Pin refs/heads/* into the remote-tracking
|
|
391
|
+
# refs so only branches are considered.
|
|
392
|
+
git fetch --quiet origin \
|
|
393
|
+
"+refs/heads/${SRC}:refs/remotes/origin/${SRC}" \
|
|
394
|
+
"+refs/heads/${TGT}:refs/remotes/origin/${TGT}"
|
|
395
|
+
git checkout -B "${TGT}" "origin/${TGT}"
|
|
396
|
+
|
|
397
|
+
if git merge --no-ff --no-commit "origin/${SRC}" >/dev/null 2>&1; then
|
|
398
|
+
# Became clean at git level (e.g. mergeability was still computing).
|
|
399
|
+
git commit -m "chore(sync): cascade ${SRC} -> ${TGT} [skip ci]" >/dev/null
|
|
400
|
+
if land_on_target_via_pr "Cascade merged cleanly at git level and landed on ${TGT} via PR."; then return 0; fi
|
|
401
|
+
notify "merged cleanly but could not be landed on ${TGT} via PR."
|
|
402
|
+
return 0
|
|
403
|
+
fi
|
|
404
|
+
|
|
405
|
+
local CONFLICTS
|
|
406
|
+
CONFLICTS=$(git diff --name-only --diff-filter=U)
|
|
407
|
+
echo "Conflicted files:"; echo "${CONFLICTS}"
|
|
408
|
+
|
|
409
|
+
# --- Inline resolver (pure bash/git, NO mokocli dependency) -------------
|
|
410
|
+
# * manifest <version> conflicts (*.xml with <extension) -> version =
|
|
411
|
+
# max(TGT, main) via `sort -V`, preserving the file's indentation.
|
|
412
|
+
# * .mokogit/** .gitea/** .github/** -> take main/canonical (--theirs).
|
|
413
|
+
# * docs / CHANGELOG / stamp files -> keep TGT (--ours).
|
|
414
|
+
# Anything still conflicted falls through to cascade_resolve.php then human.
|
|
415
|
+
resolve_manifest_version() {
|
|
416
|
+
local f="$1"
|
|
417
|
+
local nonver
|
|
418
|
+
nonver=$(awk '
|
|
419
|
+
/^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
|
|
420
|
+
(c==1||c==2) && $0 !~ /<version>[^<]*<\/version>/ {print "X"}
|
|
421
|
+
' "$f")
|
|
422
|
+
[ -n "$nonver" ] && return 1
|
|
423
|
+
local ours theirs maxv
|
|
424
|
+
ours=$(awk '
|
|
425
|
+
/^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
|
|
426
|
+
c==1' "$f" | sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' | head -n1)
|
|
427
|
+
theirs=$(awk '
|
|
428
|
+
/^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
|
|
429
|
+
c==2' "$f" | sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' | head -n1)
|
|
430
|
+
[ -z "$ours" ] && [ -z "$theirs" ] && return 1
|
|
431
|
+
maxv=$(printf '%s\n%s\n' "$ours" "$theirs" | grep -v '^$' | sort -V | tail -n1)
|
|
432
|
+
[ -z "$maxv" ] && return 1
|
|
433
|
+
awk -v maxv="$maxv" '
|
|
434
|
+
/^<<<<<<< /{c=1;next}
|
|
435
|
+
/^=======$/{c=2;next}
|
|
436
|
+
/^>>>>>>> /{c=0;next}
|
|
437
|
+
{
|
|
438
|
+
if (c==2) next
|
|
439
|
+
if (($0 ~ /<version>[^<]*<\/version>/) && (c==1)) {
|
|
440
|
+
sub(/<version>[^<]*<\/version>/, "<version>" maxv "</version>")
|
|
441
|
+
}
|
|
442
|
+
print
|
|
443
|
+
}' "$f" > "${f}.resolved" && mv "${f}.resolved" "$f"
|
|
444
|
+
! grep -qE '^(<<<<<<< |=======$|>>>>>>> )' "$f"
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
# resolve_makefile_version: auto-resolve a Makefile conflict whose ONLY
|
|
448
|
+
# conflicting content is the PROJECT_VERSION assignment, taking
|
|
449
|
+
# version = max(TGT, main) via `sort -V`. This closes the strand-dev gap:
|
|
450
|
+
# a version change that reaches the production branch out of band (e.g. a
|
|
451
|
+
# hotfix bump made directly on stable, bypassing bump-on-dev) can now
|
|
452
|
+
# cascade back to dev instead of parking an unresolved conflict.
|
|
453
|
+
resolve_makefile_version() {
|
|
454
|
+
local f="$1"
|
|
455
|
+
local nonver
|
|
456
|
+
nonver=$(awk '
|
|
457
|
+
/^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
|
|
458
|
+
(c==1||c==2) && $0 !~ /^[ \t]*PROJECT_VERSION[ \t]*[:?]*=/ {print "X"}
|
|
459
|
+
' "$f")
|
|
460
|
+
[ -n "$nonver" ] && return 1
|
|
461
|
+
local ours theirs maxv
|
|
462
|
+
ours=$(awk '
|
|
463
|
+
/^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
|
|
464
|
+
c==1' "$f" | sed -n 's/^[ \t]*PROJECT_VERSION[ \t]*[:?]*=[ \t]*\([0-9][0-9.]*\).*/\1/p' | head -n1)
|
|
465
|
+
theirs=$(awk '
|
|
466
|
+
/^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
|
|
467
|
+
c==2' "$f" | sed -n 's/^[ \t]*PROJECT_VERSION[ \t]*[:?]*=[ \t]*\([0-9][0-9.]*\).*/\1/p' | head -n1)
|
|
468
|
+
[ -z "$ours" ] && [ -z "$theirs" ] && return 1
|
|
469
|
+
maxv=$(printf '%s\n%s\n' "$ours" "$theirs" | grep -v '^$' | sort -V | tail -n1)
|
|
470
|
+
[ -z "$maxv" ] && return 1
|
|
471
|
+
awk -v maxv="$maxv" '
|
|
472
|
+
/^<<<<<<< /{c=1;next}
|
|
473
|
+
/^=======$/{c=2;next}
|
|
474
|
+
/^>>>>>>> /{c=0;next}
|
|
475
|
+
{
|
|
476
|
+
if (c==2) next
|
|
477
|
+
if (($0 ~ /^[ \t]*PROJECT_VERSION[ \t]*[:?]*=/) && (c==1)) {
|
|
478
|
+
sub(/[0-9][0-9.]*[0-9]/, maxv)
|
|
479
|
+
}
|
|
480
|
+
print
|
|
481
|
+
}' "$f" > "${f}.resolved" && mv "${f}.resolved" "$f"
|
|
482
|
+
! grep -qE '^(<<<<<<< |=======$|>>>>>>> )' "$f"
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# resolve_json_version: same idea for a package.json conflict whose ONLY
|
|
486
|
+
# conflicting content is the top-level "version" field -> version = max.
|
|
487
|
+
resolve_json_version() {
|
|
488
|
+
local f="$1"
|
|
489
|
+
local nonver
|
|
490
|
+
nonver=$(awk '
|
|
491
|
+
/^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
|
|
492
|
+
(c==1||c==2) && $0 !~ /"version"[ \t]*:/ {print "X"}
|
|
493
|
+
' "$f")
|
|
494
|
+
[ -n "$nonver" ] && return 1
|
|
495
|
+
local ours theirs maxv
|
|
496
|
+
ours=$(awk '
|
|
497
|
+
/^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
|
|
498
|
+
c==1' "$f" | sed -n 's/.*"version"[ \t]*:[ \t]*"\([^"]*\)".*/\1/p' | head -n1)
|
|
499
|
+
theirs=$(awk '
|
|
500
|
+
/^<<<<<<< /{c=1;next} /^=======$/{c=2;next} /^>>>>>>> /{c=0;next}
|
|
501
|
+
c==2' "$f" | sed -n 's/.*"version"[ \t]*:[ \t]*"\([^"]*\)".*/\1/p' | head -n1)
|
|
502
|
+
[ -z "$ours" ] && [ -z "$theirs" ] && return 1
|
|
503
|
+
maxv=$(printf '%s\n%s\n' "$ours" "$theirs" | grep -v '^$' | sort -V | tail -n1)
|
|
504
|
+
[ -z "$maxv" ] && return 1
|
|
505
|
+
awk -v maxv="$maxv" '
|
|
506
|
+
/^<<<<<<< /{c=1;next}
|
|
507
|
+
/^=======$/{c=2;next}
|
|
508
|
+
/^>>>>>>> /{c=0;next}
|
|
509
|
+
{
|
|
510
|
+
if (c==2) next
|
|
511
|
+
if (($0 ~ /"version"[ \t]*:/) && (c==1)) {
|
|
512
|
+
sub(/"[0-9][^"]*"/, "\"" maxv "\"")
|
|
513
|
+
}
|
|
514
|
+
print
|
|
515
|
+
}' "$f" > "${f}.resolved" && mv "${f}.resolved" "$f"
|
|
516
|
+
! grep -qE '^(<<<<<<< |=======$|>>>>>>> )' "$f"
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
local f
|
|
520
|
+
for f in ${CONFLICTS}; do
|
|
521
|
+
case "$f" in
|
|
522
|
+
.mokogit/*|.gitea/*|.github/*)
|
|
523
|
+
git checkout --theirs -- "$f" && git add -- "$f" \
|
|
524
|
+
&& echo "resolved (theirs/main): $f" ;;
|
|
525
|
+
*CHANGELOG*|*/docs/*|docs/*|*.md|*VERSION*|*.stamp)
|
|
526
|
+
git checkout --ours -- "$f" && git add -- "$f" \
|
|
527
|
+
&& echo "resolved (ours/${TGT}): $f" ;;
|
|
528
|
+
*.xml)
|
|
529
|
+
if grep -q '<extension' "$f" 2>/dev/null && resolve_manifest_version "$f"; then
|
|
530
|
+
git add -- "$f"; echo "resolved (manifest version=max): $f"
|
|
531
|
+
else
|
|
532
|
+
echo "unresolved (xml, not a simple version stamp): $f"
|
|
533
|
+
fi ;;
|
|
534
|
+
Makefile|*/Makefile)
|
|
535
|
+
if resolve_makefile_version "$f"; then
|
|
536
|
+
git add -- "$f"; echo "resolved (Makefile PROJECT_VERSION=max): $f"
|
|
537
|
+
else
|
|
538
|
+
echo "unresolved (Makefile, not a simple version bump): $f"
|
|
539
|
+
fi ;;
|
|
540
|
+
package.json|*/package.json)
|
|
541
|
+
if resolve_json_version "$f"; then
|
|
542
|
+
git add -- "$f"; echo "resolved (package.json version=max): $f"
|
|
543
|
+
else
|
|
544
|
+
echo "unresolved (package.json, not a simple version bump): $f"
|
|
545
|
+
fi ;;
|
|
546
|
+
composer.json|*/composer.json)
|
|
547
|
+
if resolve_json_version "$f"; then
|
|
548
|
+
git add -- "$f"; echo "resolved (composer.json version=max): $f"
|
|
549
|
+
else
|
|
550
|
+
echo "unresolved (composer.json, not a simple version bump): $f"
|
|
551
|
+
fi ;;
|
|
552
|
+
*)
|
|
553
|
+
echo "unresolved (needs deeper resolver): $f" ;;
|
|
554
|
+
esac
|
|
555
|
+
done
|
|
556
|
+
|
|
557
|
+
# If the inline pass cleared every conflict, land it -- no mokocli needed.
|
|
558
|
+
if [ -z "$(git diff --name-only --diff-filter=U)" ]; then
|
|
559
|
+
git commit -m "chore(sync): cascade ${SRC} -> ${TGT} (auto-resolved inline) [skip ci]" >/dev/null
|
|
560
|
+
if land_on_target_via_pr "Cascade auto-resolved inline (manifest version=max(${TGT},main), .mokogit/.gitea/.github -> main, docs/stamps -> ${TGT}) with no mokocli dependency, and landed on ${TGT} via PR."; then return 0; fi
|
|
561
|
+
notify "was auto-resolved inline but could not be landed on ${TGT} via PR."
|
|
562
|
+
return 0
|
|
563
|
+
fi
|
|
564
|
+
|
|
565
|
+
echo "Inline pass left conflicts; falling back to cascade_resolve.php..."
|
|
566
|
+
echo "Still conflicted:"; git diff --name-only --diff-filter=U
|
|
567
|
+
|
|
568
|
+
# --- Fallback: cascade_resolve.php (existing behaviour) -----------------
|
|
569
|
+
# BAILS (exit 2) on real code conflicts, so those still get a human.
|
|
570
|
+
# Standard mokocli install: FRESH from the MokoGIT Composer registry
|
|
571
|
+
# (PRIMARY); /opt/mokocli is a FAILSAFE only (may be stale). Registry path
|
|
572
|
+
# /api/packages/** is anon-allowlisted. git clone is the last resort.
|
|
573
|
+
local RESOLVER=""
|
|
574
|
+
local REG='{"type":"composer","url":"https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer"}'
|
|
575
|
+
if ! command -v composer >/dev/null 2>&1; then
|
|
576
|
+
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1 || true
|
|
577
|
+
fi
|
|
578
|
+
rm -rf /tmp/mokocli
|
|
579
|
+
if command -v composer >/dev/null 2>&1 \
|
|
580
|
+
&& composer create-project --no-install --no-interaction --no-progress --quiet \
|
|
581
|
+
--repository="${REG}" mokoconsulting/mokocli /tmp/mokocli >/dev/null 2>&1 \
|
|
582
|
+
&& [ -f /tmp/mokocli/cli/cascade_resolve.php ]; then
|
|
583
|
+
RESOLVER=/tmp/mokocli/cli/cascade_resolve.php
|
|
584
|
+
if command -v sudo >/dev/null 2>&1 && [ -d /opt/mokocli ] \
|
|
585
|
+
&& ! cmp -s /tmp/mokocli/cli/version_reset_dev.php /opt/mokocli/cli/version_reset_dev.php 2>/dev/null; then
|
|
586
|
+
sudo cp -a /tmp/mokocli/. /opt/mokocli/ 2>/dev/null || true
|
|
587
|
+
fi
|
|
588
|
+
elif [ -f /opt/mokocli/cli/cascade_resolve.php ]; then
|
|
589
|
+
echo "::warning::registry install failed — using /opt/mokocli FAILSAFE cascade_resolve.php (may be stale)"
|
|
590
|
+
RESOLVER=/opt/mokocli/cli/cascade_resolve.php
|
|
591
|
+
elif git clone --depth 1 --quiet \
|
|
592
|
+
"https://x-access-token:${MOKOGIT_TOKEN}@${MOKOGIT_URL#https://}/MokoConsulting/mokocli.git" \
|
|
593
|
+
/tmp/mokocli 2>/dev/null \
|
|
594
|
+
|| git clone --depth 1 --quiet "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli 2>/dev/null; then
|
|
595
|
+
RESOLVER=/tmp/mokocli/cli/cascade_resolve.php
|
|
596
|
+
else
|
|
597
|
+
echo "::warning::On-demand mokocli install failed; cascade_resolve.php is UNAVAILABLE. Inline resolver handled the routine classes; any remaining conflicts need a human."
|
|
598
|
+
RESOLVER=/tmp/mokocli/cli/cascade_resolve.php
|
|
599
|
+
fi
|
|
600
|
+
|
|
601
|
+
if [ -f "$RESOLVER" ] && php "$RESOLVER" --path . --theirs-ref "origin/${SRC}"; then
|
|
602
|
+
git commit -m "chore(sync): cascade ${SRC} -> ${TGT} (auto-resolved) [skip ci]" >/dev/null
|
|
603
|
+
if land_on_target_via_pr "cascade_resolve auto-resolved the merge (manifests/workflows/docs/stamps, modify-delete) and landed on ${TGT} via PR."; then return 0; fi
|
|
604
|
+
notify "was auto-resolved but could not be landed on ${TGT} via PR."
|
|
605
|
+
return 0
|
|
606
|
+
fi
|
|
607
|
+
|
|
608
|
+
# Resolver unavailable, or it bailed on a real (code) conflict -> human.
|
|
609
|
+
git merge --abort || true
|
|
610
|
+
notify "has code conflicts cascade_resolve could not safely auto-resolve."
|
|
611
|
+
return 0
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
# Cascade main into every ladder target that exists, high -> low.
|
|
615
|
+
RC=0
|
|
616
|
+
for TGT in ${CASCADE_TARGETS}; do
|
|
617
|
+
echo "::group::cascade ${SRC} -> ${TGT}"
|
|
618
|
+
cascade_to "${TGT}" || { echo "::warning::cascade_to ${TGT} returned non-zero"; RC=1; }
|
|
619
|
+
echo "::endgroup::"
|
|
620
|
+
done
|
|
621
|
+
|
|
622
|
+
# R4 (MokoCLI #967): optional SECOND pass -- cascade dev into its child
|
|
623
|
+
# BRANCHES. Strict no-op unless DEV_CHILD_TARGETS is set (defaults empty),
|
|
624
|
+
# so no existing repo changes behaviour. Repoint SRC to `dev` so the same
|
|
625
|
+
# cascade_to/helpers flow dev -> each child; guarded by dev existing. A
|
|
626
|
+
# child named `dev` would be self-referential, so it is skipped. dev ->
|
|
627
|
+
# child REPOS is the SYNC engine's job and is intentionally not done here.
|
|
628
|
+
if [ -n "${DEV_CHILD_TARGETS// /}" ]; then
|
|
629
|
+
if branch_exists dev; then
|
|
630
|
+
SRC="dev"
|
|
631
|
+
echo "Dev-child cascade source branch: ${SRC}; targets: ${DEV_CHILD_TARGETS}"
|
|
632
|
+
for TGT in ${DEV_CHILD_TARGETS}; do
|
|
633
|
+
if [ "${TGT}" = "dev" ]; then
|
|
634
|
+
echo "Skipping self-referential dev-child target 'dev'."; continue
|
|
635
|
+
fi
|
|
636
|
+
echo "::group::cascade ${SRC} -> ${TGT}"
|
|
637
|
+
cascade_to "${TGT}" || { echo "::warning::cascade_to ${TGT} returned non-zero"; RC=1; }
|
|
638
|
+
echo "::endgroup::"
|
|
639
|
+
done
|
|
640
|
+
else
|
|
641
|
+
echo "DEV_CHILD_TARGETS set but no dev branch - skipping dev-child cascade."
|
|
642
|
+
fi
|
|
643
|
+
fi
|
|
644
|
+
|
|
645
|
+
exit "${RC}"
|