Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c2fa97d60 | |||
| 4e249cb752 |
+11
-21
@@ -1,41 +1,31 @@
|
|||||||
# EditorConfig helps maintain consistent coding styles across different editors and IDEs
|
# EditorConfig https://editorconfig.org
|
||||||
# https://editorconfig.org/
|
|
||||||
|
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
# Default settings — Tabs preferred, width = 2 spaces
|
|
||||||
[*]
|
[*]
|
||||||
charset = utf-8
|
indent_style = tab
|
||||||
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
indent_style = tab
|
insert_final_newline = true
|
||||||
tab_width = 2
|
|
||||||
|
|
||||||
# PowerShell scripts — tabs, 2-space visual width
|
|
||||||
[*.ps1]
|
[*.ps1]
|
||||||
indent_style = tab
|
|
||||||
tab_width = 2
|
|
||||||
end_of_line = crlf
|
end_of_line = crlf
|
||||||
|
|
||||||
# Markdown files — keep trailing whitespace for line breaks
|
|
||||||
[*.md]
|
[*.md]
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
# JSON / YAML files — tabs, 2-space visual width
|
|
||||||
[*.{json,yml,yaml}]
|
[*.{json,yml,yaml}]
|
||||||
indent_style = tab
|
indent_style = space
|
||||||
tab_width = 2
|
indent_size = 2
|
||||||
|
|
||||||
# Makefiles — always tabs, default width
|
[*.{mak,Makefile}]
|
||||||
[Makefile]
|
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
tab_width = 2
|
|
||||||
|
|
||||||
# Windows batch scripts — keep CRLF endings
|
[*.bat]
|
||||||
[*.{bat,cmd}]
|
|
||||||
end_of_line = crlf
|
end_of_line = crlf
|
||||||
|
|
||||||
# Shell scripts — ensure LF endings
|
|
||||||
[*.sh]
|
[*.sh]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Delete stable release (not yet tested)
|
- name: Delete stable release (not yet tested)
|
||||||
env:
|
env:
|
||||||
GIT_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "Blocking stable release: ${{ github.event.release.tag_name }}"
|
echo "Blocking stable release: ${{ github.event.release.tag_name }}"
|
||||||
echo "Reason: Code has not been fully tested yet."
|
echo "Reason: Code has not been fully tested yet."
|
||||||
@@ -22,12 +22,12 @@ jobs:
|
|||||||
|
|
||||||
# Delete the release via API
|
# Delete the release via API
|
||||||
curl -s -X DELETE \
|
curl -s -X DELETE \
|
||||||
-H "Authorization: token ${GIT_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ github.event.release.id }}"
|
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/${{ github.event.release.id }}"
|
||||||
|
|
||||||
# Also delete the tag to keep things clean
|
# Also delete the tag to keep things clean
|
||||||
curl -s -X DELETE \
|
curl -s -X DELETE \
|
||||||
-H "Authorization: token ${GIT_TOKEN}" \
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/tags/${{ github.event.release.tag_name }}" || true
|
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/tags/${{ github.event.release.tag_name }}" || true
|
||||||
|
|
||||||
echo "Stable release blocked and deleted."
|
echo "Stable release blocked and deleted."
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
name: Pre-Release Package
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version (e.g. 1.0.0-alpha.1)"
|
|
||||||
required: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Determine version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "TAG=v${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build package zip
|
|
||||||
run: |
|
|
||||||
cd source
|
|
||||||
for dir in packages/*/; do
|
|
||||||
pkg_name=$(basename "$dir")
|
|
||||||
cd "$dir"
|
|
||||||
zip -r "../../${pkg_name}.zip" . -x "*.git*"
|
|
||||||
cd ../..
|
|
||||||
done
|
|
||||||
mkdir -p pkg_build
|
|
||||||
mv *.zip pkg_build/
|
|
||||||
cp pkg_*.xml pkg_build/ 2>/dev/null || true
|
|
||||||
cp script.php pkg_build/ 2>/dev/null || true
|
|
||||||
cd pkg_build
|
|
||||||
zip -r "../${{ github.event.repository.name }}-${{ steps.version.outputs.VERSION }}.zip" .
|
|
||||||
|
|
||||||
- name: Create Pre-Release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
id: create_release
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.version.outputs.TAG }}
|
|
||||||
release_name: "${{ github.event.repository.name }} ${{ steps.version.outputs.TAG }}"
|
|
||||||
draft: false
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
- name: Upload Release Asset
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
|
||||||
asset_path: ./source/${{ github.event.repository.name }}-${{ steps.version.outputs.VERSION }}.zip
|
|
||||||
asset_name: ${{ github.event.repository.name }}-${{ steps.version.outputs.VERSION }}.zip
|
|
||||||
asset_content_type: application/zip
|
|
||||||
+7
-1
@@ -1,2 +1,8 @@
|
|||||||
# Claude Code
|
*.min.css
|
||||||
|
*.min.js
|
||||||
|
node_modules/
|
||||||
.claude/
|
.claude/
|
||||||
|
.mcp.json
|
||||||
|
TODO.md
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
|||||||
@@ -4,3 +4,6 @@
|
|||||||
[submodule "packages/MokoSuiteCRM"]
|
[submodule "packages/MokoSuiteCRM"]
|
||||||
path = packages/MokoSuiteCRM
|
path = packages/MokoSuiteCRM
|
||||||
url = https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCRM.git
|
url = https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteCRM.git
|
||||||
|
[submodule "packages/MokoSuiteERP"]
|
||||||
|
path = packages/MokoSuiteERP
|
||||||
|
url = https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteERP.git
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Release
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogit/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
|
|
||||||
MOKOGIT_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.MOKOGIT_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.MOKOGIT_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.MOKOGIT_TOKEN }}" \
|
|
||||||
--repo-url "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Release
|
|
||||||
# BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11.
|
|
||||||
#
|
|
||||||
# +========================================================================+
|
|
||||||
# | CASCADE MAIN -> DEV |
|
|
||||||
# +========================================================================+
|
|
||||||
# | dev mirrors main and is reset on every release. |
|
|
||||||
# | delete+recreate cannot run against a protected branch, so this |
|
|
||||||
# | force-pushes main -> dev. The automation identity is force-push |
|
|
||||||
# | allowlisted on dev via branch-protection.yml. |
|
|
||||||
# | Runs AFTER the release workflow completes, so dev picks up the |
|
|
||||||
# | fully version-bumped + changelog-promoted main (not the pre-bump |
|
|
||||||
# | state). Force-push (not merge) => no version-file conflicts. |
|
|
||||||
# +========================================================================+
|
|
||||||
|
|
||||||
name: "Cascade Main -> Dev"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Universal: Build & Release"]
|
|
||||||
types: [completed]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: cascade-dev-${{ github.repository }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cascade:
|
|
||||||
name: Reset dev to main
|
|
||||||
if: >-
|
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
|
||||||
github.event.workflow_run.conclusion == 'success')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Force dev to main
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
git init -q sync && cd sync
|
|
||||||
git config user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config user.name "mokogit-actions[bot]"
|
|
||||||
git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git"
|
|
||||||
git fetch -q --depth=1 origin main
|
|
||||||
|
|
||||||
if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
|
|
||||||
# dev exists -> force it to match main (dev is a mirror of main)
|
|
||||||
git push --force origin FETCH_HEAD:refs/heads/dev
|
|
||||||
echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
else
|
|
||||||
# dev missing (e.g. renamed to rc mid-cycle) -> recreate from main
|
|
||||||
git push origin FETCH_HEAD:refs/heads/dev
|
|
||||||
echo "dev created from main" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
fi
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogit/workflows/ci-issue-reporter.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Reusable workflow — creates/updates a MokoGIT 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:
|
|
||||||
MOKOGIT_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:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
run: |
|
|
||||||
MOKOGIT_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
|
||||||
git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
|
||||||
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
|
||||||
|
|
||||||
- name: Report CI failure
|
|
||||||
env:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
MOKOGIT_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 }}"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,87 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Maintenance
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogit/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:
|
|
||||||
MOKOGIT_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.MOKOGIT_TOKEN }}
|
|
||||||
|
|
||||||
- name: Delete merged branches
|
|
||||||
env:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
run: |
|
|
||||||
echo "=== Merged Branch Cleanup ==="
|
|
||||||
API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}"
|
|
||||||
|
|
||||||
# List branches via API
|
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \
|
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
|
||||||
|
|
||||||
DELETED=0
|
|
||||||
for BRANCH in $BRANCHES; do
|
|
||||||
# Skip protected branches
|
|
||||||
case "$BRANCH" in
|
|
||||||
main|master|dev|develop|rc|beta|alpha|release|release/*|production|stable|staging|hotfix/*|version/*) 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 ${MOKOGIT_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:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
run: |
|
|
||||||
echo "=== Workflow Run Cleanup ==="
|
|
||||||
API="${MOKOGIT_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 ${MOKOGIT_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 ${MOKOGIT_TOKEN}" \
|
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
|
||||||
DELETED=$((DELETED + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Deleted ${DELETED} old workflow run(s)"
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.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 MokoGIT 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:
|
|
||||||
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
validate-metadata:
|
|
||||||
name: "Validate Joomla Metadata"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Skip on template repos (Template-*) — they have no real extension manifest to validate.
|
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup MokoCLI tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_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:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
run: |
|
|
||||||
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
|
||||||
--path . \
|
|
||||||
--token "${MOKOGIT_TOKEN}" \
|
|
||||||
--org "${GIT_ORG}" \
|
|
||||||
--repo "${GIT_REPO}" \
|
|
||||||
--api-base "${MOKOGIT_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
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.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:
|
|
||||||
GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
|
||||||
runs-on: release
|
|
||||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
||||||
if: >-
|
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
github.event_name == 'push'
|
|
||||||
)
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
token: ${{ secrets.MOKOGIT_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.MOKOGIT_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 "mokogit-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "mokogit-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_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 "${GIT_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 "${GIT_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="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
php ${MOKO_CLI}/release_create.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GIT_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="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_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.MOKOGIT_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.MOKOGIT_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="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
php ${MOKO_CLI}/release_package.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
|
|
||||||
--repo "${GIT_REPO}" --output /tmp || true
|
|
||||||
|
|
||||||
# updates.xml is generated dynamically by MokoGIT 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="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGIT_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
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,130 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow.Template
|
|
||||||
# INGROUP: MokoCLI.CI
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
|
||||||
# PATH: /.mokogit/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.MOKOGIT_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
|
|
||||||
@@ -1,469 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Release
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
|
||||||
# VERSION: 05.01.00
|
|
||||||
# BRIEF: Universal build & release � detects platform from MokoGIT repo metadata (API)
|
|
||||||
#
|
|
||||||
# +=======================================================================+
|
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
|
||||||
# +=======================================================================+
|
|
||||||
# | |
|
|
||||||
# | Reads MokoGIT repo metadata (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:
|
|
||||||
- '.mokogit/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
|
|
||||||
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
|
||||||
GIT_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
|
|
||||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
||||||
if: >-
|
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(
|
|
||||||
(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.MOKOGIT_TOKEN }}
|
|
||||||
fetch-depth: 1
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Setup MokoCLI tools
|
|
||||||
env:
|
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_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.MOKOGIT_TOKEN }}" \
|
|
||||||
--api-base "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_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 "mokogit-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "mokogit-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_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.MOKOGIT_TOKEN }}"
|
|
||||||
|
|
||||||
- name: Update RC release notes from CHANGELOG.md
|
|
||||||
run: |
|
|
||||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGIT_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
|
|
||||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
||||||
if: >-
|
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(
|
|
||||||
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.MOKOGIT_TOKEN }}
|
|
||||||
fetch-depth: 0
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Configure git for bot pushes
|
|
||||||
run: |
|
|
||||||
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "mokogit-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_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.MOKOGIT_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.MOKOGIT_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="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGIT_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="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGIT_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="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
php ${MOKO_CLI}/release_mirror.php \
|
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
|
||||||
--token "${{ secrets.MOKOGIT_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 (dev reset moved to cascade-dev.yml)"
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGIT_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"
|
|
||||||
|
|
||||||
# dev is reset from main by the dedicated "Cascade Main -> Dev" workflow
|
|
||||||
# (cascade-dev.yml), which runs after this release completes.
|
|
||||||
echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: "Step 12: Create version branch from main"
|
|
||||||
if: steps.version.outputs.skip != 'true'
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGIT_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="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
php ${MOKO_CLI}/version_reset_dev.php \
|
|
||||||
--token "${{ secrets.MOKOGIT_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](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
||||||
# PATH: /.mokogit/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
|
|
||||||
# SECURITY: the PR head ref is attacker-controllable. Pass it (and the
|
|
||||||
# repo/token) through env so the Actions engine cannot splice it into the
|
|
||||||
# shell source, and validate it to a safe charset before use.
|
|
||||||
env:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
API_BASE: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
if ! printf '%s' "${BRANCH}" | grep -Eq '^[A-Za-z0-9._/-]+$'; then
|
|
||||||
echo "::error::unsafe branch name; refusing to proceed"; exit 1
|
|
||||||
fi
|
|
||||||
API="${API_BASE}/api/v1/repos/${REPO}/branches"
|
|
||||||
# URL-encode the branch name's slashes (no PHP dependency on the runner)
|
|
||||||
ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
|
|
||||||
|
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
|
||||||
-H "Authorization: token ${MOKOGIT_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
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Release
|
|
||||||
# BRIEF: Reset dev to main after each release (cascade). Moved out of auto-release Step 11.
|
|
||||||
#
|
|
||||||
# +========================================================================+
|
|
||||||
# | CASCADE MAIN -> DEV |
|
|
||||||
# +========================================================================+
|
|
||||||
# | dev mirrors main and is reset on every release. |
|
|
||||||
# | delete+recreate cannot run against a protected branch, so this |
|
|
||||||
# | force-pushes main -> dev. The automation identity is force-push |
|
|
||||||
# | allowlisted on dev via branch-protection.yml. |
|
|
||||||
# | Runs AFTER the release workflow completes, so dev picks up the |
|
|
||||||
# | fully version-bumped + changelog-promoted main (not the pre-bump |
|
|
||||||
# | state). Force-push (not merge) => no version-file conflicts. |
|
|
||||||
# +========================================================================+
|
|
||||||
|
|
||||||
name: "Cascade Main -> Dev"
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Universal: Build & Release"]
|
|
||||||
types: [completed]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: cascade-dev-${{ github.repository }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cascade:
|
|
||||||
name: Reset dev to main
|
|
||||||
if: >-
|
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
|
||||||
github.event.workflow_run.conclusion == 'success')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Force dev to main
|
|
||||||
env:
|
|
||||||
TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
SERVER: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
git init -q sync && cd sync
|
|
||||||
git config user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config user.name "mokogit-actions[bot]"
|
|
||||||
git remote add origin "https://x-access-token:${TOKEN}@${SERVER#https://}/${REPO}.git"
|
|
||||||
git fetch -q --depth=1 origin main
|
|
||||||
|
|
||||||
if git ls-remote --exit-code --heads origin dev >/dev/null 2>&1; then
|
|
||||||
# dev exists -> force it to match main (dev is a mirror of main)
|
|
||||||
git push --force origin FETCH_HEAD:refs/heads/dev
|
|
||||||
echo "dev reset to main" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
else
|
|
||||||
# dev missing (e.g. renamed to rc mid-cycle) -> recreate from main
|
|
||||||
git push origin FETCH_HEAD:refs/heads/dev
|
|
||||||
echo "dev created from main" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
fi
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.CI
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
||||||
# PATH: /.mokogit/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
|
|
||||||
# Skip on template repos (Template-*) — they hold placeholder scaffolding, not buildable source.
|
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
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
|
|
||||||
# Run only when lint succeeded; always() forces evaluation so a skipped
|
|
||||||
# lint (e.g. template repos) skips this job cleanly instead of hanging.
|
|
||||||
if: ${{ always() && needs.lint.result == 'success' }}
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogit/workflows/ci-issue-reporter.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Reusable workflow — creates/updates a MokoGIT 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:
|
|
||||||
MOKOGIT_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:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
run: |
|
|
||||||
MOKOGIT_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
|
||||||
git clone --depth 1 --filter=blob:none --sparse "${MOKOGIT_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
|
||||||
cd /tmp/mokocli && git sparse-checkout set cli/ci_issue_reporter.sh
|
|
||||||
|
|
||||||
- name: Report CI failure
|
|
||||||
env:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
MOKOGIT_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 }}"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,92 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Security
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# 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 || 'git-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
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.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:
|
|
||||||
MOKOGIT_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
|
|
||||||
# SECURITY: never interpolate github.event.* into a run: script — the
|
|
||||||
# Actions engine splices the raw value into the shell source (command
|
|
||||||
# injection via a crafted issue title). Pass everything through env so
|
|
||||||
# the values arrive as ordinary shell variables that are not re-parsed.
|
|
||||||
env:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
ISSUE_NUM: ${{ github.event.issue.number }}
|
|
||||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
|
||||||
run: |
|
|
||||||
TOKEN="${MOKOGIT_TOKEN}"
|
|
||||||
API="${MOKOGIT_URL}/api/v1/repos/${REPO}"
|
|
||||||
|
|
||||||
# Build slug from title: lowercase, replace non-alnum with dash, trim.
|
|
||||||
# printf (not echo) so a title beginning with "-" is not read as flags.
|
|
||||||
SLUG=$(printf '%s' "${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="${MOKOGIT_URL}/${REPO}"
|
|
||||||
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
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Notifications
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogit/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:
|
|
||||||
- "Universal: Build & Release"
|
|
||||||
- "Joomla: Extension CI"
|
|
||||||
- "Generic: Project CI"
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-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}"
|
|
||||||
@@ -1,527 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.CI
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# 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
|
|
||||||
# Skip on template repos (Template-*) — no real manifest/source/changelog to validate.
|
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
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: |
|
|
||||||
# Platform comes from the MokoGIT metadata API (public GET); manifest.xml is no longer used.
|
|
||||||
API="${GITHUB_SERVER_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${GITHUB_REPOSITORY}/metadata"
|
|
||||||
PLATFORM="$(curl -sf "$API" 2>/dev/null | python3 -c "import sys, json; print(json.load(sys.stdin).get('platform') or '')" 2>/dev/null || true)"
|
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Detected platform: $PLATFORM"
|
|
||||||
|
|
||||||
- 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 MokoGIT
|
|
||||||
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogit\|git\.mokoconsulting\.tech' || true)
|
|
||||||
if [ -n "$RAW_URLS" ]; then
|
|
||||||
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGIT. Use the MokoGIT Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
|
||||||
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]
|
|
||||||
# Run only when both gates succeeded; always() forces evaluation so a skipped
|
|
||||||
# validate (e.g. template repos) skips this job cleanly instead of hanging.
|
|
||||||
if: ${{ always() && needs.branch-policy.result == 'success' && needs.validate.result == 'success' }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Trigger RC pre-release
|
|
||||||
env:
|
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
BRANCH: ${{ github.head_ref }}
|
|
||||||
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
run: |
|
|
||||||
curl -s -X POST "${MOKOGIT_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${MOKOGIT_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
|
||||||
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: ./.mokogit/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
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
|
||||||
# PATH: /.mokogit/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 }}
|
|
||||||
GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
TOKEN: ${{ secrets.MOKOGIT_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="${GIT_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"
|
|
||||||
@@ -1,700 +0,0 @@
|
|||||||
# ============================================================================
|
|
||||||
# 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: MokoGIT.Workflow
|
|
||||||
# INGROUP: MokoCLI.Validation
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/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,.mokogit/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: .mokogit/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.MOKOGIT_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|mokogit-actions[bot])
|
|
||||||
ALLOWED=true
|
|
||||||
PERMISSION=admin
|
|
||||||
METHOD="hardcoded allowlist"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
# Detect platform and check permissions via API
|
|
||||||
API_BASE="${GITHUB_API_URL:-${GIT_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 MokoGIT |'
|
|
||||||
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: ./.mokogit/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: ./.mokogit/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
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,130 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: MokoGIT.Workflow.Template
|
|
||||||
# INGROUP: MokoCLI.CI
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Joomla
|
|
||||||
# PATH: /.mokogit/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.MOKOGIT_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,110 @@
|
|||||||
|
---
|
||||||
|
name: Architecture Decision Record (ADR)
|
||||||
|
about: Propose or document an architectural decision
|
||||||
|
title: '[ADR] '
|
||||||
|
labels: 'architecture, decision'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ADR Number
|
||||||
|
ADR-XXXX
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- [ ] Proposed
|
||||||
|
- [ ] Accepted
|
||||||
|
- [ ] Deprecated
|
||||||
|
- [ ] Superseded by ADR-XXXX
|
||||||
|
|
||||||
|
## Context
|
||||||
|
Describe the issue or problem that motivates this decision.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
State the architecture decision and provide rationale.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
### Positive
|
||||||
|
- List positive consequences
|
||||||
|
|
||||||
|
### Negative
|
||||||
|
- List negative consequences or trade-offs
|
||||||
|
|
||||||
|
### Neutral
|
||||||
|
- List neutral aspects
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Pros
|
||||||
|
- Cons
|
||||||
|
- Why not chosen
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
1. Step 1
|
||||||
|
2. Step 2
|
||||||
|
3. Step 3
|
||||||
|
|
||||||
|
## Stakeholders
|
||||||
|
- **Decision Makers**: @user1, @user2
|
||||||
|
- **Consulted**: @user3, @user4
|
||||||
|
- **Informed**: team-name
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
### Architecture Diagram
|
||||||
|
```
|
||||||
|
[Add diagram or link]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
- Dependency 1
|
||||||
|
- Dependency 2
|
||||||
|
|
||||||
|
### Impact Analysis
|
||||||
|
- **Performance**: [Impact description]
|
||||||
|
- **Security**: [Impact description]
|
||||||
|
- **Scalability**: [Impact description]
|
||||||
|
- **Maintainability**: [Impact description]
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
- [ ] Unit tests
|
||||||
|
- [ ] Integration tests
|
||||||
|
- [ ] Performance tests
|
||||||
|
- [ ] Security tests
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
- [ ] Architecture documentation updated
|
||||||
|
- [ ] API documentation updated
|
||||||
|
- [ ] Developer guide updated
|
||||||
|
- [ ] Runbook created
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
Describe how to migrate from current state to new architecture.
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
Describe how to rollback if issues occur.
|
||||||
|
|
||||||
|
## Timeline
|
||||||
|
- **Proposal Date**:
|
||||||
|
- **Decision Date**:
|
||||||
|
- **Implementation Start**:
|
||||||
|
- **Expected Completion**:
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related ADRs:
|
||||||
|
- External resources:
|
||||||
|
- RFCs:
|
||||||
|
|
||||||
|
## Review Checklist
|
||||||
|
- [ ] Aligns with enterprise architecture principles
|
||||||
|
- [ ] Security implications reviewed
|
||||||
|
- [ ] Performance implications reviewed
|
||||||
|
- [ ] Cost implications reviewed
|
||||||
|
- [ ] Compliance requirements met
|
||||||
|
- [ ] Team consensus achieved
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug or issue with the project
|
||||||
|
title: '[BUG] '
|
||||||
|
labels: 'bug'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Bug Description
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Scroll down to '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
A clear and concise description of what actually happened.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
- **Project**: [e.g., MokoDoliTools, moko-cassiopeia]
|
||||||
|
- **Version**: [e.g., 1.2.3]
|
||||||
|
- **Platform**: [e.g., Dolibarr 18.0, Joomla 5.0]
|
||||||
|
- **PHP Version**: [e.g., 8.1]
|
||||||
|
- **Database**: [e.g., MySQL 8.0, PostgreSQL 14]
|
||||||
|
- **Browser** (if applicable): [e.g., Chrome 120, Firefox 121]
|
||||||
|
- **OS**: [e.g., Ubuntu 22.04, Windows 11]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
## Possible Solution
|
||||||
|
If you have suggestions on how to fix the issue, please describe them here.
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar issues before creating this one
|
||||||
|
- [ ] I have provided all the requested information
|
||||||
|
- [ ] I have tested this on the latest stable version
|
||||||
|
- [ ] I have checked the documentation and couldn't find a solution
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: 💼 Enterprise Support
|
||||||
|
url: https://mokoconsulting.tech/enterprise
|
||||||
|
about: Enterprise-level support and consultation services
|
||||||
|
- name: 💬 Ask a Question
|
||||||
|
url: https://mokoconsulting.tech/
|
||||||
|
about: Get help or ask questions through our website
|
||||||
|
- name: 📚 MokoStandards Documentation
|
||||||
|
url: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
about: View our coding standards and best practices
|
||||||
|
- name: 🔒 Report a Security Vulnerability
|
||||||
|
url: https://git.mokoconsulting.tech/mokoconsulting-tech/.github-private/security/advisories/new
|
||||||
|
about: Report security vulnerabilities privately (for critical issues)
|
||||||
|
- name: 💡 Community Discussions
|
||||||
|
url: https://github.com/orgs/mokoconsulting-tech/discussions
|
||||||
|
about: Join community discussions and Q&A
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
name: Documentation Issue
|
||||||
|
about: Report an issue with documentation
|
||||||
|
title: '[DOCS] '
|
||||||
|
labels: 'documentation'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Documentation Issue
|
||||||
|
|
||||||
|
**Location**:
|
||||||
|
<!-- Specify the file, page, or section with the issue -->
|
||||||
|
|
||||||
|
## Issue Type
|
||||||
|
<!-- Mark the relevant option with an "x" -->
|
||||||
|
- [ ] Typo or grammar error
|
||||||
|
- [ ] Outdated information
|
||||||
|
- [ ] Missing documentation
|
||||||
|
- [ ] Unclear explanation
|
||||||
|
- [ ] Broken links
|
||||||
|
- [ ] Missing examples
|
||||||
|
- [ ] Other (specify below)
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Clearly describe the documentation issue -->
|
||||||
|
|
||||||
|
## Current Content
|
||||||
|
<!-- Quote or describe the current documentation (if applicable) -->
|
||||||
|
```
|
||||||
|
Current text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suggested Improvement
|
||||||
|
<!-- Provide your suggestion for how to improve the documentation -->
|
||||||
|
```
|
||||||
|
Suggested text here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context, screenshots, or references -->
|
||||||
|
|
||||||
|
## Standards Alignment
|
||||||
|
- [ ] Follows MokoStandards documentation guidelines
|
||||||
|
- [ ] Uses en_US/en_GB localization
|
||||||
|
- [ ] Includes proper SPDX headers where applicable
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar documentation issues
|
||||||
|
- [ ] I have provided a clear description
|
||||||
|
- [ ] I have suggested an improvement (if applicable)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest a new feature or enhancement
|
||||||
|
title: '[FEATURE] '
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Feature Description
|
||||||
|
A clear and concise description of the feature you'd like to see.
|
||||||
|
|
||||||
|
## Problem or Use Case
|
||||||
|
Describe the problem this feature would solve or the use case it addresses.
|
||||||
|
Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
## Proposed Solution
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
## Alternative Solutions
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
Describe how this feature would benefit users:
|
||||||
|
- Who would use this feature?
|
||||||
|
- What problems does it solve?
|
||||||
|
- What value does it add?
|
||||||
|
|
||||||
|
## Implementation Details (Optional)
|
||||||
|
If you have ideas about how this could be implemented, share them here:
|
||||||
|
- Technical approach
|
||||||
|
- Files/components that might need changes
|
||||||
|
- Any concerns or challenges you foresee
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
Add any other context, mockups, or screenshots about the feature request here.
|
||||||
|
|
||||||
|
## Relevant Standards
|
||||||
|
Does this relate to any standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] Accessibility (WCAG 2.1 AA)
|
||||||
|
- [ ] Localization (en_US/en_GB)
|
||||||
|
- [ ] Security best practices
|
||||||
|
- [ ] Code quality standards
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have searched for similar feature requests before creating this one
|
||||||
|
- [ ] I have clearly described the use case and benefits
|
||||||
|
- [ ] I have considered alternative solutions
|
||||||
|
- [ ] This feature aligns with the project's goals and scope
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: Joomla Extension Issue
|
||||||
|
about: Report an issue with a Joomla extension
|
||||||
|
title: '[JOOMLA] '
|
||||||
|
labels: 'joomla'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Issue Type
|
||||||
|
- [ ] Component Issue
|
||||||
|
- [ ] Module Issue
|
||||||
|
- [ ] Plugin Issue
|
||||||
|
- [ ] Template Issue
|
||||||
|
|
||||||
|
## Extension Details
|
||||||
|
- **Extension Name**: [e.g., moko-cassiopeia]
|
||||||
|
- **Extension Version**: [e.g., 1.2.3]
|
||||||
|
- **Extension Type**: [Component / Module / Plugin / Template]
|
||||||
|
|
||||||
|
## Joomla Environment
|
||||||
|
- **Joomla Version**: [e.g., 4.4.0, 5.0.0]
|
||||||
|
- **PHP Version**: [e.g., 8.1.0]
|
||||||
|
- **Database**: [MySQL / PostgreSQL / MariaDB]
|
||||||
|
- **Database Version**: [e.g., 8.0]
|
||||||
|
- **Server**: [Apache / Nginx / IIS]
|
||||||
|
- **Hosting**: [Shared / VPS / Dedicated / Cloud]
|
||||||
|
|
||||||
|
## Issue Description
|
||||||
|
Provide a clear and detailed description of the issue.
|
||||||
|
|
||||||
|
## Steps to Reproduce
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '...'
|
||||||
|
3. Configure '...'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
What you expected to happen.
|
||||||
|
|
||||||
|
## Actual Behavior
|
||||||
|
What actually happened.
|
||||||
|
|
||||||
|
## Error Messages
|
||||||
|
```
|
||||||
|
# Paste any error messages from Joomla error logs
|
||||||
|
# Location: administrator/logs/error.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Console Errors
|
||||||
|
```javascript
|
||||||
|
// Paste any JavaScript console errors (F12 in browser)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
Add screenshots to help explain the issue.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
```ini
|
||||||
|
# Paste extension configuration (sanitize sensitive data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installed Extensions
|
||||||
|
List other installed extensions that might conflict:
|
||||||
|
- Extension 1 (version)
|
||||||
|
- Extension 2 (version)
|
||||||
|
|
||||||
|
## Template Overrides
|
||||||
|
- [ ] Using template overrides
|
||||||
|
- [ ] Custom CSS
|
||||||
|
- [ ] Custom JavaScript
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
- **Multilingual Site**: [Yes / No]
|
||||||
|
- **Cache Enabled**: [Yes / No]
|
||||||
|
- **Debug Mode**: [Yes / No]
|
||||||
|
- **SEF URLs**: [Yes / No]
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] I have cleared Joomla cache
|
||||||
|
- [ ] I have disabled other extensions to test for conflicts
|
||||||
|
- [ ] I have checked Joomla error logs
|
||||||
|
- [ ] I have tested with a default Joomla template
|
||||||
|
- [ ] I have checked browser console for JavaScript errors
|
||||||
|
- [ ] I have searched for similar issues
|
||||||
|
- [ ] I am using a supported Joomla version
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: Question
|
||||||
|
about: Ask a question about usage, features, or best practices
|
||||||
|
title: '[QUESTION] '
|
||||||
|
labels: ['question']
|
||||||
|
assignees: ['jmiller']
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## Question
|
||||||
|
|
||||||
|
**Your question:**
|
||||||
|
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
**What are you trying to accomplish?**
|
||||||
|
|
||||||
|
|
||||||
|
**What have you already tried?**
|
||||||
|
|
||||||
|
|
||||||
|
**Category**:
|
||||||
|
- [ ] Script usage
|
||||||
|
- [ ] Configuration
|
||||||
|
- [ ] Workflow setup
|
||||||
|
- [ ] Documentation interpretation
|
||||||
|
- [ ] Best practices
|
||||||
|
- [ ] Integration
|
||||||
|
- [ ] Other: __________
|
||||||
|
|
||||||
|
## Environment (if relevant)
|
||||||
|
|
||||||
|
**Your setup**:
|
||||||
|
- Operating System:
|
||||||
|
- Version:
|
||||||
|
|
||||||
|
## What You've Researched
|
||||||
|
|
||||||
|
**Documentation reviewed**:
|
||||||
|
- [ ] README.md
|
||||||
|
- [ ] Project documentation
|
||||||
|
- [ ] Other (specify): __________
|
||||||
|
|
||||||
|
**Similar issues/questions found**:
|
||||||
|
- #
|
||||||
|
- #
|
||||||
|
|
||||||
|
## Expected Outcome
|
||||||
|
|
||||||
|
**What result are you hoping for?**
|
||||||
|
|
||||||
|
|
||||||
|
## Code/Configuration Samples
|
||||||
|
|
||||||
|
**Relevant code or configuration** (if applicable):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Your code here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
|
||||||
|
**Any other relevant information:**
|
||||||
|
|
||||||
|
|
||||||
|
**Screenshots** (if helpful):
|
||||||
|
|
||||||
|
|
||||||
|
## Urgency
|
||||||
|
|
||||||
|
- [ ] Urgent (blocking work)
|
||||||
|
- [ ] Normal (can work on other things meanwhile)
|
||||||
|
- [ ] Low priority (just curious)
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] I have searched existing issues and discussions
|
||||||
|
- [ ] I have reviewed relevant documentation
|
||||||
|
- [ ] I have provided sufficient context
|
||||||
|
- [ ] I have included code/configuration samples if relevant
|
||||||
|
- [ ] This is a genuine question (not a bug report or feature request)
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
name: Request for Comments (RFC)
|
||||||
|
about: Propose a significant change for community discussion
|
||||||
|
title: '[RFC] '
|
||||||
|
labels: 'rfc, discussion'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## RFC Summary
|
||||||
|
One-paragraph summary of the proposal.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
Why are we doing this? What use cases does it support? What is the expected outcome?
|
||||||
|
|
||||||
|
## Detailed Design
|
||||||
|
### Overview
|
||||||
|
Provide a detailed explanation of the proposed change.
|
||||||
|
|
||||||
|
### API Changes (if applicable)
|
||||||
|
```php
|
||||||
|
// Before
|
||||||
|
function oldApi($param1) { }
|
||||||
|
|
||||||
|
// After
|
||||||
|
function newApi($param1, $param2) { }
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Experience Changes
|
||||||
|
Describe how users will interact with this change.
|
||||||
|
|
||||||
|
### Implementation Approach
|
||||||
|
High-level implementation strategy.
|
||||||
|
|
||||||
|
## Drawbacks
|
||||||
|
Why should we *not* do this?
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
What other designs have been considered? What is the impact of not doing this?
|
||||||
|
|
||||||
|
### Alternative 1
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
### Alternative 2
|
||||||
|
- Description
|
||||||
|
- Trade-offs
|
||||||
|
|
||||||
|
## Adoption Strategy
|
||||||
|
How will existing users adopt this? Is this a breaking change?
|
||||||
|
|
||||||
|
### Migration Guide
|
||||||
|
```bash
|
||||||
|
# Steps to migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deprecation Timeline
|
||||||
|
- **Announcement**:
|
||||||
|
- **Deprecation**:
|
||||||
|
- **Removal**:
|
||||||
|
|
||||||
|
## Unresolved Questions
|
||||||
|
- Question 1
|
||||||
|
- Question 2
|
||||||
|
|
||||||
|
## Future Possibilities
|
||||||
|
What future work does this enable?
|
||||||
|
|
||||||
|
## Impact Assessment
|
||||||
|
### Performance
|
||||||
|
Expected performance impact.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
Security considerations and implications.
|
||||||
|
|
||||||
|
### Compatibility
|
||||||
|
- **Backward Compatible**: [Yes / No]
|
||||||
|
- **Breaking Changes**: [List]
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
Long-term maintenance considerations.
|
||||||
|
|
||||||
|
## Community Input
|
||||||
|
### Stakeholders
|
||||||
|
- [ ] Core team
|
||||||
|
- [ ] Module developers
|
||||||
|
- [ ] End users
|
||||||
|
- [ ] Enterprise customers
|
||||||
|
|
||||||
|
### Feedback Period
|
||||||
|
**Duration**: [e.g., 2 weeks]
|
||||||
|
**Deadline**: [date]
|
||||||
|
|
||||||
|
## Implementation Timeline
|
||||||
|
### Phase 1: Design
|
||||||
|
- [ ] RFC discussion
|
||||||
|
- [ ] Design finalization
|
||||||
|
- [ ] Approval
|
||||||
|
|
||||||
|
### Phase 2: Implementation
|
||||||
|
- [ ] Core implementation
|
||||||
|
- [ ] Tests
|
||||||
|
- [ ] Documentation
|
||||||
|
|
||||||
|
### Phase 3: Release
|
||||||
|
- [ ] Beta release
|
||||||
|
- [ ] Feedback collection
|
||||||
|
- [ ] Stable release
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
How will we measure success?
|
||||||
|
- Metric 1
|
||||||
|
- Metric 2
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Related RFCs:
|
||||||
|
- External documentation:
|
||||||
|
- Prior art:
|
||||||
|
|
||||||
|
## Open Questions for Community
|
||||||
|
1. Question 1?
|
||||||
|
2. Question 2?
|
||||||
|
|
||||||
|
---
|
||||||
|
**Note**: This RFC is open for community discussion. Please provide feedback in the comments below.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
name: Security Vulnerability Report
|
||||||
|
about: Report a security vulnerability (use only for non-critical issues)
|
||||||
|
title: '[SECURITY] '
|
||||||
|
labels: 'security'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## ⚠️ IMPORTANT: Private Disclosure Required
|
||||||
|
|
||||||
|
**For critical security vulnerabilities, DO NOT use this template.**
|
||||||
|
Follow the process in [SECURITY.md](../SECURITY.md) for responsible disclosure.
|
||||||
|
|
||||||
|
Use this template only for:
|
||||||
|
- Security improvements
|
||||||
|
- Non-critical security suggestions
|
||||||
|
- Security documentation updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Issue
|
||||||
|
|
||||||
|
**Severity**:
|
||||||
|
<!-- Low, Medium, or informational only -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
<!-- Describe the security concern or improvement suggestion -->
|
||||||
|
|
||||||
|
## Affected Components
|
||||||
|
<!-- List the affected files, features, or components -->
|
||||||
|
|
||||||
|
## Suggested Mitigation
|
||||||
|
<!-- Describe how this could be addressed -->
|
||||||
|
|
||||||
|
## Standards Reference
|
||||||
|
Does this relate to security standards in [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)?
|
||||||
|
- [ ] SPDX license identifiers
|
||||||
|
- [ ] Secret management
|
||||||
|
- [ ] Dependency security
|
||||||
|
- [ ] Access control
|
||||||
|
- [ ] Other: [specify]
|
||||||
|
|
||||||
|
## Additional Context
|
||||||
|
<!-- Add any other context about the security concern -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
- [ ] This is NOT a critical vulnerability requiring private disclosure
|
||||||
|
- [ ] I have reviewed the SECURITY.md policy
|
||||||
|
- [ ] I have provided sufficient detail for evaluation
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: Version Bump
|
||||||
|
about: Request or track a version change
|
||||||
|
title: '[VERSION] '
|
||||||
|
labels: 'version, type: version'
|
||||||
|
assignees: 'jmiller'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version Change
|
||||||
|
|
||||||
|
**Current version**: <!-- e.g., 01.02.03 -->
|
||||||
|
**Requested version**: <!-- e.g., 01.03.00 -->
|
||||||
|
**Change type**: <!-- patch / minor / major -->
|
||||||
|
|
||||||
|
## Reason
|
||||||
|
|
||||||
|
<!-- Why is this version bump needed? -->
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
- [ ] README.md `VERSION:` field updated
|
||||||
|
- [ ] CHANGELOG.md entry added
|
||||||
|
- [ ] Module descriptor version updated (Dolibarr: `$this->version`, Joomla: `<version>`)
|
||||||
|
- [ ] All file headers will be auto-propagated by `sync-version-on-merge` workflow
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.gitea/workflows/branch-protection.yml
|
||||||
|
# BRIEF: Apply standardised branch protection rules to all governed repositories
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | BRANCH PROTECTION SETUP |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Applies protection rules for: main, dev, rc, beta, alpha |
|
||||||
|
# | |
|
||||||
|
# | main — Require PR, block rejected reviews, no force push |
|
||||||
|
# | dev — Allow push, no force push, no delete |
|
||||||
|
# | rc — Allow push, no force push, no delete |
|
||||||
|
# | beta — Allow push, no force push, no delete |
|
||||||
|
# | alpha — Allow push, no force push, no delete |
|
||||||
|
# | |
|
||||||
|
# | jmiller has override authority on all branches. |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: Branch Protection Setup
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 2 * * 1' # Weekly Monday 02:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dry_run:
|
||||||
|
description: 'Preview mode (no changes)'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
repos:
|
||||||
|
description: 'Comma-separated repo names (empty = all governed repos)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://git.mokoconsulting.tech
|
||||||
|
GITEA_ORG: MokoConsulting
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
protect:
|
||||||
|
name: Apply Branch Protection Rules
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Determine target repos
|
||||||
|
id: repos
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|
||||||
|
# Platform/standards/infra repos to exclude
|
||||||
|
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
|
||||||
|
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
||||||
|
|
||||||
|
if [ -n "${{ inputs.repos }}" ]; then
|
||||||
|
# User-specified repos
|
||||||
|
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
|
||||||
|
else
|
||||||
|
# Fetch all org repos
|
||||||
|
PAGE=1
|
||||||
|
REPOS=""
|
||||||
|
while true; do
|
||||||
|
BATCH=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
|
||||||
|
| jq -r '.[].name // empty')
|
||||||
|
[ -z "$BATCH" ] && break
|
||||||
|
REPOS="$REPOS $BATCH"
|
||||||
|
PAGE=$((PAGE + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# Filter out excluded repos
|
||||||
|
FILTERED=""
|
||||||
|
for REPO in $REPOS; do
|
||||||
|
SKIP=false
|
||||||
|
for EX in $EXCLUDE; do
|
||||||
|
if [ "$REPO" = "$EX" ]; then
|
||||||
|
SKIP=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$SKIP" = "false" ]; then
|
||||||
|
FILTERED="$FILTERED $REPO"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
REPOS="$FILTERED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "repos=$REPOS" >> "$GITHUB_OUTPUT"
|
||||||
|
COUNT=$(echo "$REPOS" | wc -w)
|
||||||
|
echo "📋 Target repos (${COUNT}): $REPOS"
|
||||||
|
|
||||||
|
- name: Apply protection rules
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1"
|
||||||
|
REPOS="${{ steps.repos.outputs.repos }}"
|
||||||
|
|
||||||
|
SUCCESS=0
|
||||||
|
FAILED=0
|
||||||
|
SKIPPED=0
|
||||||
|
|
||||||
|
# ── Rule definitions ──────────────────────────────────────
|
||||||
|
# Only the CI bot (jmiller token) can push directly.
|
||||||
|
# All human contributors must use PRs.
|
||||||
|
# Force push disabled on all branches.
|
||||||
|
|
||||||
|
RULE_MAIN='{
|
||||||
|
"rule_name": "main",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"dismiss_stale_approvals": true,
|
||||||
|
"block_on_rejected_reviews": true,
|
||||||
|
"block_on_outdated_branch": false,
|
||||||
|
"priority": 1
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_DEV='{
|
||||||
|
"rule_name": "dev",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 2
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_RC='{
|
||||||
|
"rule_name": "rc",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 3
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_BETA='{
|
||||||
|
"rule_name": "beta",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 4
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_ALPHA='{
|
||||||
|
"rule_name": "alpha",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 5
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA")
|
||||||
|
RULE_NAMES=("main" "dev" "rc" "beta" "alpha")
|
||||||
|
|
||||||
|
# ── Apply rules to each repo ──────────────────────────────
|
||||||
|
for REPO in $REPOS; do
|
||||||
|
echo ""
|
||||||
|
echo "═══ ${REPO} ═══"
|
||||||
|
|
||||||
|
for i in "${!RULES[@]}"; do
|
||||||
|
RULE="${RULES[$i]}"
|
||||||
|
NAME="${RULE_NAMES[$i]}"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
echo " [DRY RUN] Would apply rule: ${NAME}"
|
||||||
|
SKIPPED=$((SKIPPED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete existing rule if present (idempotent recreate)
|
||||||
|
ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g')
|
||||||
|
curl -sS -o /dev/null -w "" \
|
||||||
|
-X DELETE \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create rule
|
||||||
|
RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$RULE" \
|
||||||
|
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections")
|
||||||
|
|
||||||
|
HTTP=$(echo "$RESPONSE" | tail -1)
|
||||||
|
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
if [ "$HTTP" = "201" ]; then
|
||||||
|
echo " ✅ ${NAME}"
|
||||||
|
SUCCESS=$((SUCCESS + 1))
|
||||||
|
else
|
||||||
|
echo " ❌ ${NAME} (HTTP ${HTTP}): $(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
echo " ✅ Success: ${SUCCESS}"
|
||||||
|
echo " ❌ Failed: ${FAILED}"
|
||||||
|
echo " ⏭️ Skipped: ${SKIPPED}"
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
echo "::warning::${FAILED} rule(s) failed to apply"
|
||||||
|
fi
|
||||||
@@ -1,66 +1,66 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogit/workflows/auto-bump.yml
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
# VERSION: 09.02.00
|
# VERSION: 09.02.00
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
|
|
||||||
name: "Universal: Auto Version Bump"
|
name: "Universal: Auto Version Bump"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- rc
|
- rc
|
||||||
- 'feature/**'
|
- 'feature/**'
|
||||||
- 'patch/**'
|
- 'patch/**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump:
|
bump:
|
||||||
name: Version Bump
|
name: Version Bump
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup MokoCLI tools
|
- name: Setup mokocli tools
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
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
|
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
|
fi
|
||||||
if [ -d "/opt/mokocli/cli" ]; then
|
if [ -d "/opt/mokocli/cli" ]; then
|
||||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||||
/tmp/mokocli
|
/tmp/mokocli
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/version_auto_bump.php \
|
php ${MOKO_CLI}/version_auto_bump.php \
|
||||||
--path . --branch "${GITHUB_REF_NAME}" \
|
--path . --branch "${GITHUB_REF_NAME}" \
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--repo-url "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
+62
-110
@@ -3,42 +3,33 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/mokocli
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from MokoGIT repo metadata (API)
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
#
|
#
|
||||||
# +=======================================================================+
|
# +========================================================================+
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
# +=======================================================================+
|
# +========================================================================+
|
||||||
# | |
|
# | |
|
||||||
# | Reads MokoGIT repo metadata (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, type-prefixed packages |
|
# | joomla: XML manifest, type-prefixed packages |
|
||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
# +=======================================================================+
|
# +========================================================================+
|
||||||
|
|
||||||
name: "Universal: Build & Release"
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, closed]
|
types: [opened, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
|
||||||
- '.mokogit/workflows/**'
|
|
||||||
- '*.md'
|
|
||||||
- 'wiki/**'
|
|
||||||
- '.editorconfig'
|
|
||||||
- '.gitignore'
|
|
||||||
- '.gitattributes'
|
|
||||||
- '.gitmessage'
|
|
||||||
- 'LICENSE'
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
action:
|
action:
|
||||||
@@ -52,38 +43,32 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────────
|
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||||
promote-rc:
|
promote-rc:
|
||||||
name: Promote to RC
|
name: Promote to RC
|
||||||
runs-on: release
|
runs-on: release
|
||||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
||||||
if: >-
|
if: >-
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||||
(
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
(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:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Setup MokoCLI tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
if [ -f /opt/mokocli/cli/version_bump.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
@@ -106,28 +91,28 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/branch_rename.php \
|
php ${MOKO_CLI}/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
--pr "${{ github.event.pull_request.number }}"
|
--pr "${{ github.event.pull_request.number }}"
|
||||||
|
|
||||||
- name: Checkout rc and configure git
|
- name: Checkout rc and configure git
|
||||||
run: |
|
run: |
|
||||||
git fetch origin rc
|
git fetch origin rc
|
||||||
git checkout rc
|
git checkout rc
|
||||||
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "mokogit-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: Update RC release notes from CHANGELOG.md
|
- name: Update RC release notes from CHANGELOG.md
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
# Extract [Unreleased] section from changelog
|
||||||
NOTES=""
|
NOTES=""
|
||||||
@@ -164,31 +149,26 @@ jobs:
|
|||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
release:
|
release:
|
||||||
name: Build & Release Pipeline
|
name: Build & Release Pipeline
|
||||||
runs-on: release
|
runs-on: release
|
||||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
||||||
if: >-
|
if: >-
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
github.event.pull_request.merged == true ||
|
||||||
(
|
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||||
github.event.pull_request.merged == true ||
|
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
|
||||||
)
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Configure git for bot pushes
|
- name: Configure git for bot pushes
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "mokogit-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
- name: Check for merge conflict markers
|
- name: Check for merge conflict markers
|
||||||
run: |
|
run: |
|
||||||
@@ -203,9 +183,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "No conflict markers found"
|
echo "No conflict markers found"
|
||||||
|
|
||||||
- name: Setup MokoCLI tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
@@ -252,7 +232,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
--path . --stability stable ${BUMP_FLAG} --branch main \
|
--path . --stability stable ${BUMP_FLAG} --branch main \
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: "Read published version"
|
- name: "Read published version"
|
||||||
id: version
|
id: version
|
||||||
@@ -261,51 +241,15 @@ jobs:
|
|||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
if [[ "$PLATFORM" == joomla* ]]; then
|
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
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 "branch=main" >> "$GITHUB_OUTPUT"
|
||||||
echo "Published version: ${VERSION}"
|
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="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
|
||||||
TOKEN="${{ secrets.MOKOGIT_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
|
- name: Update release notes and promote changelog
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Get the stable release info (version and ID)
|
# Get the stable release info (version and ID)
|
||||||
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
RELEASE_JSON=$(curl -sf -H "Authorization: token ${TOKEN}" \
|
||||||
@@ -373,10 +317,10 @@ jobs:
|
|||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_mirror.php \
|
php ${MOKO_CLI}/release_mirror.php \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
--branch main 2>&1 || true
|
--branch main 2>&1 || true
|
||||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -398,28 +342,36 @@ jobs:
|
|||||||
&& echo "main branch pushed to GitHub mirror" \
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
|| echo "WARNING: GitHub mirror push failed"
|
|| echo "WARNING: GitHub mirror push failed"
|
||||||
|
|
||||||
- name: "Step 11: Delete rc branch (dev reset moved to cascade-dev.yml)"
|
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Delete rc branch (ephemeral — created by promote-rc)
|
# Delete rc branch (ephemeral — created by promote-rc)
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||||
|
|
||||||
# dev is reset from main by the dedicated "Cascade Main -> Dev" workflow
|
# Delete dev branch
|
||||||
# (cascade-dev.yml), which runs after this release completes.
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
echo "rc cleaned; dev reset handled by cascade-dev.yml" >> $GITHUB_STEP_SUMMARY
|
"${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"
|
- name: "Step 12: Create version branch from main"
|
||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
BRANCH_NAME="version/${VERSION}"
|
BRANCH_NAME="version/${VERSION}"
|
||||||
MAIN_SHA=$(git rev-parse HEAD)
|
MAIN_SHA=$(git rev-parse HEAD)
|
||||||
@@ -439,9 +391,9 @@ jobs:
|
|||||||
if: steps.version.outputs.skip != 'true'
|
if: steps.version.outputs.skip != 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${MOKOGIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/version_reset_dev.php \
|
php ${MOKO_CLI}/version_reset_dev.php \
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "${API_BASE}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
# -- Summary --------------------------------------------------------------
|
# -- Summary --------------------------------------------------------------
|
||||||
@@ -465,5 +417,5 @@ jobs:
|
|||||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "| Release | [View](${MOKOGIT_URL}/${GIT_ORG}/${GIT_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
+5
-6
@@ -3,10 +3,10 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Universal
|
# INGROUP: MokoStandards.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogit/workflows/branch-cleanup.yml
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Delete feature branches after PR merge
|
# BRIEF: Delete feature branches after PR merge
|
||||||
|
|
||||||
@@ -33,11 +33,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||||
# URL-encode the branch name's slashes (no PHP dependency on the runner)
|
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||||
ENCODED=$(printf '%s' "${BRANCH}" | sed 's|/|%2F|g')
|
|
||||||
|
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
-H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \
|
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||||
|
|
||||||
if [ "$STATUS" = "204" ]; then
|
if [ "$STATUS" = "204" ]; then
|
||||||
@@ -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"
|
||||||
@@ -3,22 +3,16 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.CI
|
# INGROUP: MokoStandards.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
||||||
# PATH: /.mokogit/workflows/ci-generic.yml
|
# PATH: /.gitea/workflows/ci-generic.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
||||||
|
|
||||||
name: "Generic: Project CI"
|
name: "Generic: Project CI"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
- dev/**
|
|
||||||
- rc/**
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -32,8 +26,6 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
name: Lint & Validate
|
name: Lint & Validate
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip on template repos (Template-*) — they hold placeholder scaffolding, not buildable source.
|
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -132,9 +124,6 @@ jobs:
|
|||||||
name: Tests
|
name: Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: lint
|
needs: lint
|
||||||
# Run only when lint succeeded; always() forces evaluation so a skipped
|
|
||||||
# lint (e.g. template repos) skips this job cleanly instead of hanging.
|
|
||||||
if: ${{ always() && needs.lint.result == 'success' }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -0,0 +1,903 @@
|
|||||||
|
# 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
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow.Template
|
||||||
|
# INGROUP: MokoStandards.CI
|
||||||
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/ci-joomla.yml.template
|
||||||
|
# VERSION: 04.06.00
|
||||||
|
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
|
||||||
|
|
||||||
|
name: "Joomla: Extension CI"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'dev/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-validate:
|
||||||
|
name: Lint & Validate
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
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 php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
php -v && composer --version
|
||||||
|
|
||||||
|
- name: Setup mokocli tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
|
run: |
|
||||||
|
if [ -d "/opt/mokocli" ] || [ -d "/tmp/mokocli" ]; then
|
||||||
|
echo "mokocli already available on runner — skipping clone"
|
||||||
|
else
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokocli.git" \
|
||||||
|
/tmp/mokocli 2>/dev/null || echo "mokocli clone skipped — continuing without it"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install \
|
||||||
|
--no-interaction \
|
||||||
|
--prefer-dist \
|
||||||
|
--optimize-autoloader
|
||||||
|
else
|
||||||
|
echo "No composer.json found — skipping dependency install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: PHP syntax check
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
for DIR in src/ htdocs/; do
|
||||||
|
if [ -d "$DIR" ]; then
|
||||||
|
FOUND=1
|
||||||
|
while IFS= read -r -d '' FILE; do
|
||||||
|
OUTPUT=$(php -l "$FILE" 2>&1)
|
||||||
|
if echo "$OUTPUT" | grep -q "Parse error"; then
|
||||||
|
echo "::error file=${FILE}::${OUTPUT}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$DIR" -name "*.php" -print0)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "### PHP Syntax Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} syntax error(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: XML manifest validation
|
||||||
|
run: |
|
||||||
|
echo "### XML Manifest Validation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Find the extension manifest (XML with <extension tag)
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -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 "No Joomla extension manifest found (XML file with \`<extension\` tag)." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Manifest found: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Validate well-formed XML
|
||||||
|
php -r "
|
||||||
|
\$xml = @simplexml_load_file('$MANIFEST');
|
||||||
|
if (\$xml === false) {
|
||||||
|
echo 'INVALID';
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
echo 'VALID';
|
||||||
|
" > /tmp/xml_result 2>&1
|
||||||
|
XML_RESULT=$(cat /tmp/xml_result)
|
||||||
|
if [ "$XML_RESULT" != "VALID" ]; then
|
||||||
|
echo "Manifest is not well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check required tags: name, version, author
|
||||||
|
for TAG in name version author; do
|
||||||
|
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
|
||||||
|
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Found required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Namespace is required for components/plugins but not packages
|
||||||
|
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||||
|
if [ "$EXT_TYPE" != "package" ]; then
|
||||||
|
if ! grep -q "<namespace" "$MANIFEST" 2>/dev/null; then
|
||||||
|
echo "Missing required tag: \`<namespace>\` (required for Joomla 5+ ${EXT_TYPE} extensions)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Found required tag: \`<namespace>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Package extension — \`<namespace>\` not required." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**${ERRORS} manifest issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check language files referenced in manifest
|
||||||
|
run: |
|
||||||
|
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -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 [ -n "$MANIFEST" ]; then
|
||||||
|
# Extract language file references from manifest
|
||||||
|
LANG_FILES=$(grep -oP 'language\s+tag="[^"]*"[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||||
|
if [ -z "$LANG_FILES" ]; then
|
||||||
|
echo "No language file references found in manifest — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
while IFS= read -r LANG_FILE; do
|
||||||
|
LANG_FILE=$(echo "$LANG_FILE" | xargs)
|
||||||
|
if [ -z "$LANG_FILE" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Check in common locations
|
||||||
|
FOUND=0
|
||||||
|
for BASE in "." "src" "htdocs"; do
|
||||||
|
if [ -f "${BASE}/${LANG_FILE}" ]; then
|
||||||
|
FOUND=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$FOUND" -eq 0 ]; then
|
||||||
|
echo "Missing language file: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Language file present: \`${LANG_FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
done <<< "$LANG_FILES"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No manifest found — skipping language check." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**${ERRORS} missing language file(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Language file check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check index.html files in directories
|
||||||
|
run: |
|
||||||
|
echo "### Index.html Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
MISSING=0
|
||||||
|
CHECKED=0
|
||||||
|
|
||||||
|
for DIR in src/ htdocs/; do
|
||||||
|
if [ -d "$DIR" ]; then
|
||||||
|
while IFS= read -r -d '' SUBDIR; do
|
||||||
|
CHECKED=$((CHECKED + 1))
|
||||||
|
if [ ! -f "${SUBDIR}/index.html" ]; then
|
||||||
|
echo "Missing index.html in: \`${SUBDIR}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$DIR" -type d -print0)
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${CHECKED}" -eq 0 ]; then
|
||||||
|
echo "No src/ or htdocs/ directories found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "${MISSING}" -gt 0 ]; then
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**${MISSING} director(ies) missing index.html out of ${CHECKED} checked.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check config.xml and access.xml for components
|
||||||
|
run: |
|
||||||
|
echo "### Component Config & ACL Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Find all component manifests (XML with type="component")
|
||||||
|
COMP_MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<extension[^>]*type="component"' {} ; 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$COMP_MANIFESTS" ]; then
|
||||||
|
echo "No component extensions found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
for MANIFEST in $COMP_MANIFESTS; do
|
||||||
|
COMP_DIR=$(dirname "$MANIFEST")
|
||||||
|
COMP_NAME=$(basename "$COMP_DIR")
|
||||||
|
echo "Component: `${COMP_NAME}` (manifest: `${MANIFEST}`)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check access.xml exists
|
||||||
|
ACCESS_FILE=$(find "$COMP_DIR" -name "access.xml" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$ACCESS_FILE" ]; then
|
||||||
|
echo "- Missing `access.xml` — ACL permissions will not work." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
if ! php -r "@simplexml_load_file('$ACCESS_FILE') ?: exit(1);" 2>/dev/null; then
|
||||||
|
echo "- `access.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
for ACTION in core.admin core.manage; do
|
||||||
|
if ! grep -q "name=\"${ACTION}\"" "$ACCESS_FILE" 2>/dev/null; then
|
||||||
|
echo "- `access.xml` missing required action: `${ACTION}`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "- `access.xml`: valid" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check config.xml exists
|
||||||
|
CONFIG_FILE=$(find "$COMP_DIR" -name "config.xml" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$CONFIG_FILE" ]; then
|
||||||
|
echo "- Missing `config.xml` — component Options page will be empty." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
if ! php -r "@simplexml_load_file('$CONFIG_FILE') ?: exit(1);" 2>/dev/null; then
|
||||||
|
echo "- `config.xml` is not well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "- `config.xml`: valid" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} config/ACL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Component config & ACL check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SQL schema validation
|
||||||
|
run: |
|
||||||
|
echo "### SQL Schema Validation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Find SQL files in source/htdocs
|
||||||
|
SQL_FILES=$(find . -name "*.sql" -path "*/sql/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||||
|
if [ -z "$SQL_FILES" ]; then
|
||||||
|
echo "No SQL files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Found $(echo "$SQL_FILES" | wc -l) SQL file(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
for FILE in $SQL_FILES; do
|
||||||
|
# Basic syntax check: balanced parentheses, no empty files
|
||||||
|
SIZE=$(wc -c < "$FILE" | tr -d ' ')
|
||||||
|
if [ "$SIZE" -eq 0 ]; then
|
||||||
|
echo "- Empty SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for common SQL errors
|
||||||
|
if grep -qP '^\s*$' "$FILE" && [ "$SIZE" -lt 5 ]; then
|
||||||
|
echo "- Whitespace-only SQL file: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "- \`${FILE}\`: ${SIZE} bytes" >> $GITHUB_STEP_SUMMARY
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check update SQL files follow version numbering pattern
|
||||||
|
UPDATE_DIR=$(find . -path "*/sql/updates/mysql" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$UPDATE_DIR" ]; then
|
||||||
|
BAD_NAMES=0
|
||||||
|
for UFILE in "$UPDATE_DIR"/*.sql; do
|
||||||
|
[ ! -f "$UFILE" ] && continue
|
||||||
|
BASENAME=$(basename "$UFILE" .sql)
|
||||||
|
if ! echo "$BASENAME" | grep -qP '^\d+\.\d+\.\d+'; then
|
||||||
|
echo "- Update file \`${UFILE}\` does not follow version naming (expected X.Y.Z.sql)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
BAD_NAMES=$((BAD_NAMES + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$BAD_NAMES" -gt 0 ]; then
|
||||||
|
ERRORS=$((ERRORS + BAD_NAMES))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} SQL issue(s) found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**SQL schema validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Manifest file references check
|
||||||
|
run: |
|
||||||
|
echo "### Manifest File References" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -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 "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
|
||||||
|
# Check <filename> references
|
||||||
|
FILENAMES=$(grep -oP '<filename[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||||
|
for F in $FILENAMES; do
|
||||||
|
if [ ! -f "${MANIFEST_DIR}/${F}" ] && [ ! -d "${MANIFEST_DIR}/${F}" ]; then
|
||||||
|
echo "- Missing: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check <folder> references
|
||||||
|
FOLDERS=$(grep -oP '<folder[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||||
|
for F in $FOLDERS; do
|
||||||
|
if [ ! -d "${MANIFEST_DIR}/${F}" ]; then
|
||||||
|
echo "- Missing folder: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check <file> references in package manifests (ZIP files won't exist in source)
|
||||||
|
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||||
|
if [ "$EXT_TYPE" != "package" ]; then
|
||||||
|
FILES=$(grep -oP '<file[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null || true)
|
||||||
|
for F in $FILES; do
|
||||||
|
if [ ! -f "${MANIFEST_DIR}/${F}" ]; then
|
||||||
|
echo "- Missing file: \`${F}\` (referenced in manifest)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} missing file reference(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Manifest file references check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Form XML validation
|
||||||
|
run: |
|
||||||
|
echo "### Form XML Validation" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
FORM_FILES=$(find . -name "*.xml" -path "*/forms/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||||
|
if [ -z "$FORM_FILES" ]; then
|
||||||
|
echo "No form XML files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Found $(echo "$FORM_FILES" | wc -l) form file(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
for FILE in $FORM_FILES; do
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
if ! php -r "@simplexml_load_file('$FILE') ?: exit(1);" 2>/dev/null; then
|
||||||
|
echo "- \`${FILE}\`: malformed XML" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
# Check for valid Joomla form structure
|
||||||
|
if ! grep -qE '<form|<field|<fieldset' "$FILE" 2>/dev/null; then
|
||||||
|
echo "- \`${FILE}\`: no \`<form>\`, \`<field>\`, or \`<fieldset>\` elements found" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "- \`${FILE}\`: valid" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} form XML issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Form XML validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Deprecated Joomla API check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### Deprecated Joomla API Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
SRC_DIR=""
|
||||||
|
for DIR in source/ src/ htdocs/; do
|
||||||
|
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$SRC_DIR" ]; then
|
||||||
|
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
# Joomla 3/4 deprecated patterns that break in Joomla 6
|
||||||
|
PATTERNS=(
|
||||||
|
'JFactory::'
|
||||||
|
'JText::'
|
||||||
|
'JHtml::'
|
||||||
|
'JRoute::'
|
||||||
|
'JUri::'
|
||||||
|
'JLog::'
|
||||||
|
'JTable::'
|
||||||
|
'JInput'
|
||||||
|
'CMSFactory::\$application'
|
||||||
|
'JApplicationCms'
|
||||||
|
)
|
||||||
|
|
||||||
|
for PATTERN in "${PATTERNS[@]}"; do
|
||||||
|
HITS=$(grep -rnl "$PATTERN" "$SRC_DIR" --include="*.php" 2>/dev/null || true)
|
||||||
|
if [ -n "$HITS" ]; then
|
||||||
|
COUNT=$(echo "$HITS" | wc -l)
|
||||||
|
echo "- \`${PATTERN}\` found in ${COUNT} file(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + COUNT))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$WARNINGS" -gt 0 ]; then
|
||||||
|
echo "**${WARNINGS} deprecated API usage(s) found.** These will break in Joomla 6." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**No deprecated APIs found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Template output escaping check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### Template Output Escaping" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
TMPL_FILES=$(find . -name "*.php" -path "*/tmpl/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||||
|
if [ -z "$TMPL_FILES" ]; then
|
||||||
|
echo "No template files found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Found $(echo "$TMPL_FILES" | wc -l) template file(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
for FILE in $TMPL_FILES; do
|
||||||
|
# Check for unescaped output: <?= $var ?> or echo $var without escape()
|
||||||
|
UNESCAPED=$(grep -nP '<\?=\s*\$(?!this->escape)' "$FILE" 2>/dev/null || true)
|
||||||
|
if [ -n "$UNESCAPED" ]; then
|
||||||
|
HITS=$(echo "$UNESCAPED" | wc -l)
|
||||||
|
echo "- \`${FILE}\`: ${HITS} unescaped \`<?= \$var ?>\` output(s) — use \`<?= \$this->escape(\$var) ?>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + HITS))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for echo without escaping in template context
|
||||||
|
RAW_ECHO=$(grep -nP '^\s*echo\s+\$(?!this->escape)' "$FILE" 2>/dev/null || true)
|
||||||
|
if [ -n "$RAW_ECHO" ]; then
|
||||||
|
HITS=$(echo "$RAW_ECHO" | wc -l)
|
||||||
|
echo "- \`${FILE}\`: ${HITS} raw \`echo \$var\` — consider \`echo \$this->escape(\$var)\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
WARNINGS=$((WARNINGS + HITS))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$WARNINGS" -gt 0 ]; then
|
||||||
|
echo "**${WARNINGS} potential XSS risk(s) in templates.** Review unescaped output." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**All template output appears properly escaped.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Namespace consistency check
|
||||||
|
run: |
|
||||||
|
echo "### Namespace Consistency" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Find component/plugin manifests with <namespace> tags
|
||||||
|
MANIFESTS=$(find . -maxdepth 4 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*" -exec grep -l '<namespace' {} \; 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -z "$MANIFESTS" ]; then
|
||||||
|
echo "No manifests with \`<namespace>\` found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
for MANIFEST in $MANIFESTS; do
|
||||||
|
NS_PATH=$(grep -oP '<namespace[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
||||||
|
[ -z "$NS_PATH" ] && continue
|
||||||
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
|
|
||||||
|
echo "Manifest: \`${MANIFEST}\` → namespace \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check PHP files have matching namespace
|
||||||
|
while IFS= read -r -d '' PHP_FILE; do
|
||||||
|
FILE_NS=$(grep -oP '^\s*namespace\s+\K[^;]+' "$PHP_FILE" 2>/dev/null | head -1)
|
||||||
|
[ -z "$FILE_NS" ] && continue
|
||||||
|
|
||||||
|
# Namespace should start with the manifest namespace path
|
||||||
|
if ! echo "$FILE_NS" | grep -qF "${NS_PATH}"; then
|
||||||
|
echo "- \`${PHP_FILE}\`: namespace \`${FILE_NS}\` doesn't match manifest \`${NS_PATH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$MANIFEST_DIR" -name "*.php" -path "*/src/*" -not -path "./vendor/*" -print0 2>/dev/null)
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} namespace mismatch(es).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Namespace consistency check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SPDX license header check
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
echo "### SPDX License Headers" >> $GITHUB_STEP_SUMMARY
|
||||||
|
MISSING=0
|
||||||
|
|
||||||
|
SRC_DIR=""
|
||||||
|
for DIR in source/ src/ htdocs/; do
|
||||||
|
[ -d "$DIR" ] && SRC_DIR="$DIR" && break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$SRC_DIR" ]; then
|
||||||
|
echo "No source directory found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
TOTAL=0
|
||||||
|
while IFS= read -r -d '' FILE; do
|
||||||
|
TOTAL=$((TOTAL + 1))
|
||||||
|
if ! head -10 "$FILE" | grep -qi "SPDX"; then
|
||||||
|
echo "- Missing SPDX header: \`${FILE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
MISSING=$((MISSING + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$SRC_DIR" -name "*.php" -not -path "./vendor/*" -print0)
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
|
echo "**${MISSING}/${TOTAL} PHP file(s) missing SPDX license header.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**All ${TOTAL} PHP files have SPDX headers.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Service provider check
|
||||||
|
run: |
|
||||||
|
echo "### Service Provider Check" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
PROVIDERS=$(find . -name "provider.php" -path "*/services/*" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
||||||
|
if [ -z "$PROVIDERS" ]; then
|
||||||
|
echo "No service providers found — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
for FILE in $PROVIDERS; do
|
||||||
|
# Must return a ServiceProviderInterface
|
||||||
|
if ! grep -qP 'ServiceProviderInterface|ComponentInterface|MVCFactoryInterface|DispatcherInterface' "$FILE" 2>/dev/null; then
|
||||||
|
echo "- \`${FILE}\`: does not reference ServiceProviderInterface or component interfaces" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "- \`${FILE}\`: valid service provider" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Must have return statement
|
||||||
|
if ! grep -qP '^\s*return\s+new\s+' "$FILE" 2>/dev/null; then
|
||||||
|
echo "- \`${FILE}\`: missing \`return new ...\` statement" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ "${ERRORS}" -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} service provider issue(s).**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
release-readiness:
|
||||||
|
name: Release Readiness Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Validate release readiness
|
||||||
|
run: |
|
||||||
|
echo "## Release Readiness" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=0
|
||||||
|
|
||||||
|
# Extract version from README.md
|
||||||
|
README_VERSION=$(grep -oP '^\s*VERSION:\s*\K[0-9]{2}\.[0-9]{2}\.[0-9]{2}' README.md | head -1)
|
||||||
|
if [ -z "$README_VERSION" ]; then
|
||||||
|
echo "No VERSION found in README.md FILE INFORMATION block." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "README version: \`${README_VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the extension manifest
|
||||||
|
MANIFEST=""
|
||||||
|
for XML_FILE in $(find . -maxdepth 2 -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 "No Joomla extension manifest found." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# Check <version> matches README VERSION
|
||||||
|
MANIFEST_VERSION=$(grep -oP '<version>\K[^<]+' "$MANIFEST" | head -1)
|
||||||
|
if [ -z "$MANIFEST_VERSION" ]; then
|
||||||
|
echo "No \`<version>\` tag in manifest." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
elif [ -n "$README_VERSION" ] && [ "$MANIFEST_VERSION" != "$README_VERSION" ]; then
|
||||||
|
echo "Manifest version \`${MANIFEST_VERSION}\` does not match README \`${README_VERSION}\`." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Manifest version: \`${MANIFEST_VERSION}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check extension type, element, client attributes
|
||||||
|
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
||||||
|
if [ -z "$EXT_TYPE" ]; then
|
||||||
|
echo "Missing \`type\` attribute on \`<extension>\` tag." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "Extension type: \`${EXT_TYPE}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Element check (component/module/plugin name)
|
||||||
|
HAS_ELEMENT=$(grep -cP '<(element|name)>' "$MANIFEST" 2>/dev/null || echo "0")
|
||||||
|
if [ "$HAS_ELEMENT" -eq 0 ]; then
|
||||||
|
echo "Missing \`<element>\` or \`<name>\` in manifest." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Client attribute for site/admin modules and plugins
|
||||||
|
if echo "$EXT_TYPE" | grep -qP "^(module|plugin)$"; then
|
||||||
|
HAS_CLIENT=$(grep -cP '<extension[^>]*\bclient=' "$MANIFEST" 2>/dev/null || echo "0")
|
||||||
|
if [ "$HAS_CLIENT" -eq 0 ]; then
|
||||||
|
echo "Missing \`client\` attribute for ${EXT_TYPE} extension." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check updates.xml exists
|
||||||
|
if [ -f "updates.xml" ] || [ -f "updates.xml" ]; then
|
||||||
|
echo "Update XML present." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "No updates.xml found." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check CHANGELOG.md exists
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
echo "CHANGELOG.md present." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "No CHANGELOG.md found." >> $GITHUB_STEP_SUMMARY
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ $ERRORS -gt 0 ]; then
|
||||||
|
echo "**${ERRORS} issue(s) must be resolved before release.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "**Extension is ready for release.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
test:
|
||||||
|
name: Tests (PHP ${{ matrix.php }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint-and-validate
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
php: ['8.2', '8.3']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP ${{ matrix.php }}
|
||||||
|
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 php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
php -v && composer --version
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install \
|
||||||
|
--no-interaction \
|
||||||
|
--prefer-dist \
|
||||||
|
--optimize-autoloader
|
||||||
|
else
|
||||||
|
echo "No composer.json found — skipping dependency install"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
echo "### Test Results (PHP ${{ matrix.php }})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
if [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
||||||
|
vendor/bin/phpunit --testdox 2>&1 | tee /tmp/test-output.log
|
||||||
|
EXIT=${PIPESTATUS[0]}
|
||||||
|
if [ $EXIT -eq 0 ]; then
|
||||||
|
echo "All tests passed." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "Test failures detected — see log." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat /tmp/test-output.log >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
exit $EXIT
|
||||||
|
else
|
||||||
|
echo "No phpunit.xml found — skipping tests." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
static-analysis:
|
||||||
|
name: PHPStan Analysis
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: lint-and-validate
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
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 php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
php -v && composer --version
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
env:
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_TOKEN || secrets.GA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
if [ -f "composer.json" ]; then
|
||||||
|
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install PHPStan
|
||||||
|
run: |
|
||||||
|
if ! command -v vendor/bin/phpstan &> /dev/null; then
|
||||||
|
composer require --dev phpstan/phpstan --no-interaction 2>/dev/null || \
|
||||||
|
composer global require phpstan/phpstan --no-interaction
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run PHPStan
|
||||||
|
run: |
|
||||||
|
echo "### PHPStan Static Analysis" >> $GITHUB_STEP_SUMMARY
|
||||||
|
PHPSTAN="vendor/bin/phpstan"
|
||||||
|
if [ ! -f "$PHPSTAN" ]; then
|
||||||
|
PHPSTAN=$(composer global config bin-dir --absolute 2>/dev/null)/phpstan
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Determine source directory
|
||||||
|
SRC_DIR=""
|
||||||
|
for DIR in src/ htdocs/ lib/; do
|
||||||
|
if [ -d "$DIR" ]; then
|
||||||
|
SRC_DIR="$DIR"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$SRC_DIR" ]; then
|
||||||
|
echo "No source directory found (src/, htdocs/, lib/) — skipping." >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use repo phpstan.neon if present, otherwise use baseline config
|
||||||
|
ARGS="analyse ${SRC_DIR} --memory-limit=512M --no-progress --error-format=table"
|
||||||
|
if [ -f "phpstan.neon" ] || [ -f "phpstan.neon.dist" ]; then
|
||||||
|
echo "Using project PHPStan config." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
ARGS="$ARGS --level=3"
|
||||||
|
echo "No phpstan.neon found — using level 3 (type inference)." >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
$PHPSTAN $ARGS 2>&1 | tee /tmp/phpstan-output.txt
|
||||||
|
EXIT=${PIPESTATUS[0]}
|
||||||
|
|
||||||
|
if [ $EXIT -eq 0 ]; then
|
||||||
|
echo "**No errors found.**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
ERRORS=$(grep -c "ERROR" /tmp/phpstan-output.txt 2>/dev/null || echo "some")
|
||||||
|
echo "**${ERRORS} error(s) found.** Review output above." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
tail -30 /tmp/phpstan-output.txt >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
exit $EXIT
|
||||||
|
|
||||||
|
pre-release:
|
||||||
|
name: Build RC Pre-Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [lint-and-validate, test]
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger pre-release build
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
run: |
|
||||||
|
curl -s -X POST \
|
||||||
|
"${GITEA_URL:-https://git.mokoconsulting.tech}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" \
|
||||||
|
-H "Authorization: token ${GA_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
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Maintenance
|
# INGROUP: MokoStandards.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.mokogit/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
@@ -33,30 +33,30 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# List branches via API
|
# List branches via API
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for BRANCH in $BRANCHES; do
|
for BRANCH in $BRANCHES; do
|
||||||
# Skip protected branches
|
# Skip protected branches
|
||||||
case "$BRANCH" in
|
case "$BRANCH" in
|
||||||
main|master|dev|develop|rc|beta|alpha|release|release/*|production|stable|staging|hotfix/*|version/*) continue ;;
|
main|master|develop|release/*|hotfix/*) continue ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
echo " Deleting merged branch: ${BRANCH}"
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -66,20 +66,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_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)
|
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
|
# Get old completed runs
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${MOKOGIT_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs?status=completed&limit=50" | \
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for RUN_ID in $RUNS; do
|
for RUN_ID in $RUNS; do
|
||||||
curl -sS -X DELETE -H "Authorization: token ${MOKOGIT_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Security
|
# INGROUP: MokoStandards.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
@@ -34,7 +34,7 @@ permissions:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-security' }}
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
gitleaks:
|
gitleaks:
|
||||||
+7
-7
@@ -3,9 +3,9 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 01.00.00
|
# VERSION: 06.00.16
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
@@ -19,7 +19,7 @@ permissions:
|
|||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-branch:
|
create-branch:
|
||||||
@@ -28,8 +28,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create branch and comment
|
- name: Create branch and comment
|
||||||
run: |
|
run: |
|
||||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
API="${MOKOGIT_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
ISSUE_NUM="${{ github.event.issue.number }}"
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
ISSUE_TITLE="${{ github.event.issue.title }}"
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
echo "Created branch: ${BRANCH}"
|
echo "Created branch: ${BRANCH}"
|
||||||
|
|
||||||
# Comment on issue with branch link
|
# Comment on issue with branch link
|
||||||
REPO_URL="${MOKOGIT_URL}/${{ github.repository }}"
|
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
||||||
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||||
|
|
||||||
curl -sf -X POST \
|
curl -sf -X POST \
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Notifications
|
# INGROUP: MokoStandards.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.mokogit/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ name: "Universal: Notifications"
|
|||||||
on:
|
on:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows:
|
workflows:
|
||||||
- "Universal: Build & Release"
|
- "Joomla Build & Release"
|
||||||
- "Joomla: Extension CI"
|
- "Joomla Extension CI"
|
||||||
- "Generic: Project CI"
|
- "Deploy"
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ permissions:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'git-releases' }}
|
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-releases' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
notify:
|
notify:
|
||||||
File diff suppressed because it is too large
Load Diff
+13
-15
@@ -3,12 +3,12 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Validation
|
# INGROUP: mokocli.Validation
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
|
# PATH: /templates/workflows/joomla/pr-metadata-check.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Validate MokoGIT metadata matches Joomla extension manifest on PRs
|
# BRIEF: Validate MokoGitea metadata matches Joomla extension manifest on PRs
|
||||||
|
|
||||||
name: "Joomla: Metadata Validation"
|
name: "Joomla: Metadata Validation"
|
||||||
|
|
||||||
@@ -20,24 +20,22 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
MOKOGIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate-metadata:
|
validate-metadata:
|
||||||
name: "Validate Joomla Metadata"
|
name: "Validate Joomla Metadata"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Skip on template repos (Template-*) — they have no real extension manifest to validate.
|
|
||||||
if: ${{ !startsWith(github.event.repository.name, 'Template-') }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup MokoCLI tools
|
- name: Setup mokocli tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
if [ -f /opt/mokocli/cli/joomla_metadata_validate.php ] && [ -f /opt/mokocli/vendor/autoload.php ]; then
|
||||||
@@ -57,14 +55,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Validate metadata against Joomla manifest
|
- name: Validate metadata against Joomla manifest
|
||||||
env:
|
env:
|
||||||
MOKOGIT_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
php ${MOKO_CLI}/joomla_metadata_validate.php \
|
||||||
--path . \
|
--path . \
|
||||||
--token "${MOKOGIT_TOKEN}" \
|
--token "${GITEA_TOKEN}" \
|
||||||
--org "${GIT_ORG}" \
|
--org "${GITEA_ORG}" \
|
||||||
--repo "${GIT_REPO}" \
|
--repo "${GITEA_REPO}" \
|
||||||
--api-base "${MOKOGIT_URL}/api/v1" \
|
--api-base "${GITEA_URL}/api/v1" \
|
||||||
--ci
|
--ci
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.02.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
# BRIEF: Auto pre-release on push to dev/alpha/beta/rc branches
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
@@ -40,38 +40,29 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GIT_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GIT_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
name: "Build Pre-Release (${{ inputs.stability || github.ref_name }})"
|
||||||
runs-on: release
|
runs-on: release
|
||||||
# Skip on template repos (Template-*) — they scaffold other repos and do not release.
|
|
||||||
if: >-
|
if: >-
|
||||||
!startsWith(github.event.repository.name, 'Template-') &&
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(
|
github.event_name == 'push'
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
github.event_name == 'push'
|
|
||||||
)
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGIT_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
ref: ${{ github.ref_name }}
|
ref: ${{ github.ref_name }}
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Update submodules to main
|
- name: Setup mokocli tools
|
||||||
run: |
|
|
||||||
git submodule foreach --quiet 'git checkout main && git pull --quiet origin main' 2>/dev/null || true
|
|
||||||
|
|
||||||
- name: Setup MokoCLI tools
|
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
# Use pre-installed /opt/mokocli if available (updated by cron every 6h)
|
||||||
@@ -97,20 +88,8 @@ jobs:
|
|||||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
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
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||||
if [ "${{ github.event_name }}" = "push" ]; then
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
@@ -156,9 +135,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Commit version bump
|
# Commit version bump
|
||||||
git config --local user.email "mokogit-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "mokogit-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGIT_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
git add -A
|
git add -A
|
||||||
git diff --cached --quiet || {
|
git diff --cached --quiet || {
|
||||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||||
@@ -168,12 +147,12 @@ jobs:
|
|||||||
# Auto-detect element via manifest_element.php
|
# Auto-detect element via manifest_element.php
|
||||||
php ${MOKO_CLI}/manifest_element.php \
|
php ${MOKO_CLI}/manifest_element.php \
|
||||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||||
--repo "${GIT_REPO}" --github-output
|
--repo "${GITEA_REPO}" --github-output
|
||||||
|
|
||||||
# Read back element outputs
|
# Read back element outputs
|
||||||
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
ZIP_NAME=$(grep '^zip_name=' "$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 "${GIT_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
@@ -187,22 +166,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: release
|
id: release
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_create.php \
|
php ${MOKO_CLI}/release_create.php \
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GIT_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
- name: Update release notes from CHANGELOG.md
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
@@ -213,7 +190,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Update release body via API
|
# Update release body via API
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGIT_TOKEN }}" \
|
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)
|
"${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
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
@@ -225,7 +202,7 @@ jobs:
|
|||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
data=payload, method='PATCH',
|
data=payload, method='PATCH',
|
||||||
headers={
|
headers={
|
||||||
'Authorization': 'token ${{ secrets.MOKOGIT_TOKEN }}',
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
})
|
})
|
||||||
urllib.request.urlopen(req)
|
urllib.request.urlopen(req)
|
||||||
@@ -235,25 +212,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Build package and upload
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_package.php \
|
php ${MOKO_CLI}/release_package.php \
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
--token "${{ secrets.MOKOGIT_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GIT_REPO}" --output /tmp || true
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
# updates.xml is generated dynamically by MokoGIT license server
|
# updates.xml is generated dynamically by MokoGitea license server
|
||||||
# No need to build, commit, or sync updates.xml from workflows
|
# No need to build, commit, or sync updates.xml from workflows
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
if: steps.eligibility.outputs.proceed == 'true'
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GIT_URL}/api/v1/repos/${GIT_ORG}/${GIT_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
TOKEN="${{ secrets.MOKOGIT_TOKEN }}"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
php ${MOKO_CLI}/release_cascade.php \
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
--stability "${{ steps.meta.outputs.stability }}" \
|
--stability "${{ steps.meta.outputs.stability }}" \
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: MokoGIT.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: MokoCLI.Universal
|
# INGROUP: mokocli.Universal
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.mokogit/workflows/rc-revert.yml
|
# PATH: /.mokogitea/workflows/rc-revert.yml
|
||||||
# VERSION: 09.23.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
||||||
|
|
||||||
@@ -29,20 +29,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Rename branch
|
- name: Rename branch
|
||||||
env:
|
|
||||||
BRANCH: ${{ github.event.pull_request.head.ref }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
GIT_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
TOKEN: ${{ secrets.MOKOGIT_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
# 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/}"
|
SUFFIX="${BRANCH#rc/}"
|
||||||
DEV_BRANCH="dev/${SUFFIX}"
|
DEV_BRANCH="dev/${SUFFIX}"
|
||||||
API="${GIT_URL}/api/v1/repos/${REPO}/branches"
|
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
# Create dev/ branch from rc/ branch
|
# Create dev/ branch from rc/ branch
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X POST \
|
||||||
@@ -50,22 +42,25 @@ jobs:
|
|||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
-d "{\"new_branch_name\": \"${DEV_BRANCH}\", \"old_branch_name\": \"${BRANCH}\"}" \
|
||||||
"${API}" 2>/dev/null || true)
|
"${API}" 2>/dev/null || true)
|
||||||
|
|
||||||
if [ "$STATUS" = "201" ]; then
|
if [ "$STATUS" = "201" ]; then
|
||||||
echo "Created branch: ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
echo "Created branch: ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
else
|
||||||
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"; exit 1
|
echo "::error::Failed to create ${DEV_BRANCH} from ${BRANCH} (HTTP ${STATUS})"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Read BRANCH from the environment inside PHP (getenv, no string interpolation -> no PHP injection)
|
# Delete rc/ branch
|
||||||
ENCODED=$(php -r 'echo rawurlencode(getenv("BRANCH"));')
|
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||||
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
-H "Authorization: token ${TOKEN}" \
|
-H "Authorization: token ${TOKEN}" \
|
||||||
"${API}/${ENCODED}" 2>/dev/null || true)
|
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||||
|
|
||||||
if [ "$STATUS" = "204" ]; then
|
if [ "$STATUS" = "204" ]; then
|
||||||
echo "Deleted branch: ${BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
else
|
||||||
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
echo "::warning::Failed to delete ${BRANCH} (HTTP ${STATUS})"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "### RC Reverted" >> "$GITHUB_STEP_SUMMARY"
|
echo "### RC Reverted" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "${BRANCH} → ${DEV_BRANCH}" >> "$GITHUB_STEP_SUMMARY"
|
echo "${BRANCH} → ${DEV_BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
+712
-700
File diff suppressed because it is too large
Load Diff
@@ -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.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:
|
||||||
|
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.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: |
|
||||||
|
GITEA_URL="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}"
|
||||||
|
git clone --depth 1 "${GITEA_URL}/MokoConsulting/mokocli.git" /tmp/mokocli
|
||||||
|
|
||||||
|
- 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}
|
||||||
+18
-9
@@ -1,12 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
<!--
|
||||||
|
INGROUP: MokoSuiteField.Documentation
|
||||||
|
BRIEF: Version history using Keep a Changelog
|
||||||
|
-->
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [01.01.00] - 2026-06-12
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Initial scaffold: field service management for MokoSuite
|
- **Repository** -- initial repo creation with scaffolding
|
||||||
- 12 database tables for technicians, work orders, service agreements, equipment, vehicles, estimates
|
- **System Plugin** -- Extension class, service provider
|
||||||
- 7 helpers: Dispatch, WorkOrder, ServiceAgreement, Equipment, Estimate, TruckStock, Vehicle
|
- **SQL Schema** -- 10 tables: work_orders, equipment, equipment_history, technicians, parts, truck_inventory, work_order_parts, checklists, pm_agreements, dispatches
|
||||||
- Admin views: Dashboard, Work Orders, Technicians, Service Agreements, Equipment, Dispatch, Vehicles
|
- **Admin Component** -- 9 views: dashboard, work orders, equipment, technicians, parts, truck inventory, checklists, PM agreements, dispatches
|
||||||
- Site views: Tech Mobile (tablet), Book Service (public form)
|
- **Webservices Plugin** -- API route stubs
|
||||||
- API controller with 6 endpoints
|
- **Configuration** -- settings across basic, dispatch, billing, scheduling
|
||||||
- Task scheduler: service reminders, agreement renewals, equipment warranty, truck stock reorder
|
- **Access Control** -- permissions for granular role-based access
|
||||||
- Joomla 6 architecture (PHP 8.3+)
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# MokoSuiteField
|
||||||
|
|
||||||
|
Work orders, technician dispatch, equipment registry, parts inventory, and PM agreements for Joomla 6.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Package** | `pkg_mokosuitefield` |
|
||||||
|
| **Layer** | 2 (requires: Client, CRM) |
|
||||||
|
| **Language** | PHP 8.3+ |
|
||||||
|
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
||||||
|
| **Wiki** | [MokoSuiteField Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/wiki) |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Joomla **package** -- Layer 2 add-on. CRM contacts as customers/technicians, work order management with dispatch, equipment tracking, parts inventory with truck stock, preventive maintenance agreements.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: `Authored-by: Moko Consulting`
|
||||||
|
- **Workflow directory**: `.mokogitea/`
|
||||||
|
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoCLI/wiki)
|
||||||
|
- **Changelog**: `[Unreleased]` only -- release system assigns versions
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
- PHP 8.3+ / Joomla 6 patterns
|
||||||
|
- `$this->getDatabase()` in models, `Factory::getContainer()->get(DatabaseInterface::class)` in helpers
|
||||||
|
- `Factory::getApplication()->getIdentity()` for user
|
||||||
+22
-33
@@ -1,46 +1,35 @@
|
|||||||
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
<!--
|
||||||
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
This file is part of a Moko Consulting project.
|
SPDX-License-Identifier: CC-BY-4.0
|
||||||
|
|
||||||
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: 06.00.18
|
|
||||||
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
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
## Our Pledge
|
## Our Pledge
|
||||||
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone.
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, colour, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
## Our Standards
|
## 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.
|
Examples of behaviour that contributes to a positive environment:
|
||||||
|
|
||||||
|
- Using welcoming and inclusive language
|
||||||
|
- Being respectful of differing viewpoints and experiences
|
||||||
|
- Accepting constructive criticism gracefully
|
||||||
|
- Focusing on what is best for the community
|
||||||
|
|
||||||
## Enforcement
|
## 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
|
Instances of abusive, harassing, or otherwise unacceptable behaviour may be
|
||||||
1. **Correction** — Private warning
|
reported by contacting the project team at
|
||||||
2. **Warning** — Formal warning and limited interaction
|
[info@mokoconsulting.tech](mailto:info@mokoconsulting.tech).
|
||||||
3. **Temporary Ban** — Time-boxed exclusion
|
|
||||||
4. **Permanent Ban** — Removal from the community
|
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
Adapted from the Contributor Covenant v2.1.
|
|
||||||
|
This Code of Conduct is adapted from the
|
||||||
|
[Contributor Covenant](https://www.contributor-covenant.org), version 2.1.
|
||||||
|
|||||||
+88
-161
@@ -1,161 +1,88 @@
|
|||||||
# Contributing to Moko Consulting Projects
|
<!--
|
||||||
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
-->
|
||||||
## Branching Workflow
|
|
||||||
|
# Contributing to Moko Consulting Projects
|
||||||
```
|
|
||||||
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
Thank you for your interest in contributing! This guide explains our workflow,
|
||||||
```
|
conventions, and how to get your changes merged.
|
||||||
|
|
||||||
### Step by step
|
## Branching Workflow
|
||||||
|
|
||||||
1. **Create a feature branch** from `dev`:
|
We use a **stability-gated** branching model:
|
||||||
```bash
|
|
||||||
git checkout dev && git pull
|
```
|
||||||
git checkout -b feature/my-change
|
feature/* ──── PR ───→ dev
|
||||||
```
|
│ RC cut → rc → main
|
||||||
|
fix/* ───────── PR ────────────┘
|
||||||
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.
|
1. **Create a branch** from `dev`:
|
||||||
|
- `feature/<short-name>` for new functionality
|
||||||
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
- `fix/<short-name>` for bug fixes
|
||||||
- This automatically renames the source branch to `rc` (release candidate)
|
- `chore/<short-name>` for maintenance
|
||||||
- An RC pre-release is built and uploaded
|
2. **Open a PR** into `dev`.
|
||||||
|
3. **CI must pass** before merge.
|
||||||
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
4. **Release cuts**: `dev → rc → main` are handled by maintainers.
|
||||||
- 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
|
> **Never commit directly to `main` or `dev`.**
|
||||||
- When the draft PR is created, the branch is renamed to `rc`
|
|
||||||
|
## Version Policy
|
||||||
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
|
||||||
|
All repositories use the **XX.YY.ZZ** versioning scheme (two-digit segments):
|
||||||
7. **Merging to main** triggers the stable release pipeline:
|
|
||||||
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
- `XX` -- major (breaking changes)
|
||||||
- Stability suffix stripped (clean version)
|
- `YY` -- minor (new features, backward-compatible)
|
||||||
- Git release created with ZIP/tar.gz packages
|
- `ZZ` -- patch (bug fixes, security patches)
|
||||||
- `updates.xml` updated (Joomla extensions)
|
|
||||||
- `dev` branch recreated from `main`
|
**Stability suffixes** may be appended during pre-release:
|
||||||
|
|
||||||
### Branch summary
|
| Suffix | Meaning | Example |
|
||||||
|
|---|---|---|
|
||||||
| Branch | Purpose | Created by |
|
| `-alpha.N` | Early testing | `06.01.00-alpha.1` |
|
||||||
|--------|---------|-----------|
|
| `-beta.N` | Feature complete | `06.01.00-beta.2` |
|
||||||
| `feature/*` | New features and fixes | Developer |
|
| `-rc.N` | Release candidate | `06.01.00-rc.1` |
|
||||||
| `dev` | Integration branch | Auto-recreated after release |
|
| *(none)* | Stable release | `06.01.00` |
|
||||||
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
|
||||||
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
## Auto-Bump
|
||||||
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
|
||||||
| `main` | Stable releases | Protected, merge only |
|
Version bumps are **automated** via the `auto-bump` workflow:
|
||||||
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
|
||||||
|
- Merges into `dev` trigger a minor/patch bump.
|
||||||
### Protected branches
|
- The workflow updates all version references (manifests, changelog, etc.).
|
||||||
|
- **Do not manually edit version numbers** -- let the workflow handle it.
|
||||||
| Branch | Direct push | Merge via |
|
|
||||||
|--------|------------|-----------|
|
## Commit Messages
|
||||||
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
|
||||||
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
We follow [Conventional Commits](https://www.conventionalcommits.org/):
|
||||||
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
|
||||||
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
```
|
||||||
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
<type>(<scope>): <short description>
|
||||||
| `feature/*` | Open | N/A (source branch) |
|
|
||||||
|
<optional body>
|
||||||
## Version Policy
|
|
||||||
|
<optional footer>
|
||||||
### Format
|
```
|
||||||
|
|
||||||
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `build`, `revert`
|
||||||
|
|
||||||
- **XX** — Major version (breaking changes)
|
## Pull Request Checklist
|
||||||
- **YY** — Minor version (new features, bumped on release to main)
|
|
||||||
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
Before submitting a PR, ensure:
|
||||||
|
|
||||||
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
- [ ] Branch is based on latest `dev`
|
||||||
|
- [ ] Commit messages follow conventional commits
|
||||||
### Stability suffixes
|
- [ ] CHANGELOG.md updated under `[Unreleased]`
|
||||||
|
- [ ] No `TODO.md`, `.claude/`, `.mcp.json`, or minified files included
|
||||||
Each branch appends a suffix to indicate stability:
|
- [ ] Code follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)
|
||||||
|
- [ ] All CI checks pass
|
||||||
| Branch | Suffix | Example |
|
|
||||||
|--------|--------|---------|
|
## Code of Conduct
|
||||||
| `main` | (none) | `02.09.00` |
|
|
||||||
| `dev` | `-dev` | `02.09.01-dev` |
|
All contributors are expected to follow our [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||||
| `feature/*` | `-dev` | `02.09.01-dev` |
|
|
||||||
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
## Questions?
|
||||||
| `beta` | `-beta` | `02.09.01-beta` |
|
|
||||||
| `rc` | `-rc` | `02.09.01-rc` |
|
Open a [Question issue](../../issues/new?template=question.md) or contact
|
||||||
|
us at [hello@mokoconsulting.tech](mailto:hello@mokoconsulting.tech).
|
||||||
### 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:
|
|
||||||
|
|
||||||
- `.mokogit/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>*
|
|
||||||
|
|||||||
+27
-97
@@ -1,119 +1,49 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
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
|
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: 06.00.18
|
|
||||||
PATH: /GOVERNANCE.md
|
|
||||||
BRIEF: Project governance rules, roles, and decision process for Template-Joomla
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[](https://github.com/mokoconsulting-tech/MokoStandards)
|
# Governance
|
||||||
|
|
||||||
# Project Governance
|
## Project Leadership
|
||||||
|
|
||||||
## Overview
|
This repository is maintained by **Moko Consulting** under a **sole operator** model.
|
||||||
|
|
||||||
This document defines the governance model for the `Template-Joomla` repository within the
|
- **Lead Maintainer**: Jonathan Miller (@jmiller)
|
||||||
`mokoconsulting-tech` organization. It is automatically maintained by
|
- **Organisation**: [Moko Consulting](https://mokoconsulting.tech)
|
||||||
[MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) v04.00.04.
|
|
||||||
|
|
||||||
Full governance policy is defined in the MokoStandards source repository:
|
## Decision Making
|
||||||
[docs/policy/GOVERNANCE.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/GOVERNANCE.md)
|
|
||||||
|
|
||||||
---
|
- All architectural decisions are made by the lead maintainer.
|
||||||
|
- Community feedback is welcome via [RFC issues](../../issues/new?template=rfc.md).
|
||||||
|
- Breaking changes are documented via [ADR issues](../../issues/new?template=adr.md).
|
||||||
|
|
||||||
## Roles and Responsibilities
|
## Contribution Policy
|
||||||
|
|
||||||
### Maintainer
|
- **All changes** must go through a pull request (PR).
|
||||||
|
- **CI checks** are mandatory before merge.
|
||||||
|
- **Direct push** to `main` and `dev` is restricted to automated workflows.
|
||||||
|
|
||||||
**GitHub**: @mokoconsulting-tech
|
## Code of Conduct
|
||||||
|
|
||||||
**Authority**: Final decision-making authority on all matters for this repository.
|
All participants must adhere to our [Code of Conduct](CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
**Responsibilities**:
|
## Licensing
|
||||||
- Review and merge pull requests
|
|
||||||
- Maintain code quality and standards compliance
|
|
||||||
- Manage releases and versioning
|
|
||||||
- Respond to issues and security reports
|
|
||||||
|
|
||||||
### Contributors
|
All contributions are licensed under the same license as the project
|
||||||
|
(GPL-3.0-or-later unless otherwise stated in the repository root).
|
||||||
|
|
||||||
**Authority**: Submit changes via pull requests.
|
## Security
|
||||||
|
|
||||||
**Requirements**:
|
Security vulnerabilities should be reported privately.
|
||||||
- Read and accept `CODE_OF_CONDUCT.md`
|
See [SECURITY.md](SECURITY.md) for details.
|
||||||
- Follow `CONTRIBUTING.md` guidelines
|
|
||||||
|
|
||||||
---
|
## Dispute Resolution
|
||||||
|
|
||||||
## Decision-Making
|
Disputes are resolved by the lead maintainer. For escalation,
|
||||||
|
contact [info@mokoconsulting.tech](mailto:info@mokoconsulting.tech).
|
||||||
|
|
||||||
All changes must be submitted as pull requests. The maintainer (@mokoconsulting-tech)
|
## Changes to Governance
|
||||||
reviews and approves all changes before they are merged.
|
|
||||||
|
|
||||||
### Sole Operator Policy
|
This document may be updated at any time by the lead maintainer.
|
||||||
|
Significant changes will be announced via an RFC issue.
|
||||||
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 |
|
|
||||||
|
|||||||
@@ -1,3 +1,44 @@
|
|||||||
# MokoSuiteField
|
<!--
|
||||||
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
MokoSuite Field Service - dispatch, work orders, scheduling, mobile tech, plumbing, electrical, HVAC, service agreements. Layer 2 add-on for MokoSuite (requires CRM).
|
# MokoSuite Field
|
||||||
|
|
||||||
|
Work orders, technician dispatch, equipment registry, parts inventory, and PM agreements module for MokoSuite on Joomla 6.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
MokoSuiteField is a Layer 2 module in the MokoSuite platform, building on MokoSuiteClient (Layer 0) and MokoSuiteCRM (Layer 1) to provide complete field service management operations.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Work Orders** -- service requests with categories, priorities, scheduling, photos, signatures
|
||||||
|
- **Technician Management** -- profiles linked to CRM contacts, specialties, certifications, GPS tracking
|
||||||
|
- **Equipment Registry** -- customer equipment tracking with service history, warranty, condition ratings
|
||||||
|
- **Parts Inventory** -- warehouse stock with pricing, minimum levels, supplier tracking
|
||||||
|
- **Truck Inventory** -- per-technician mobile parts stock with restocking alerts
|
||||||
|
- **Checklists** -- configurable inspection and safety checklists by category
|
||||||
|
- **PM Agreements** -- preventive maintenance contracts with visit scheduling and auto-renewal
|
||||||
|
- **Dispatch** -- technician assignment with distance/ETA tracking and multi-attempt offers
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Joomla 6.x
|
||||||
|
- PHP 8.3+
|
||||||
|
- MokoSuiteClient (Layer 0)
|
||||||
|
- MokoSuiteCRM (Layer 1)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Install via Joomla Extension Manager using the package file `pkg_mokosuitefield.zip`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GNU General Public License v3.0 or later -- see [LICENSE](LICENSE).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Documentation](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/wiki)
|
||||||
|
- [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteField/issues)
|
||||||
|
- [MokoSuite Platform](https://mokoconsulting.tech)
|
||||||
|
|||||||
+54
-205
@@ -1,241 +1,90 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
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: 06.00.18
|
|
||||||
BRIEF: Security vulnerability reporting and handling policy
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Security 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
|
## Supported Versions
|
||||||
|
|
||||||
Security updates are provided for the following versions:
|
| Version | Supported |
|
||||||
|
|---|---|
|
||||||
| Version | Supported |
|
| Latest stable | ✅ Full support |
|
||||||
| ------- | ------------------ |
|
| Previous major | ⚠️ Critical fixes only |
|
||||||
| 01.x.x | :white_check_mark: |
|
| Older | ❌ No support |
|
||||||
| < 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
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
### Where to Report
|
**Do not report security vulnerabilities via public issues.**
|
||||||
|
|
||||||
**DO NOT** create public GitHub issues for security vulnerabilities.
|
Instead, please report them privately:
|
||||||
|
|
||||||
Report security vulnerabilities privately to:
|
1. **Email**: [security@mokoconsulting.tech](mailto:security@mokoconsulting.tech)
|
||||||
|
2. **Subject**: `[SECURITY] <Repository Name> - <Brief Description>`
|
||||||
**Email**: `security@mokoconsulting.tech`
|
|
||||||
|
|
||||||
**Subject Line**: `[SECURITY] Template-Joomla - Brief Description`
|
|
||||||
|
|
||||||
### What to Include
|
### What to Include
|
||||||
|
|
||||||
A complete vulnerability report should include:
|
- Description of the vulnerability
|
||||||
|
- Steps to reproduce
|
||||||
1. **Description**: Clear explanation of the vulnerability
|
- Affected versions
|
||||||
2. **Impact**: Potential security impact and severity assessment
|
- Potential impact
|
||||||
3. **Affected Versions**: Which versions are vulnerable
|
- Suggested fix (if any)
|
||||||
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
|
## Severity Classification
|
||||||
|
|
||||||
Vulnerabilities are classified using the following severity levels:
|
| Severity | Description | Response Time |
|
||||||
|
|---|---|---|
|
||||||
|
| **Critical** | Remote code execution, SQL injection, auth bypass | 24 hours |
|
||||||
|
| **High** | XSS, CSRF, privilege escalation | 48 hours |
|
||||||
|
| **Medium** | Information disclosure, path traversal | 72 hours |
|
||||||
|
| **Low** | Best practice violation, hardening suggestion | Next release |
|
||||||
|
|
||||||
### Critical
|
## Remediation Timeline
|
||||||
* Remote code execution
|
|
||||||
* Authentication bypass
|
|
||||||
* Data breach or exposure of sensitive information
|
|
||||||
* **Fix Timeline**: 7 days
|
|
||||||
|
|
||||||
### High
|
1. **Acknowledgement**: Within 24 hours of report
|
||||||
* Privilege escalation
|
2. **Assessment**: Within 72 hours
|
||||||
* SQL injection or command injection
|
3. **Fix development**: Based on severity
|
||||||
* Cross-site scripting (XSS) with significant impact
|
4. **Release**: Patch release with security advisory
|
||||||
* **Fix Timeline**: 14 days
|
5. **Disclosure**: Coordinated disclosure after fix is available
|
||||||
|
|
||||||
### 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
|
## Security Best Practices
|
||||||
|
|
||||||
For projects using this template:
|
### For Contributors
|
||||||
|
|
||||||
### Required Controls
|
- Never commit secrets, credentials, or API keys
|
||||||
|
- Use parameterised queries (no raw SQL concatenation)
|
||||||
|
- Validate and sanitise all user input
|
||||||
|
- Follow Joomla API for access control checks
|
||||||
|
- Use Joomla's `HTMLHelper` for output escaping
|
||||||
|
- Include SPDX license headers in all source files
|
||||||
|
|
||||||
* Enable GitHub security features (Dependabot, code scanning)
|
### For Users
|
||||||
* 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
|
- Keep Joomla and all extensions updated
|
||||||
|
- Use strong, unique passwords
|
||||||
|
- Enable two-factor authentication
|
||||||
|
- Review file permissions regularly
|
||||||
|
- Monitor Joomla error logs
|
||||||
|
|
||||||
* Follow Joomla security best practices
|
## Security Updates
|
||||||
* 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
|
Security patches are delivered through the standard update channel.
|
||||||
|
Critical fixes may receive an emergency out-of-band release.
|
||||||
|
|
||||||
* Validate all inputs
|
## Responsible Disclosure
|
||||||
* 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
|
We follow coordinated disclosure practices:
|
||||||
|
|
||||||
All repositories SHOULD implement:
|
- We will work with reporters to understand and reproduce the issue
|
||||||
|
- We will develop and test a fix
|
||||||
|
- We will credit reporters (with permission) in security advisories
|
||||||
|
- We ask that reporters allow reasonable time for a fix before public disclosure
|
||||||
|
|
||||||
**CodeQL Analysis**:
|
## Contact
|
||||||
* 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**:
|
- **Security team**: [security@mokoconsulting.tech](mailto:security@mokoconsulting.tech)
|
||||||
* Weekly scans for vulnerable dependencies
|
- **General**: [hello@mokoconsulting.tech](mailto:hello@mokoconsulting.tech)
|
||||||
* 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
|
Thank you for helping keep Moko Consulting projects secure.
|
||||||
|
|
||||||
| 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 |
|
|
||||||
|
|||||||
+23
-24
@@ -1,27 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "mokoconsulting/mokojoomgallery",
|
"name": "mokoconsulting/mokojoomgallery",
|
||||||
"description": "Photo gallery management for Joomla — galleries, images, thumbnails, lightbox, and frontend display",
|
"description": "Joomla extension by Moko Consulting",
|
||||||
"type": "joomla-package",
|
"type": "joomla-package",
|
||||||
"version": "01.00.00",
|
"license": "GPL-3.0-or-later",
|
||||||
"license": "GPL-3.0-or-later",
|
"homepage": "https://mokoconsulting.tech",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Moko Consulting",
|
"name": "Moko Consulting",
|
||||||
"email": "hello@mokoconsulting.tech",
|
"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
|
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"squizlabs/php_codesniffer": "^3.0",
|
||||||
|
"phpstan/phpstan": "^2.0",
|
||||||
|
"joomla/coding-standards": "dev-main"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
Submodule packages/MokoSuiteCRM updated: 01d601528c...6c78af16e6
+1
-1
Submodule packages/MokoSuiteClient updated: 0a9125e519...6cd16d9845
Submodule
+1
Submodule packages/MokoSuiteERP added at 8c76969ee8
+16
-28
@@ -1,32 +1,20 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
# FILE INFORMATION
|
||||||
# PHPStan configuration for Joomla extension repositories.
|
# DEFGROUP: Config.StaticAnalysis
|
||||||
# Extends the base MokoStandards config and adds Joomla framework class stubs
|
# INGROUP: Development
|
||||||
# so PHPStan can resolve Factory, CMSApplication, User, Table, etc.
|
# BRIEF: PHPStan configuration for Joomla extension static analysis
|
||||||
# without requiring a full Joomla installation.
|
|
||||||
|
|
||||||
parameters:
|
parameters:
|
||||||
level: 5
|
level: 5
|
||||||
|
paths:
|
||||||
paths:
|
- src
|
||||||
- src
|
scanDirectories:
|
||||||
|
# Joomla framework stubs (if available)
|
||||||
excludePaths:
|
- vendor/joomla
|
||||||
- vendor
|
ignoreErrors:
|
||||||
- node_modules
|
# Joomla service-container architecture: Factory/Container returns mixed
|
||||||
|
- '#Call to an undefined method Joomla\\CMS\\Application\\.*::get#i'
|
||||||
# Joomla framework stubs — resolved via the enterprise package from vendor/
|
- '#Call to method .* on an unknown class Joomla\\Cms\\Extension\\.*#'
|
||||||
stubFiles:
|
# Joomla MVC pattern: Table::getInstance returns Table|bool
|
||||||
- vendor/mokoconsulting-tech/enterprise/templates/stubs/joomla.php
|
- '#Method Joomla\\CMS\\Table\\Table::getInstance#'
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
Authored-by: Moko Consulting
|
||||||
|
-->
|
||||||
|
<access component="com_mokosuitefield">
|
||||||
|
<section name="component">
|
||||||
|
<action name="core.admin" title="JACTION_ADMIN" />
|
||||||
|
<action name="core.options" title="JACTION_OPTIONS" />
|
||||||
|
<action name="core.manage" title="JACTION_MANAGE" />
|
||||||
|
<action name="core.create" title="JACTION_CREATE" />
|
||||||
|
<action name="core.delete" title="JACTION_DELETE" />
|
||||||
|
<action name="core.edit" title="JACTION_EDIT" />
|
||||||
|
<action name="core.edit.state" title="JACTION_EDITSTATE" />
|
||||||
|
</section>
|
||||||
|
</access>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
Authored-by: Moko Consulting
|
||||||
|
-->
|
||||||
|
<extension type="component" method="upgrade">
|
||||||
|
<name>com_mokosuitefield</name>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
<creationDate>2026-06-27</creationDate>
|
||||||
|
<author>Moko Consulting</author>
|
||||||
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
|
<copyright>Copyright (C) 2026 Moko Consulting</copyright>
|
||||||
|
<license>GPL-3.0-or-later</license>
|
||||||
|
<description>COM_MOKOSUITEFIELD_DESCRIPTION</description>
|
||||||
|
|
||||||
|
<namespace path="src">Moko\Component\MokoSuiteField</namespace>
|
||||||
|
|
||||||
|
<administration>
|
||||||
|
<menu>COM_MOKOSUITEFIELD</menu>
|
||||||
|
<submenu>
|
||||||
|
<menu link="option=com_mokosuitefield&view=fielddashboard" view="fielddashboard" img="icon-home">COM_MOKOSUITEFIELD_MENU_DASHBOARD</menu>
|
||||||
|
<menu link="option=com_mokosuitefield&view=fieldworkorders" view="fieldworkorders" img="icon-file-2">COM_MOKOSUITEFIELD_MENU_WORKORDERS</menu>
|
||||||
|
<menu link="option=com_mokosuitefield&view=fieldtechnicians" view="fieldtechnicians" img="icon-users">COM_MOKOSUITEFIELD_MENU_TECHNICIANS</menu>
|
||||||
|
<menu link="option=com_mokosuitefield&view=fieldequipment" view="fieldequipment" img="icon-cogs">COM_MOKOSUITEFIELD_MENU_EQUIPMENT</menu>
|
||||||
|
<menu link="option=com_mokosuitefield&view=fieldparts" view="fieldparts" img="icon-cube">COM_MOKOSUITEFIELD_MENU_PARTS</menu>
|
||||||
|
<menu link="option=com_mokosuitefield&view=fieldchecklists" view="fieldchecklists" img="icon-checklist">COM_MOKOSUITEFIELD_MENU_CHECKLISTS</menu>
|
||||||
|
<menu link="option=com_mokosuitefield&view=fieldagreements" view="fieldagreements" img="icon-contract">COM_MOKOSUITEFIELD_MENU_AGREEMENTS</menu>
|
||||||
|
<menu link="option=com_mokosuitefield&view=fielddispatches" view="fielddispatches" img="icon-location">COM_MOKOSUITEFIELD_MENU_DISPATCHES</menu>
|
||||||
|
</submenu>
|
||||||
|
<files folder=".">
|
||||||
|
<folder>src</folder>
|
||||||
|
<folder>tmpl</folder>
|
||||||
|
<folder>services</folder>
|
||||||
|
<folder>language</folder>
|
||||||
|
<filename>access.xml</filename>
|
||||||
|
<filename>config.xml</filename>
|
||||||
|
</files>
|
||||||
|
</administration>
|
||||||
|
|
||||||
|
<languages folder="language">
|
||||||
|
<language tag="en-GB">en-GB/com_mokosuitefield.ini</language>
|
||||||
|
<language tag="en-GB">en-GB/com_mokosuitefield.sys.ini</language>
|
||||||
|
</languages>
|
||||||
|
</extension>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
Authored-by: Moko Consulting
|
||||||
|
-->
|
||||||
|
<config>
|
||||||
|
<fieldset name="permissions" label="JCONFIG_PERMISSIONS_LABEL" description="JCONFIG_PERMISSIONS_DESC">
|
||||||
|
<field
|
||||||
|
name="rules"
|
||||||
|
type="rules"
|
||||||
|
label="JCONFIG_PERMISSIONS_LABEL"
|
||||||
|
validate="rules"
|
||||||
|
filter="rules"
|
||||||
|
component="com_mokosuitefield"
|
||||||
|
section="component"
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
</config>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
; Authored-by: Moko Consulting
|
||||||
|
|
||||||
|
COM_MOKOSUITEFIELD="MokoSuite Field"
|
||||||
|
COM_MOKOSUITEFIELD_DESCRIPTION="Field service management for contractors."
|
||||||
|
COM_MOKOSUITEFIELD_MENU_DASHBOARD="Dashboard"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_WORKORDERS="Work Orders"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_TECHNICIANS="Technicians"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_EQUIPMENT="Equipment"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_PARTS="Parts"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_CHECKLISTS="Checklists"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_AGREEMENTS="PM Agreements"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_DISPATCHES="Dispatches"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
; Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
; SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
; Authored-by: Moko Consulting
|
||||||
|
|
||||||
|
COM_MOKOSUITEFIELD="MokoSuite Field"
|
||||||
|
COM_MOKOSUITEFIELD_DESCRIPTION="Field service management for contractors."
|
||||||
|
COM_MOKOSUITEFIELD_MENU_DASHBOARD="Dashboard"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_WORKORDERS="Work Orders"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_TECHNICIANS="Technicians"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_EQUIPMENT="Equipment"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_PARTS="Parts"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_CHECKLISTS="Checklists"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_AGREEMENTS="PM Agreements"
|
||||||
|
COM_MOKOSUITEFIELD_MENU_DISPATCHES="Dispatches"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
|
||||||
|
use Joomla\CMS\Extension\ComponentInterface;
|
||||||
|
use Joomla\CMS\Extension\MVCComponent;
|
||||||
|
use Joomla\CMS\Extension\Service\Provider\ComponentDispatcherFactory;
|
||||||
|
use Joomla\CMS\Extension\Service\Provider\MVCFactory;
|
||||||
|
use Joomla\CMS\Extension\Service\Provider\RouterFactory;
|
||||||
|
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||||
|
use Joomla\CMS\Component\Router\RouterFactoryInterface;
|
||||||
|
use Joomla\DI\Container;
|
||||||
|
use Joomla\DI\ServiceProviderInterface;
|
||||||
|
|
||||||
|
return new class () implements ServiceProviderInterface {
|
||||||
|
public function register(Container $container): void
|
||||||
|
{
|
||||||
|
$container->registerServiceProvider(new ComponentDispatcherFactory('\\Moko\\Component\\MokoSuiteField'));
|
||||||
|
$container->registerServiceProvider(new MVCFactory('\\Moko\\Component\\MokoSuiteField'));
|
||||||
|
$container->registerServiceProvider(new RouterFactory('\\Moko\\Component\\MokoSuiteField'));
|
||||||
|
|
||||||
|
$container->set(
|
||||||
|
ComponentInterface::class,
|
||||||
|
function (Container $container) {
|
||||||
|
$component = new MVCComponent();
|
||||||
|
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
|
||||||
|
$component->setDispatcherFactory($container->get(ComponentDispatcherFactoryInterface::class));
|
||||||
|
$component->setRouterFactory($container->get(RouterFactoryInterface::class));
|
||||||
|
|
||||||
|
return $component;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\Controller;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\Controller\BaseController;
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
class DisplayController extends BaseController
|
||||||
|
{
|
||||||
|
protected $default_view = 'fielddashboard';
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\View\FieldAgreements;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
class HtmlView extends BaseHtmlView
|
||||||
|
{
|
||||||
|
public function display($tpl = null): void
|
||||||
|
{
|
||||||
|
ToolbarHelper::title('MokoSuite Field — PM Agreements', 'contract');
|
||||||
|
|
||||||
|
parent::display($tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\View\FieldChecklists;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
class HtmlView extends BaseHtmlView
|
||||||
|
{
|
||||||
|
public function display($tpl = null): void
|
||||||
|
{
|
||||||
|
ToolbarHelper::title('MokoSuite Field — Checklists', 'checklist');
|
||||||
|
|
||||||
|
parent::display($tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\View\FieldDashboard;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
class HtmlView extends BaseHtmlView
|
||||||
|
{
|
||||||
|
public function display($tpl = null): void
|
||||||
|
{
|
||||||
|
ToolbarHelper::title('MokoSuite Field — Dashboard', 'home');
|
||||||
|
|
||||||
|
parent::display($tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\View\FieldDispatches;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
class HtmlView extends BaseHtmlView
|
||||||
|
{
|
||||||
|
public function display($tpl = null): void
|
||||||
|
{
|
||||||
|
ToolbarHelper::title('MokoSuite Field — Dispatches', 'location');
|
||||||
|
|
||||||
|
parent::display($tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\View\FieldEquipment;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
class HtmlView extends BaseHtmlView
|
||||||
|
{
|
||||||
|
public function display($tpl = null): void
|
||||||
|
{
|
||||||
|
ToolbarHelper::title('MokoSuite Field — Equipment', 'cogs');
|
||||||
|
|
||||||
|
parent::display($tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\View\FieldParts;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
class HtmlView extends BaseHtmlView
|
||||||
|
{
|
||||||
|
public function display($tpl = null): void
|
||||||
|
{
|
||||||
|
ToolbarHelper::title('MokoSuite Field — Parts', 'cube');
|
||||||
|
|
||||||
|
parent::display($tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\View\FieldTechnicians;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
class HtmlView extends BaseHtmlView
|
||||||
|
{
|
||||||
|
public function display($tpl = null): void
|
||||||
|
{
|
||||||
|
ToolbarHelper::title('MokoSuite Field — Technicians', 'users');
|
||||||
|
|
||||||
|
parent::display($tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\View\FieldWorkorders;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
||||||
|
use Joomla\CMS\Toolbar\ToolbarHelper;
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
class HtmlView extends BaseHtmlView
|
||||||
|
{
|
||||||
|
public function display($tpl = null): void
|
||||||
|
{
|
||||||
|
ToolbarHelper::title('MokoSuite Field — Work Orders', 'file-2');
|
||||||
|
|
||||||
|
parent::display($tpl);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
?>
|
||||||
|
<div class="mokosuitefield-agreements">
|
||||||
|
<h2>MokoSuite Field — PM Agreements</h2>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
?>
|
||||||
|
<div class="mokosuitefield-checklists">
|
||||||
|
<h2>MokoSuite Field — Checklists</h2>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
?>
|
||||||
|
<div class="mokosuitefield-dashboard">
|
||||||
|
<h2>MokoSuite Field — Dashboard</h2>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
?>
|
||||||
|
<div class="mokosuitefield-dispatches">
|
||||||
|
<h2>MokoSuite Field — Dispatches</h2>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
?>
|
||||||
|
<div class="mokosuitefield-equipment">
|
||||||
|
<h2>MokoSuite Field — Equipment</h2>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
?>
|
||||||
|
<div class="mokosuitefield-parts">
|
||||||
|
<h2>MokoSuite Field — Parts</h2>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
?>
|
||||||
|
<div class="mokosuitefield-technicians">
|
||||||
|
<h2>MokoSuite Field — Technicians</h2>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @license GPL-3.0-or-later
|
||||||
|
* @author Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
\defined('_JEXEC') or die;
|
||||||
|
?>
|
||||||
|
<div class="mokosuitefield-workorders">
|
||||||
|
<h2>MokoSuite Field — Work Orders</h2>
|
||||||
|
</div>
|
||||||
@@ -2,10 +2,23 @@
|
|||||||
<access component="com_mokosuitefield">
|
<access component="com_mokosuitefield">
|
||||||
<section name="component">
|
<section name="component">
|
||||||
<action name="core.admin" title="JACTION_ADMIN" />
|
<action name="core.admin" title="JACTION_ADMIN" />
|
||||||
|
<action name="core.options" title="JACTION_OPTIONS" />
|
||||||
<action name="core.manage" title="JACTION_MANAGE" />
|
<action name="core.manage" title="JACTION_MANAGE" />
|
||||||
<action name="core.create" title="JACTION_CREATE" />
|
<action name="core.create" title="JACTION_CREATE" />
|
||||||
|
<action name="core.delete" title="JACTION_DELETE" />
|
||||||
<action name="core.edit" title="JACTION_EDIT" />
|
<action name="core.edit" title="JACTION_EDIT" />
|
||||||
<action name="field.dispatch" title="Dispatch Work Orders" />
|
<action name="field.workorders.manage" title="Manage Work Orders" />
|
||||||
<action name="field.estimates" title="Create Estimates" />
|
<action name="field.workorders.dispatch" title="Dispatch Work Orders" />
|
||||||
|
<action name="field.workorders.complete" title="Complete Work Orders" />
|
||||||
|
<action name="field.workorders.invoice" title="Invoice Work Orders" />
|
||||||
|
<action name="field.equipment.manage" title="Manage Equipment" />
|
||||||
|
<action name="field.technicians.manage" title="Manage Technicians" />
|
||||||
|
<action name="field.parts.manage" title="Manage Parts" />
|
||||||
|
<action name="field.truckinventory.manage" title="Manage Truck Inventory" />
|
||||||
|
<action name="field.checklists.manage" title="Manage Checklists" />
|
||||||
|
<action name="field.pmagreements.manage" title="Manage PM Agreements" />
|
||||||
|
<action name="field.dispatch.view" title="View Dispatch Board" />
|
||||||
|
<action name="field.reports" title="View Reports" />
|
||||||
|
<action name="field.settings" title="Manage Settings" />
|
||||||
</section>
|
</section>
|
||||||
</access>
|
</access>
|
||||||
|
|||||||
@@ -1,25 +1,36 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<config>
|
<config>
|
||||||
<fieldset name="basic" label="Field Service Settings">
|
<fieldset name="basic" label="Company Defaults">
|
||||||
<field name="company_name" type="text" default="" label="Company Name" />
|
<field name="company_name" type="text" default="" label="Company Name" />
|
||||||
<field name="default_trade" type="list" default="general" label="Default Trade">
|
<field name="default_currency" type="text" default="USD" label="Default Currency" />
|
||||||
<option value="general">General</option>
|
<field name="timezone" type="text" default="UTC" label="Operating Timezone" />
|
||||||
<option value="plumbing">Plumbing</option>
|
<field name="distance_unit" type="list" default="mi" label="Distance Unit">
|
||||||
<option value="electrical">Electrical</option>
|
<option value="mi">Miles</option>
|
||||||
<option value="hvac">HVAC</option>
|
<option value="km">Kilometres</option>
|
||||||
</field>
|
</field>
|
||||||
<field name="wo_prefix" type="text" default="WO" label="Work Order Prefix" />
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="dispatch" label="Dispatch">
|
<fieldset name="dispatch" label="Dispatch">
|
||||||
<field name="auto_dispatch" type="radio" default="0" label="Auto-Dispatch" class="btn-group btn-group-yesno"><option value="1">JYES</option><option value="0">JNO</option></field>
|
<field name="max_dispatch_radius" type="number" default="50" label="Max Dispatch Radius" hint="In distance units" />
|
||||||
<field name="default_service_radius" type="number" default="30" label="Default Service Radius (miles)" />
|
<field name="auto_assign" type="radio" default="0" label="Auto-Assign Technician" class="btn-group btn-group-yesno">
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
|
<field name="priority_routing" type="radio" default="1" label="Priority-Based Routing" class="btn-group btn-group-yesno">
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="billing" label="Billing">
|
<fieldset name="billing" label="Billing">
|
||||||
<field name="default_labor_rate" type="number" default="125" step="0.01" label="Default Labor Rate ($/hr)" />
|
<field name="default_labor_rate" type="number" default="85.00" step="0.01" label="Default Labor Rate ($/hr)" />
|
||||||
<field name="overtime_multiplier" type="number" default="1.5" step="0.1" label="Overtime Multiplier" />
|
<field name="markup_percentage" type="number" default="30" step="1" label="Parts Markup (%)" />
|
||||||
<field name="travel_charge" type="number" default="0" step="0.01" label="Travel Charge ($)" />
|
<field name="tax_rate" type="number" default="0" step="0.01" label="Tax Rate (%)" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="permissions" label="Permissions">
|
<fieldset name="scheduling" label="Scheduling">
|
||||||
<field name="rules" type="rules" component="com_mokosuitefield" section="component" />
|
<field name="business_hours_start" type="text" default="08:00" label="Business Hours Start" />
|
||||||
|
<field name="business_hours_end" type="text" default="17:00" label="Business Hours End" />
|
||||||
|
<field name="weekend_service" type="radio" default="0" label="Weekend Service" class="btn-group btn-group-yesno">
|
||||||
|
<option value="1">JYES</option>
|
||||||
|
<option value="0">JNO</option>
|
||||||
|
</field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</config>
|
</config>
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
* Authored-by: Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
|
|
||||||
use Joomla\CMS\Extension\ComponentInterface;
|
use Joomla\CMS\Extension\ComponentInterface;
|
||||||
use Joomla\CMS\Extension\MVCComponent;
|
use Joomla\CMS\Extension\MVCComponent;
|
||||||
|
use Joomla\CMS\Dispatcher\ComponentDispatcherFactoryInterface;
|
||||||
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
|
||||||
use Joomla\DI\Container;
|
use Joomla\DI\Container;
|
||||||
use Joomla\DI\ServiceProviderInterface;
|
use Joomla\DI\ServiceProviderInterface;
|
||||||
|
|
||||||
return new class implements ServiceProviderInterface {
|
return new class implements ServiceProviderInterface {
|
||||||
public function register(Container $container): void
|
public function register(Container $container): void {
|
||||||
{
|
|
||||||
$container->set(ComponentInterface::class, function (Container $container) {
|
$container->set(ComponentInterface::class, function (Container $container) {
|
||||||
$component = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class));
|
$c = new MVCComponent($container->get(ComponentDispatcherFactoryInterface::class));
|
||||||
$component->setMVCFactory($container->get(MVCFactoryInterface::class));
|
$c->setMVCFactory($container->get(MVCFactoryInterface::class));
|
||||||
return $component;
|
return $c;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
* Authored-by: Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteField\Administrator\Controller;
|
namespace Moko\Component\MokoSuiteField\Administrator\Controller;
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
defined('_JEXEC') or die;
|
||||||
@@ -7,5 +14,5 @@ use Joomla\CMS\MVC\Controller\BaseController;
|
|||||||
|
|
||||||
class DisplayController extends BaseController
|
class DisplayController extends BaseController
|
||||||
{
|
{
|
||||||
protected $default_view = 'dashboard';
|
protected $default_view = 'fielddashboard';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Moko\Component\MokoSuiteField\Administrator\Model;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
||||||
|
|
||||||
class DispatchModel extends BaseDatabaseModel
|
|
||||||
{
|
|
||||||
public function getTodayBoard(): array
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$db->setQuery($db->getQuery(true)
|
|
||||||
->select('t.id AS tech_id, cd.name AS tech_name, t.trade, t.status AS tech_status')
|
|
||||||
->select('(SELECT COUNT(*) FROM #__mokosuitefield_work_orders wo WHERE wo.technician_id = t.id AND wo.scheduled_date = CURDATE() AND wo.status NOT IN (' . $db->quote('completed') . ',' . $db->quote('cancelled') . ')) AS pending_jobs')
|
|
||||||
->select('(SELECT COUNT(*) FROM #__mokosuitefield_work_orders wo WHERE wo.technician_id = t.id AND wo.scheduled_date = CURDATE() AND wo.status = ' . $db->quote('completed') . ') AS completed_jobs')
|
|
||||||
->from($db->quoteName('#__mokosuitefield_technicians', 't'))
|
|
||||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id')
|
|
||||||
->where($db->quoteName('t.status') . ' != ' . $db->quote('inactive'))
|
|
||||||
->order('cd.name ASC'));
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getGpsLog(int $techId, string $date = ''): array
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$date = $date ?: date('Y-m-d');
|
|
||||||
|
|
||||||
$db->setQuery($db->getQuery(true)
|
|
||||||
->select('*')
|
|
||||||
->from('#__mokosuitefield_dispatch_log')
|
|
||||||
->where('technician_id = ' . $techId)
|
|
||||||
->where('DATE(recorded_at) = ' . $db->quote($date))
|
|
||||||
->order('recorded_at ASC'));
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Moko\Component\MokoSuiteField\Administrator\Model;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
||||||
|
|
||||||
class EquipmentModel extends BaseDatabaseModel
|
|
||||||
{
|
|
||||||
public function getItems(string $type = '', string $status = '', int $locationId = 0, string $search = '', int $limit = 50): array
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('eq.*, loc.name AS location_name, loc.address, cd.name AS customer_name')
|
|
||||||
->from($db->quoteName('#__mokosuitefield_equipment', 'eq'))
|
|
||||||
->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = eq.location_id')
|
|
||||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = eq.contact_id')
|
|
||||||
->order('eq.name ASC');
|
|
||||||
|
|
||||||
if ($type) $query->where($db->quoteName('eq.type') . ' = ' . $db->quote($type));
|
|
||||||
if ($status) $query->where($db->quoteName('eq.status') . ' = ' . $db->quote($status));
|
|
||||||
if ($locationId) $query->where('eq.location_id = ' . $locationId);
|
|
||||||
if ($search) $query->where('(' . $db->quoteName('eq.name') . ' LIKE ' . $db->quote('%' . $search . '%')
|
|
||||||
. ' OR ' . $db->quoteName('eq.serial_number') . ' LIKE ' . $db->quote('%' . $search . '%') . ')');
|
|
||||||
|
|
||||||
$db->setQuery($query, 0, $limit);
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Moko\Component\MokoSuiteField\Administrator\Model;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
||||||
|
|
||||||
class EstimatesModel extends BaseDatabaseModel
|
|
||||||
{
|
|
||||||
public function getItems(string $status = '', string $search = '', int $techId = 0, int $limit = 50): array
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('e.*, cd.name AS customer_name, loc.address')
|
|
||||||
->from($db->quoteName('#__mokosuitefield_estimates', 'e'))
|
|
||||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = e.contact_id')
|
|
||||||
->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = e.location_id')
|
|
||||||
->order('e.created DESC');
|
|
||||||
|
|
||||||
if ($status) $query->where($db->quoteName('e.status') . ' = ' . $db->quote($status));
|
|
||||||
if ($techId) $query->where('e.technician_id = ' . $techId);
|
|
||||||
if ($search) $query->where('(' . $db->quoteName('cd.name') . ' LIKE ' . $db->quote('%' . $search . '%')
|
|
||||||
. ' OR ' . $db->quoteName('e.estimate_number') . ' LIKE ' . $db->quote('%' . $search . '%') . ')');
|
|
||||||
|
|
||||||
$db->setQuery($query, 0, $limit);
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
* Authored-by: Moko Consulting
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Moko\Component\MokoSuiteField\Administrator\Model;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
||||||
|
|
||||||
|
class FieldDashboardModel extends BaseDatabaseModel
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Moko\Component\MokoSuiteField\Administrator\Model;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
||||||
|
|
||||||
class ServiceAgreementsModel extends BaseDatabaseModel
|
|
||||||
{
|
|
||||||
public function getItems(string $status = '', string $search = '', int $limit = 50): array
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('sa.*, cd.name AS customer_name, loc.address')
|
|
||||||
->from($db->quoteName('#__mokosuitefield_service_agreements', 'sa'))
|
|
||||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = sa.contact_id')
|
|
||||||
->join('LEFT', $db->quoteName('#__mokosuitefield_locations', 'loc') . ' ON loc.id = sa.location_id')
|
|
||||||
->order('sa.end_date ASC');
|
|
||||||
|
|
||||||
if ($status) $query->where($db->quoteName('sa.status') . ' = ' . $db->quote($status));
|
|
||||||
if ($search) $query->where($db->quoteName('cd.name') . ' LIKE ' . $db->quote('%' . $search . '%'));
|
|
||||||
|
|
||||||
$db->setQuery($query, 0, $limit);
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getExpiringSoon(int $days = 30): array
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$db->setQuery($db->getQuery(true)
|
|
||||||
->select('sa.*, cd.name AS customer_name')
|
|
||||||
->from($db->quoteName('#__mokosuitefield_service_agreements', 'sa'))
|
|
||||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = sa.contact_id')
|
|
||||||
->where($db->quoteName('sa.status') . ' = ' . $db->quote('active'))
|
|
||||||
->where($db->quoteName('sa.end_date') . ' BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL ' . $days . ' DAY)')
|
|
||||||
->order('sa.end_date ASC'));
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Moko\Component\MokoSuiteField\Administrator\Model;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
|
|
||||||
|
|
||||||
class TechniciansModel extends BaseDatabaseModel
|
|
||||||
{
|
|
||||||
public function getItems(string $status = '', string $trade = '', string $search = '', int $limit = 50): array
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('t.*, cd.name AS tech_name, cd.email_to, cd.telephone')
|
|
||||||
->select('(SELECT COUNT(*) FROM #__mokosuitefield_work_orders wo WHERE wo.technician_id = t.id AND wo.status IN (' . $db->quote('dispatched') . ',' . $db->quote('in_progress') . ')) AS active_jobs')
|
|
||||||
->from($db->quoteName('#__mokosuitefield_technicians', 't'))
|
|
||||||
->join('LEFT', $db->quoteName('#__contact_details', 'cd') . ' ON cd.id = t.contact_id')
|
|
||||||
->order('cd.name ASC');
|
|
||||||
|
|
||||||
if ($status) $query->where($db->quoteName('t.status') . ' = ' . $db->quote($status));
|
|
||||||
if ($trade) $query->where($db->quoteName('t.trade') . ' = ' . $db->quote($trade));
|
|
||||||
if ($search) $query->where($db->quoteName('cd.name') . ' LIKE ' . $db->quote('%' . $search . '%'));
|
|
||||||
|
|
||||||
$db->setQuery($query, 0, $limit);
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user