44 Commits

Author SHA1 Message Date
jmiller 50d298ad1a chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-28 20:14:33 +00:00
jmiller d8ccbcf31d chore: sync pr-metadata-check.yml from Template-Joomla 2026-06-28 07:47:22 +00:00
jmiller 0a3047f207 chore: sync SECURITY.md from Template-Joomla 2026-06-28 07:45:57 +00:00
jmiller b808a48e80 chore: sync GOVERNANCE.md from Template-Joomla 2026-06-28 07:42:30 +00:00
jmiller a70ec0a11d chore: sync CONTRIBUTING.md from Template-Joomla 2026-06-28 07:40:44 +00:00
jmiller 7f06abf982 chore: sync CODE_OF_CONDUCT.md from Template-Joomla 2026-06-28 07:37:43 +00:00
jmiller 26ad046c72 chore: sync composer.json from Template-Joomla 2026-06-28 07:35:42 +00:00
jmiller 78bcbec205 chore: sync phpstan.neon from Template-Joomla 2026-06-28 07:34:21 +00:00
jmiller 6c632319f9 chore: sync .editorconfig from Template-Joomla 2026-06-28 07:33:47 +00:00
jmiller 5ccc8e790e chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-27 20:49:29 +00:00
jmiller 4853d5ba1a chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-27 05:33:45 +00:00
jmiller 4d9eaa71e6 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-27 00:51:17 +00:00
jmiller 6d683fbb60 chore: sync version-set.yml from Template-Generic [skip ci] 2026-06-27 00:51:16 +00:00
jmiller 0ca87c68b4 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-27 00:51:16 +00:00
jmiller 27a5c98e20 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-27 00:51:15 +00:00
jmiller a0249dcbc5 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-27 00:51:14 +00:00
jmiller 66e58304ed chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-27 00:51:13 +00:00
jmiller f248680a76 chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-27 00:51:13 +00:00
jmiller 334f18ea4c chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-27 00:51:12 +00:00
jmiller 7104bab631 chore: sync ci-issue-reporter.yml from Template-Generic [skip ci] 2026-06-27 00:51:11 +00:00
jmiller 87e1b04432 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-27 00:51:10 +00:00
jmiller 29d317ffd9 chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-27 00:51:10 +00:00
jmiller c642ab7351 Merge pull request 'chore: add .claude to .gitignore' (#7) from chore/gitignore into main
chore: add .claude to .gitignore [skip ci]
2026-06-25 14:03:08 +00:00
jmiller 3a5e158019 chore: add .claude to .gitignore [skip ci] 2026-06-25 08:55:50 -05:00
jmiller 22ad221533 chore: remove security-audit.yml -- handled by MokoGitea 2026-06-23 18:27:20 +00:00
jmiller f615bdb15f chore: remove deploy-manual.yml -- no longer needed 2026-06-23 17:59:47 +00:00
jmiller 0ee9de17f9 chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-23 17:52:46 +00:00
jmiller 407c5f181e chore: remove deprecated .mokogitea/workflows/deploy-manual.yml [skip ci] 2026-06-23 17:36:45 +00:00
jmiller f9922a8d29 chore: sync workflow-sync-trigger.yml from Template-Generic [skip ci] 2026-06-23 15:52:36 +00:00
jmiller 33e1532ddb chore: sync security-audit.yml from Template-Generic [skip ci] 2026-06-23 15:52:36 +00:00
jmiller 402daf75f5 chore: sync repo-health.yml from Template-Generic [skip ci] 2026-06-23 15:52:35 +00:00
jmiller e9b95ea757 chore: sync rc-revert.yml from Template-Generic [skip ci] 2026-06-23 15:52:35 +00:00
jmiller f79cd069b1 chore: sync pre-release.yml from Template-Generic [skip ci] 2026-06-23 15:52:34 +00:00
jmiller 75e1c84482 chore: sync pr-check.yml from Template-Generic [skip ci] 2026-06-23 15:52:34 +00:00
jmiller 9109227a69 chore: sync notify.yml from Template-Generic [skip ci] 2026-06-23 15:52:33 +00:00
jmiller 56056cd365 chore: sync issue-branch.yml from Template-Generic [skip ci] 2026-06-23 15:52:33 +00:00
jmiller 221ea5f5b9 chore: sync gitleaks.yml from Template-Generic [skip ci] 2026-06-23 15:52:32 +00:00
jmiller 657caa0c37 chore: sync deploy-manual.yml from Template-Generic [skip ci] 2026-06-23 15:52:32 +00:00
jmiller 505f725402 chore: sync cleanup.yml from Template-Generic [skip ci] 2026-06-23 15:52:31 +00:00
jmiller b696e4b818 chore: sync ci-generic.yml from Template-Generic [skip ci] 2026-06-23 15:52:31 +00:00
jmiller a0065ae05a chore: sync cascade-dev.yml from Template-Generic [skip ci] 2026-06-23 15:52:31 +00:00
jmiller 18b8822d27 chore: sync branch-cleanup.yml from Template-Generic [skip ci] 2026-06-23 15:52:30 +00:00
jmiller 505d43ddd1 chore: sync auto-release.yml from Template-Generic [skip ci] 2026-06-23 15:52:30 +00:00
jmiller 8013bd9f7a chore: sync auto-bump.yml from Template-Generic [skip ci] 2026-06-23 15:52:29 +00:00
26 changed files with 3822 additions and 0 deletions
+41
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
# Claude Code
.claude/
+66
View File
@@ -0,0 +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
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"
+469
View File
@@ -0,0 +1,469 @@
# 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-tech/mokocli
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.01.00
# BRIEF: Universal build & release detects platform from manifest.xml
#
# +=======================================================================+
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +=======================================================================+
# | |
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
# | |
# | Platform-specific: |
# | joomla: XML manifest, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +=======================================================================+
name: "Universal: Build & Release"
on:
pull_request:
types: [opened, synchronize, closed]
branches:
- main
paths-ignore:
- '.mokogitea/workflows/**'
- '*.md'
- 'wiki/**'
- '.editorconfig'
- '.gitignore'
- '.gitattributes'
- '.gitmessage'
- 'LICENSE'
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: false
type: choice
default: release
options:
- release
- promote-rc
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
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 }}
permissions:
contents: write
jobs:
# ── 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:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 1
submodules: recursive
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
if [ -f /opt/mokocli/cli/version_bump.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: Rename branch to rc
run: |
php ${MOKO_CLI}/branch_rename.php \
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
--api-base "${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
--pr "${{ github.event.pull_request.number }}"
- name: Checkout rc and configure git
run: |
git fetch origin rc
git checkout rc
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Publish RC release
run: |
php ${MOKO_CLI}/release_publish.php \
--path . --stability rc --bump minor --branch rc \
--token "${{ secrets.MOKOGITEA_TOKEN }}"
- name: Update RC release notes from CHANGELOG.md
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Extract [Unreleased] section from changelog
NOTES=""
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
fi
[ -z "$NOTES" ] && NOTES="Release candidate"
# Find the RC release and update its body
RELEASE_ID=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/release-candidate" \
| python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "RC release notes updated from CHANGELOG.md"
fi
- name: Summary
if: always()
run: |
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) ─────────────────────────
release:
name: Build & Release Pipeline
runs-on: release
if: >-
github.event.pull_request.merged == true ||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
token: ${{ secrets.MOKOGITEA_TOKEN }}
fetch-depth: 0
submodules: recursive
- name: Configure git for bot pushes
run: |
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found — aborting release"
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Setup mokocli tools
env:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
run: |
if [ -f /opt/mokocli/cli/version_bump.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: "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: |
# Fix/patch branches: version was already bumped by pre-release, just strip suffix
# Feature/dev branches: bump minor for the new stable release
HEAD_REF="${{ github.event.pull_request.head.ref || 'dev' }}"
case "$HEAD_REF" in
fix/*|patch/*|hotfix/*|bugfix/*) BUMP="none" ;;
*) BUMP="minor" ;;
esac
echo "level=${BUMP}" >> "$GITHUB_OUTPUT"
echo "Bump level: ${BUMP} (from branch: ${HEAD_REF})"
- name: "Publish stable release"
run: |
BUMP_FLAG=""
if [ "${{ steps.bump.outputs.level }}" != "none" ]; then
BUMP_FLAG="--bump ${{ steps.bump.outputs.level }}"
fi
php ${MOKO_CLI}/release_publish.php \
--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="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Get the stable release info (version and ID)
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/releases/tags/stable" 2>/dev/null || echo '{}')
RELEASE_ID=$(python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" <<< "$RELEASE_JSON" 2>/dev/null || true)
# Extract version from release name (e.g. "06.17.00" or "v06.17.00")
VERSION=$(python3 -c "
import json, sys, re
r = json.load(sys.stdin)
name = r.get('name', '')
m = re.search(r'(\d+\.\d+\.\d+)', name)
print(m.group(1) if m else '')
" <<< "$RELEASE_JSON" 2>/dev/null || true)
# Extract [Unreleased] section from changelog
NOTES=""
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
fi
[ -z "$NOTES" ] && NOTES="Stable release"
# Update release body via API
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${TOKEN}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
# Promote [Unreleased] → [version] in CHANGELOG.md and reset
if [ -n "$VERSION" ] && [ -f "CHANGELOG.md" ]; then
DATE=$(date +%Y-%m-%d)
python3 -c "
import sys
version, date = sys.argv[1], sys.argv[2]
content = open('CHANGELOG.md').read()
old = '## [Unreleased]'
new = f'## [Unreleased]\n\n## [{version}] --- {date}'
content = content.replace(old, new, 1)
open('CHANGELOG.md', 'w').write(content)
" "$VERSION" "$DATE"
git add CHANGELOG.md
git commit -m "chore: promote changelog [Unreleased] → [${VERSION}]" || true
git push origin main || true
echo "Changelog promoted: [Unreleased] → [${VERSION}]"
fi
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
- name: "Step 9: Mirror release to GitHub"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
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="${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" \
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
--branch main 2>&1 || true
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
- name: "Step 10: Push main to GitHub mirror"
if: >-
steps.version.outputs.skip != 'true' &&
secrets.GH_MIRROR_TOKEN != ''
continue-on-error: true
run: |
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
git fetch origin main --depth=1
git push github origin/main:refs/heads/main --force 2>/dev/null \
&& echo "main branch pushed to GitHub mirror" \
|| echo "WARNING: GitHub mirror push failed"
- name: "Step 11: Delete rc branch and recreate dev from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
API_BASE="${MOKOGITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
# Delete rc branch (ephemeral — created by promote-rc)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/rc" 2>/dev/null \
&& echo "Deleted rc branch" || echo "rc branch not found"
# Delete dev branch
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
# Recreate dev from main (now includes version bump + changelog promotion)
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
-H "Content-Type: application/json" \
"${API_BASE}/branches" \
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
- name: "Step 12: Create version branch from main"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
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}"
MAIN_SHA=$(git rev-parse HEAD)
# Delete old version branch if it exists (same version re-release)
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
# Create version/XX.YY.ZZ from main
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Post-release: Reset dev version"
if: steps.version.outputs.skip != 'true'
continue-on-error: true
run: |
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
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
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](${MOKOGITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
fi
+48
View File
@@ -0,0 +1,48 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Universal
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
# PATH: /.mokogitea/workflows/branch-cleanup.yml
# VERSION: 01.00.00
# BRIEF: Delete feature branches after PR merge
name: "Branch Cleanup"
on:
pull_request:
types: [closed]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
cleanup:
name: Delete merged branch
runs-on: ubuntu-latest
if: >-
github.event.pull_request.merged == true &&
github.event.pull_request.head.ref != 'dev' &&
github.event.pull_request.head.ref != 'main'
steps:
- name: Delete source branch
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API}/${ENCODED}" 2>/dev/null || true)
if [ "$STATUS" = "204" ]; then
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
elif [ "$STATUS" = "404" ]; then
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
else
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
fi
+10
View File
@@ -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"
+197
View File
@@ -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 }}"
+87
View File
@@ -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)"
+126
View File
@@ -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
+92
View File
@@ -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
+73
View File
@@ -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
+70
View File
@@ -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
View File
@@ -0,0 +1,521 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: moko-platform.CI
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
# PATH: /templates/workflows/universal/pr-check.yml.template
# VERSION: 09.23.00
# BRIEF: PR gate — branch policy + code validation before merge
name: "Universal: PR Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ── Branch Policy ──────────────────────────────────────────────────────
branch-policy:
name: Branch Policy
runs-on: ubuntu-latest
steps:
- name: Check branch merge target
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
patch/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
ALLOWED=false
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
rc)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="RC branch can only merge into 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# ── Secret Scanning ──────────────────────────────────────────────────
gitleaks:
name: 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
- name: Scan PR commits for secrets
run: |
if gitleaks detect --source . --verbose \
--log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} 2>&1; then
echo "**No secrets detected.**" >> $GITHUB_STEP_SUMMARY
else
echo "::error::Potential secrets detected in PR commits"
exit 1
fi
# ── Code Validation ────────────────────────────────────────────────────
validate:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check for merge conflict markers
run: |
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
if [ -n "$CONFLICTS" ]; then
echo "::error::Merge conflict markers found in source files"
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "No conflict markers found"
- name: Detect platform
id: platform
run: |
# Read platform from XML manifest (<platform> tag) or plain text fallback
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
- name: Setup PHP
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
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: PHP syntax check
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
echo "PHP lint: ${ERRORS} error(s)"
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
- name: Joomla JEXEC guard check
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
while IFS= read -r -d '' file; do
# Skip vendor, node_modules, and index.html stub files
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
# Check first 10 lines for JEXEC or JPATH guard
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
echo "::error file=${file}::Missing JEXEC guard: ${file}"
ERRORS=$((ERRORS + 1))
fi
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
if [ "$ERRORS" -gt 0 ]; then
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "JEXEC guard: OK"
- name: Joomla directory listing protection
if: steps.platform.outputs.platform == 'joomla'
run: |
MISSING=0
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && exit 0
while IFS= read -r dir; do
if [ ! -f "${dir}/index.html" ]; then
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
MISSING=$((MISSING + 1))
fi
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
if [ "$MISSING" -gt 0 ]; then
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
fi
echo "Directory protection: ${MISSING} missing (advisory)"
- name: Joomla script file and asset checks
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
[ -z "$MANIFEST" ] && exit 0
MANIFEST_DIR=$(dirname "$MANIFEST")
# Check scriptfile exists if declared
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
if [ -n "$SCRIPTFILE" ]; then
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
ERRORS=$((ERRORS + 1))
else
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
fi
fi
# Require joomla.asset.json and validate it
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$ASSET_JSON" ]; then
echo "::error::joomla.asset.json not found — Joomla asset system is required"
ERRORS=$((ERRORS + 1))
else
if command -v php &> /dev/null; then
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
echo "::error::joomla.asset.json is not valid JSON"
ERRORS=$((ERRORS + 1))
}
fi
echo "joomla.asset.json: valid"
fi
# Validate all XML files in src/ are well-formed
XML_ERRORS=0
if command -v php &> /dev/null; then
while IFS= read -r -d '' xmlfile; do
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
XML_ERRORS=$((XML_ERRORS + 1))
fi
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
fi
if [ "$XML_ERRORS" -gt 0 ]; then
echo "::error::${XML_ERRORS} XML file(s) are malformed"
ERRORS=$((ERRORS + 1))
else
echo "XML well-formedness: OK"
fi
[ "$ERRORS" -gt 0 ] && exit 1
echo "Joomla asset checks: OK"
- name: Validate platform manifest
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -z "$MANIFEST" ]; then
echo "::warning::No Joomla manifest found (WaaS site)"
exit 0
fi
echo "Manifest: ${MANIFEST}"
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
fi
for ELEMENT in name version description; do
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
done
# Block legacy raw/branch update server URLs on MokoGitea
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
if [ -n "$RAW_URLS" ]; then
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
echo "$RAW_URLS"
exit 1
fi
echo "Joomla manifest valid"
;;
dolibarr)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
if [ -z "$MOD_FILE" ]; then
echo "::error::No mod*.class.php found"
exit 1
fi
echo "Dolibarr module: ${MOD_FILE}"
;;
*)
echo "Generic platform — no manifest validation"
;;
esac
- name: Check update stream format
run: |
PLATFORM="${{ steps.platform.outputs.platform }}"
case "$PLATFORM" in
joomla)
if [ -f "updates.xml" ]; then
if command -v php &> /dev/null; then
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
fi
echo "updates.xml valid"
fi
;;
dolibarr)
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
;;
esac
- name: Validate Joomla language files
if: steps.platform.outputs.platform == 'joomla'
run: |
ERRORS=0
WARNINGS=0
# Require both en-GB and en-US language directories
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -z "$LANG_ROOT" ]; then
echo "No language/ directory found — skipping"
exit 0
fi
if [ ! -d "$LANG_ROOT/en-GB" ]; then
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
ERRORS=$((ERRORS + 1))
fi
if [ ! -d "$LANG_ROOT/en-US" ]; then
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
ERRORS=$((ERRORS + 1))
fi
# Check that en-GB and en-US have matching .ini files
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
[ ! -f "$GB_INI" ] && continue
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
if [ ! -f "$US_INI" ]; then
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
ERRORS=$((ERRORS + 1))
fi
done
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
[ ! -f "$US_INI" ] && continue
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
if [ ! -f "$GB_INI" ]; then
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
ERRORS=$((ERRORS + 1))
fi
done
fi
# Find all .ini language files
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
if [ -z "$INI_FILES" ]; then
echo "No .ini language files found"
[ "$ERRORS" -gt 0 ] && exit 1
exit 0
fi
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
for FILE in $INI_FILES; do
FNAME=$(basename "$FILE")
LINENUM=0
SEEN_KEYS=""
while IFS= read -r line || [ -n "$line" ]; do
LINENUM=$((LINENUM + 1))
# Skip empty lines and comments
[ -z "$line" ] && continue
echo "$line" | grep -qE '^\s*;' && continue
echo "$line" | grep -qE '^\s*$' && continue
# Must match KEY="VALUE" format
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
ERRORS=$((ERRORS + 1))
continue
fi
# Extract key and check for duplicates
KEY=$(echo "$line" | sed 's/=.*//')
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
ERRORS=$((ERRORS + 1))
fi
SEEN_KEYS="${SEEN_KEYS}
${KEY}"
done < "$FILE"
echo " ${FILE}: checked ${LINENUM} lines"
done
# Cross-check en-GB vs en-US key consistency
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
for GB_FILE in "$GB_DIR"/*.ini; do
[ ! -f "$GB_FILE" ] && continue
FNAME=$(basename "$GB_FILE")
US_FILE="$US_DIR/$FNAME"
[ ! -f "$US_FILE" ] && continue
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
# Keys in en-GB but not en-US
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_US" ]; then
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
# Keys in en-US but not en-GB
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
if [ -n "$MISSING_GB" ]; then
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
WARNINGS=$((WARNINGS + 1))
fi
done
fi
{
echo "### Language File Validation"
echo "| Metric | Count |"
echo "|---|---|"
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
echo "| Errors | ${ERRORS} |"
echo "| Warnings | ${WARNINGS} |"
} >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
echo "::error::Language validation failed with ${ERRORS} error(s)"
exit 1
fi
echo "Language files: OK (${WARNINGS} warning(s))"
- name: Check changelog has unreleased entry
run: |
if [ ! -f "CHANGELOG.md" ]; then
echo "::warning::No CHANGELOG.md found"
exit 0
fi
# Check for content under [Unreleased] section
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
echo "::error::CHANGELOG.md missing [Unreleased] section"
exit 1
fi
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
- name: Verify package source
run: |
SOURCE_DIR="src"
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
if [ ! -d "$SOURCE_DIR" ]; then
echo "::warning::No src/ or htdocs/ directory"
exit 0
fi
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
echo "Source: ${FILE_COUNT} files"
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
# ── Pre-Release RC Build ─────────────────────────────────────────────────
pre-release:
name: Build RC Package
runs-on: ubuntu-latest
needs: [branch-policy, validate]
steps:
- name: Trigger RC pre-release
env:
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: ${{ github.head_ref }}
MOKOGITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
run: |
curl -s -X POST "${MOKOGITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
# ── Issue Reporter ──────────────────────────────────────────────────────
report-issues:
name: Report Issues
needs: [branch-policy, validate]
if: >-
always() &&
needs.validate.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
gate: "PR Validation"
workflow: "PR Check"
severity: error
details: "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
secrets: inherit
@@ -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
+273
View File
@@ -0,0 +1,273 @@
# 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: /templates/workflows/universal/pre-release.yml.template
# VERSION: 05.02.00
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
name: "Universal: Pre-Release"
on:
push:
branches:
- dev
- 'fix/**'
- 'patch/**'
- 'hotfix/**'
- 'bugfix/**'
- 'chore/**'
- alpha
- beta
- rc
workflow_dispatch:
inputs:
stability:
description: 'Pre-release channel'
required: true
type: choice
options:
- development
- alpha
- beta
- release-candidate
permissions:
contents: write
env:
GITEA_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:
build:
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
runs-on: release
if: >-
github.event_name == 'workflow_dispatch' ||
github.event_name == 'push'
steps:
- name: Checkout
uses: actions/checkout@v4
with:
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:
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
run: |
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/cli/manifest_element.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: Detect platform
id: platform
run: |
# Auto-detect and update platform if not set in manifest
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
case "${{ github.ref_name }}" in
rc) STABILITY="release-candidate" ;;
alpha) STABILITY="alpha" ;;
beta) STABILITY="beta" ;;
*) STABILITY="development" ;;
esac
else
STABILITY="${{ inputs.stability || 'development' }}"
fi
case "$STABILITY" in
development) SUFFIX="-dev"; TAG="development" ;;
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
beta) SUFFIX="-beta"; TAG="beta" ;;
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
esac
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
case "$STABILITY" in
release-candidate) BUMP="minor" ;;
*) BUMP="patch" ;;
esac
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
# Set stability suffix and verify consistency
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
php ${MOKO_CLI}/version_set_platform.php \
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
# Append suffix for output
if [ -n "$SUFFIX" ]; then
VERSION="${VERSION}${SUFFIX}"
fi
# Commit version bump
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
git config --local user.name "gitea-actions[bot]"
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
git add -A
git diff --cached --quiet || {
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
git push origin HEAD 2>&1
}
# Auto-detect element via manifest_element.php
php ${MOKO_CLI}/manifest_element.php \
--path . --version "$VERSION" --stability "$STABILITY" \
--repo "${GITEA_REPO}" --github-output
# Read back element outputs
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
- name: Create release
id: release
if: steps.eligibility.outputs.proceed == 'true'
run: |
TAG="${{ steps.meta.outputs.tag }}"
VERSION="${{ steps.meta.outputs.version }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_create.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--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 }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
if [ -f "CHANGELOG.md" ]; then
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
else
NOTES="Release ${VERSION}"
fi
# Update release body via API
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ -n "$RELEASE_ID" ]; then
python3 -c "
import json, urllib.request
body = open('/dev/stdin').read()
payload = json.dumps({'body': body}).encode()
req = urllib.request.Request(
'${API_BASE}/releases/${RELEASE_ID}',
data=payload, method='PATCH',
headers={
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
'Content-Type': 'application/json'
})
urllib.request.urlopen(req)
" <<< "$NOTES"
echo "Release notes updated from CHANGELOG.md"
fi
- name: Build package and upload
id: package
if: steps.eligibility.outputs.proceed == 'true'
run: |
VERSION="${{ steps.meta.outputs.version }}"
TAG="${{ steps.meta.outputs.tag }}"
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
php ${MOKO_CLI}/release_package.php \
--path . --version "$VERSION" --tag "$TAG" \
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
--repo "${GITEA_REPO}" --output /tmp || true
# updates.xml is generated dynamically by MokoGitea license server
# 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}"
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
php ${MOKO_CLI}/release_cascade.php \
--stability "${{ steps.meta.outputs.stability }}" \
--token "${TOKEN}" \
--api-base "${API_BASE}"
- name: Summary
if: always()
run: |
VERSION="${{ steps.meta.outputs.version }}"
STABILITY="${{ steps.meta.outputs.stability }}"
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
SHA256="${{ steps.package.outputs.sha256_zip }}"
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
+71
View File
@@ -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"
+700
View File
@@ -0,0 +1,700 @@
# ============================================================================
# Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
#
# This file is part of a Moko Consulting project.
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokocli.Validation
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
# PATH: /templates/workflows/joomla/repo_health.yml.template
# VERSION: 09.23.00
# BRIEF: Enforces repository guardrails by validating scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
name: "Generic: Repo Health"
defaults:
run:
shell: bash
on:
workflow_dispatch:
inputs:
profile:
description: 'Validation profile: all, scripts, or repo'
required: true
default: all
type: choice
options:
- all
- scripts
- repo
pull_request:
branches:
- main
permissions:
contents: read
env:
# Scripts governance policy
SCRIPTS_REQUIRED_DIRS:
SCRIPTS_ALLOWED_DIRS: scripts,scripts/fix,scripts/lib,scripts/release,scripts/run,scripts/validate
# Repo health policy
REPO_REQUIRED_ARTIFACTS: README.md,LICENSE,CHANGELOG.md,CONTRIBUTING.md,CODE_OF_CONDUCT.md,.mokogitea/workflows/
REPO_OPTIONAL_FILES: SECURITY.md,GOVERNANCE.md,.editorconfig,.gitattributes,.gitignore,README.md,docs/
REPO_DISALLOWED_DIRS:
REPO_DISALLOWED_FILES: TODO.md,todo.md
# Extended checks toggles
EXTENDED_CHECKS: "true"
# File / directory variables
DOCS_INDEX: docs/docs-index.md
SCRIPT_DIR: scripts
WORKFLOWS_DIR: .mokogitea/workflows
SHELLCHECK_PATTERN: '*.sh'
SPDX_FILE_GLOBS: '*.sh,*.php,*.js,*.ts,*.css,*.xml,*.yml,*.yaml'
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
access_check:
name: Access control
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
outputs:
allowed: ${{ steps.perm.outputs.allowed }}
permission: ${{ steps.perm.outputs.permission }}
steps:
- name: Check actor permission (admin only)
id: perm
env:
TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
REPO: ${{ github.repository }}
ACTOR: ${{ github.actor }}
run: |
set -euo pipefail
ALLOWED=false
PERMISSION=unknown
METHOD=""
# Hardcoded authorized users — always allowed
case "$ACTOR" in
jmiller|gitea-actions[bot])
ALLOWED=true
PERMISSION=admin
METHOD="hardcoded allowlist"
;;
*)
# Detect platform and check permissions via API
API_BASE="${GITHUB_API_URL:-${GITEA_API_URL:-https://api.github.com}}"
RESP=$(curl -sf -H "Authorization: token ${TOKEN}" \
"${API_BASE}/repos/${REPO}/collaborators/${ACTOR}/permission" 2>/dev/null || echo '{}')
PERMISSION=$(echo "$RESP" | grep -oP '"permission"\s*:\s*"\K[^"]+' || echo "unknown")
if [ "$PERMISSION" = "admin" ] || [ "$PERMISSION" = "maintain" ] || [ "$PERMISSION" = "owner" ]; then
ALLOWED=true
fi
METHOD="collaborator API"
;;
esac
echo "permission=${PERMISSION}" >> "$GITHUB_OUTPUT"
echo "allowed=${ALLOWED}" >> "$GITHUB_OUTPUT"
{
echo "## Access Authorization"
echo ""
echo "| Field | Value |"
echo "|-------|-------|"
echo "| **Actor** | \`${ACTOR}\` |"
echo "| **Repository** | \`${REPO}\` |"
echo "| **Permission** | \`${PERMISSION}\` |"
echo "| **Method** | ${METHOD} |"
echo "| **Authorized** | ${ALLOWED} |"
echo ""
if [ "$ALLOWED" = "true" ]; then
echo "${ACTOR} authorized (${METHOD})"
else
echo "${ACTOR} is NOT authorized. Requires admin or maintain role."
fi
} >> "${GITHUB_STEP_SUMMARY}"
- name: Deny execution when not permitted
if: ${{ steps.perm.outputs.allowed != 'true' }}
run: |
set -euo pipefail
printf '%s\n' 'ERROR: Access denied. Admin permission required.' >> "${GITHUB_STEP_SUMMARY}"
exit 1
scripts_governance:
name: Scripts governance
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Scripts folder checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'repo' ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes scripts governance'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
if [ ! -d "${SCRIPT_DIR}" ]; then
{
printf '%s\n' '### Scripts governance'
printf '%s\n' 'Status: OK (advisory)'
printf '%s\n' 'scripts/ directory not present. No scripts governance enforced.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
if [ -n "${SCRIPTS_REQUIRED_DIRS:-}" ]; then IFS=',' read -r -a required_dirs <<< "${SCRIPTS_REQUIRED_DIRS}"; else required_dirs=(); fi
IFS=',' read -r -a allowed_dirs <<< "${SCRIPTS_ALLOWED_DIRS}"
missing_dirs=()
unapproved_dirs=()
for d in "${required_dirs[@]}"; do
req="${d%/}"
[ ! -d "${req}" ] && missing_dirs+=("${req}/")
done
while IFS= read -r d; do
allowed=false
for a in "${allowed_dirs[@]}"; do
a_norm="${a%/}"
[ "${d%/}" = "${a_norm}" ] && allowed=true
done
[ "${allowed}" = false ] && unapproved_dirs+=("${d%/}/")
done < <(find "${SCRIPT_DIR}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sed 's#^\./##')
{
printf '%s\n' '### Scripts governance'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Area | Status | Notes |'
printf '%s\n' '|---|---|---|'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Required directories | Warning | Missing required subfolders |'
else
printf '%s\n' '| Required directories | OK | All required subfolders present |'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' '| Directory policy | Warning | Unapproved directories detected |'
else
printf '%s\n' '| Directory policy | OK | No unapproved directories |'
fi
printf '%s\n' '| Enforcement mode | Advisory | scripts folder is optional |'
printf '\n'
if [ "${#missing_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Missing required script directories:'
for m in "${missing_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Missing required script directories: none.'
printf '\n'
fi
if [ "${#unapproved_dirs[@]}" -gt 0 ]; then
printf '%s\n' 'Unapproved script directories detected:'
for m in "${unapproved_dirs[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
else
printf '%s\n' 'Unapproved script directories detected: none.'
printf '\n'
fi
printf '%s\n' 'Scripts governance completed in advisory mode.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
repo_health:
name: Repository health
needs: access_check
if: ${{ needs.access_check.outputs.allowed == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Repository health checks
env:
PROFILE_RAW: ${{ github.event.inputs.profile }}
run: |
set -euo pipefail
profile="${PROFILE_RAW:-all}"
case "${profile}" in
all|scripts|repo) ;;
*)
printf '%s\n' "ERROR: Unknown profile: ${profile}" >> "${GITHUB_STEP_SUMMARY}"
exit 1
;;
esac
if [ "${profile}" = 'scripts' ]; then
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' 'Status: SKIPPED'
printf '%s\n' 'Reason: profile excludes repository health'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 0
fi
IFS=',' read -r -a required_artifacts <<< "${REPO_REQUIRED_ARTIFACTS}"
IFS=',' read -r -a optional_files <<< "${REPO_OPTIONAL_FILES}"
if [ -n "${REPO_DISALLOWED_DIRS:-}" ]; then IFS=',' read -r -a disallowed_dirs <<< "${REPO_DISALLOWED_DIRS}"; else disallowed_dirs=(); fi
IFS=',' read -r -a disallowed_files <<< "${REPO_DISALLOWED_FILES:-}"
missing_required=()
missing_optional=()
# Source directory: src/ or htdocs/ (either is valid for extension repos)
SOURCE_DIR=""
if [ -d "src" ]; then
SOURCE_DIR="src"
elif [ -d "htdocs" ]; then
SOURCE_DIR="htdocs"
elif [ -d "deploy" ] || [ -d "cli" ] || [ -d "monitoring" ]; then
# Platform/tooling repos don't need src/
SOURCE_DIR=""
else
missing_required+=("src/ or htdocs/ (source directory required)")
fi
for item in "${required_artifacts[@]}"; do
if printf '%s' "${item}" | grep -q '/$'; then
d="${item%/}"
[ ! -d "${d}" ] && missing_required+=("${item}")
else
[ ! -f "${item}" ] && missing_required+=("${item}")
fi
done
for f in "${optional_files[@]}"; do
if printf '%s' "${f}" | grep -q '/$'; then
d="${f%/}"
[ ! -d "${d}" ] && missing_optional+=("${f}")
else
[ ! -f "${f}" ] && missing_optional+=("${f}")
fi
done
for d in "${disallowed_dirs[@]}"; do
d_norm="${d%/}"
[ -d "${d_norm}" ] && missing_required+=("${d_norm}/ (disallowed)")
done
for f in "${disallowed_files[@]}"; do
[ -f "${f}" ] && missing_required+=("${f} (disallowed)")
done
git fetch origin --prune
dev_paths=()
dev_branches=()
while IFS= read -r b; do
name="${b#origin/}"
if [ "${name}" = 'dev' ]; then
dev_branches+=("${name}")
else
dev_paths+=("${name}")
fi
done < <(git branch -r --list 'origin/dev*' | sed 's/^ *//')
if [ "${#dev_paths[@]}" -eq 0 ] && [ "${#dev_branches[@]}" -eq 0 ]; then
missing_required+=("dev or dev/* branch")
fi
content_warnings=()
if [ -f 'CHANGELOG.md' ] && ! grep -Eq '^# Changelog' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md missing '# Changelog' header")
fi
if [ -f 'CHANGELOG.md' ] && grep -Eq '^[# ]*Unreleased' CHANGELOG.md; then
content_warnings+=("CHANGELOG.md contains Unreleased section (review release readiness)")
fi
if [ -f 'LICENSE' ] && ! grep -qiE 'GNU GENERAL PUBLIC LICENSE|GPL' LICENSE; then
content_warnings+=("LICENSE does not look like a GPL text")
fi
if [ -f 'README.md' ] && ! grep -qiE 'moko|Moko' README.md; then
content_warnings+=("README.md missing expected brand keyword")
fi
export PROFILE_RAW="${profile}"
export MISSING_REQUIRED="$(printf '%s\n' "${missing_required[@]:-}")"
export MISSING_OPTIONAL="$(printf '%s\n' "${missing_optional[@]:-}")"
export CONTENT_WARNINGS="$(printf '%s\n' "${content_warnings[@]:-}")"
report_json=$(printf '{"profile":"%s","missing_required":%d,"missing_optional":%d,"content_warnings":%d}' "$profile" "${#missing_required[@]}" "${#missing_optional[@]}" "${#content_warnings[@]}")
{
printf '%s\n' '### Repository health'
printf '%s\n' "Profile: ${profile}"
printf '%s\n' '| Metric | Value |'
printf '%s\n' '|---|---|'
printf '%s\n' "| Missing required | ${#missing_required[@]} |"
printf '%s\n' "| Missing optional | ${#missing_optional[@]} |"
printf '%s\n' "| Content warnings | ${#content_warnings[@]} |"
printf '\n'
printf '%s\n' '### Guardrails report (JSON)'
printf '%s\n' '```json'
printf '%s\n' "${report_json}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${#missing_required[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing required repo artifacts'
for m in "${missing_required[@]}"; do printf '%s\n' "- ${m}"; done
printf '%s\n' 'ERROR: Guardrails failed. Missing required repository artifacts.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
exit 1
fi
if [ "${#missing_optional[@]}" -gt 0 ]; then
{
printf '%s\n' '### Missing optional repo artifacts'
for m in "${missing_optional[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
if [ "${#content_warnings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Repo content warnings'
for m in "${content_warnings[@]}"; do printf '%s\n' "- ${m}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
# -- Joomla-specific checks --
joomla_findings=()
MANIFEST="$(find . -maxdepth 2 -name '*.xml' -exec grep -l '<extension' {} \; 2>/dev/null | head -1 || true)"
if [ -z "${MANIFEST}" ]; then
joomla_findings+=("Joomla XML manifest not found (no *.xml with <extension> tag)")
else
if ! grep -qP '<version>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <version> tag missing")
fi
if ! grep -qP 'type="(component|module|plugin|library|package|template|language)"' "${MANIFEST}"; then
joomla_findings+=("XML manifest: type attribute missing or invalid")
fi
if ! grep -qP '<name>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <name> tag missing")
fi
if ! grep -qP '<author>' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <author> tag missing")
fi
if ! grep -qP '<namespace' "${MANIFEST}"; then
joomla_findings+=("XML manifest: <namespace> missing (required for Joomla 5+)")
fi
fi
INI_COUNT="$(find . -name '*.ini' -type f 2>/dev/null | wc -l)"
if [ "${INI_COUNT}" -eq 0 ]; then
joomla_findings+=("No .ini language files found")
fi
if [ ! -f 'updates.xml' ]; then
joomla_findings+=("updates.xml missing in root (required for Joomla update server)")
fi
if [ -n "${SOURCE_DIR}" ]; then
INDEX_DIRS=("${SOURCE_DIR}" "${SOURCE_DIR}/admin" "${SOURCE_DIR}/site")
for dir in "${INDEX_DIRS[@]}"; do
if [ -d "${dir}" ] && [ ! -f "${dir}/index.html" ]; then
joomla_findings+=("${dir}/index.html missing (directory listing protection)")
fi
done
fi
if [ "${#joomla_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' '| Check | Status |'
printf '%s\n' '|---|---|'
for f in "${joomla_findings[@]}"; do
printf '%s\n' "| ${f} | Warning |"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
else
{
printf '%s\n' '### Joomla extension checks'
printf '%s\n' 'All Joomla-specific checks passed.'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
extended_enabled="${EXTENDED_CHECKS:-true}"
extended_findings=()
if [ "${extended_enabled}" = 'true' ]; then
if [ -f '.github/CODEOWNERS' ] || [ -f 'CODEOWNERS' ] || [ -f 'docs/CODEOWNERS' ]; then
:
else
extended_findings+=("CODEOWNERS not found (.github/CODEOWNERS preferred)")
fi
if ls "${WORKFLOWS_DIR}"/*.yml >/dev/null 2>&1 || ls "${WORKFLOWS_DIR}"/*.yaml >/dev/null 2>&1; then
bad_refs="$(grep -RIn --include='*.yml' --include='*.yaml' -E '^[[:space:]]*uses:[[:space:]]*[^#]+@(main|master)\b' "${WORKFLOWS_DIR}" 2>/dev/null || true)"
if [ -n "${bad_refs}" ]; then
extended_findings+=("Workflows reference actions @main/@master (pin versions): see log excerpt")
{
printf '%s\n' '### Workflow pinning advisory'
printf '%s\n' 'Found uses: entries pinned to main/master:'
printf '%s\n' '```'
printf '%s\n' "${bad_refs}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
if [ -f "${DOCS_INDEX}" ]; then
missing_links=""
while IFS= read -r docline; do
for link in $(echo "$docline" | grep -oE '\]\([^)]+\)' | sed 's/\](//' | sed 's/)$//' || true); do
case "$link" in http://*|https://*|"#"*|mailto:*) continue ;; esac
linkpath="${link%%#*}"
linkpath="${linkpath%%\?*}"
[ -z "$linkpath" ] && continue
if [ "${linkpath:0:1}" = "/" ]; then
testpath="${linkpath#/}"
else
testpath="$(dirname "${DOCS_INDEX}")/${linkpath}"
fi
[ ! -e "$testpath" ] && missing_links="${missing_links}${testpath} "
done
done < "${DOCS_INDEX}"
if [ -n "${missing_links}" ]; then
extended_findings+=("docs/docs-index.md contains broken relative links")
{
printf '%s\n' '### Docs index link integrity'
printf '%s\n' 'Broken relative links:'
for bl in ${missing_links}; do
printf '%s\n' "- ${bl}"
done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
if [ -d "${SCRIPT_DIR}" ]; then
if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update -qq
sudo apt-get install -y shellcheck >/dev/null
fi
sc_out=''
while IFS= read -r shf; do
[ -z "${shf}" ] && continue
out_one="$(shellcheck -S warning -x "${shf}" 2>/dev/null || true)"
if [ -n "${out_one}" ]; then
sc_out="${sc_out}${out_one}\n"
fi
done < <(find "${SCRIPT_DIR}" -type f -name "${SHELLCHECK_PATTERN}" 2>/dev/null | sort)
if [ -n "${sc_out}" ]; then
extended_findings+=("ShellCheck warnings detected (advisory)")
sc_head="$(printf '%s' "${sc_out}" | head -n 200)"
{
printf '%s\n' '### ShellCheck (advisory)'
printf '%s\n' '```'
printf '%s\n' "${sc_head}"
printf '%s\n' '```'
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
spdx_missing=()
IFS=',' read -r -a spdx_globs <<< "${SPDX_FILE_GLOBS}"
spdx_args=()
for g in "${spdx_globs[@]}"; do spdx_args+=("${g}"); done
while IFS= read -r f; do
[ -z "${f}" ] && continue
if ! head -n 40 "${f}" | grep -q 'SPDX-License-Identifier:'; then
spdx_missing+=("${f}")
fi
done < <(git ls-files "${spdx_args[@]}" 2>/dev/null || true)
if [ "${#spdx_missing[@]}" -gt 0 ]; then
extended_findings+=("SPDX header missing in some tracked files (advisory)")
{
printf '%s\n' '### SPDX header advisory'
printf '%s\n' 'Files missing SPDX-License-Identifier (first 40 lines scan):'
for f in "${spdx_missing[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
stale_cutoff_days=180
stale_branches="$(git for-each-ref --format='%(refname:short) %(committerdate:unix)' refs/remotes/origin 2>/dev/null | awk -v now="$(date +%s)" -v days="${stale_cutoff_days}" '{if (now-$2 > days*86400) print $1}' | head -50)"
if [ -n "${stale_branches}" ]; then
extended_findings+=("Stale remote branches detected (advisory)")
{
printf '%s\n' '### Git hygiene advisory'
printf '%s\n' "Branches with last commit older than ${stale_cutoff_days} days (sample up to 50):"
while IFS= read -r b; do [ -n "${b}" ] && printf '%s\n' "- ${b}"; done <<< "${stale_branches}"
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
fi
{
printf '%s\n' '### Guardrails coverage matrix'
printf '%s\n' '| Domain | Status | Notes |'
printf '%s\n' '|---|---|---|'
printf '%s\n' '| Access control | OK | Admin-only execution gate |'
printf '%s\n' '| Release policy | N/A | Releases handled by MokoGitea |'
printf '%s\n' '| Scripts governance | OK | Directory policy and advisory reporting |'
printf '%s\n' '| Repo required artifacts | OK | Required, optional, disallowed enforcement |'
printf '%s\n' '| Repo content heuristics | OK | Brand, license, changelog structure |'
if [ "${extended_enabled}" = 'true' ]; then
if [ "${#extended_findings[@]}" -gt 0 ]; then
printf '%s\n' '| Extended checks | Warning | See extended findings below |'
else
printf '%s\n' '| Extended checks | OK | No findings |'
fi
else
printf '%s\n' '| Extended checks | SKIPPED | EXTENDED_CHECKS disabled |'
fi
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
if [ "${extended_enabled}" = 'true' ] && [ "${#extended_findings[@]}" -gt 0 ]; then
{
printf '%s\n' '### Extended findings (advisory)'
for f in "${extended_findings[@]}"; do printf '%s\n' "- ${f}"; done
printf '\n'
} >> "${GITHUB_STEP_SUMMARY}"
fi
printf '%s\n' 'Repository health guardrails passed.' >> "${GITHUB_STEP_SUMMARY}"
site-health:
name: Site Health
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Uptime check
if: env.URLS != ''
run: |
echo "$URLS" > /tmp/urls.txt
php monitoring/uptime-probe.php --urls /tmp/urls.txt --timeout 15 || echo "::warning::Some sites are down"
rm -f /tmp/urls.txt
env:
URLS: ${{ vars.MONITORED_URLS }}
- name: SSL certificate check
if: env.DOMAINS != ''
run: |
echo "$DOMAINS" > /tmp/domains.txt
php monitoring/ssl-check.php --domains /tmp/domains.txt --warn-days 30 || echo "::warning::SSL certificates expiring soon"
rm -f /tmp/domains.txt
env:
DOMAINS: ${{ vars.MONITORED_DOMAINS }}
- name: Summary
if: always()
run: |
echo "### Site Health" >> $GITHUB_STEP_SUMMARY
echo "Uptime and SSL checks completed." >> $GITHUB_STEP_SUMMARY
# ═══════════════════════════════════════════════════════════════════════
# Issue Reporter — file issues for failed gates
# ═══════════════════════════════════════════════════════════════════════
report-scripts:
name: "Report: Scripts Governance"
needs: [access_check, scripts_governance]
if: >-
always() &&
needs.scripts_governance.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
gate: "Scripts Governance"
workflow: "Repo Health"
severity: error
details: "Scripts directory policy violations detected. Review required and allowed directories."
secrets: inherit
report-health:
name: "Report: Repository Health"
needs: [access_check, repo_health]
if: >-
always() &&
needs.repo_health.result == 'failure'
uses: ./.mokogitea/workflows/ci-issue-reporter.yml
with:
gate: "Repository Health"
workflow: "Repo Health"
severity: error
details: "Repository health checks failed — missing required artifacts, disallowed files, or content warnings. Check the CI run summary."
secrets: inherit
+130
View File
@@ -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}
+46
View File
@@ -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
View File
@@ -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
View File
@@ -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
-->
[![MokoStandards](https://img.shields.io/badge/MokoStandards-04.00.04-blue)](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
View File
@@ -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 |
+27
View File
@@ -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
}
}
+32
View File
@@ -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