@mokoconsulting/mcp-mokosuite 1.1.0-dev.372 → 1.1.0
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/.mokogit/workflows/auto-release.yml +142 -24
- package/.mokogit/workflows/cleanup.yml +4 -3
- package/.mokogit/workflows/issue-branch.yml +1 -1
- package/.mokogit/workflows/npm-build-test.yml +310 -28
- package/.mokogit/workflows/npm-publish.yml +98 -109
- package/.mokogit/workflows/pre-release.yml +20 -3
- package/.mokogit/workflows/rc-revert.yml +4 -1
- package/CHANGELOG.md +0 -17
- package/Makefile +72 -48
- package/dist/index.js +1 -28
- package/package.json +1 -1
- package/src/index.ts +1 -59
- package/.mokogit/workflows/cascade-dev.yml +0 -266
- package/.mokogit/workflows/changelog-validation.yml +0 -117
- package/.mokogit/workflows/custom-workflow-sync-trigger.yml +0 -83
- package/.mokogit/workflows/gitleaks.yml +0 -94
- package/.mokogit/workflows/notify.yml +0 -73
- package/.mokogit/workflows/pr-check.yml +0 -693
- package/.mokogit/workflows/push-notify.yml +0 -43
- package/.mokogit/workflows/sync-version-on-merge.yml +0 -130
- package/.mokogit/workflows/version-set.yml +0 -131
|
@@ -1,693 +0,0 @@
|
|
|
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.CI
|
|
8
|
-
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
9
|
-
# PATH: /.mokogit/workflows/pr-check.yml
|
|
10
|
-
# VERSION: 09.24.00
|
|
11
|
-
# BRIEF: PR gate — branch policy + code validation before merge
|
|
12
|
-
|
|
13
|
-
name: "Universal: PR Check"
|
|
14
|
-
|
|
15
|
-
on:
|
|
16
|
-
pull_request:
|
|
17
|
-
types: [opened, synchronize, reopened, edited]
|
|
18
|
-
|
|
19
|
-
permissions:
|
|
20
|
-
contents: read
|
|
21
|
-
pull-requests: write
|
|
22
|
-
|
|
23
|
-
env:
|
|
24
|
-
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
25
|
-
|
|
26
|
-
concurrency:
|
|
27
|
-
# storm-reduction: collapse superseded validation runs on the same ref
|
|
28
|
-
group: ${{ github.workflow }}-${{ github.ref }}
|
|
29
|
-
cancel-in-progress: true
|
|
30
|
-
|
|
31
|
-
jobs:
|
|
32
|
-
# ── Branch Policy ──────────────────────────────────────────────────────
|
|
33
|
-
branch-policy:
|
|
34
|
-
name: Branch Policy
|
|
35
|
-
runs-on: ubuntu-latest
|
|
36
|
-
steps:
|
|
37
|
-
- name: Check branch merge target
|
|
38
|
-
run: |
|
|
39
|
-
HEAD="${{ github.head_ref }}"
|
|
40
|
-
BASE="${{ github.base_ref }}"
|
|
41
|
-
|
|
42
|
-
echo "PR: ${HEAD} → ${BASE}"
|
|
43
|
-
|
|
44
|
-
ALLOWED=true
|
|
45
|
-
REASON=""
|
|
46
|
-
|
|
47
|
-
case "$HEAD" in
|
|
48
|
-
feature/*|feat/*)
|
|
49
|
-
if [ "$BASE" != "dev" ]; then
|
|
50
|
-
ALLOWED=false
|
|
51
|
-
REASON="Feature branches must target 'dev', not '${BASE}'"
|
|
52
|
-
fi
|
|
53
|
-
;;
|
|
54
|
-
fix/*|bugfix/*)
|
|
55
|
-
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
|
56
|
-
ALLOWED=false
|
|
57
|
-
REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
|
|
58
|
-
fi
|
|
59
|
-
;;
|
|
60
|
-
patch/*)
|
|
61
|
-
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
|
|
62
|
-
ALLOWED=false
|
|
63
|
-
REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
|
|
64
|
-
fi
|
|
65
|
-
;;
|
|
66
|
-
hotfix/*)
|
|
67
|
-
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
|
68
|
-
ALLOWED=false
|
|
69
|
-
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
|
70
|
-
fi
|
|
71
|
-
;;
|
|
72
|
-
rc)
|
|
73
|
-
if [ "$BASE" != "main" ]; then
|
|
74
|
-
ALLOWED=false
|
|
75
|
-
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
|
76
|
-
fi
|
|
77
|
-
;;
|
|
78
|
-
dev)
|
|
79
|
-
if [ "$BASE" != "main" ]; then
|
|
80
|
-
ALLOWED=false
|
|
81
|
-
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
|
82
|
-
fi
|
|
83
|
-
;;
|
|
84
|
-
esac
|
|
85
|
-
|
|
86
|
-
if [ "$ALLOWED" = false ]; then
|
|
87
|
-
echo "::error::${REASON}"
|
|
88
|
-
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
|
89
|
-
echo "" >> $GITHUB_STEP_SUMMARY
|
|
90
|
-
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
|
91
|
-
echo "" >> $GITHUB_STEP_SUMMARY
|
|
92
|
-
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
|
93
|
-
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
|
94
|
-
echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
|
95
|
-
echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
|
|
96
|
-
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
|
97
|
-
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
|
98
|
-
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
|
99
|
-
exit 1
|
|
100
|
-
fi
|
|
101
|
-
|
|
102
|
-
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
|
103
|
-
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
|
104
|
-
|
|
105
|
-
# ── Docs Update Gate (main PRs) ─────────────────────────────────────────
|
|
106
|
-
require-docs:
|
|
107
|
-
name: Require Docs Update
|
|
108
|
-
runs-on: ubuntu-latest
|
|
109
|
-
# Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
|
|
110
|
-
if: ${{ github.base_ref == 'main' }}
|
|
111
|
-
steps:
|
|
112
|
-
- name: Checkout
|
|
113
|
-
uses: actions/checkout@v4
|
|
114
|
-
with:
|
|
115
|
-
fetch-depth: 0
|
|
116
|
-
|
|
117
|
-
- name: Require README.md and CHANGELOG.md in the PR diff
|
|
118
|
-
run: |
|
|
119
|
-
BASE="${{ github.event.pull_request.base.sha }}"
|
|
120
|
-
HEAD="${{ github.event.pull_request.head.sha }}"
|
|
121
|
-
CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
|
|
122
|
-
if [ -z "$CHANGED" ]; then
|
|
123
|
-
git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
|
|
124
|
-
CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
|
|
125
|
-
fi
|
|
126
|
-
echo "Changed files in PR:"
|
|
127
|
-
echo "$CHANGED"
|
|
128
|
-
MISSING=""
|
|
129
|
-
echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
|
|
130
|
-
echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
|
|
131
|
-
if [ -n "$MISSING" ]; then
|
|
132
|
-
echo "::error::PRs into main must update: ${MISSING}"
|
|
133
|
-
{
|
|
134
|
-
echo "## Docs Update Required"
|
|
135
|
-
echo ""
|
|
136
|
-
echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
|
|
137
|
-
echo ""
|
|
138
|
-
echo "Not updated in this PR: **${MISSING}**"
|
|
139
|
-
} >> "$GITHUB_STEP_SUMMARY"
|
|
140
|
-
exit 1
|
|
141
|
-
fi
|
|
142
|
-
echo "Docs update present (README.md + CHANGELOG.md)"
|
|
143
|
-
echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
|
|
144
|
-
|
|
145
|
-
# ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
|
|
146
|
-
wiki-reminder:
|
|
147
|
-
name: Wiki Update Reminder
|
|
148
|
-
runs-on: ubuntu-latest
|
|
149
|
-
if: ${{ github.base_ref == 'main' }}
|
|
150
|
-
steps:
|
|
151
|
-
- name: Remind to update the wiki
|
|
152
|
-
env:
|
|
153
|
-
TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
|
|
154
|
-
SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
155
|
-
REPO: ${{ github.repository }}
|
|
156
|
-
PR: ${{ github.event.pull_request.number }}
|
|
157
|
-
run: |
|
|
158
|
-
set -uo pipefail
|
|
159
|
-
{
|
|
160
|
-
echo "## Wiki Update Reminder"
|
|
161
|
-
echo ""
|
|
162
|
-
echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
|
|
163
|
-
echo ""
|
|
164
|
-
echo "- ${SERVER}/${REPO}/wiki"
|
|
165
|
-
echo ""
|
|
166
|
-
echo "_Non-blocking reminder._"
|
|
167
|
-
} >> "$GITHUB_STEP_SUMMARY"
|
|
168
|
-
# Sticky PR comment: create-or-UPDATE a single comment keyed by a hidden
|
|
169
|
-
# marker (no duplicate reminders across pushes). Best-effort, never fails.
|
|
170
|
-
LIST="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
|
|
171
|
-
EDIT="${SERVER}/api/v1/repos/${REPO}/issues/comments"
|
|
172
|
-
MARKER="<!-- wiki-reminder -->"
|
|
173
|
-
BODY="$(printf '%s\n\n%s' "${MARKER}" "**Wiki reminder:** docs are wiki-first -- if this PR changes behavior, usage, config, or standards, please update the repo wiki before/after merge. _(non-blocking)_")"
|
|
174
|
-
if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
|
|
175
|
-
CID="$(curl -sf -H "Authorization: token ${TOKEN}" "${LIST}?limit=100" 2>/dev/null \
|
|
176
|
-
| python3 -c "import sys,json; c=[x for x in json.load(sys.stdin) if '${MARKER}' in (x.get('body') or '')]; print(c[-1]['id'] if c else '')" 2>/dev/null || true)"
|
|
177
|
-
PAYLOAD="$(python3 -c "import json,sys; print(json.dumps({'body': sys.stdin.read()}))" <<<"${BODY}")"
|
|
178
|
-
if [ -n "${CID}" ]; then
|
|
179
|
-
curl -sf -X PATCH -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${EDIT}/${CID}" >/dev/null 2>&1 || true
|
|
180
|
-
else
|
|
181
|
-
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${LIST}" >/dev/null 2>&1 || true
|
|
182
|
-
fi
|
|
183
|
-
fi
|
|
184
|
-
echo "Wiki reminder emitted (non-blocking, sticky)."
|
|
185
|
-
|
|
186
|
-
validate:
|
|
187
|
-
name: Validate PR
|
|
188
|
-
runs-on: ubuntu-latest
|
|
189
|
-
# Skip on template repos (Template-*) — no real manifest/source/changelog to validate.
|
|
190
|
-
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
191
|
-
|
|
192
|
-
steps:
|
|
193
|
-
- name: Checkout
|
|
194
|
-
uses: actions/checkout@v4
|
|
195
|
-
|
|
196
|
-
- name: Check for merge conflict markers
|
|
197
|
-
run: |
|
|
198
|
-
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogit' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
|
199
|
-
if [ -n "$CONFLICTS" ]; then
|
|
200
|
-
echo "::error::Merge conflict markers found in source files"
|
|
201
|
-
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
|
202
|
-
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
203
|
-
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
|
204
|
-
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
205
|
-
exit 1
|
|
206
|
-
fi
|
|
207
|
-
echo "No conflict markers found"
|
|
208
|
-
|
|
209
|
-
- name: Reject tracked updates.xml
|
|
210
|
-
run: |
|
|
211
|
-
# updates.xml is generated and served by the MokoGIT update server
|
|
212
|
-
# (derived from the published stream releases). It must never be
|
|
213
|
-
# committed to the repo. Fail the PR if a tracked copy appears.
|
|
214
|
-
HITS=$(git ls-files | grep -iE '(^|/)updates\.xml$' || true)
|
|
215
|
-
if [ -n "$HITS" ]; then
|
|
216
|
-
echo "::error::updates.xml must not be committed — it is generated by the MokoGIT update server."
|
|
217
|
-
printf '%s\n' "$HITS" | while IFS= read -r f; do
|
|
218
|
-
[ -n "$f" ] && echo "::error file=${f}::Remove ${f} (served by the update server, not stored in-repo)"
|
|
219
|
-
done
|
|
220
|
-
{
|
|
221
|
-
echo "## Blocked: tracked updates.xml"
|
|
222
|
-
echo "The MokoGIT update server generates this file. Remove it from the repo:"
|
|
223
|
-
echo '```'
|
|
224
|
-
printf '%s\n' "$HITS"
|
|
225
|
-
echo '```'
|
|
226
|
-
} >> "$GITHUB_STEP_SUMMARY"
|
|
227
|
-
exit 1
|
|
228
|
-
fi
|
|
229
|
-
echo "No tracked updates.xml — OK"
|
|
230
|
-
|
|
231
|
-
- name: Detect platform
|
|
232
|
-
id: platform
|
|
233
|
-
run: |
|
|
234
|
-
# Platform comes from the MokoGIT metadata API (public GET).
|
|
235
|
-
API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata"
|
|
236
|
-
PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)"
|
|
237
|
-
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
|
238
|
-
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
|
239
|
-
echo "Detected platform: $PLATFORM"
|
|
240
|
-
|
|
241
|
-
- name: Setup PHP
|
|
242
|
-
if: steps.platform.outputs.platform == 'joomla'
|
|
243
|
-
run: |
|
|
244
|
-
if ! command -v php &> /dev/null; then
|
|
245
|
-
sudo apt-get update -qq
|
|
246
|
-
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
|
247
|
-
fi
|
|
248
|
-
|
|
249
|
-
- name: PHP syntax check
|
|
250
|
-
if: steps.platform.outputs.platform == 'joomla'
|
|
251
|
-
run: |
|
|
252
|
-
ERRORS=0
|
|
253
|
-
while IFS= read -r -d '' file; do
|
|
254
|
-
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
|
255
|
-
ERRORS=$((ERRORS + 1))
|
|
256
|
-
fi
|
|
257
|
-
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
|
258
|
-
echo "PHP lint: ${ERRORS} error(s)"
|
|
259
|
-
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
|
260
|
-
|
|
261
|
-
- name: Joomla JEXEC guard check
|
|
262
|
-
if: steps.platform.outputs.platform == 'joomla'
|
|
263
|
-
run: |
|
|
264
|
-
ERRORS=0
|
|
265
|
-
while IFS= read -r -d '' file; do
|
|
266
|
-
# Skip vendor, node_modules, and index.html stub files
|
|
267
|
-
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
|
268
|
-
# Scan the whole file for the JEXEC/JPATH guard: it is placed after
|
|
269
|
-
# the SPDX/file-header docblock, which commonly runs past 20 lines.
|
|
270
|
-
if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then
|
|
271
|
-
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
|
272
|
-
ERRORS=$((ERRORS + 1))
|
|
273
|
-
fi
|
|
274
|
-
done < <(find . -name "*.php" \( -path "*/src/*" -o -path "*/source/*" \) -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
|
275
|
-
if [ "$ERRORS" -gt 0 ]; then
|
|
276
|
-
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
|
277
|
-
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
|
278
|
-
echo "${ERRORS} file(s) in src/ or source/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
|
279
|
-
exit 1
|
|
280
|
-
fi
|
|
281
|
-
echo "JEXEC guard: OK"
|
|
282
|
-
|
|
283
|
-
- name: Joomla directory listing protection
|
|
284
|
-
if: steps.platform.outputs.platform == 'joomla'
|
|
285
|
-
run: |
|
|
286
|
-
MISSING=0
|
|
287
|
-
SOURCE_DIR="src"; [ -d source ] && SOURCE_DIR="source"
|
|
288
|
-
[ ! -d "$SOURCE_DIR" ] && exit 0
|
|
289
|
-
while IFS= read -r dir; do
|
|
290
|
-
if [ ! -f "${dir}/index.html" ]; then
|
|
291
|
-
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
|
|
292
|
-
MISSING=$((MISSING + 1))
|
|
293
|
-
fi
|
|
294
|
-
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
|
|
295
|
-
if [ "$MISSING" -gt 0 ]; then
|
|
296
|
-
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
|
|
297
|
-
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
|
|
298
|
-
fi
|
|
299
|
-
echo "Directory protection: ${MISSING} missing (advisory)"
|
|
300
|
-
|
|
301
|
-
- name: Joomla script file and asset checks
|
|
302
|
-
if: steps.platform.outputs.platform == 'joomla'
|
|
303
|
-
run: |
|
|
304
|
-
ERRORS=0
|
|
305
|
-
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
306
|
-
[ -z "$MANIFEST" ] && exit 0
|
|
307
|
-
MANIFEST_DIR=$(dirname "$MANIFEST")
|
|
308
|
-
|
|
309
|
-
# Check scriptfile exists if declared
|
|
310
|
-
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
|
|
311
|
-
if [ -n "$SCRIPTFILE" ]; then
|
|
312
|
-
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
|
|
313
|
-
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
|
|
314
|
-
ERRORS=$((ERRORS + 1))
|
|
315
|
-
else
|
|
316
|
-
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
|
|
317
|
-
fi
|
|
318
|
-
fi
|
|
319
|
-
|
|
320
|
-
# Require joomla.asset.json and validate it
|
|
321
|
-
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
|
|
322
|
-
if [ -z "$ASSET_JSON" ]; then
|
|
323
|
-
echo "::error::joomla.asset.json not found — Joomla asset system is required"
|
|
324
|
-
ERRORS=$((ERRORS + 1))
|
|
325
|
-
else
|
|
326
|
-
if command -v php &> /dev/null; then
|
|
327
|
-
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
|
|
328
|
-
echo "::error::joomla.asset.json is not valid JSON"
|
|
329
|
-
ERRORS=$((ERRORS + 1))
|
|
330
|
-
}
|
|
331
|
-
fi
|
|
332
|
-
echo "joomla.asset.json: valid"
|
|
333
|
-
fi
|
|
334
|
-
|
|
335
|
-
# Validate all XML files in src/ are well-formed
|
|
336
|
-
XML_ERRORS=0
|
|
337
|
-
if command -v php &> /dev/null; then
|
|
338
|
-
while IFS= read -r -d '' xmlfile; do
|
|
339
|
-
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
|
|
340
|
-
XML_ERRORS=$((XML_ERRORS + 1))
|
|
341
|
-
fi
|
|
342
|
-
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
|
|
343
|
-
fi
|
|
344
|
-
if [ "$XML_ERRORS" -gt 0 ]; then
|
|
345
|
-
echo "::error::${XML_ERRORS} XML file(s) are malformed"
|
|
346
|
-
ERRORS=$((ERRORS + 1))
|
|
347
|
-
else
|
|
348
|
-
echo "XML well-formedness: OK"
|
|
349
|
-
fi
|
|
350
|
-
|
|
351
|
-
[ "$ERRORS" -gt 0 ] && exit 1
|
|
352
|
-
echo "Joomla asset checks: OK"
|
|
353
|
-
|
|
354
|
-
- name: Validate platform manifest
|
|
355
|
-
run: |
|
|
356
|
-
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
357
|
-
case "$PLATFORM" in
|
|
358
|
-
joomla)
|
|
359
|
-
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
360
|
-
if [ -z "$MANIFEST" ]; then
|
|
361
|
-
echo "::warning::No Joomla manifest found (MokoSuite site)"
|
|
362
|
-
exit 0
|
|
363
|
-
fi
|
|
364
|
-
echo "Manifest: ${MANIFEST}"
|
|
365
|
-
if command -v php &> /dev/null; then
|
|
366
|
-
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
|
367
|
-
fi
|
|
368
|
-
for ELEMENT in name version description; do
|
|
369
|
-
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
|
370
|
-
done
|
|
371
|
-
# Block legacy raw/branch update server URLs on MokoGIT
|
|
372
|
-
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogit\|git\.mokoconsulting\.tech' || true)
|
|
373
|
-
if [ -n "$RAW_URLS" ]; then
|
|
374
|
-
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGIT. Use the MokoGIT Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
|
375
|
-
echo "$RAW_URLS"
|
|
376
|
-
exit 1
|
|
377
|
-
fi
|
|
378
|
-
echo "Joomla manifest valid"
|
|
379
|
-
;;
|
|
380
|
-
*)
|
|
381
|
-
echo "Generic platform — no manifest validation"
|
|
382
|
-
;;
|
|
383
|
-
esac
|
|
384
|
-
|
|
385
|
-
- name: Validate Joomla language files
|
|
386
|
-
if: steps.platform.outputs.platform == 'joomla'
|
|
387
|
-
run: |
|
|
388
|
-
ERRORS=0
|
|
389
|
-
WARNINGS=0
|
|
390
|
-
|
|
391
|
-
# en-GB / en-US locale parity. This is NON-FATAL: the release packager
|
|
392
|
-
# (mokocli release_package.php) AUTO-CREATES any missing sibling locale
|
|
393
|
-
# file/dir from the "familiar" locale at build time, so a mismatch here
|
|
394
|
-
# is a heads-up, not a blocker. Genuine .ini syntax errors below stay fatal.
|
|
395
|
-
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
|
396
|
-
if [ -z "$LANG_ROOT" ]; then
|
|
397
|
-
echo "No language/ directory found — skipping"
|
|
398
|
-
exit 0
|
|
399
|
-
fi
|
|
400
|
-
|
|
401
|
-
if [ ! -d "$LANG_ROOT/en-GB" ]; then
|
|
402
|
-
echo "::warning::Missing en-GB language directory (${LANG_ROOT}/en-GB) — the release will auto-create it from en-US"
|
|
403
|
-
WARNINGS=$((WARNINGS + 1))
|
|
404
|
-
fi
|
|
405
|
-
if [ ! -d "$LANG_ROOT/en-US" ]; then
|
|
406
|
-
echo "::warning::Missing en-US language directory (${LANG_ROOT}/en-US) — the release will auto-create it from en-GB"
|
|
407
|
-
WARNINGS=$((WARNINGS + 1))
|
|
408
|
-
fi
|
|
409
|
-
|
|
410
|
-
# Warn (don't fail) when en-GB and en-US .ini sets differ — auto-healed at release.
|
|
411
|
-
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
|
|
412
|
-
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
|
|
413
|
-
[ ! -f "$GB_INI" ] && continue
|
|
414
|
-
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
|
|
415
|
-
if [ ! -f "$US_INI" ]; then
|
|
416
|
-
echo "::warning::$(basename "$GB_INI") exists in en-GB but missing from en-US — the release will auto-create it"
|
|
417
|
-
WARNINGS=$((WARNINGS + 1))
|
|
418
|
-
fi
|
|
419
|
-
done
|
|
420
|
-
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
|
|
421
|
-
[ ! -f "$US_INI" ] && continue
|
|
422
|
-
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
|
|
423
|
-
if [ ! -f "$GB_INI" ]; then
|
|
424
|
-
echo "::warning::$(basename "$US_INI") exists in en-US but missing from en-GB — the release will auto-create it"
|
|
425
|
-
WARNINGS=$((WARNINGS + 1))
|
|
426
|
-
fi
|
|
427
|
-
done
|
|
428
|
-
fi
|
|
429
|
-
|
|
430
|
-
# Find all .ini language files
|
|
431
|
-
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
|
|
432
|
-
if [ -z "$INI_FILES" ]; then
|
|
433
|
-
echo "No .ini language files found"
|
|
434
|
-
[ "$ERRORS" -gt 0 ] && exit 1
|
|
435
|
-
exit 0
|
|
436
|
-
fi
|
|
437
|
-
|
|
438
|
-
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
|
|
439
|
-
|
|
440
|
-
for FILE in $INI_FILES; do
|
|
441
|
-
FNAME=$(basename "$FILE")
|
|
442
|
-
LINENUM=0
|
|
443
|
-
SEEN_KEYS=""
|
|
444
|
-
|
|
445
|
-
while IFS= read -r line || [ -n "$line" ]; do
|
|
446
|
-
LINENUM=$((LINENUM + 1))
|
|
447
|
-
|
|
448
|
-
# Skip empty lines and comments
|
|
449
|
-
[ -z "$line" ] && continue
|
|
450
|
-
echo "$line" | grep -qE '^\s*;' && continue
|
|
451
|
-
echo "$line" | grep -qE '^\s*$' && continue
|
|
452
|
-
|
|
453
|
-
# Must match KEY="VALUE" format
|
|
454
|
-
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
|
|
455
|
-
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
|
|
456
|
-
ERRORS=$((ERRORS + 1))
|
|
457
|
-
continue
|
|
458
|
-
fi
|
|
459
|
-
|
|
460
|
-
# Extract key and check for duplicates
|
|
461
|
-
KEY=$(echo "$line" | sed 's/=.*//')
|
|
462
|
-
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
|
|
463
|
-
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
|
|
464
|
-
ERRORS=$((ERRORS + 1))
|
|
465
|
-
fi
|
|
466
|
-
SEEN_KEYS="${SEEN_KEYS}
|
|
467
|
-
${KEY}"
|
|
468
|
-
done < "$FILE"
|
|
469
|
-
|
|
470
|
-
echo " ${FILE}: checked ${LINENUM} lines"
|
|
471
|
-
done
|
|
472
|
-
|
|
473
|
-
# Cross-check en-GB vs en-US key consistency
|
|
474
|
-
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
|
475
|
-
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
|
476
|
-
|
|
477
|
-
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
|
|
478
|
-
for GB_FILE in "$GB_DIR"/*.ini; do
|
|
479
|
-
[ ! -f "$GB_FILE" ] && continue
|
|
480
|
-
FNAME=$(basename "$GB_FILE")
|
|
481
|
-
US_FILE="$US_DIR/$FNAME"
|
|
482
|
-
[ ! -f "$US_FILE" ] && continue
|
|
483
|
-
|
|
484
|
-
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
|
|
485
|
-
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
|
|
486
|
-
|
|
487
|
-
# Keys in en-GB but not en-US
|
|
488
|
-
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
|
489
|
-
if [ -n "$MISSING_US" ]; then
|
|
490
|
-
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
|
|
491
|
-
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
|
|
492
|
-
WARNINGS=$((WARNINGS + 1))
|
|
493
|
-
fi
|
|
494
|
-
|
|
495
|
-
# Keys in en-US but not en-GB
|
|
496
|
-
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
|
497
|
-
if [ -n "$MISSING_GB" ]; then
|
|
498
|
-
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
|
|
499
|
-
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
|
|
500
|
-
WARNINGS=$((WARNINGS + 1))
|
|
501
|
-
fi
|
|
502
|
-
done
|
|
503
|
-
fi
|
|
504
|
-
|
|
505
|
-
{
|
|
506
|
-
echo "### Language File Validation"
|
|
507
|
-
echo "| Metric | Count |"
|
|
508
|
-
echo "|---|---|"
|
|
509
|
-
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
|
|
510
|
-
echo "| Errors | ${ERRORS} |"
|
|
511
|
-
echo "| Warnings | ${WARNINGS} |"
|
|
512
|
-
} >> $GITHUB_STEP_SUMMARY
|
|
513
|
-
|
|
514
|
-
if [ "$ERRORS" -gt 0 ]; then
|
|
515
|
-
echo "::error::Language validation failed with ${ERRORS} error(s)"
|
|
516
|
-
exit 1
|
|
517
|
-
fi
|
|
518
|
-
echo "Language files: OK (${WARNINGS} warning(s))"
|
|
519
|
-
|
|
520
|
-
- name: Check changelog has unreleased entry
|
|
521
|
-
run: |
|
|
522
|
-
if [ ! -f "CHANGELOG.md" ]; then
|
|
523
|
-
echo "::warning::No CHANGELOG.md found"
|
|
524
|
-
exit 0
|
|
525
|
-
fi
|
|
526
|
-
# Check for content under [Unreleased] section
|
|
527
|
-
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
|
528
|
-
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
|
529
|
-
exit 1
|
|
530
|
-
fi
|
|
531
|
-
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
|
532
|
-
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
|
533
|
-
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
|
534
|
-
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
|
535
|
-
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
|
536
|
-
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
|
537
|
-
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
|
538
|
-
exit 1
|
|
539
|
-
fi
|
|
540
|
-
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
|
541
|
-
|
|
542
|
-
- name: Verify package source
|
|
543
|
-
run: |
|
|
544
|
-
SOURCE_DIR="source"
|
|
545
|
-
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="src"
|
|
546
|
-
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
547
|
-
if [ ! -d "$SOURCE_DIR" ]; then
|
|
548
|
-
echo "::warning::No src/ or htdocs/ directory"
|
|
549
|
-
exit 0
|
|
550
|
-
fi
|
|
551
|
-
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
|
552
|
-
echo "Source: ${FILE_COUNT} files"
|
|
553
|
-
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
|
554
|
-
|
|
555
|
-
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
|
556
|
-
pre-release:
|
|
557
|
-
name: Build RC Package
|
|
558
|
-
runs-on: ubuntu-latest
|
|
559
|
-
# Independent job (no `needs:`) — needs-dependent jobs stall unscheduled on
|
|
560
|
-
# the MokoGIT runner (they sit in `waiting` with started_at=epoch and never
|
|
561
|
-
# run, leaving the check permanently pending). The Joomla Extension CI
|
|
562
|
-
# "Build RC Pre-Release" job uses this same independent pattern for exactly
|
|
563
|
-
# this reason. Guard template repos directly; branch policy is enforced by
|
|
564
|
-
# its own required check, and the pre-release workflow re-validates before
|
|
565
|
-
# publishing, so gating here on validate is not load-bearing.
|
|
566
|
-
if: github.event_name == 'pull_request' && !startsWith(github.event.repository.name, 'Template-')
|
|
567
|
-
|
|
568
|
-
steps:
|
|
569
|
-
- name: Trigger RC pre-release
|
|
570
|
-
env:
|
|
571
|
-
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
|
|
572
|
-
REPO: ${{ github.repository }}
|
|
573
|
-
BRANCH: ${{ github.head_ref }}
|
|
574
|
-
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
575
|
-
run: |
|
|
576
|
-
curl -s -X POST "${MOKOGIT_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGIT_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
|
577
|
-
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
|
578
|
-
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
|
579
|
-
|
|
580
|
-
# ── Issue Reporter ──────────────────────────────────────────────────────
|
|
581
|
-
report-issues:
|
|
582
|
-
name: Report Issues
|
|
583
|
-
# No `needs:` — the MokoGIT Actions scheduler does not offer needs-dependent
|
|
584
|
-
# jobs to runners (they stall "waiting" and are reaped by
|
|
585
|
-
# ABANDONED_JOB_TIMEOUT). This job runs independently and polls the run API
|
|
586
|
-
# for the Validate PR result, then files an issue only when it failed.
|
|
587
|
-
if: always()
|
|
588
|
-
runs-on: ubuntu-latest
|
|
589
|
-
steps:
|
|
590
|
-
- name: Resolve Validate PR result (run API)
|
|
591
|
-
id: gate
|
|
592
|
-
env:
|
|
593
|
-
TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
|
|
594
|
-
SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
595
|
-
REPO: ${{ github.repository }}
|
|
596
|
-
RUN_ID: ${{ github.run_id }}
|
|
597
|
-
run: |
|
|
598
|
-
set -u
|
|
599
|
-
api="${SERVER}/api/v1/repos/${REPO}/actions/runs/${RUN_ID}/jobs"
|
|
600
|
-
val="unknown"
|
|
601
|
-
for i in $(seq 1 120); do
|
|
602
|
-
resp=$(curl -sf -H "Authorization: token ${TOKEN}" "$api" 2>/dev/null || echo '{}')
|
|
603
|
-
done_n=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Validate PR")|select(.status=="completed")]|length' 2>/dev/null || echo 0)
|
|
604
|
-
if [ "${done_n:-0}" -ge 1 ]; then
|
|
605
|
-
val=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Validate PR")][0].conclusion // "unknown"' 2>/dev/null || echo unknown)
|
|
606
|
-
break
|
|
607
|
-
fi
|
|
608
|
-
sleep 15
|
|
609
|
-
done
|
|
610
|
-
echo "validate=${val}" >> "$GITHUB_OUTPUT"
|
|
611
|
-
echo "Validate PR result: ${val}"
|
|
612
|
-
- name: Clone MokoCLI
|
|
613
|
-
if: steps.gate.outputs.validate == 'failure'
|
|
614
|
-
env:
|
|
615
|
-
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
616
|
-
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
617
|
-
run: |
|
|
618
|
-
git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
|
619
|
-
cd /tmp/mokocli && git sparse-checkout set cli
|
|
620
|
-
- name: Report CI failure
|
|
621
|
-
if: steps.gate.outputs.validate == 'failure'
|
|
622
|
-
env:
|
|
623
|
-
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
624
|
-
MOKOGIT_URL: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
625
|
-
# Route inputs through env vars (avoid ${{ }} shell-injection in run:).
|
|
626
|
-
INPUT_GATE: "PR Validation"
|
|
627
|
-
INPUT_DETAILS: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
|
628
|
-
INPUT_SEVERITY: error
|
|
629
|
-
INPUT_WORKFLOW: "PR Check"
|
|
630
|
-
run: |
|
|
631
|
-
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
|
|
632
|
-
/tmp/mokocli/cli/ci_issue_reporter.sh \
|
|
633
|
-
--gate "$INPUT_GATE" \
|
|
634
|
-
--details "$INPUT_DETAILS" \
|
|
635
|
-
--severity "$INPUT_SEVERITY" \
|
|
636
|
-
--workflow "$INPUT_WORKFLOW"
|
|
637
|
-
|
|
638
|
-
# ── PR Check Summary (sticky comment) ──────────────────────────────────────
|
|
639
|
-
# Create-or-update ONE comment (hidden marker) reflecting the gate outcome, so
|
|
640
|
-
# the PR carries a live status line refreshed on every push. Best-effort.
|
|
641
|
-
pr-summary:
|
|
642
|
-
name: PR Check Summary (sticky comment)
|
|
643
|
-
runs-on: ubuntu-latest
|
|
644
|
-
# No `needs:` — poll the run API for gate results (the scheduler does not
|
|
645
|
-
# dispatch needs-dependent jobs; they stall and are reaped).
|
|
646
|
-
if: always()
|
|
647
|
-
steps:
|
|
648
|
-
- name: Upsert sticky summary comment
|
|
649
|
-
env:
|
|
650
|
-
TOKEN: ${{ secrets.MOKOGIT_TOKEN || github.token }}
|
|
651
|
-
SERVER: ${{ vars.MOKOGIT_URL || 'https://git.mokoconsulting.tech' }}
|
|
652
|
-
REPO: ${{ github.repository }}
|
|
653
|
-
PR: ${{ github.event.pull_request.number }}
|
|
654
|
-
RUN_ID: ${{ github.run_id }}
|
|
655
|
-
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
656
|
-
HEAD_REF: ${{ github.head_ref }}
|
|
657
|
-
BASE_REF: ${{ github.base_ref }}
|
|
658
|
-
run: |
|
|
659
|
-
set -uo pipefail
|
|
660
|
-
{ [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; } || { echo "no token/PR; skip"; exit 0; }
|
|
661
|
-
# Poll the run API for Branch Policy + Validate PR results (no `needs:`).
|
|
662
|
-
api="${SERVER}/api/v1/repos/${REPO}/actions/runs/${RUN_ID}/jobs"
|
|
663
|
-
BP=unknown; VAL=unknown
|
|
664
|
-
for i in $(seq 1 120); do
|
|
665
|
-
resp=$(curl -sf -H "Authorization: token ${TOKEN}" "$api" 2>/dev/null || echo '{}')
|
|
666
|
-
pend=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Branch Policy" or .name=="Validate PR")|select(.status!="completed")]|length' 2>/dev/null || echo 1)
|
|
667
|
-
tot=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Branch Policy" or .name=="Validate PR")]|length' 2>/dev/null || echo 0)
|
|
668
|
-
if [ "${tot:-0}" -ge 2 ] && [ "${pend:-1}" = "0" ]; then
|
|
669
|
-
BP=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Branch Policy")][0].conclusion // "unknown"')
|
|
670
|
-
VAL=$(printf '%s' "$resp" | jq -r '[.jobs[]?|select(.name=="Validate PR")][0].conclusion // "unknown"')
|
|
671
|
-
break
|
|
672
|
-
fi
|
|
673
|
-
sleep 15
|
|
674
|
-
done
|
|
675
|
-
icon() { case "$1" in success) printf '✅';; failure) printf '❌';; skipped) printf '⏭️';; *) printf '⚪';; esac; }
|
|
676
|
-
MARKER="<!-- pr-check-summary -->"
|
|
677
|
-
BODY="$(printf '%s\n## PR Check Summary\n\n- %s Branch policy (`%s` → `%s`)\n- %s Validation\n\n_Updated %s · [run](%s)_' \
|
|
678
|
-
"${MARKER}" \
|
|
679
|
-
"$(icon "${BP}")" "${HEAD_REF}" "${BASE_REF}" \
|
|
680
|
-
"$(icon "${VAL}")" \
|
|
681
|
-
"$(date -u +%FT%TZ)" "${RUN_URL}")"
|
|
682
|
-
LIST="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
|
|
683
|
-
EDIT="${SERVER}/api/v1/repos/${REPO}/issues/comments"
|
|
684
|
-
CID="$(curl -sf -H "Authorization: token ${TOKEN}" "${LIST}?limit=100" 2>/dev/null \
|
|
685
|
-
| python3 -c "import sys,json; c=[x for x in json.load(sys.stdin) if '${MARKER}' in (x.get('body') or '')]; print(c[-1]['id'] if c else '')" 2>/dev/null || true)"
|
|
686
|
-
PAYLOAD="$(python3 -c "import json,sys; print(json.dumps({'body': sys.stdin.read()}))" <<<"${BODY}")"
|
|
687
|
-
if [ -n "${CID}" ]; then
|
|
688
|
-
curl -sf -X PATCH -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${EDIT}/${CID}" >/dev/null 2>&1 || true
|
|
689
|
-
echo "updated sticky summary #${CID} on PR #${PR}"
|
|
690
|
-
else
|
|
691
|
-
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -d "${PAYLOAD}" "${LIST}" >/dev/null 2>&1 || true
|
|
692
|
-
echo "created sticky summary on PR #${PR}"
|
|
693
|
-
fi
|