Template
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b56994f16 | |||
| 51807ff944 | |||
| 5cd184a7dc | |||
| a02da1ad84 | |||
| 73628c509b |
+1
-1
@@ -113,7 +113,7 @@ releases/
|
|||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
out/
|
out/
|
||||||
/site/
|
site/
|
||||||
*.map
|
*.map
|
||||||
*.css.map
|
*.css.map
|
||||||
*.js.map
|
*.js.map
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
<name>Template-Joomla</name>
|
<name>Template-Joomla</name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Template repository for Joomla extensions (plugins, modules, components, templates)</description>
|
<description>Template repository for Joomla extensions (plugins, modules, components, templates)</description>
|
||||||
<version>01.02.00</version>
|
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ name: "Universal: Auto Version Bump"
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
- dev
|
||||||
- rc
|
- rc
|
||||||
- 'feature/**'
|
- 'feature/**'
|
||||||
- 'patch/**'
|
- 'patch/**'
|
||||||
|
|||||||
@@ -109,19 +109,89 @@ jobs:
|
|||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: Update RC release notes from CHANGELOG.md
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
|
release:
|
||||||
|
name: Build & Release Pipeline
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure git for bot pushes
|
||||||
|
run: |
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
- name: Check for merge conflict markers
|
||||||
|
run: |
|
||||||
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
|
if [ -n "$CONFLICTS" ]; then
|
||||||
|
echo "::error::Merge conflict markers found — aborting release"
|
||||||
|
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "No conflict markers found"
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
|
run: |
|
||||||
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
|
echo Using pre-installed /opt/moko-platform
|
||||||
|
echo MOKO_CLI=/opt/moko-platform/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/moko-platform-api
|
||||||
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: "Publish stable release"
|
||||||
|
run: |
|
||||||
|
php ${MOKO_CLI}/release_publish.php \
|
||||||
|
--path . --stability stable --bump minor --branch main \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
- name: Update release notes from CHANGELOG.md
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
[ -z "$NOTES" ] && NOTES="Release candidate"
|
[ -z "$NOTES" ] && NOTES="Stable release"
|
||||||
else
|
else
|
||||||
NOTES="Release candidate"
|
NOTES="Stable release"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
"${API_BASE}/releases/tags/release-candidate" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
"${API_BASE}/releases/tags/stable" | 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
|
||||||
python3 -c "
|
python3 -c "
|
||||||
@@ -137,11 +207,118 @@ jobs:
|
|||||||
})
|
})
|
||||||
urllib.request.urlopen(req)
|
urllib.request.urlopen(req)
|
||||||
" <<< "$NOTES"
|
" <<< "$NOTES"
|
||||||
echo "RC release notes updated from CHANGELOG.md"
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Summary
|
# -- 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="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/release_mirror.php \
|
||||||
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
|
--branch main 2>&1 || true
|
||||||
|
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||||
|
- name: "Step 10: Push main to GitHub mirror"
|
||||||
|
if: >-
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||||
|
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||||
|
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||||
|
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||||
|
git fetch origin main --depth=1
|
||||||
|
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||||
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
|
|| echo "WARNING: GitHub mirror push failed"
|
||||||
|
|
||||||
|
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
# Delete rc branch (ephemeral — created by promote-rc)
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||||
|
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||||
|
|
||||||
|
# Delete dev branch
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||||
|
|
||||||
|
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/branches" \
|
||||||
|
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||||
|
|
||||||
|
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: "Step 12: Create version branch from main"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
|
BRANCH_NAME="version/${VERSION}"
|
||||||
|
MAIN_SHA=$(git rev-parse HEAD)
|
||||||
|
|
||||||
|
# Delete old version branch if it exists (same version re-release)
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||||
|
|
||||||
|
# Create version/XX.YY.ZZ from main
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||||
|
|
||||||
|
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||||
|
- name: "Post-release: Reset dev version"
|
||||||
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/version_reset_dev.php \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
# -- Summary --------------------------------------------------------------
|
||||||
|
- name: Pipeline Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
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](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoStandards.CI
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/Template-Generic
|
|
||||||
# PATH: /.gitea/workflows/ci-generic.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: CI pipeline — lint, validate, and test for generic projects (PHP + Node.js)
|
|
||||||
|
|
||||||
name: "Generic: Project CI"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
- dev/**
|
|
||||||
- rc/**
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# ── Lint & Validate ───────────────────────────────────────────────────
|
|
||||||
lint:
|
|
||||||
name: Lint & Validate
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Detect toolchain
|
|
||||||
id: detect
|
|
||||||
run: |
|
|
||||||
HAS_PHP=false
|
|
||||||
HAS_NODE=false
|
|
||||||
[ -f "composer.json" ] && HAS_PHP=true
|
|
||||||
[ -f "package.json" ] && HAS_NODE=true
|
|
||||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Toolchain: PHP=$HAS_PHP Node=$HAS_NODE"
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
php -v
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install PHP dependencies
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "composer.json" ]; then
|
|
||||||
composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Node.js dependencies
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "package.json" ]; then
|
|
||||||
npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: PHP syntax check
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
ERRORS=0
|
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
|
||||||
echo "::error file=${file}::PHP syntax error"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -not -path "./node_modules/*" -print0)
|
|
||||||
|
|
||||||
echo "## PHP Lint" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$ERRORS" -eq 0 ]; then
|
|
||||||
echo "All PHP files passed syntax check." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "${ERRORS} file(s) with syntax errors." >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: TypeScript/JavaScript lint
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "node_modules/.bin/eslint" ]; then
|
|
||||||
npx eslint src/ --quiet 2>&1 || { echo "::error::ESLint errors found"; exit 1; }
|
|
||||||
echo "## ESLint" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "All files passed ESLint." >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ -f ".eslintrc.json" ] || [ -f ".eslintrc.js" ] || [ -f "eslint.config.js" ]; then
|
|
||||||
echo "::warning::ESLint config found but eslint not installed"
|
|
||||||
else
|
|
||||||
echo "No ESLint configured — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: TypeScript compile check
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "tsconfig.json" ] && [ -f "node_modules/.bin/tsc" ]; then
|
|
||||||
npx tsc --noEmit 2>&1 || { echo "::error::TypeScript compilation errors"; exit 1; }
|
|
||||||
echo "## TypeScript" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "TypeScript compilation passed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: PHPStan static analysis
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "phpstan.neon" ] && [ -f "vendor/bin/phpstan" ]; then
|
|
||||||
vendor/bin/phpstan analyse --no-progress 2>&1 || { echo "::warning::PHPStan found issues"; }
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Tests ─────────────────────────────────────────────────────────────
|
|
||||||
test:
|
|
||||||
name: Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: lint
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Detect toolchain
|
|
||||||
id: detect
|
|
||||||
run: |
|
|
||||||
HAS_PHP=false
|
|
||||||
HAS_NODE=false
|
|
||||||
[ -f "composer.json" ] && HAS_PHP=true
|
|
||||||
[ -f "package.json" ] && HAS_NODE=true
|
|
||||||
echo "has_php=$HAS_PHP" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "has_node=$HAS_NODE" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '20'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
[ -f "composer.json" ] && composer install --no-interaction --prefer-dist --quiet 2>/dev/null || true
|
|
||||||
[ -f "package.json" ] && { npm ci --quiet 2>/dev/null || npm install --quiet 2>/dev/null || true; }
|
|
||||||
|
|
||||||
- name: Run PHP tests
|
|
||||||
if: steps.detect.outputs.has_php == 'true'
|
|
||||||
run: |
|
|
||||||
if [ -f "vendor/bin/phpunit" ]; then
|
|
||||||
vendor/bin/phpunit --testdox 2>&1
|
|
||||||
echo "## PHPUnit" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ -f "phpunit.xml" ] || [ -f "phpunit.xml.dist" ]; then
|
|
||||||
echo "::warning::PHPUnit config found but phpunit not installed"
|
|
||||||
else
|
|
||||||
echo "No PHPUnit configured — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run Node.js tests
|
|
||||||
if: steps.detect.outputs.has_node == 'true'
|
|
||||||
run: |
|
|
||||||
if jq -e '.scripts.test' package.json > /dev/null 2>&1; then
|
|
||||||
npm test 2>&1
|
|
||||||
echo "## Node.js Tests" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Tests passed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "No test script in package.json — skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build check
|
|
||||||
run: |
|
|
||||||
if [ -f "Makefile" ]; then
|
|
||||||
make build 2>&1 || echo "::warning::Build failed or not configured"
|
|
||||||
elif [ -f "package.json" ] && jq -e '.scripts.build' package.json > /dev/null 2>&1; then
|
|
||||||
npm run build 2>&1 || echo "::warning::Build failed"
|
|
||||||
fi
|
|
||||||
@@ -245,413 +245,10 @@ jobs:
|
|||||||
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
|
echo "All ${CHECKED} directories contain index.html." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
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:
|
release-readiness:
|
||||||
name: Release Readiness Check
|
name: Release Readiness Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
# VERSION: 01.02.00
|
# VERSION: 01.00.00
|
||||||
# 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"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ on:
|
|||||||
- "Joomla Build & Release"
|
- "Joomla Build & Release"
|
||||||
- "Joomla Extension CI"
|
- "Joomla Extension CI"
|
||||||
- "Deploy"
|
- "Deploy"
|
||||||
|
- "Cascade Main → Dev"
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokoplatform.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||||
@@ -58,25 +58,25 @@ jobs:
|
|||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||||
|
|
||||||
- name: Setup mokoplatform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
# Use pre-installed /opt/mokoplatform if available (updated by cron every 6h)
|
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||||
if [ -f /opt/mokoplatform/cli/version_bump.php ] && [ -f /opt/mokoplatform/cli/manifest_element.php ] && [ -f /opt/mokoplatform/vendor/autoload.php ]; then
|
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
||||||
echo Using pre-installed /opt/mokoplatform
|
echo Using pre-installed /opt/moko-platform
|
||||||
echo MOKO_CLI=/opt/mokoplatform/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
echo Falling back to fresh clone
|
echo Falling back to fresh clone
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
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
|
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
|
||||||
rm -rf /tmp/mokoplatform-api
|
rm -rf /tmp/moko-platform-api
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/mokoplatform.git
|
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/mokoplatform-api
|
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
||||||
cd /tmp/mokoplatform-api && composer install --no-dev --no-interaction --quiet
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/tmp/mokoplatform-api/cli >> $GITHUB_ENV
|
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoPlatform.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /.mokogitea/workflows/rc-revert.yml
|
|
||||||
# VERSION: 09.23.00
|
|
||||||
# BRIEF: Rename rc/ branch back to dev/ when PR is closed without merge
|
|
||||||
|
|
||||||
name: "RC Revert"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
env:
|
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
revert:
|
|
||||||
name: Rename rc/ back to dev/
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.merged == false &&
|
|
||||||
startsWith(github.event.pull_request.head.ref, 'rc/')
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Rename branch
|
|
||||||
run: |
|
|
||||||
BRANCH="${{ github.event.pull_request.head.ref }}"
|
|
||||||
SUFFIX="${BRANCH#rc/}"
|
|
||||||
DEV_BRANCH="dev/${SUFFIX}"
|
|
||||||
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Delete rc/ branch
|
|
||||||
ENCODED=$(php -r "echo rawurlencode('${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,67 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: MokoPlatform.Universal
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokoplatform
|
|
||||||
# PATH: /.mokogitea/workflows/workflow-sync-trigger.yml
|
|
||||||
# VERSION: 01.00.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: Run workflow sync
|
|
||||||
env:
|
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
run: |
|
|
||||||
MOKOPLATFORM="/opt/mokoplatform"
|
|
||||||
if [ ! -f "${MOKOPLATFORM}/cli/workflow_sync.php" ]; then
|
|
||||||
echo "::error::mokoplatform not found at ${MOKOPLATFORM}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
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 "${MOKOPLATFORM}/cli/workflow_sync.php" ${ARGS}
|
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
## [01.00.00] --- 2026-06-03
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
|
- Migrated all workflow and template paths from `.github/` to `.mokogitea/`
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
DEFGROUP: MokoStandards-Template-Joomla-Plugin
|
DEFGROUP: MokoStandards-Template-Joomla-Plugin
|
||||||
INGROUP: MokoStandards-Template-Joomla-Plugin.Documentation
|
INGROUP: MokoStandards-Template-Joomla-Plugin.Documentation
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Joomla-Plugin/
|
REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Joomla-Plugin/
|
||||||
VERSION: 01.02.00
|
VERSION: 01.00.00
|
||||||
PATH: ./CODE_OF_CONDUCT.md
|
PATH: ./CODE_OF_CONDUCT.md
|
||||||
BRIEF: Community expectations and enforcement guidelines
|
BRIEF: Community expectations and enforcement guidelines
|
||||||
NOTE: Adapted with attribution from the Contributor Covenant v2.1
|
NOTE: Adapted with attribution from the Contributor Covenant v2.1
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
DEFGROUP: mokoconsulting-tech.MokoStandards-Template-Joomla-Plugin
|
DEFGROUP: mokoconsulting-tech.MokoStandards-Template-Joomla-Plugin
|
||||||
INGROUP: MokoStandards.Governance
|
INGROUP: MokoStandards.Governance
|
||||||
REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Joomla-Plugin
|
REPO: https://github.com/mokoconsulting-tech/MokoStandards-Template-Joomla-Plugin
|
||||||
VERSION: 01.02.00
|
VERSION: 04.00.04
|
||||||
PATH: /GOVERNANCE.md
|
PATH: /GOVERNANCE.md
|
||||||
BRIEF: Project governance rules, roles, and decision process for MokoStandards-Template-Joomla-Plugin
|
BRIEF: Project governance rules, roles, and decision process for MokoStandards-Template-Joomla-Plugin
|
||||||
-->
|
-->
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: MokoStandards-Template-Joomla-Plugin
|
|||||||
INGROUP: MokoStandards-Template-Joomla-Plugin.Documentation
|
INGROUP: MokoStandards-Template-Joomla-Plugin.Documentation
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Joomla-Plugin
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-Template-Joomla-Plugin
|
||||||
PATH: /SECURITY.md
|
PATH: /SECURITY.md
|
||||||
VERSION: 01.02.00
|
VERSION: 01.00.00
|
||||||
BRIEF: Security vulnerability reporting and handling policy
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user