1 Commits

Author SHA1 Message Date
mokogitea-actions[bot] 1779e454a9 chore(version): pre-release bump to 01.00.38-dev [skip ci] 2026-07-13 20:53:14 +00:00
23 changed files with 409 additions and 1436 deletions
+3 -2
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Release # INGROUP: MokoCLI.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/auto-bump.yml # PATH: /.mokogitea/workflows/auto-bump.yml
# VERSION: 09.02.00 # VERSION: 09.02.00
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits) # BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
@@ -34,7 +34,8 @@ jobs:
if: >- if: >-
!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[skip ci]') &&
!contains(github.event.head_commit.message, '[skip bump]') && !contains(github.event.head_commit.message, '[skip bump]') &&
!startsWith(github.event.head_commit.message, 'Merge pull request') !startsWith(github.event.head_commit.message, 'Merge pull request') &&
!startsWith(github.event.repository.name, 'Template-')
steps: steps:
- name: Checkout - name: Checkout
+74 -15
View File
@@ -5,16 +5,16 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Release # INGROUP: MokoCLI.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /templates/workflows/universal/auto-release.yml.template # PATH: /.mokogitea/workflows/auto-release.yml
# VERSION: 05.01.00 # VERSION: 05.01.00
# BRIEF: Universal build & release detects platform from MokoGitea repo metadata (API) # BRIEF: Universal build & release detects platform from metadata API
# #
# +=======================================================================+ # +=======================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE | # | UNIVERSAL BUILD & RELEASE PIPELINE |
# +=======================================================================+ # +=======================================================================+
# | | # | |
# | Reads MokoGitea repo metadata (joomla|dolibarr|generic) to branch logic. | # | Reads metadata API (joomla|dolibarr|generic) to branch logic. |
# | | # | |
# | Platform-specific: | # | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages | # | joomla: XML manifest, type-prefixed packages |
@@ -104,11 +104,36 @@ jobs:
- name: Rename branch to rc - name: Rename branch to rc
run: | run: |
php ${MOKO_CLI}/branch_rename.php \ API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \ AUTH="Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}"
--token "${{ secrets.MOKOGITEA_TOKEN }}" \ FROM="${{ github.event.pull_request.head.ref || 'dev' }}"
--api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \ PR="${{ github.event.pull_request.number }}"
--pr "${{ github.event.pull_request.number }}"
# Resolve the source branch HEAD commit.
SRC_JSON=$(curl -sf -H "$AUTH" "${API_BASE}/branches/${FROM}") \
|| { echo "::error::Source branch ${FROM} not found"; exit 1; }
SRC_SHA=$(printf '%s' "$SRC_JSON" | python3 -c "import sys, json; print(json.load(sys.stdin)['commit']['id'])" 2>/dev/null || true)
[ -n "$SRC_SHA" ] || { echo "::error::Could not resolve HEAD of ${FROM}"; exit 1; }
# Point rc at the source commit via git push. Gitea's git/refs PATCH API
# returns HTTP 405 on ANY protected branch (force or not, even for a user in
# the force-push allowlist), so it cannot move a protected rc. git push honors
# the push + force-push allowlists and creates rc if it is absent.
PUSH_URL="https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@${MOKOGITEA_URL#https://}/${GITEA_ORG}/${GITEA_REPO}.git"
git config --global user.name "mokogitea-actions[bot]"
git config --global user.email "actions@mokoconsulting.tech"
git fetch --no-tags "$PUSH_URL" "${FROM}"
git push --force "$PUSH_URL" "FETCH_HEAD:refs/heads/rc" \
|| { echo "::error::Failed to point rc at ${FROM} (${SRC_SHA}) via git push"; exit 1; }
echo "rc set to ${FROM} (${SRC_SHA})"
# Repoint the PR at rc, then delete the old source branch (non-fatal).
if [ -n "$PR" ]; then
curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" \
"${API_BASE}/pulls/${PR}" -d '{"head":"rc"}' >/dev/null || true
fi
curl -s -X DELETE -H "$AUTH" "${API_BASE}/branches/${FROM}" >/dev/null || true
echo "Renamed ${FROM} -> rc"
- name: Checkout rc and configure git - name: Checkout rc and configure git
run: | run: |
@@ -349,13 +374,47 @@ jobs:
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d) DATE=$(date +%Y-%m-%d)
python3 -c " python3 -c "
import sys import sys, re
version, date = sys.argv[1], sys.argv[2] version, date = sys.argv[1], sys.argv[2]
content = open('CHANGELOG.md').read() lines = open('CHANGELOG.md').read().split('\n')
old = '## [Unreleased]' h2 = re.compile(r'^##\s+\[([^\]]+)\]')
new = f'## [Unreleased]\n\n## [{version}] --- {date}' header, sections, cur = [], [], None
content = content.replace(old, new, 1) for ln in lines:
open('CHANGELOG.md', 'w').write(content) m = h2.match(ln)
if m:
if cur: sections.append(cur)
cur = {'label': m.group(1).strip(), 'head': ln, 'body': []}
elif cur is None:
header.append(ln)
else:
cur['body'].append(ln)
if cur: sections.append(cur)
def nonempty(b): return any(x.strip() for x in b)
def trim(b):
b = b[:]
while b and not b[0].strip(): b.pop(0)
while b and not b[-1].strip(): b.pop()
return b
unreleased, order, bykey = [], [], {}
for s in sections:
key = s['label'].lower()
if key == 'unreleased':
if nonempty(s['body']): unreleased += s['body']
continue
if not nonempty(s['body']): continue # drop blank release sections
if key in bykey: bykey[key]['body'] += [''] + s['body'] # merge duplicate heading (never drop content)
else: bykey[key] = s; order.append(key)
rest, seen = [bykey[k] for k in order], set(order)
out = []
htxt = '\n'.join(header).rstrip()
if htxt: out += [htxt, '']
out += ['## [Unreleased]', '']
promote = bool(unreleased) and version.lower() not in seen
if unreleased and not promote: out += trim(unreleased) + ['']
if promote: out += ['## [%s] --- %s' % (version, date), ''] + trim(unreleased) + ['']
for s in rest: out += [s['head'], ''] + trim(s['body']) + ['']
res = re.sub(r'\n{3,}', '\n\n', '\n'.join(out)).rstrip('\n') + '\n'
open('CHANGELOG.md', 'w').write(res)
" "$VERSION" "$DATE" " "$VERSION" "$DATE"
git add CHANGELOG.md git add CHANGELOG.md
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Universal # INGROUP: MokoCLI.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/branch-cleanup.yml # PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge # BRIEF: Delete feature branches after PR merge
+167 -42
View File
@@ -1,65 +1,190 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech> # Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Release # INGROUP: MokoCLI.Cascade
# BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11. # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# # PATH: /.mokogitea/workflows/cascade-dev.yml
# +========================================================================+ # VERSION: 02.01.00
# | CASCADE MAIN -> DEV | # BRIEF: Cascade main -> dev; auto-merge clean, auto-resolve VERSION-stamp-only conflicts, else notify
# +========================================================================+
# | 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" name: "Cascade Main -> Dev"
on: on:
workflow_run: push:
workflows: ["Universal: Build & Release"] branches:
types: [completed] - 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: workflow_dispatch:
concurrency:
group: cascade-dev-${{ github.repository }}
cancel-in-progress: true
permissions: permissions:
contents: write 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: jobs:
cascade: cascade:
name: Reset dev to main name: Cascade main -> dev
if: >-
!startsWith(github.event.repository.name, 'Template-') &&
(github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Force dev to main - 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: env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
REPO: ${{ github.repository }} REPO: ${{ github.repository }}
run: | run: |
set -euo pipefail set -uo pipefail
git init -q sync && cd sync API="${MOKOGITEA_URL}/api/v1/repos/${REPO}"
git config user.email "mokogitea-actions[bot]@mokoconsulting.tech" AUTH="Authorization: token ${TOKEN}"
git config user.name "mokogitea-actions[bot]" jqget() { python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('$1',''))" 2>/dev/null; }
git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git"
git fetch -q --depth=1 origin main
if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then # 0. dev must exist
# dev exists -> force it to match main (dev is a mirror of main) if ! curl -sf -H "$AUTH" "${API}/branches/dev" >/dev/null 2>&1; then
git push --force origin FETCH_HEAD:refs/heads/dev echo "No dev branch - nothing to cascade."; exit 0
echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY"
else
# 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 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
+5 -4
View File
@@ -131,10 +131,11 @@ jobs:
test: test:
name: Tests name: Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: lint # Independent job (no `needs: lint`): the MokoGitea Actions scheduler does not
# Run only when lint succeeded; always() forces evaluation so a skipped # offer the dependent 2nd job of a needs-chain to runners, so it stalls in
# lint (e.g. template repos) skips this job cleanly instead of hanging. # "waiting" and is reaped by ABANDONED_JOB_TIMEOUT. Guard template repos
if: ${{ always() && needs.lint.result == 'success' }} # directly (same condition lint uses) instead of gating on lint's result.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps: steps:
- name: Checkout - name: Checkout
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Universal # INGROUP: MokoCLI.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml # PATH: /.mokogitea/workflows/ci-issue-reporter.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails. # BRIEF: Reusable workflow — creates/updates a MokoGitea issue when a CI gate fails.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Security # INGROUP: MokoCLI.Security
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/gitleaks.yml.template # PATH: /.mokogitea/workflows/gitleaks.yml
# VERSION: 01.00.00 # VERSION: 01.00.00
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens # BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
# #
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Automation # INGROUP: MokoCLI.Automation
# VERSION: 01.00.00 # VERSION: 01.00.38
# BRIEF: Auto-create feature branch when an issue is opened # BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch" name: "Universal: Issue Branch"
+4 -4
View File
@@ -46,13 +46,13 @@ jobs:
WORKFLOW="${{ github.event.workflow_run.name }}" WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}" URL="${{ github.event.workflow_run.html_url }}"
curl -sS \ curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \
-H "Title: ${REPO} released" \ -H "Title: ${REPO} released" \
-H "Tags: white_check_mark,package" \ -H "Tags: white_check_mark,package" \
-H "Priority: default" \ -H "Priority: default" \
-H "Click: ${URL}" \ -H "Click: ${URL}" \
-d "${WORKFLOW} completed successfully." \ -d "${WORKFLOW} completed successfully." \
"${NTFY_URL}/${NTFY_TOPIC}" "${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)"
- name: Notify on failure - name: Notify on failure
if: github.event.workflow_run.conclusion == 'failure' if: github.event.workflow_run.conclusion == 'failure'
@@ -61,10 +61,10 @@ jobs:
WORKFLOW="${{ github.event.workflow_run.name }}" WORKFLOW="${{ github.event.workflow_run.name }}"
URL="${{ github.event.workflow_run.html_url }}" URL="${{ github.event.workflow_run.html_url }}"
curl -sS \ curl -sS --retry 3 --retry-connrefused --retry-delay 2 --max-time 20 \
-H "Title: ${REPO} workflow failed" \ -H "Title: ${REPO} workflow failed" \
-H "Tags: x,warning" \ -H "Tags: x,warning" \
-H "Priority: high" \ -H "Priority: high" \
-H "Click: ${URL}" \ -H "Click: ${URL}" \
-d "${WORKFLOW} failed. Check the run for details." \ -d "${WORKFLOW} failed. Check the run for details." \
"${NTFY_URL}/${NTFY_TOPIC}" "${NTFY_URL}/${NTFY_TOPIC}" || echo "::warning::ntfy notification could not be delivered (non-fatal)"
+88 -12
View File
@@ -5,8 +5,8 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.CI # INGROUP: MokoCLI.CI
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /templates/workflows/universal/pr-check.yml.template # PATH: /.mokogitea/workflows/pr-check.yml
# VERSION: 09.23.00 # VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge # BRIEF: PR gate — branch policy + code validation before merge
@@ -47,15 +47,15 @@ jobs:
fi fi
;; ;;
fix/*|bugfix/*) fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'" REASON="Fix branches must target 'dev' or 'main', not '${BASE}'"
fi fi
;; ;;
patch/*) patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ] && [ "$BASE" != "main" ]; then
ALLOWED=false ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'" REASON="Patch branches must target 'dev', 'rc', or 'main', not '${BASE}'"
fi fi
;; ;;
hotfix/*) hotfix/*)
@@ -86,7 +86,8 @@ jobs:
echo "" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY echo "- \`fix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`patch/*\` → \`dev\`, \`rc\`, or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
@@ -96,6 +97,80 @@ jobs:
echo "Branch policy: OK (${HEAD} → ${BASE})" echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Docs Update Gate (main PRs) ─────────────────────────────────────────
require-docs:
name: Require Docs Update
runs-on: ubuntu-latest
# Enforce only on PRs merging into main: README.md + CHANGELOG.md must both be updated.
if: ${{ github.base_ref == 'main' }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Require README.md and CHANGELOG.md in the PR diff
run: |
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
CHANGED="$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || true)"
if [ -z "$CHANGED" ]; then
git fetch -q origin "${{ github.base_ref }}" 2>/dev/null || true
CHANGED="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" 2>/dev/null || true)"
fi
echo "Changed files in PR:"
echo "$CHANGED"
MISSING=""
echo "$CHANGED" | grep -qxE 'README\.md' || MISSING="README.md"
echo "$CHANGED" | grep -qxE 'CHANGELOG\.md' || MISSING="${MISSING:+$MISSING, }CHANGELOG.md"
if [ -n "$MISSING" ]; then
echo "::error::PRs into main must update: ${MISSING}"
{
echo "## Docs Update Required"
echo ""
echo "PRs merging into \`main\` must update both **README.md** and **CHANGELOG.md**."
echo ""
echo "Not updated in this PR: **${MISSING}**"
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
echo "Docs update present (README.md + CHANGELOG.md)"
echo "## Docs Update: Passed" >> "$GITHUB_STEP_SUMMARY"
# ── Wiki Update Reminder (main PRs, non-blocking) ───────────────────────
wiki-reminder:
name: Wiki Update Reminder
runs-on: ubuntu-latest
if: ${{ github.base_ref == 'main' }}
steps:
- name: Remind to update the wiki
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
run: |
set -uo pipefail
{
echo "## Wiki Update Reminder"
echo ""
echo "Docs are **wiki-first** at MokoConsulting. If this change affects behavior, usage, configuration, or standards, update the repo wiki:"
echo ""
echo "- ${SERVER}/${REPO}/wiki"
echo ""
echo "_Non-blocking reminder._"
} >> "$GITHUB_STEP_SUMMARY"
# Post a single PR comment (idempotent via hidden marker); best-effort, never fails.
API="${SERVER}/api/v1/repos/${REPO}/issues/${PR}/comments"
if [ -n "${TOKEN:-}" ] && [ -n "${PR:-}" ]; then
existing="$(curl -sf -H "Authorization: token ${TOKEN}" "$API" 2>/dev/null | grep -c 'wiki-reminder' || true)"
if [ "${existing:-0}" -eq 0 ]; then
curl -sf -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" -X POST "$API" \
-d '{"body":"<!-- wiki-reminder -->\n\n**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)_"}' >/dev/null 2>&1 || true
fi
fi
echo "Wiki reminder emitted (non-blocking)."
# ── Secret Scanning ────────────────────────────────────────────────── # ── Secret Scanning ──────────────────────────────────────────────────
gitleaks: gitleaks:
name: Secret Scan name: Secret Scan
@@ -135,7 +210,7 @@ jobs:
- name: Check for merge conflict markers - name: Check for merge conflict markers
run: | run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --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) CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --exclude-dir='.git' --exclude-dir='.mokogitea' --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)
if [ -n "$CONFLICTS" ]; then if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found in source files" echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
@@ -149,7 +224,7 @@ jobs:
- name: Detect platform - name: Detect platform
id: platform id: platform
run: | run: |
# Platform comes from the MokoGitea metadata API (public GET); manifest.xml is no longer used. # Platform comes from the MokoGitea metadata API (public GET).
API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata" API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata"
PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)" PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)"
[ -z "$PLATFORM" ] && PLATFORM="generic" [ -z "$PLATFORM" ] && PLATFORM="generic"
@@ -183,8 +258,9 @@ jobs:
while IFS= read -r -d '' file; do while IFS= read -r -d '' file; do
# Skip vendor, node_modules, and index.html stub files # Skip vendor, node_modules, and index.html stub files
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
# Check first 10 lines for JEXEC or JPATH guard # Scan the whole file for the JEXEC/JPATH guard: it is placed after
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then # the SPDX/file-header docblock, which commonly runs past 20 lines.
if ! grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]" "$file"; then
echo "::error file=${file}::Missing JEXEC guard: ${file}" echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1)) ERRORS=$((ERRORS + 1))
fi fi
@@ -275,7 +351,7 @@ jobs:
joomla) joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1) MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)" echo "::warning::No Joomla manifest found (MokoSuite site)"
exit 0 exit 0
fi fi
echo "Manifest: ${MANIFEST}" echo "Manifest: ${MANIFEST}"
+7 -9
View File
@@ -3,8 +3,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
# #
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: Gitea.Workflow
# INGROUP: MokoCLI.Validation # INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template # PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
# VERSION: 01.00.00 # VERSION: 01.00.00
@@ -20,7 +20,7 @@ permissions:
contents: read contents: read
env: env:
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }} GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }} GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }} GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
@@ -28,14 +28,12 @@ jobs:
validate-metadata: validate-metadata:
name: "Validate Joomla Metadata" name: "Validate Joomla Metadata"
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Skip on template repos (Template-*) — they have no real extension manifest to validate.
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Setup MokoCLI tools - name: Setup mokocli tools
env: env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
@@ -57,14 +55,14 @@ jobs:
- name: Validate metadata against Joomla manifest - name: Validate metadata against Joomla manifest
env: env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
run: | run: |
php ${MOKO_CLI}/joomla_metadata_validate.php \ php ${MOKO_CLI}/joomla_metadata_validate.php \
--path . \ --path . \
--token "${MOKOGITEA_TOKEN}" \ --token "${GITEA_TOKEN}" \
--org "${GITEA_ORG}" \ --org "${GITEA_ORG}" \
--repo "${GITEA_REPO}" \ --repo "${GITEA_REPO}" \
--api-base "${MOKOGITEA_URL}/api/v1" \ --api-base "${GITEA_URL}/api/v1" \
--ci --ci
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
+11 -5
View File
@@ -5,9 +5,9 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Release # INGROUP: MokoCLI.Release
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /templates/workflows/universal/pre-release.yml.template # PATH: /.mokogitea/workflows/pre-release.yml
# VERSION: 05.02.00 # VERSION: 05.02.01
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches # BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release" name: "Universal: Pre-Release"
@@ -162,7 +162,13 @@ jobs:
git add -A git add -A
git diff --cached --quiet || { git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]" git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1 # Push the bump commit, but do NOT fail the release if the target branch
# is protected and the release identity is not on the push allowlist.
# The build proceeds from the in-tree bumped version regardless; if the
# push is rejected, the next run simply re-bumps from the same base.
if ! git push origin HEAD 2>&1; then
echo "::warning::Version-bump commit could not be pushed (protected branch?). Building from in-tree version ${VERSION} anyway."
fi
} }
# Auto-detect element via manifest_element.php # Auto-detect element via manifest_element.php
@@ -274,4 +280,4 @@ jobs:
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
+3 -2
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Universal # INGROUP: MokoCLI.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /.mokogitea/workflows/rc-revert.yml # PATH: /.mokogitea/workflows/rc-revert.yml
# VERSION: 09.23.00 # VERSION: 09.23.00
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge # BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
@@ -25,7 +25,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: >- if: >-
github.event.pull_request.merged == false && github.event.pull_request.merged == false &&
startsWith(github.event.pull_request.head.ref, 'rc/') startsWith(github.event.pull_request.head.ref, 'rc/') &&
!startsWith(github.event.repository.name, 'Template-')
steps: steps:
- name: Rename branch - name: Rename branch
+2 -2
View File
@@ -8,8 +8,8 @@
# FILE INFORMATION # FILE INFORMATION
# DEFGROUP: MokoGitea.Workflow # DEFGROUP: MokoGitea.Workflow
# INGROUP: MokoCLI.Validation # INGROUP: MokoCLI.Validation
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli # REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
# PATH: /templates/workflows/joomla/repo_health.yml.template # PATH: /.mokogitea/workflows/repo-health.yml
# VERSION: 09.23.00 # VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts. # BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================ # ============================================================================
+1
View File
@@ -34,6 +34,7 @@ jobs:
set-version: set-version:
name: Set Version to ${{ inputs.version }} name: Set Version to ${{ inputs.version }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
steps: steps:
- name: Validate version format - name: Validate version format
+1 -1
View File
@@ -14,7 +14,7 @@
DEFGROUP: Template-Joomla DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation INGROUP: Template-Joomla.Documentation
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/ REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
VERSION: 01.00.29 VERSION: 01.00.38
PATH: ./CODE_OF_CONDUCT.md PATH: ./CODE_OF_CONDUCT.md
BRIEF: Community expectations and enforcement guidelines BRIEF: Community expectations and enforcement guidelines
NOTE: Adapted with attribution from the Contributor Covenant v2.1 NOTE: Adapted with attribution from the Contributor Covenant v2.1
+1 -1
View File
@@ -19,7 +19,7 @@
DEFGROUP: mokoconsulting-tech.Template-Joomla DEFGROUP: mokoconsulting-tech.Template-Joomla
INGROUP: MokoStandards.Governance INGROUP: MokoStandards.Governance
REPO: https://github.com/mokoconsulting-tech/Template-Joomla REPO: https://github.com/mokoconsulting-tech/Template-Joomla
VERSION: 01.00.29 VERSION: 01.00.38
PATH: /GOVERNANCE.md PATH: /GOVERNANCE.md
BRIEF: Project governance rules, roles, and decision process for Template-Joomla BRIEF: Project governance rules, roles, and decision process for Template-Joomla
--> -->
+1 -1
View File
@@ -23,7 +23,7 @@ DEFGROUP: Template-Joomla
INGROUP: Template-Joomla.Documentation INGROUP: Template-Joomla.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
PATH: /SECURITY.md PATH: /SECURITY.md
VERSION: 01.00.29 VERSION: 01.00.38
BRIEF: Security vulnerability reporting and handling policy BRIEF: Security vulnerability reporting and handling policy
--> -->
@@ -5,7 +5,7 @@
<creationDate>2026-06-23</creationDate> <creationDate>2026-06-23</creationDate>
<copyright>Copyright (C) 2026 Moko Consulting.</copyright> <copyright>Copyright (C) 2026 Moko Consulting.</copyright>
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<version>01.00.29</version> <version>01.00.38</version>
<namespace path="src">Moko\Component\MokoSuiteSupport</namespace> <namespace path="src">Moko\Component\MokoSuiteSupport</namespace>
<administration> <administration>
<files folder="admin"><folder>services</folder><folder>src</folder><folder>tmpl</folder></files> <files folder="admin"><folder>services</folder><folder>src</folder><folder>tmpl</folder></files>
@@ -8,7 +8,7 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>
<authorUrl>https://mokoconsulting.tech</authorUrl> <authorUrl>https://mokoconsulting.tech</authorUrl>
<version>01.00.29</version> <version>01.00.38</version>
<php_minimum>8.3</php_minimum> <php_minimum>8.3</php_minimum>
<description>PLG_SYSTEM_MOKOSUITESUPPORT_DESC</description> <description>PLG_SYSTEM_MOKOSUITESUPPORT_DESC</description>
<namespace path="src">Moko\Plugin\System\MokoSuiteSupport</namespace> <namespace path="src">Moko\Plugin\System\MokoSuiteSupport</namespace>
@@ -3,7 +3,7 @@
<name>Web Services - MokoSuite Support</name> <name>Web Services - MokoSuite Support</name>
<element>mokosuitesupport</element> <element>mokosuitesupport</element>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<version>01.00.29</version> <version>01.00.38</version>
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteSupport</namespace> <namespace path="src">Moko\Plugin\WebServices\MokoSuiteSupport</namespace>
<files><folder>src</folder><folder>services</folder></files> <files><folder>src</folder><folder>services</folder></files>
+1 -1
View File
@@ -2,7 +2,7 @@
<extension type="package" method="upgrade"> <extension type="package" method="upgrade">
<name>Package - MokoSuite Support</name> <name>Package - MokoSuite Support</name>
<packagename>mokosuitesupport</packagename> <packagename>mokosuitesupport</packagename>
<version>01.00.29</version> <version>01.00.38</version>
<creationDate>2026-06-21</creationDate> <creationDate>2026-06-21</creationDate>
<author>Moko Consulting</author> <author>Moko Consulting</author>
<authorEmail>hello@mokoconsulting.tech</authorEmail> <authorEmail>hello@mokoconsulting.tech</authorEmail>