Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ae85bbe8b | |||
| 931da100e5 | |||
| 2072d6b011 | |||
| 3e13452dbc | |||
| 35e60ef1c7 | |||
| fc3793e315 | |||
| 10f82cd9db | |||
| 93bb9d5e0f | |||
| 3679bb1f8e | |||
| f843f2c9a7 | |||
| b98e145109 | |||
| 9e5d4b03cd | |||
| faa8c9b2e8 | |||
| b91cbcfb25 | |||
| cfdf0c88e9 | |||
| 9fb2e62a1b | |||
| 8d3a2b7ed5 | |||
| 8822485f4d | |||
| a04e8d2da6 | |||
| 16bc82ecba | |||
| 1ebf34ce8a | |||
| 959a278125 | |||
| e4e5449bef | |||
| 8010be007c | |||
| d0d99db593 | |||
| 38f28b3166 | |||
| fd9730a39c | |||
| 42b8632cfc | |||
| 9889f1b529 | |||
| 8dcf26c708 | |||
| 4ab94afe0c | |||
| 3484f55591 | |||
| 5a1984a239 | |||
| 5c2e18c22c | |||
| f5d3911ac6 | |||
| 4bbe54a688 | |||
| 1f5e37d9fa | |||
| 2a724446ce | |||
| a4d1850c58 | |||
| 8befe3a64a | |||
| a8b202108b | |||
| 8f7f8c5fa6 | |||
| d02f51e1e1 | |||
| 435ef08ca7 | |||
| d2af38e7d9 | |||
| bb33d294d5 | |||
| 40b85b533b | |||
| 194cb41371 | |||
| e75b257818 | |||
| 34774148f0 | |||
| 0a8095bf0c | |||
| 0ecb311894 | |||
| e751e124b1 | |||
| ff9288d93b | |||
| 7435ebc62e | |||
| c3a333f4c1 | |||
| b27fcdb7ce | |||
| 74edae4d4d | |||
| 4cf53595fd | |||
| 5aaa60adb6 | |||
| 67cf8ad771 | |||
| 61cd784f57 | |||
| 2c10a70b59 | |||
| c7b6803c24 | |||
| c056878e29 | |||
| 3057235b0d | |||
| c8c93fa10f | |||
| 60c570c5fb | |||
| 3f75d06efc | |||
| 98a0bd0637 | |||
| 3d443b3092 | |||
| 032a1f3bdc | |||
| 6687db05c4 | |||
| e475ab24ae | |||
| ad26508b82 | |||
| 30b995bf2b | |||
| 63c4e832e8 | |||
| 5ca3c80114 | |||
| a4c8488781 | |||
| 0d900b50d3 | |||
| e95e612a44 | |||
| 37debe909c | |||
| df55a2c7c5 | |||
| 379b262f90 | |||
| d1b7f9787f | |||
| 8f25cdcc98 | |||
| cc1485d8c1 | |||
| 759af569d1 | |||
| ba779a8fc1 | |||
| 1bff03696c | |||
| bb77c65244 | |||
| fbccca11bb | |||
| c5c492463e | |||
| 9af651d2be | |||
| 502dfa40d9 | |||
| d831d01240 | |||
| f05f0e08a8 | |||
| 3c4962c368 | |||
| edc3d0582d | |||
| 45bfdb1232 | |||
| 0075c616d9 | |||
| f4644826cb | |||
| 7d12b42408 | |||
| dcb02ce52c | |||
| daec39b756 | |||
| 5c799e8fb1 | |||
| f1bbfd064a | |||
| 774fee24fd | |||
| 69b554f4a6 |
@@ -0,0 +1,41 @@
|
||||
# EditorConfig helps maintain consistent coding styles across different editors and IDEs
|
||||
# https://editorconfig.org/
|
||||
|
||||
root = true
|
||||
|
||||
# Default settings — Tabs preferred, width = 2 spaces
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
|
||||
# PowerShell scripts — tabs, 2-space visual width
|
||||
[*.ps1]
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
end_of_line = crlf
|
||||
|
||||
# Markdown files — keep trailing whitespace for line breaks
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# JSON / YAML files — tabs, 2-space visual width
|
||||
[*.{json,yml,yaml}]
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
|
||||
# Makefiles — always tabs, default width
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
tab_width = 2
|
||||
|
||||
# Windows batch scripts — keep CRLF endings
|
||||
[*.{bat,cmd}]
|
||||
end_of_line = crlf
|
||||
|
||||
# Shell scripts — ensure LF endings
|
||||
[*.sh]
|
||||
end_of_line = lf
|
||||
@@ -0,0 +1,2 @@
|
||||
# Claude Code
|
||||
.claude/
|
||||
@@ -1,66 +1,66 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup mokocli tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
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
|
||||
fi
|
||||
if [ -d "/opt/mokocli/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||
/tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||
# VERSION: 09.02.00
|
||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||
|
||||
name: "Universal: Auto Version Bump"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- rc
|
||||
- 'feature/**'
|
||||
- 'patch/**'
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump:
|
||||
name: Version Bump
|
||||
runs-on: release
|
||||
if: >-
|
||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup mokocli tools
|
||||
run: |
|
||||
if ! command -v composer &> /dev/null; then
|
||||
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
|
||||
fi
|
||||
if [ -d "/opt/mokocli/cli" ]; then
|
||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||
else
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||
/tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
php ${MOKO_CLI}/version_auto_bump.php \
|
||||
--path . --branch "${GITHUB_REF_NAME}" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
# VERSION: 05.00.00
|
||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||
#
|
||||
# +========================================================================+
|
||||
# +=======================================================================+
|
||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||
# +========================================================================+
|
||||
# +=======================================================================+
|
||||
# | |
|
||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||
# | |
|
||||
@@ -21,15 +21,24 @@
|
||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||
# | generic: README-only, no update stream |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
# +=======================================================================+
|
||||
|
||||
name: "Universal: Build & Release"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, closed]
|
||||
types: [opened, synchronize, closed]
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- '.mokogitea/workflows/**'
|
||||
- '*.md'
|
||||
- 'wiki/**'
|
||||
- '.editorconfig'
|
||||
- '.gitignore'
|
||||
- '.gitattributes'
|
||||
- '.gitmessage'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
action:
|
||||
@@ -43,7 +52,7 @@ on:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
@@ -51,12 +60,13 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
|
||||
promote-rc:
|
||||
name: Promote to RC
|
||||
runs-on: release
|
||||
if: >-
|
||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||
(github.event.action == 'synchronize' && github.event.pull_request.merged != true) ||
|
||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||
|
||||
steps:
|
||||
@@ -92,7 +102,7 @@ jobs:
|
||||
php ${MOKO_CLI}/branch_rename.php \
|
||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
--api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||
--pr "${{ github.event.pull_request.number }}"
|
||||
|
||||
- name: Checkout rc and configure git
|
||||
@@ -111,7 +121,7 @@ jobs:
|
||||
|
||||
- name: Update RC release notes from CHANGELOG.md
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Extract [Unreleased] section from changelog
|
||||
@@ -149,7 +159,7 @@ jobs:
|
||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||
# ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
|
||||
release:
|
||||
name: Build & Release Pipeline
|
||||
runs-on: release
|
||||
@@ -205,6 +215,12 @@ jobs:
|
||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: "Detect platform"
|
||||
id: platform
|
||||
run: |
|
||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output 2>/dev/null || true
|
||||
|
||||
- name: "Determine version bump level"
|
||||
id: bump
|
||||
run: |
|
||||
@@ -228,9 +244,57 @@ jobs:
|
||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
- name: "Read published version"
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "")
|
||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [[ "$PLATFORM" == joomla* ]]; then
|
||||
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "branch=main" >> "$GITHUB_OUTPUT"
|
||||
echo "Published version: ${VERSION}"
|
||||
|
||||
- name: "Create semver tag for non-Joomla repos"
|
||||
id: semver
|
||||
if: |
|
||||
steps.version.outputs.skip != 'true' &&
|
||||
!startsWith(steps.platform.outputs.platform, 'joomla')
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
SEMVER_TAG="v${VERSION}"
|
||||
|
||||
echo "Creating semver tag: ${SEMVER_TAG}"
|
||||
|
||||
# Create the git tag via API
|
||||
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
|
||||
-X POST -H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API_BASE}/tags" \
|
||||
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
||||
echo "Created semver tag: ${SEMVER_TAG}"
|
||||
elif [ "$HTTP_CODE" = "409" ]; then
|
||||
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
|
||||
else
|
||||
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
|
||||
fi
|
||||
|
||||
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update release notes and promote changelog
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Get the stable release info (version and ID)
|
||||
@@ -299,7 +363,7 @@ jobs:
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/release_mirror.php \
|
||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||
@@ -328,7 +392,7 @@ jobs:
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
|
||||
# Delete rc branch (ephemeral — created by promote-rc)
|
||||
@@ -352,7 +416,7 @@ jobs:
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||
BRANCH_NAME="version/${VERSION}"
|
||||
@@ -373,7 +437,7 @@ jobs:
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
php ${MOKO_CLI}/version_reset_dev.php \
|
||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||
--branch dev --path . 2>&1 || true
|
||||
@@ -399,5 +463,5 @@ jobs:
|
||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Release | [View](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||
name: "Cascade Main → Dev (DISABLED)"
|
||||
on: workflow_dispatch
|
||||
jobs:
|
||||
noop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||
@@ -0,0 +1,197 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||
# PATH: /.gitea/workflows/ci-generic.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||
|
||||
name: "Generic: Project CI"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- dev
|
||||
- dev/**
|
||||
- rc/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
# ── Lint & Validate ───────────────────────────────────────────────────
|
||||
lint:
|
||||
name: Lint & Validate
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect toolchain
|
||||
id: detect
|
||||
run: |
|
||||
HAS_PHP=false
|
||||
HAS_NODE=false
|
||||
[ -f "composer.json" ] && HAS_PHP=true
|
||||
[ -f "package.json" ] && HAS_NODE=true
|
||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||
fi
|
||||
php -v
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install PHP dependencies
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if [ -f "composer.json" ]; then
|
||||
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if [ -f "package.json" ]; then
|
||||
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: PHP syntax check
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
ERRORS=0
|
||||
while IFS= read -r -d '' file; do
|
||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||
echo "::error file=${file}::PHP syntax error"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
|
||||
|
||||
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$ERRORS" -eq 0 ]; then
|
||||
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: TypeScript/JavaScript lint
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if [ -f "node_modules/.bin/eslint" ]; then
|
||||
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
|
||||
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
|
||||
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
|
||||
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
||||
echo "::warning::ESLint config found but eslint not installed"
|
||||
else
|
||||
echo "No ESLint configured — skipping"
|
||||
fi
|
||||
|
||||
- name: TypeScript compile check
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
|
||||
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
|
||||
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
|
||||
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
- name: PHPStan static analysis
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
|
||||
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
|
||||
fi
|
||||
|
||||
# ── Tests ─────────────────────────────────────────────────────────────
|
||||
test:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: lint
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect toolchain
|
||||
id: detect
|
||||
run: |
|
||||
HAS_PHP=false
|
||||
HAS_NODE=false
|
||||
[ -f "composer.json" ] && HAS_PHP=true
|
||||
[ -f "package.json" ] && HAS_NODE=true
|
||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup PHP
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
||||
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
|
||||
|
||||
- name: Run PHP tests
|
||||
if: steps.detect.outputs.has_php == 'true'
|
||||
run: |
|
||||
if [ -f "vendor/bin/phpunit" ]; then
|
||||
vendor/bin/phpunit --testdox 2>&1
|
||||
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
||||
echo "::warning::PHPUnit config found but phpunit not installed"
|
||||
else
|
||||
echo "No PHPUnit configured — skipping"
|
||||
fi
|
||||
|
||||
- name: Run Node.js tests
|
||||
if: steps.detect.outputs.has_node == 'true'
|
||||
run: |
|
||||
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
|
||||
npm test 2>&1
|
||||
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "No test script in package.json — skipping"
|
||||
fi
|
||||
|
||||
- name: Build check
|
||||
run: |
|
||||
if [ -f "Makefile" ]; then
|
||||
make build 2>&1 || echo "::warning::Build failed or not configured"
|
||||
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
|
||||
npm run build 2>&1 || echo "::warning::Build failed"
|
||||
fi
|
||||
@@ -0,0 +1,68 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/ci-issue-reporter.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Reusable workflow — creates/updates a Gitea issue when a CI gate fails.
|
||||
# Clones MokoCLI and runs cli/ci_issue_reporter.sh.
|
||||
|
||||
name: "Universal: CI Issue Reporter"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
gate:
|
||||
description: "CI gate name (e.g. PR Validation, Repository Health)"
|
||||
required: true
|
||||
type: string
|
||||
details:
|
||||
description: "Human-readable failure description"
|
||||
required: true
|
||||
type: string
|
||||
severity:
|
||||
description: "error or warning"
|
||||
required: false
|
||||
type: string
|
||||
default: "error"
|
||||
workflow:
|
||||
description: "Workflow name for the issue title"
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
secrets:
|
||||
MOKOGITEA_TOKEN:
|
||||
required: true
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
report:
|
||||
name: "Report: ${{ inputs.gate }}"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Clone MokoCLI
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||
git clone --depth 1 --filter=blob:none --sparse "${MOKOGITEA_URL}/MokoConsulting/MokoCLI.git" /tmp/mokocli
|
||||
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
||||
|
||||
- name: Report CI failure
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
run: |
|
||||
chmod +x /tmp/mokocli/cli/ci_issue_reporter.sh
|
||||
/tmp/mokocli/cli/ci_issue_reporter.sh \
|
||||
--gate "${{ inputs.gate }}" \
|
||||
--details "${{ inputs.details }}" \
|
||||
--severity "${{ inputs.severity }}" \
|
||||
--workflow "${{ inputs.workflow }}"
|
||||
@@ -0,0 +1,87 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Maintenance
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/cleanup.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||
|
||||
name: "Universal: Repository Cleanup"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * 0' # Weekly on Sunday at 03:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: Clean Merged Branches
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
|
||||
- name: Delete merged branches
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
echo "=== Merged Branch Cleanup ==="
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
|
||||
# List branches via API
|
||||
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||
|
||||
DELETED=0
|
||||
for BRANCH in $BRANCHES; do
|
||||
# Skip protected branches
|
||||
case "$BRANCH" in
|
||||
main|master|develop|release/*|hotfix/*) continue ;;
|
||||
esac
|
||||
|
||||
# Check if branch is merged into main
|
||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||
echo " Deleting merged branch: ${BRANCH}"
|
||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Deleted ${DELETED} merged branch(es)"
|
||||
|
||||
- name: Clean old workflow runs
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
echo "=== Workflow Run Cleanup ==="
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# Get old completed runs
|
||||
RUNS=$(curl -sS -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
"${API}/actions/runs?status=completed&limit=50" | \
|
||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||
|
||||
DELETED=0
|
||||
for RUN_ID in $RUNS; do
|
||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGITEA_TOKEN}" \
|
||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||
DELETED=$((DELETED + 1))
|
||||
done
|
||||
|
||||
echo "Deleted ${DELETED} old workflow run(s)"
|
||||
@@ -0,0 +1,126 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Deploy
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
||||
# VERSION: 04.07.00
|
||||
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
||||
|
||||
name: "Universal: Deploy to Dev (Manual)"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
clear_remote:
|
||||
description: 'Delete all remote files before uploading'
|
||||
required: false
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: SFTP Deploy to Dev
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
|
||||
- name: Setup PHP
|
||||
run: |
|
||||
php -v && composer --version
|
||||
|
||||
- name: Setup MokoStandards tools
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||
run: |
|
||||
git clone --depth 1 --branch main --quiet \
|
||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||
/tmp/mokostandards-api 2>/dev/null || true
|
||||
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
||||
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Check FTP configuration
|
||||
id: check
|
||||
env:
|
||||
HOST: ${{ vars.DEV_FTP_HOST }}
|
||||
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
||||
PORT: ${{ vars.DEV_FTP_PORT }}
|
||||
run: |
|
||||
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
||||
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
||||
|
||||
REMOTE="${PATH_VAR%/}"
|
||||
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
[ -z "$PORT" ] && PORT="22"
|
||||
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Deploy via SFTP
|
||||
if: steps.check.outputs.skip != 'true'
|
||||
env:
|
||||
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||
run: |
|
||||
SOURCE_DIR="src"
|
||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
||||
|
||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
||||
> /tmp/sftp-config.json
|
||||
|
||||
if [ -n "$SFTP_KEY" ]; then
|
||||
echo "$SFTP_KEY" > /tmp/deploy_key
|
||||
chmod 600 /tmp/deploy_key
|
||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||
else
|
||||
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
||||
fi
|
||||
|
||||
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
||||
|
||||
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
||||
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||
else
|
||||
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||
fi
|
||||
|
||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||
|
||||
- name: Summary
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
||||
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -0,0 +1,92 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Security
|
||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||
# PATH: /templates/workflows/gitleaks.yml.template
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||
#
|
||||
# +========================================================================+
|
||||
# | SECRET SCANNING |
|
||||
# +========================================================================+
|
||||
# | |
|
||||
# | Scans commits for leaked secrets using Gitleaks. |
|
||||
# | |
|
||||
# | - PR scan: only new commits in the PR |
|
||||
# | - Scheduled: full repo scan weekly |
|
||||
# | - Alerts via ntfy on findings |
|
||||
# | |
|
||||
# +========================================================================+
|
||||
|
||||
name: "Universal: Secret Scanning"
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * 1' # Weekly Monday 05:00 UTC
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||
|
||||
jobs:
|
||||
gitleaks:
|
||||
name: Gitleaks Secret Scan
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Gitleaks
|
||||
run: |
|
||||
GITLEAKS_VERSION="8.21.2"
|
||||
curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||
| tar -xz -C /usr/local/bin gitleaks
|
||||
gitleaks version
|
||||
|
||||
- name: Scan for secrets
|
||||
id: scan
|
||||
run: |
|
||||
echo "### Secret Scanning" >> $GITHUB_STEP_SUMMARY
|
||||
ARGS="--source . --verbose --report-format json --report-path /tmp/gitleaks-report.json"
|
||||
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
# Scan only PR commits
|
||||
ARGS="$ARGS --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
|
||||
echo "Scanning PR commits only" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "Full repository scan" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
if gitleaks detect $ARGS 2>&1; then
|
||||
echo "result=clean" >> "$GITHUB_OUTPUT"
|
||||
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
|
||||
else
|
||||
echo "result=found" >> "$GITHUB_OUTPUT"
|
||||
FINDINGS=$(jq length /tmp/gitleaks-report.json 2>/dev/null || echo "unknown")
|
||||
echo "**${FINDINGS} potential secret(s) detected.**" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Review the findings and rotate any exposed credentials immediately." >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Notify on findings
|
||||
if: failure() && steps.scan.outputs.result == 'found'
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} — secrets detected in code" \
|
||||
-H "Tags: rotating_light,key" \
|
||||
-H "Priority: urgent" \
|
||||
-d "Gitleaks found potential secrets. Review and rotate credentials immediately." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Automation
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Auto-create feature branch when an issue is opened
|
||||
|
||||
name: "Universal: Issue Branch"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
env:
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
|
||||
jobs:
|
||||
create-branch:
|
||||
name: Create feature branch
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create branch and comment
|
||||
run: |
|
||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||
API="${MOKOGITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||
|
||||
# Build slug from title: lowercase, replace non-alnum with dash, trim
|
||||
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
|
||||
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
|
||||
|
||||
# Check dev branch exists
|
||||
DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API}/branches/dev" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "${DEV_EXISTS}" != "200" ]; then
|
||||
echo "No dev branch -- skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create branch from dev
|
||||
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/branches" \
|
||||
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "${HTTP}" = "201" ]; then
|
||||
echo "Created branch: ${BRANCH}"
|
||||
|
||||
# Comment on issue with branch link
|
||||
REPO_URL="${MOKOGITEA_URL}/${{ github.repository }}"
|
||||
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||
|
||||
curl -sf -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${API}/issues/${ISSUE_NUM}/comments" \
|
||||
-d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
|
||||
|
||||
echo "Commented on issue #${ISSUE_NUM}"
|
||||
else
|
||||
echo "Failed to create branch (HTTP ${HTTP}) -- may already exist"
|
||||
fi
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards.Notifications
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||
# PATH: /.gitea/workflows/notify.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||
|
||||
name: "Universal: Notifications"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- "Joomla Build & Release"
|
||||
- "Joomla Extension CI"
|
||||
- "Deploy"
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
name: Send Notification
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' ||
|
||||
github.event.workflow_run.conclusion == 'failure'
|
||||
|
||||
steps:
|
||||
- name: Notify on success (releases only)
|
||||
if: >-
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
contains(github.event.workflow_run.name, 'Release')
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||
URL="${{ github.event.workflow_run.html_url }}"
|
||||
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} released" \
|
||||
-H "Tags: white_check_mark,package" \
|
||||
-H "Priority: default" \
|
||||
-H "Click: ${URL}" \
|
||||
-d "${WORKFLOW} completed successfully." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||
|
||||
- name: Notify on failure
|
||||
if: github.event.workflow_run.conclusion == 'failure'
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
WORKFLOW="${{ github.event.workflow_run.name }}"
|
||||
URL="${{ github.event.workflow_run.html_url }}"
|
||||
|
||||
curl -sS \
|
||||
-H "Title: ${REPO} workflow failed" \
|
||||
-H "Tags: x,warning" \
|
||||
-H "Priority: high" \
|
||||
-H "Click: ${URL}" \
|
||||
-d "${WORKFLOW} failed. Check the run for details." \
|
||||
"${NTFY_URL}/${NTFY_TOPIC}"
|
||||
+521
-508
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Validation
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs
|
||||
|
||||
name: "Joomla: Metadata Validation"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, converted_to_draft, ready_for_review]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||
|
||||
jobs:
|
||||
validate-metadata:
|
||||
name: "Validate Joomla Metadata"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup mokocli tools
|
||||
env:
|
||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||
run: |
|
||||
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||
echo Using pre-installed /opt/mokocli
|
||||
echo MOKO_CLI=/opt/mokocli/cli >> $GITHUB_ENV
|
||||
else
|
||||
echo Falling back to fresh clone
|
||||
if ! command -v composer > /dev/null 2>&1; then
|
||||
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
|
||||
fi
|
||||
rm -rf /tmp/mokocli
|
||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git
|
||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokocli
|
||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||
echo MOKO_CLI=/tmp/mokocli/cli >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Validate metadata against Joomla manifest
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
||||
--path . \
|
||||
--token "${MOKOGITEA_TOKEN}" \
|
||||
--org "${GITEA_ORG}" \
|
||||
--repo "${GITEA_REPO}" \
|
||||
--api-base "${MOKOGITEA_URL}/api/v1" \
|
||||
--ci
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "::error::Joomla metadata mismatch — update delivery will fail. Run 'php cli/joomla_metadata_validate.php' locally to see details."
|
||||
exit 1
|
||||
fi
|
||||
@@ -7,7 +7,7 @@
|
||||
# INGROUP: mokocli.Release
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||
# VERSION: 05.01.00
|
||||
# VERSION: 05.02.00
|
||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||
|
||||
name: "Universal: Pre-Release"
|
||||
@@ -59,6 +59,11 @@ jobs:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
ref: ${{ github.ref_name }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Update submodules to main
|
||||
run: |
|
||||
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
|
||||
|
||||
- name: Setup mokocli tools
|
||||
env:
|
||||
@@ -88,8 +93,20 @@ jobs:
|
||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||
|
||||
- name: Check platform eligibility (Joomla only)
|
||||
id: eligibility
|
||||
run: |
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
|
||||
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
|
||||
fi
|
||||
|
||||
- name: Resolve metadata and bump version
|
||||
id: meta
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
run: |
|
||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
@@ -166,6 +183,7 @@ jobs:
|
||||
|
||||
- name: Create release
|
||||
id: release
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
@@ -176,6 +194,7 @@ jobs:
|
||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||
|
||||
- name: Update release notes from CHANGELOG.md
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
run: |
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
@@ -212,6 +231,7 @@ jobs:
|
||||
|
||||
- name: Build package and upload
|
||||
id: package
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
run: |
|
||||
VERSION="${{ steps.meta.outputs.version }}"
|
||||
TAG="${{ steps.meta.outputs.tag }}"
|
||||
@@ -225,6 +245,7 @@ jobs:
|
||||
# No need to build, commit, or sync updates.xml from workflows
|
||||
|
||||
- name: "Delete lesser pre-release channels (cascade)"
|
||||
if: steps.eligibility.outputs.proceed == 'true'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||
# VERSION: 09.23.00
|
||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||
|
||||
name: "RC Revert"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
revert:
|
||||
name: Rename rc/ back to dev/
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.pull_request.merged == false &&
|
||||
startsWith(github.event.pull_request.head.ref, 'rc/')
|
||||
|
||||
steps:
|
||||
- name: Rename branch
|
||||
env:
|
||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
REPO: ${{ github.repository }}
|
||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||
TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# BRANCH is attacker-controlled (PR head ref). Strict allowlist before ANY use.
|
||||
if ! printf '%s' "$BRANCH" | grep -Eq '^rc/[A-Za-z0-9._/-]+$'; then
|
||||
echo "::error::Refusing unsafe branch name: $BRANCH"; exit 1
|
||||
fi
|
||||
SUFFIX="${BRANCH#rc/}"
|
||||
DEV_BRANCH="dev/${SUFFIX}"
|
||||
API="${GITEA_URL}/api/v1/repos/${REPO}/branches"
|
||||
|
||||
# Create dev/ branch from rc/ branch
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||
"${API}" 2>/dev/null || true)
|
||||
if [ "$STATUS" = "201" ]; then
|
||||
echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1
|
||||
fi
|
||||
|
||||
# Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection)
|
||||
ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));')
|
||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||
if [ "$STATUS" = "204" ]; then
|
||||
echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||
fi
|
||||
|
||||
echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow.Template
|
||||
# INGROUP: MokoStandards.CI
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
# PATH: /.mokogitea/workflows/version-set.yml
|
||||
# VERSION: 01.00.00
|
||||
# BRIEF: Set or reset the extension version across all version-bearing files
|
||||
|
||||
name: "Joomla: Set Version"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version number (e.g. 01.00.00)"
|
||||
required: true
|
||||
type: string
|
||||
branch:
|
||||
description: "Branch to update (default: current)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
set-version:
|
||||
name: Set Version to ${{ inputs.version }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Validate version format
|
||||
run: |
|
||||
VERSION="${{ inputs.version }}"
|
||||
if ! echo "$VERSION" | grep -qP '^\d{2}\.\d{2}\.\d{2}$'; then
|
||||
echo "::error::Invalid version format '${VERSION}' — expected XX.YY.ZZ (e.g. 01.00.00)"
|
||||
exit 1
|
||||
fi
|
||||
echo "VERSION=${VERSION}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||
ref: ${{ inputs.branch || github.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Update manifest version
|
||||
run: |
|
||||
MANIFEST=""
|
||||
for XML_FILE in $(find . -maxdepth 3 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
||||
MANIFEST="$XML_FILE"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$MANIFEST" ]; then
|
||||
echo "::warning::No Joomla extension manifest found — skipping manifest update"
|
||||
else
|
||||
OLD_VER=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
|
||||
sed -i "s|<version>${OLD_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
|
||||
echo "Manifest: ${OLD_VER} → ${VERSION} (${MANIFEST})"
|
||||
fi
|
||||
|
||||
- name: Update README.md version
|
||||
run: |
|
||||
if [ -f "README.md" ]; then
|
||||
if grep -qP '^\s*VERSION:\s*\d' README.md; then
|
||||
sed -i -E "s/(VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" README.md
|
||||
echo "README.md version updated to ${VERSION}"
|
||||
else
|
||||
echo "::warning::No VERSION line found in README.md — skipping"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Update CHANGELOG.md
|
||||
run: |
|
||||
if [ -f "CHANGELOG.md" ]; then
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
# Check if this version already has an entry
|
||||
if grep -q "^\#\# \[${VERSION}\]" CHANGELOG.md; then
|
||||
echo "CHANGELOG.md already has entry for ${VERSION} — skipping"
|
||||
else
|
||||
# Insert new version entry after [Unreleased] or at the top after header
|
||||
if grep -q '^\#\# \[Unreleased\]' CHANGELOG.md; then
|
||||
sed -i "/^\#\# \[Unreleased\]/a\\\\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
||||
else
|
||||
sed -i "/^\# Changelog/a\\\\n## [Unreleased]\n\n## [${VERSION}] --- ${DATE}" CHANGELOG.md
|
||||
fi
|
||||
echo "CHANGELOG.md: added entry for ${VERSION}"
|
||||
fi
|
||||
else
|
||||
echo "::warning::No CHANGELOG.md found — skipping"
|
||||
fi
|
||||
|
||||
- name: Update FILE INFORMATION blocks
|
||||
run: |
|
||||
# Update VERSION in file header blocks (# VERSION: XX.YY.ZZ)
|
||||
find . -maxdepth 1 -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.php" -o -name "*.md" \) \
|
||||
-not -path "./.git/*" -not -path "./vendor/*" -print0 2>/dev/null | \
|
||||
while IFS= read -r -d '' FILE; do
|
||||
if head -20 "$FILE" | grep -qP '^\s*#?\s*VERSION:\s*\d{2}\.\d{2}\.\d{2}'; then
|
||||
sed -i -E "s/(#?\s*VERSION:\s*)[0-9]{2}\.[0-9]{2}\.[0-9]{2}/\1${VERSION}/" "$FILE"
|
||||
echo "Updated FILE INFORMATION VERSION in ${FILE}"
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Commit and push
|
||||
run: |
|
||||
git config user.name "Moko Consulting [bot]"
|
||||
git config user.email "hello@mokoconsulting.tech"
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "No version changes detected — nothing to commit"
|
||||
else
|
||||
git commit -m "chore: set version to ${VERSION} [skip bump]
|
||||
|
||||
Authored-by: Moko Consulting"
|
||||
git push
|
||||
echo "### Version Set" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Version updated to \`${VERSION}\` on branch \`${GITHUB_REF_NAME}\`" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -0,0 +1,81 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: mokocli.Universal
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
||||
# VERSION: 01.01.00
|
||||
# BRIEF: Trigger workflow sync to live repos when a PR is merged to main
|
||||
|
||||
name: "Universal: Workflow Sync Trigger"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Sync workflows to live repos
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.merged == true &&
|
||||
!contains(github.event.pull_request.title, '[skip sync]'))
|
||||
|
||||
steps:
|
||||
- name: Determine platform from repo name
|
||||
id: platform
|
||||
run: |
|
||||
REPO="${{ github.event.repository.name }}"
|
||||
case "$REPO" in
|
||||
Template-Joomla) PLATFORM="joomla" ;;
|
||||
Template-Dolibarr) PLATFORM="dolibarr" ;;
|
||||
Template-Go) PLATFORM="go" ;;
|
||||
Template-MCP) PLATFORM="mcp" ;;
|
||||
Template-Generic) PLATFORM="" ;;
|
||||
*) PLATFORM="" ;;
|
||||
esac
|
||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||
echo "Platform: ${PLATFORM:-all}"
|
||||
|
||||
- name: Clone mokocli
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
MOKOGITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||
git clone --depth 1 "${MOKOGITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||
|
||||
- name: Install PHP
|
||||
run: |
|
||||
if ! command -v php &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq php-cli php-json php-curl > /dev/null 2>&1
|
||||
fi
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd /tmp/mokocli
|
||||
composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||
|
||||
- name: Run workflow sync
|
||||
env:
|
||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||
run: |
|
||||
ARGS="--token ${MOKOGITEA_TOKEN}"
|
||||
ARGS="${ARGS} --org ${{ vars.GITEA_ORG || github.repository_owner }}"
|
||||
ARGS="${ARGS} --phase repos"
|
||||
|
||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||
if [ -n "$PLATFORM" ]; then
|
||||
ARGS="${ARGS} --platform-filter ${PLATFORM}"
|
||||
fi
|
||||
|
||||
php /tmp/mokocli/cli/workflow_sync.php ${ARGS}
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-LICENSE-IDENTIFIER: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://github.com/mokoconsulting-tech/Template-Joomla/
|
||||
VERSION: 01.01.00
|
||||
PATH: ./CODE_OF_CONDUCT.md
|
||||
BRIEF: Community expectations and enforcement guidelines
|
||||
NOTE: Adapted with attribution from the Contributor Covenant v2.1
|
||||
-->
|
||||
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone.
|
||||
|
||||
## Our Standards
|
||||
- Be empathetic and kind
|
||||
- Be respectful of differing opinions
|
||||
- Accept constructive feedback
|
||||
- Own mistakes and learn from them
|
||||
|
||||
Unacceptable behavior includes sexualized language/imagery, trolling, harassment, doxing, and other inappropriate conduct.
|
||||
|
||||
## Enforcement
|
||||
Report incidents to **hello@mokoconsulting.tech** or through GitHub Discussions if you prefer a community-visible approach. Private complaints will be reviewed promptly and fairly.
|
||||
|
||||
## Enforcement Guidelines
|
||||
1. **Correction** — Private warning
|
||||
2. **Warning** — Formal warning and limited interaction
|
||||
3. **Temporary Ban** — Time-boxed exclusion
|
||||
4. **Permanent Ban** — Removal from the community
|
||||
|
||||
## Attribution
|
||||
Adapted from the Contributor Covenant v2.1.
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
# Contributing to Moko Consulting Projects
|
||||
|
||||
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||
|
||||
## Branching Workflow
|
||||
|
||||
```
|
||||
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||
```
|
||||
|
||||
### Step by step
|
||||
|
||||
1. **Create a feature branch** from `dev`:
|
||||
```bash
|
||||
git checkout dev && git pull
|
||||
git checkout -b feature/my-change
|
||||
```
|
||||
|
||||
2. **Work and commit** on your feature branch. Push to origin.
|
||||
|
||||
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||
|
||||
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||
- This automatically renames the source branch to `rc` (release candidate)
|
||||
- An RC pre-release is built and uploaded
|
||||
|
||||
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||
- When the draft PR is created, the branch is renamed to `rc`
|
||||
|
||||
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||
|
||||
7. **Merging to main** triggers the stable release pipeline:
|
||||
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||
- Stability suffix stripped (clean version)
|
||||
- Gitea release created with ZIP/tar.gz packages
|
||||
- `updates.xml` updated (Joomla extensions)
|
||||
- `dev` branch recreated from `main`
|
||||
|
||||
### Branch summary
|
||||
|
||||
| Branch | Purpose | Created by |
|
||||
|--------|---------|-----------|
|
||||
| `feature/*` | New features and fixes | Developer |
|
||||
| `dev` | Integration branch | Auto-recreated after release |
|
||||
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||
| `main` | Stable releases | Protected, merge only |
|
||||
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||
|
||||
### Protected branches
|
||||
|
||||
| Branch | Direct push | Merge via |
|
||||
|--------|------------|-----------|
|
||||
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||
| `feature/*` | Open | N/A (source branch) |
|
||||
|
||||
## Version Policy
|
||||
|
||||
### Format
|
||||
|
||||
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||
|
||||
- **XX** — Major version (breaking changes)
|
||||
- **YY** — Minor version (new features, bumped on release to main)
|
||||
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||
|
||||
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||
|
||||
### Stability suffixes
|
||||
|
||||
Each branch appends a suffix to indicate stability:
|
||||
|
||||
| Branch | Suffix | Example |
|
||||
|--------|--------|---------|
|
||||
| `main` | (none) | `02.09.00` |
|
||||
| `dev` | `-dev` | `02.09.01-dev` |
|
||||
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||
| `beta` | `-beta` | `02.09.01-beta` |
|
||||
| `rc` | `-rc` | `02.09.01-rc` |
|
||||
|
||||
### Auto version bump
|
||||
|
||||
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||
|
||||
1. Patch version incremented
|
||||
2. Stability suffix `-dev` applied
|
||||
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||
4. Commit created with `[skip ci]` to avoid loops
|
||||
|
||||
### Release version flow
|
||||
|
||||
Version bumps happen at specific release events:
|
||||
|
||||
| Event | Bump | Example |
|
||||
|-------|------|---------|
|
||||
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||
|
||||
### Release stream copies
|
||||
|
||||
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||
|
||||
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||
|
||||
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||
|
||||
### Version files
|
||||
|
||||
The version tools update all files containing version stamps:
|
||||
|
||||
- `.mokogitea/manifest.xml` (canonical source)
|
||||
- Joomla XML manifests (`<version>` tag)
|
||||
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||
- `package.json`, `pyproject.toml`
|
||||
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||
|
||||
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||
|
||||
## Code Standards
|
||||
|
||||
- **PHP**: PSR-12, tabs for indentation
|
||||
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Use conventional commit format:
|
||||
|
||||
```
|
||||
type(scope): short description
|
||||
|
||||
Optional body with context.
|
||||
|
||||
Authored-by: Moko Consulting
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||
|
||||
Special flags in commit messages:
|
||||
- `[skip ci]` — skip all CI workflows
|
||||
- `[skip bump]` — skip auto version bump only
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
Use the repository's issue tracker with the appropriate template.
|
||||
|
||||
---
|
||||
|
||||
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify it under the terms of
|
||||
the GNU General Public License as published by the Free Software Foundation; either version 3
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
See the GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License (./LICENSE).
|
||||
|
||||
FILE INFORMATION
|
||||
DEFGROUP: mokoconsulting-tech.Template-Joomla
|
||||
INGROUP: MokoStandards.Governance
|
||||
REPO: https://github.com/mokoconsulting-tech/Template-Joomla
|
||||
VERSION: 01.01.00
|
||||
PATH: /GOVERNANCE.md
|
||||
BRIEF: Project governance rules, roles, and decision process for Template-Joomla
|
||||
-->
|
||||
|
||||
[](https://github.com/mokoconsulting-tech/MokoStandards)
|
||||
|
||||
# Project Governance
|
||||
|
||||
## Overview
|
||||
|
||||
This document defines the governance model for the `Template-Joomla` repository within the
|
||||
`mokoconsulting-tech` organization. It is automatically maintained by
|
||||
[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) v04.00.04.
|
||||
|
||||
Full governance policy is defined in the MokoStandards source repository:
|
||||
[docs/policy/GOVERNANCE.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md)
|
||||
|
||||
---
|
||||
|
||||
## Roles and Responsibilities
|
||||
|
||||
### Maintainer
|
||||
|
||||
**GitHub**: @mokoconsulting-tech
|
||||
|
||||
**Authority**: Final decision-making authority on all matters for this repository.
|
||||
|
||||
**Responsibilities**:
|
||||
- Review and merge pull requests
|
||||
- Maintain code quality and standards compliance
|
||||
- Manage releases and versioning
|
||||
- Respond to issues and security reports
|
||||
|
||||
### Contributors
|
||||
|
||||
**Authority**: Submit changes via pull requests.
|
||||
|
||||
**Requirements**:
|
||||
- Read and accept `CODE_OF_CONDUCT.md`
|
||||
- Follow `CONTRIBUTING.md` guidelines
|
||||
|
||||
---
|
||||
|
||||
## Decision-Making
|
||||
|
||||
All changes must be submitted as pull requests. The maintainer (@mokoconsulting-tech)
|
||||
reviews and approves all changes before they are merged.
|
||||
|
||||
### Sole Operator Policy
|
||||
|
||||
This organization operates under a **sole operator** model. The maintainer (@mokoconsulting-tech)
|
||||
is the sole employee and owner and may self-approve pull requests when no second reviewer is
|
||||
available. The following requirements remain mandatory regardless:
|
||||
|
||||
1. **Pull Requests Required** — all changes to protected branches go through a PR.
|
||||
2. **Automated Checks** — all CI checks must pass before merging.
|
||||
3. **Audit Trail** — issues, pull requests, and commit history are preserved.
|
||||
4. **Documentation** — changes are documented in `CHANGELOG.md`.
|
||||
|
||||
See the full policy:
|
||||
[Sole Operator Policy](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md#sole-operator-policy)
|
||||
|
||||
---
|
||||
|
||||
## Change Management
|
||||
|
||||
| Change Type | Approval | Process |
|
||||
|-------------|----------|---------|
|
||||
| Routine (docs, bug fixes) | Maintainer | PR → CI pass → merge |
|
||||
| Significant (new features) | Maintainer | PR with description → CI pass → merge |
|
||||
| Major (breaking, architecture) | Maintainer | Issue discussion → PR → CI pass → merge |
|
||||
| Emergency (security) | Maintainer | Labelled `EMERGENCY` → immediate merge → post-mortem |
|
||||
|
||||
---
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
- **Bugs / Features**: Open a [GitHub Issue](https://github.com/mokoconsulting-tech/Template-Joomla/issues)
|
||||
- **Security vulnerabilities**: See [SECURITY.md](./SECURITY.md)
|
||||
- **Code of Conduct**: See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)
|
||||
- **Contact**: dev@mokoconsulting.tech
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
| ------------- | ----------------------------------------------- |
|
||||
| Document Type | Policy |
|
||||
| Domain | Governance |
|
||||
| Applies To | mokoconsulting-tech/Template-Joomla |
|
||||
| Jurisdiction | Tennessee, USA |
|
||||
| Maintainer | @mokoconsulting-tech |
|
||||
| Standards | MokoStandards v04.00.04 |
|
||||
| Repo | https://github.com/mokoconsulting-tech/Template-Joomla |
|
||||
| Path | /GOVERNANCE.md |
|
||||
| Status | Active — auto-maintained by MokoStandards |
|
||||
+241
@@ -0,0 +1,241 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
|
||||
This file is part of a Moko Consulting project.
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: Template-Joomla
|
||||
INGROUP: Template-Joomla.Documentation
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
||||
PATH: /SECURITY.md
|
||||
VERSION: 01.01.00
|
||||
BRIEF: Security vulnerability reporting and handling policy
|
||||
-->
|
||||
|
||||
# Security Policy
|
||||
|
||||
## Purpose and Scope
|
||||
|
||||
This document defines the security vulnerability reporting, response, and disclosure policy for this Joomla Plugin template repository. It establishes the authoritative process for responsible disclosure, assessment, remediation, and communication of security issues.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security updates are provided for the following versions:
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 01.x.x | :white_check_mark: |
|
||||
| < 01.0 | :x: |
|
||||
|
||||
Only the current major version receives security updates. Users should upgrade to the latest supported version to receive security patches.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
### Where to Report
|
||||
|
||||
**DO NOT** create public GitHub issues for security vulnerabilities.
|
||||
|
||||
Report security vulnerabilities privately to:
|
||||
|
||||
**Email**: `security@mokoconsulting.tech`
|
||||
|
||||
**Subject Line**: `[SECURITY] Template-Joomla - Brief Description`
|
||||
|
||||
### What to Include
|
||||
|
||||
A complete vulnerability report should include:
|
||||
|
||||
1. **Description**: Clear explanation of the vulnerability
|
||||
2. **Impact**: Potential security impact and severity assessment
|
||||
3. **Affected Versions**: Which versions are vulnerable
|
||||
4. **Reproduction Steps**: Detailed steps to reproduce the issue
|
||||
5. **Proof of Concept**: Code, configuration, or demonstration (if applicable)
|
||||
6. **Suggested Fix**: Proposed remediation (if known)
|
||||
7. **Disclosure Timeline**: Your expectations for public disclosure
|
||||
|
||||
### Response Timeline
|
||||
|
||||
* **Initial Response**: Within 3 business days
|
||||
* **Assessment Complete**: Within 7 business days
|
||||
* **Fix Timeline**: Depends on severity (see below)
|
||||
* **Disclosure**: Coordinated with reporter
|
||||
|
||||
## Severity Classification
|
||||
|
||||
Vulnerabilities are classified using the following severity levels:
|
||||
|
||||
### Critical
|
||||
* Remote code execution
|
||||
* Authentication bypass
|
||||
* Data breach or exposure of sensitive information
|
||||
* **Fix Timeline**: 7 days
|
||||
|
||||
### High
|
||||
* Privilege escalation
|
||||
* SQL injection or command injection
|
||||
* Cross-site scripting (XSS) with significant impact
|
||||
* **Fix Timeline**: 14 days
|
||||
|
||||
### Medium
|
||||
* Information disclosure (limited scope)
|
||||
* Denial of service
|
||||
* Security misconfigurations with moderate impact
|
||||
* **Fix Timeline**: 30 days
|
||||
|
||||
### Low
|
||||
* Security best practice violations
|
||||
* Minor information leaks
|
||||
* Issues requiring user interaction or complex preconditions
|
||||
* **Fix Timeline**: 60 days or next release
|
||||
|
||||
## Remediation Process
|
||||
|
||||
1. **Acknowledgment**: Security team confirms receipt and begins investigation
|
||||
2. **Assessment**: Vulnerability is validated, severity assigned, and impact analyzed
|
||||
3. **Development**: Security patch is developed and tested
|
||||
4. **Review**: Patch undergoes security review and validation
|
||||
5. **Release**: Fixed version is released with security advisory
|
||||
6. **Disclosure**: Public disclosure follows coordinated timeline
|
||||
|
||||
## Security Advisories
|
||||
|
||||
Security advisories are published via:
|
||||
|
||||
* GitHub Security Advisories
|
||||
* Release notes and CHANGELOG.md
|
||||
* Email notification to project users (if mailing list is established)
|
||||
|
||||
Advisories include:
|
||||
|
||||
* CVE identifier (if applicable)
|
||||
* Severity rating
|
||||
* Affected versions
|
||||
* Fixed versions
|
||||
* Mitigation steps
|
||||
* Attribution (with reporter consent)
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
For projects using this template:
|
||||
|
||||
### Required Controls
|
||||
|
||||
* Enable GitHub security features (Dependabot, code scanning)
|
||||
* Implement branch protection on `main`
|
||||
* Require code review for all changes
|
||||
* Enforce signed commits (recommended)
|
||||
* Use secrets management (never commit credentials)
|
||||
* Maintain security documentation
|
||||
* Follow secure coding standards defined in MokoStandards
|
||||
|
||||
### Joomla Plugin Security
|
||||
|
||||
* Follow Joomla security best practices
|
||||
* Validate and sanitize all user input
|
||||
* Use Joomla's database API to prevent SQL injection
|
||||
* Properly escape output to prevent XSS
|
||||
* Implement proper access control checks
|
||||
* Use Joomla's session and authentication APIs
|
||||
* Keep Joomla and dependencies up to date
|
||||
|
||||
### CI/CD Security
|
||||
|
||||
* Validate all inputs
|
||||
* Sanitize outputs
|
||||
* Use least privilege access
|
||||
* Pin dependencies with hash verification
|
||||
* Scan for vulnerabilities in dependencies
|
||||
* Audit third-party actions and tools
|
||||
|
||||
#### Automated Security Scanning
|
||||
|
||||
All repositories SHOULD implement:
|
||||
|
||||
**CodeQL Analysis**:
|
||||
* Enabled for PHP and other supported languages
|
||||
* Runs on: push to main, pull requests, weekly schedule
|
||||
* Query sets: `security-extended` and `security-and-quality`
|
||||
* Configuration: `.github/workflows/codeql-analysis.yml`
|
||||
|
||||
**Dependabot Security Updates**:
|
||||
* Weekly scans for vulnerable dependencies
|
||||
* Automated pull requests for security patches
|
||||
* Configuration: `.github/dependabot.yml`
|
||||
|
||||
**Secret Scanning**:
|
||||
* Enabled by default with push protection
|
||||
* Prevents accidental credential commits
|
||||
|
||||
### Dependency Management
|
||||
|
||||
* Keep dependencies up to date
|
||||
* Monitor security advisories for dependencies
|
||||
* Remove unused dependencies
|
||||
* Audit new dependencies before adoption
|
||||
* Document security-critical dependencies
|
||||
|
||||
## Compliance and Governance
|
||||
|
||||
This security policy is aligned with MokoStandards. Deviations require documented justification.
|
||||
|
||||
Security policies are reviewed and updated at least annually or following significant security incidents.
|
||||
|
||||
## Attribution and Recognition
|
||||
|
||||
We acknowledge and appreciate responsible disclosure. With your permission, we will:
|
||||
|
||||
* Credit you in security advisories
|
||||
* List you in CHANGELOG.md for the fix release
|
||||
* Recognize your contribution publicly (if desired)
|
||||
|
||||
## Contact and Escalation
|
||||
|
||||
* **Security Team**: security@mokoconsulting.tech
|
||||
* **Primary Contact**: hello@mokoconsulting.tech
|
||||
* **Escalation**: For urgent matters requiring immediate attention, contact the maintainer directly via GitHub
|
||||
|
||||
## Out of Scope
|
||||
|
||||
The following are explicitly out of scope:
|
||||
|
||||
* Issues in third-party dependencies (report directly to maintainers)
|
||||
* Social engineering attacks
|
||||
* Physical security issues
|
||||
* Denial of service via resource exhaustion without amplification
|
||||
* Issues requiring physical access to systems
|
||||
* Theoretical vulnerabilities without proof of exploitability
|
||||
|
||||
---
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------ |
|
||||
| Document | Security Policy |
|
||||
| Path | /SECURITY.md |
|
||||
| Repository | [https://github.com/mokoconsulting-tech/Template-Joomla](https://github.com/mokoconsulting-tech/Template-Joomla) |
|
||||
| Owner | Moko Consulting |
|
||||
| Scope | Security vulnerability handling |
|
||||
| Status | Active |
|
||||
| Effective | 2026-01-16 |
|
||||
|
||||
## Revision History
|
||||
|
||||
| Date | Change Description | Author |
|
||||
| ---------- | ------------------------------------------------- | --------------- |
|
||||
| 2026-01-16 | Initial creation for template repository | Moko Consulting |
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "mokoconsulting/mokojoomgallery",
|
||||
"description": "Photo gallery management for Joomla — galleries, images, thumbnails, lightbox, and frontend display",
|
||||
"type": "joomla-package",
|
||||
"version": "01.00.00",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Moko Consulting",
|
||||
"email": "hello@mokoconsulting.tech",
|
||||
"homepage": "https://mokoconsulting.tech"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=8.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"joomla/coding-standards": "3.0.x-dev"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"config": {
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# PHPStan configuration for Joomla extension repositories.
|
||||
# Extends the base MokoStandards config and adds Joomla framework class stubs
|
||||
# so PHPStan can resolve Factory, CMSApplication, User, Table, etc.
|
||||
# without requiring a full Joomla installation.
|
||||
|
||||
parameters:
|
||||
level: 5
|
||||
|
||||
paths:
|
||||
- src
|
||||
|
||||
excludePaths:
|
||||
- vendor
|
||||
- node_modules
|
||||
|
||||
# Joomla framework stubs — resolved via the enterprise package from vendor/
|
||||
stubFiles:
|
||||
- vendor/mokoconsulting-tech/enterprise/templates/stubs/joomla.php
|
||||
|
||||
# Suppress errors that are structural in Joomla's service-container architecture
|
||||
ignoreErrors:
|
||||
# Joomla's service-based dependency injection returns mixed from getApplication()
|
||||
- '#Cannot call method .+ on Joomla\\CMS\\Application\\CMSApplication\|null#'
|
||||
# Factory::getX() patterns are safe at runtime even when nullable in stubs
|
||||
- '#Call to static method [a-zA-Z]+\(\) on an interface#'
|
||||
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
checkMissingIterableValueType: false
|
||||
checkGenericClassInNonGenericObjectType: false
|
||||
@@ -85,11 +85,30 @@ class NpoReportsController extends BaseController
|
||||
$id = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
|
||||
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
|
||||
|
||||
// Verify pledge exists and is active before cancelling
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('id, status')
|
||||
->from('#__mokosuitenpo_pledges')
|
||||
->where('id = ' . (int) $id));
|
||||
$pledge = $db->loadObject();
|
||||
|
||||
if (!$pledge) {
|
||||
http_response_code(404);
|
||||
$this->sendJson(['success' => false, 'error' => 'Pledge not found']);
|
||||
return;
|
||||
}
|
||||
if ($pledge->status !== 'active') {
|
||||
$this->sendJson(['success' => false, 'error' => 'Pledge is not active']);
|
||||
return;
|
||||
}
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->update('#__mokosuitenpo_pledges')
|
||||
->set($db->quoteName('status') . ' = ' . $db->quote('cancelled'))
|
||||
->set($db->quoteName('cancelled_at') . ' = ' . $db->quote(Factory::getDate()->toSql()))
|
||||
->where('id = ' . (int) $id));
|
||||
->where('id = ' . (int) $id)
|
||||
->where($db->quoteName('status') . ' = ' . $db->quote('active')));
|
||||
$db->execute();
|
||||
|
||||
$this->sendJson(['success' => true]);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<license>GPL-3.0-or-later</license>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||
<version>01.01.00</version>
|
||||
<version>01.07.00</version>
|
||||
<php_minimum>8.3</php_minimum>
|
||||
<description>MokoSuite NPO component</description>
|
||||
<namespace path="src">Moko\Component\MokoSuiteNpo</namespace>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
namespace Moko\Component\MokoSuiteNpo\Site\View\ThankYou;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Donation thank-you page — displays after successful donation with receipt info.
|
||||
*/
|
||||
class HtmlView extends BaseHtmlView
|
||||
{
|
||||
public ?object $donation = null;
|
||||
public ?object $receipt = null;
|
||||
|
||||
public function display($tpl = null): void
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$donationId = Factory::getApplication()->getInput()->getInt('id', 0);
|
||||
$token = Factory::getApplication()->getInput()->getString('token', '');
|
||||
|
||||
if (!$donationId || !$token) {
|
||||
parent::display($tpl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify token matches donation (prevents enumeration)
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('d.*, cd.name AS donor_name, f.name AS fund_name, c.title AS campaign_title')
|
||||
->from($db->quoteName('#__mokosuitenpo_donations', 'd'))
|
||||
->join('INNER', $db->quoteName('#__mokosuitenpo_donors', 'don') . ' ON don.id = d.donor_id')
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = don.contact_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuitenpo_funds', 'f') . ' ON f.id = d.fund_id')
|
||||
->join('LEFT', $db->quoteName('#__mokosuitenpo_campaigns', 'c') . ' ON c.id = d.campaign_id')
|
||||
->where('d.id = ' . (int) $donationId)
|
||||
->where($db->quoteName('d.confirmation_token') . ' = ' . $db->quote($token)));
|
||||
$this->donation = $db->loadObject();
|
||||
|
||||
if ($this->donation) {
|
||||
// Get tax receipt if generated
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('receipt_number, issued_date, amount')
|
||||
->from('#__mokosuitenpo_tax_receipts')
|
||||
->where('donation_id = ' . (int) $donationId));
|
||||
$this->receipt = $db->loadObject();
|
||||
}
|
||||
|
||||
parent::display($tpl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Board of directors management — member terms, committees, meeting minutes, attendance.
|
||||
*/
|
||||
class BoardManagementHelper
|
||||
{
|
||||
/**
|
||||
* Get current board members with term status.
|
||||
*/
|
||||
public static function getCurrentMembers(): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('bm.*, cd.name, cd.email_to, cd.telephone')
|
||||
->select('CASE WHEN bm.term_end < NOW() THEN ' . $db->quote('expired')
|
||||
. ' WHEN bm.term_end < DATE_ADD(NOW(), INTERVAL 90 DAY) THEN ' . $db->quote('expiring_soon')
|
||||
. ' ELSE ' . $db->quote('active') . ' END AS term_status')
|
||||
->from($db->quoteName('#__mokosuitenpo_board_members', 'bm'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = bm.contact_id')
|
||||
->where($db->quoteName('bm.status') . ' = ' . $db->quote('active'))
|
||||
->order('bm.role ASC, cd.name ASC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get committee assignments.
|
||||
*/
|
||||
public static function getCommittees(): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('c.id, c.name AS committee_name, c.description')
|
||||
->select('(SELECT COUNT(*) FROM #__mokosuitenpo_committee_members cm WHERE cm.committee_id = c.id AND cm.status = ' . $db->quote('active') . ') AS member_count')
|
||||
->select('(SELECT cd2.name FROM #__mokosuitenpo_committee_members cm2'
|
||||
. ' JOIN #__contact_details cd2 ON cd2.id = cm2.contact_id'
|
||||
. ' WHERE cm2.committee_id = c.id AND cm2.role = ' . $db->quote('chair')
|
||||
. ' AND cm2.status = ' . $db->quote('active') . ' LIMIT 1) AS chair_name')
|
||||
->from($db->quoteName('#__mokosuitenpo_committees', 'c'))
|
||||
->where($db->quoteName('c.status') . ' = ' . $db->quote('active'))
|
||||
->order('c.name ASC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get meeting attendance rate for a board member.
|
||||
*/
|
||||
public static function getAttendanceRate(int $contactId, int $months = 12): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$since = date('Y-m-d', strtotime("-{$months} months"));
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*) AS total_meetings')
|
||||
->select('SUM(CASE WHEN ma.status = ' . $db->quote('present') . ' THEN 1 ELSE 0 END) AS attended')
|
||||
->select('SUM(CASE WHEN ma.status = ' . $db->quote('absent') . ' THEN 1 ELSE 0 END) AS absent')
|
||||
->select('SUM(CASE WHEN ma.status = ' . $db->quote('excused') . ' THEN 1 ELSE 0 END) AS excused')
|
||||
->from($db->quoteName('#__mokosuitenpo_meeting_attendance', 'ma'))
|
||||
->join('INNER', $db->quoteName('#__mokosuitenpo_meetings', 'm') . ' ON m.id = ma.meeting_id')
|
||||
->where('ma.contact_id = ' . (int) $contactId)
|
||||
->where('m.meeting_date >= ' . $db->quote($since)));
|
||||
|
||||
$stats = $db->loadObject();
|
||||
$total = (int) ($stats->total_meetings ?? 0);
|
||||
|
||||
return (object) [
|
||||
'contact_id' => $contactId,
|
||||
'total_meetings' => $total,
|
||||
'attended' => (int) ($stats->attended ?? 0),
|
||||
'absent' => (int) ($stats->absent ?? 0),
|
||||
'excused' => (int) ($stats->excused ?? 0),
|
||||
'attendance_pct' => $total > 0 ? round((int) $stats->attended / $total * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get terms expiring within N days.
|
||||
*/
|
||||
public static function getExpiringTerms(int $days = 90): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$cutoff = date('Y-m-d', strtotime("+{$days} days"));
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('bm.id, bm.role, bm.term_start, bm.term_end')
|
||||
->select('cd.name, cd.email_to')
|
||||
->from($db->quoteName('#__mokosuitenpo_board_members', 'bm'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = bm.contact_id')
|
||||
->where($db->quoteName('bm.status') . ' = ' . $db->quote('active'))
|
||||
->where('bm.term_end BETWEEN ' . $db->quote(date('Y-m-d')) . ' AND ' . $db->quote($cutoff))
|
||||
->order('bm.term_end ASC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Donor retention analysis — LYBUNT/SYBUNT detection, retention rates, lapsed outreach lists.
|
||||
*/
|
||||
class DonorRetentionHelper
|
||||
{
|
||||
/**
|
||||
* Get LYBUNT donors — gave Last Year But Unfortunately Not This year.
|
||||
*/
|
||||
public static function getLybunt(int $currentYear = 0): array
|
||||
{
|
||||
$currentYear = $currentYear ?: (int) date('Y');
|
||||
$lastYear = $currentYear - 1;
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('cd.id AS contact_id, cd.name, cd.email_to, cd.telephone')
|
||||
->select('MAX(d.donation_date) AS last_donation_date')
|
||||
->select('SUM(CASE WHEN YEAR(d.donation_date) = ' . $lastYear . ' THEN d.amount ELSE 0 END) AS last_year_total')
|
||||
->from($db->quoteName('#__contact_details', 'cd'))
|
||||
->join('INNER', $db->quoteName('#__mokosuitenpo_donations', 'd') . ' ON d.contact_id = cd.id')
|
||||
->where('YEAR(d.donation_date) = ' . $lastYear)
|
||||
->where('cd.id NOT IN (SELECT d2.contact_id FROM #__mokosuitenpo_donations d2 WHERE YEAR(d2.donation_date) = ' . $currentYear . ')')
|
||||
->group('cd.id, cd.name, cd.email_to, cd.telephone')
|
||||
->order('last_year_total DESC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SYBUNT donors — gave Some Year But Unfortunately Not This year.
|
||||
*/
|
||||
public static function getSybunt(int $currentYear = 0, int $lookbackYears = 3): array
|
||||
{
|
||||
$currentYear = $currentYear ?: (int) date('Y');
|
||||
$lastYear = $currentYear - 1;
|
||||
$startYear = $currentYear - $lookbackYears;
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('cd.id AS contact_id, cd.name, cd.email_to')
|
||||
->select('COUNT(DISTINCT YEAR(d.donation_date)) AS years_donated')
|
||||
->select('MAX(d.donation_date) AS last_donation_date')
|
||||
->select('SUM(d.amount) AS lifetime_total')
|
||||
->from($db->quoteName('#__contact_details', 'cd'))
|
||||
->join('INNER', $db->quoteName('#__mokosuitenpo_donations', 'd') . ' ON d.contact_id = cd.id')
|
||||
->where('YEAR(d.donation_date) BETWEEN ' . $startYear . ' AND ' . $lastYear)
|
||||
->where('cd.id NOT IN (SELECT d2.contact_id FROM #__mokosuitenpo_donations d2 WHERE YEAR(d2.donation_date) = ' . $currentYear . ')')
|
||||
->group('cd.id, cd.name, cd.email_to')
|
||||
->order('lifetime_total DESC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate retention rate — percentage of donors who gave again this year.
|
||||
*/
|
||||
public static function getRetentionRate(int $currentYear = 0): object
|
||||
{
|
||||
$currentYear = $currentYear ?: (int) date('Y');
|
||||
$lastYear = $currentYear - 1;
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(DISTINCT d.contact_id) AS last_year_donors')
|
||||
->from($db->quoteName('#__mokosuitenpo_donations', 'd'))
|
||||
->where('YEAR(d.donation_date) = ' . $lastYear));
|
||||
$lastYearDonors = (int) $db->loadResult();
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(DISTINCT d.contact_id) AS retained')
|
||||
->from($db->quoteName('#__mokosuitenpo_donations', 'd'))
|
||||
->where('YEAR(d.donation_date) = ' . $currentYear)
|
||||
->where('d.contact_id IN (SELECT d2.contact_id FROM #__mokosuitenpo_donations d2 WHERE YEAR(d2.donation_date) = ' . $lastYear . ')'));
|
||||
$retained = (int) $db->loadResult();
|
||||
|
||||
return (object) [
|
||||
'last_year_donors' => $lastYearDonors,
|
||||
'retained' => $retained,
|
||||
'lapsed' => $lastYearDonors - $retained,
|
||||
'retention_rate' => $lastYearDonors > 0 ? round($retained / $lastYearDonors * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get donor giving trends — year-over-year comparison.
|
||||
*/
|
||||
public static function getGivingTrends(int $years = 5): array
|
||||
{
|
||||
$currentYear = (int) date('Y');
|
||||
$startYear = $currentYear - $years + 1;
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('YEAR(donation_date) AS year')
|
||||
->select('COUNT(*) AS donation_count')
|
||||
->select('COUNT(DISTINCT contact_id) AS unique_donors')
|
||||
->select('SUM(amount) AS total_amount')
|
||||
->select('AVG(amount) AS avg_donation')
|
||||
->from('#__mokosuitenpo_donations')
|
||||
->where('YEAR(donation_date) BETWEEN ' . $startYear . ' AND ' . $currentYear)
|
||||
->group('YEAR(donation_date)')
|
||||
->order('year ASC'));
|
||||
|
||||
$trends = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($trends as &$t) {
|
||||
$t->avg_donation = round((float) $t->avg_donation, 2);
|
||||
}
|
||||
|
||||
return $trends;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Fund accounting — restricted vs unrestricted funds, fund balances, GAAP compliance.
|
||||
*/
|
||||
class FundAccountingHelper
|
||||
{
|
||||
/**
|
||||
* Get fund balances summary.
|
||||
*/
|
||||
public static function getFundBalances(): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('f.id, f.name, f.type, f.description')
|
||||
->select('COALESCE((SELECT SUM(d.amount) FROM #__mokosuitenpo_donations d WHERE d.fund_id = f.id), 0) AS total_received')
|
||||
->select('COALESCE((SELECT SUM(e.amount) FROM #__mokosuitenpo_fund_expenses e WHERE e.fund_id = f.id), 0) AS total_spent')
|
||||
->from($db->quoteName('#__mokosuitenpo_funds', 'f'))
|
||||
->where($db->quoteName('f.status') . ' = ' . $db->quote('active'))
|
||||
->order('f.type ASC, f.name ASC'));
|
||||
|
||||
$funds = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($funds as &$f) {
|
||||
$f->balance = round((float) $f->total_received - (float) $f->total_spent, 2);
|
||||
$f->is_restricted = ($f->type === 'restricted' || $f->type === 'temporarily_restricted');
|
||||
}
|
||||
|
||||
return $funds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an expense against a fund.
|
||||
*/
|
||||
public static function recordExpense(int $fundId, float $amount, string $description, string $category = 'program'): int
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$now = Factory::getDate()->toSql();
|
||||
|
||||
// Verify fund exists and has sufficient balance
|
||||
$db->setQuery($db->getQuery(true)->select('type')->from('#__mokosuitenpo_funds')->where('id = ' . (int) $fundId));
|
||||
$fundType = $db->loadResult();
|
||||
|
||||
if (!$fundType) throw new \RuntimeException('Fund not found');
|
||||
|
||||
// Enforce balance check on restricted funds (GAAP compliance)
|
||||
if ($fundType === 'restricted' || $fundType === 'temporarily_restricted') {
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COALESCE((SELECT SUM(d.amount) FROM #__mokosuitenpo_donations d WHERE d.fund_id = ' . (int) $fundId . '), 0)'
|
||||
. ' - COALESCE((SELECT SUM(e.amount) FROM #__mokosuitenpo_fund_expenses e WHERE e.fund_id = ' . (int) $fundId . '), 0) AS balance')
|
||||
->from('DUAL'));
|
||||
$balance = (float) $db->loadResult();
|
||||
|
||||
if ($amount > $balance) {
|
||||
throw new \RuntimeException('Insufficient balance in restricted fund (available: $' . number_format($balance, 2) . ', requested: $' . number_format($amount, 2) . ')');
|
||||
}
|
||||
}
|
||||
|
||||
$expense = (object) [
|
||||
'fund_id' => $fundId,
|
||||
'amount' => $amount,
|
||||
'description' => $description,
|
||||
'category' => $category, // program, admin, fundraising
|
||||
'recorded_by' => Factory::getApplication()->getIdentity()->id,
|
||||
'recorded_at' => $now,
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuitenpo_fund_expenses', $expense, 'id');
|
||||
return (int) $expense->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Statement of Financial Position (nonprofit balance sheet).
|
||||
*/
|
||||
public static function getFinancialPosition(): object
|
||||
{
|
||||
$funds = self::getFundBalances();
|
||||
|
||||
$unrestricted = 0;
|
||||
$tempRestricted = 0;
|
||||
$permRestricted = 0;
|
||||
|
||||
foreach ($funds as $f) {
|
||||
switch ($f->type) {
|
||||
case 'unrestricted': $unrestricted += $f->balance; break;
|
||||
case 'temporarily_restricted': $tempRestricted += $f->balance; break;
|
||||
case 'permanently_restricted': case 'restricted': $permRestricted += $f->balance; break;
|
||||
}
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'unrestricted' => $unrestricted,
|
||||
'temporarily_restricted' => $tempRestricted,
|
||||
'permanently_restricted' => $permRestricted,
|
||||
'total_net_assets' => $unrestricted + $tempRestricted + $permRestricted,
|
||||
'fund_count' => count($funds),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get expense breakdown by category (program vs admin vs fundraising).
|
||||
*/
|
||||
public static function getExpenseBreakdown(string $from = '', string $to = ''): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$from = $from ?: date('Y-01-01');
|
||||
$to = $to ?: date('Y-12-31');
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('category')
|
||||
->select('COUNT(*) AS count, COALESCE(SUM(amount), 0) AS total')
|
||||
->from('#__mokosuitenpo_fund_expenses')
|
||||
->where('DATE(recorded_at) BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))
|
||||
->group('category'));
|
||||
|
||||
$byCategory = $db->loadObjectList('category') ?: [];
|
||||
|
||||
$program = (float) ($byCategory['program']->total ?? 0);
|
||||
$admin = (float) ($byCategory['admin']->total ?? 0);
|
||||
$fundraising = (float) ($byCategory['fundraising']->total ?? 0);
|
||||
$totalExpenses = $program + $admin + $fundraising;
|
||||
|
||||
return (object) [
|
||||
'program' => $program,
|
||||
'admin' => $admin,
|
||||
'fundraising' => $fundraising,
|
||||
'total' => $totalExpenses,
|
||||
'program_pct' => $totalExpenses > 0 ? round($program / $totalExpenses * 100, 1) : 0,
|
||||
'overhead_pct' => $totalExpenses > 0 ? round(($admin + $fundraising) / $totalExpenses * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Grant reporting — deliverable tracking, spending reports, funder compliance.
|
||||
*/
|
||||
class GrantReportingHelper
|
||||
{
|
||||
/**
|
||||
* Get grant spending report — budgeted vs actual by category.
|
||||
*/
|
||||
public static function getSpendingReport(int $grantId): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('g.*, cd.name AS funder_name')
|
||||
->from($db->quoteName('#__mokosuitenpo_grants', 'g'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = g.funder_contact_id')
|
||||
->where('g.id = ' . (int) $grantId));
|
||||
$grant = $db->loadObject();
|
||||
|
||||
if (!$grant) return (object) ['found' => false];
|
||||
|
||||
// Get expenses charged to this grant's fund
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('category, COUNT(*) AS count, COALESCE(SUM(amount), 0) AS spent')
|
||||
->from('#__mokosuitenpo_fund_expenses')
|
||||
->where('fund_id = ' . (int) ($grant->fund_id ?? 0))
|
||||
->group('category')
|
||||
->order('spent DESC'));
|
||||
$grant->spending_by_category = $db->loadObjectList() ?: [];
|
||||
|
||||
$grant->total_spent = array_sum(array_column($grant->spending_by_category, 'spent'));
|
||||
$grant->remaining = max(0, (float) ($grant->amount ?? 0) - (float) $grant->total_spent);
|
||||
$grant->utilization_pct = (float) ($grant->amount ?? 0) > 0
|
||||
? round((float) $grant->total_spent / (float) $grant->amount * 100, 1) : 0;
|
||||
|
||||
return $grant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grants requiring reports soon.
|
||||
*/
|
||||
public static function getUpcomingReportDeadlines(int $days = 30): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('g.id, g.title, g.funder, g.report_due_date, g.amount')
|
||||
->select('DATEDIFF(g.report_due_date, CURDATE()) AS days_until_due')
|
||||
->from($db->quoteName('#__mokosuitenpo_grants', 'g'))
|
||||
->where($db->quoteName('g.status') . ' IN (' . $db->quote('active') . ',' . $db->quote('reporting') . ')')
|
||||
->where($db->quoteName('g.report_due_date') . ' IS NOT NULL')
|
||||
->where($db->quoteName('g.report_due_date') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . (int) $days . ' DAY)')
|
||||
->order('g.report_due_date ASC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* In-kind donation tracking — non-cash gifts, fair market valuation, category reporting.
|
||||
*/
|
||||
class InKindDonationHelper
|
||||
{
|
||||
/**
|
||||
* Record an in-kind donation.
|
||||
*/
|
||||
public static function record(int $contactId, string $description, float $fairMarketValue, string $category = 'goods'): object
|
||||
{
|
||||
if ($fairMarketValue <= 0) {
|
||||
throw new \InvalidArgumentException('Fair market value must be positive.');
|
||||
}
|
||||
|
||||
$allowedCategories = ['goods', 'services', 'equipment', 'real_estate', 'securities', 'vehicles', 'other'];
|
||||
if (!in_array($category, $allowedCategories, true)) {
|
||||
$category = 'other';
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
$filter = \Joomla\Filter\InputFilter::getInstance();
|
||||
|
||||
$donation = (object) [
|
||||
'contact_id' => $contactId,
|
||||
'description' => $filter->clean($description, 'STRING'),
|
||||
'fair_market_value'=> $fairMarketValue,
|
||||
'category' => $category,
|
||||
'status' => 'received',
|
||||
'received_at' => Factory::getDate()->toSql(),
|
||||
];
|
||||
|
||||
$db->insertObject('#__mokosuitenpo_inkind_donations', $donation, 'id');
|
||||
|
||||
return (object) ['success' => true, 'donation_id' => (int) $donation->id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get in-kind donation summary by category for a period.
|
||||
*/
|
||||
public static function getSummary(string $from = '', string $to = ''): array
|
||||
{
|
||||
$from = $from ?: date('Y-01-01');
|
||||
$to = $to ?: date('Y-m-d');
|
||||
|
||||
if (!\DateTime::createFromFormat('Y-m-d', $from) || !\DateTime::createFromFormat('Y-m-d', $to)) {
|
||||
throw new \InvalidArgumentException('Date parameters must be Y-m-d format.');
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('ik.category')
|
||||
->select('COUNT(*) AS donation_count')
|
||||
->select('SUM(ik.fair_market_value) AS total_value')
|
||||
->select('AVG(ik.fair_market_value) AS avg_value')
|
||||
->from($db->quoteName('#__mokosuitenpo_inkind_donations', 'ik'))
|
||||
->where('DATE(ik.received_at) BETWEEN ' . $db->quote($from) . ' AND ' . $db->quote($to))
|
||||
->group('ik.category')
|
||||
->order('total_value DESC'));
|
||||
|
||||
$results = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($results as &$r) {
|
||||
$r->avg_value = round((float) $r->avg_value, 2);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get donations needing appraisal (over $5,000 threshold per IRS rules).
|
||||
*/
|
||||
public static function getNeedingAppraisal(): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('ik.*, cd.name AS donor_name')
|
||||
->from($db->quoteName('#__mokosuitenpo_inkind_donations', 'ik'))
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = ik.contact_id')
|
||||
->where('ik.fair_market_value > 5000')
|
||||
->where('ik.appraisal_date IS NULL')
|
||||
->where($db->quoteName('ik.category') . ' NOT IN (' . $db->quote('securities') . ')')
|
||||
->order('ik.fair_market_value DESC'));
|
||||
|
||||
return $db->loadObjectList() ?: [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace Moko\Plugin\System\MokoSuiteNpo\Helper;
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
|
||||
use Joomla\CMS\Factory;
|
||||
use Joomla\Database\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Pledge reminders — notify donors of unfulfilled pledges, track fulfillment progress.
|
||||
*/
|
||||
class PledgeReminderHelper
|
||||
{
|
||||
/**
|
||||
* Get unfulfilled pledges that need reminders.
|
||||
*/
|
||||
public static function getUnfulfilled(int $overdueDays = 30): array
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('p.*, cd.name AS donor_name, cd.email_to')
|
||||
->select('COALESCE((SELECT SUM(d.amount) FROM #__mokosuitenpo_donations d WHERE d.pledge_id = p.id), 0) AS amount_fulfilled')
|
||||
->from($db->quoteName('#__mokosuitenpo_pledges', 'p'))
|
||||
->join('INNER', $db->quoteName('#__mokosuitenpo_donors', 'don') . ' ON don.id = p.donor_id')
|
||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = don.contact_id')
|
||||
->where($db->quoteName('p.status') . ' = ' . $db->quote('active'))
|
||||
->where('p.due_date IS NOT NULL')
|
||||
->where('p.due_date < DATE_SUB(CURDATE(), INTERVAL ' . (int) $overdueDays . ' DAY)')
|
||||
->having('amount_fulfilled < p.amount')
|
||||
->order('p.due_date ASC'));
|
||||
|
||||
$pledges = $db->loadObjectList() ?: [];
|
||||
|
||||
foreach ($pledges as &$p) {
|
||||
$p->remaining = round((float) $p->amount - (float) $p->amount_fulfilled, 2);
|
||||
$p->fulfillment_pct = (float) $p->amount > 0 ? round((float) $p->amount_fulfilled / (float) $p->amount * 100, 1) : 0;
|
||||
}
|
||||
|
||||
return $pledges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pledge fulfillment summary.
|
||||
*/
|
||||
public static function getFulfillmentSummary(): object
|
||||
{
|
||||
$db = Factory::getContainer()->get(DatabaseInterface::class);
|
||||
|
||||
$db->setQuery($db->getQuery(true)
|
||||
->select('COUNT(*) AS total_pledges')
|
||||
->select('COALESCE(SUM(amount), 0) AS total_pledged')
|
||||
->select('COALESCE(SUM((SELECT COALESCE(SUM(d.amount), 0) FROM #__mokosuitenpo_donations d WHERE d.pledge_id = p.id)), 0) AS total_received')
|
||||
->from($db->quoteName('#__mokosuitenpo_pledges', 'p'))
|
||||
->where($db->quoteName('p.status') . ' IN (' . $db->quote('active') . ',' . $db->quote('completed') . ')'));
|
||||
|
||||
$stats = $db->loadObject() ?: (object) ['total_pledges' => 0, 'total_pledged' => 0, 'total_received' => 0];
|
||||
$stats->outstanding = round((float) $stats->total_pledged - (float) $stats->total_received, 2);
|
||||
$stats->fulfillment_rate = (float) $stats->total_pledged > 0
|
||||
? round((float) $stats->total_received / (float) $stats->total_pledged * 100, 1) : 0;
|
||||
|
||||
return $stats;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<extension type="package" method="upgrade">
|
||||
<name>Package - MokoSuite NPO</name>
|
||||
<packagename>mokosuitenpo</packagename>
|
||||
<version>01.01.00</version>
|
||||
<version>01.07.00</version>
|
||||
<creationDate>2026-06-11</creationDate>
|
||||
<author>Moko Consulting</author>
|
||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||
|
||||
Reference in New Issue
Block a user