Public Access
Merge main into dev (xmlns fix)
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
MokoStandards Repository Manifest
|
||||
Auto-generated by MokoStandards bulk sync.
|
||||
Manual edits to <governance> and <last-synced> may be overwritten.
|
||||
See: docs/standards/mokostandards-file-spec.md
|
||||
-->
|
||||
<mokostandards xmlns="https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API" schema-version="1.0">
|
||||
<identity>
|
||||
<name>MokoStandards-API</name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>MokoStandards Enterprise API — PHP implementation (Composer package: mokoconsulting-tech/enterprise)</description>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
<governance>
|
||||
<platform>standards-repository</platform>
|
||||
<standards-version>04.07.00</standards-version>
|
||||
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/MokoStandards</standards-source>
|
||||
<last-synced>2026-05-02T23:05:55+00:00</last-synced>
|
||||
</governance>
|
||||
<build>
|
||||
<language>HCL</language>
|
||||
<runtime>php:>=8.1</runtime>
|
||||
<package-type>composer</package-type>
|
||||
</build>
|
||||
<scripts>
|
||||
<script name="test" phase="test">
|
||||
<command>composer run test</command>
|
||||
<description>phpunit</description>
|
||||
<runner>composer</runner>
|
||||
</script>
|
||||
<script name="phpcs" phase="lint">
|
||||
<command>composer run phpcs</command>
|
||||
<description>phpcs --standard=phpcs.xml lib/ validate/ automation/</description>
|
||||
<runner>composer</runner>
|
||||
</script>
|
||||
<script name="phpstan" phase="lint">
|
||||
<command>composer run phpstan</command>
|
||||
<description>phpstan analyse -c phpstan.neon lib/ validate/ automation/</description>
|
||||
<runner>composer</runner>
|
||||
</script>
|
||||
<script name="validate" phase="validate">
|
||||
<command>composer run validate</command>
|
||||
<description>php bin/moko health -- --path .</description>
|
||||
<runner>composer</runner>
|
||||
</script>
|
||||
</scripts>
|
||||
</mokostandards>
|
||||
@@ -0,0 +1,136 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# FILE INFORMATION
|
||||
# DEFGROUP: Gitea.Workflow
|
||||
# INGROUP: MokoStandards-API.Automation
|
||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
# PATH: /.gitea/workflows/bulk-repo-sync.yml
|
||||
# VERSION: 04.06.12
|
||||
# BRIEF: Bulk repo sync — runs from API repo, syncs standards to all governed repos
|
||||
|
||||
name: Bulk Repository Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 1 * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Preview mode (no changes)'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
repos:
|
||||
description: 'Comma-separated repo names (empty = all)'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
exclude:
|
||||
description: 'Comma-separated repos to skip'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
force:
|
||||
description: 'Force overwrite protected files'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
bulk-sync:
|
||||
name: Sync Standards to Repositories
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.1'
|
||||
extensions: json, mbstring, curl
|
||||
tools: composer
|
||||
coverage: none
|
||||
|
||||
- name: Install Dependencies
|
||||
run: composer install --no-dev --no-interaction --prefer-dist --optimize-autoloader
|
||||
|
||||
- name: Build CLI Arguments
|
||||
id: args
|
||||
run: |
|
||||
ARGS="--org MokoConsulting"
|
||||
if [ "${{ inputs.dry_run }}" = "true" ] || [ "${{ gitea.event_name }}" = "schedule" ]; then
|
||||
ARGS="$ARGS --dry-run"
|
||||
fi
|
||||
if [ -n "${{ inputs.repos }}" ]; then
|
||||
ARGS="$ARGS --repos ${{ inputs.repos }}"
|
||||
fi
|
||||
if [ -n "${{ inputs.exclude }}" ]; then
|
||||
ARGS="$ARGS --exclude ${{ inputs.exclude }}"
|
||||
fi
|
||||
if [ "${{ inputs.force }}" = "true" ]; then
|
||||
ARGS="$ARGS --force"
|
||||
fi
|
||||
ARGS="$ARGS --yes"
|
||||
echo "args=$ARGS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run Bulk Sync
|
||||
run: |
|
||||
echo "Running: php automation/bulk_sync.php ${{ steps.args.outputs.args }}"
|
||||
php automation/bulk_sync.php ${{ steps.args.outputs.args }} 2>&1 | tee /tmp/bulk_sync.log
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GIT_PLATFORM: gitea
|
||||
GITEA_URL: https://git.mokoconsulting.tech
|
||||
GITEA_ORG: MokoConsulting
|
||||
|
||||
- name: Commit Updated Definitions
|
||||
if: success() && inputs.dry_run != 'true'
|
||||
run: |
|
||||
if [ -n "$(git status --porcelain definitions/sync/)" ]; then
|
||||
git config user.name "gitea-actions[bot]"
|
||||
git config user.email "gitea-actions[bot]@git.mokoconsulting.tech"
|
||||
git add definitions/sync/*.def.tf
|
||||
git commit -m "chore: update synced repository definitions" || true
|
||||
git push || true
|
||||
fi
|
||||
|
||||
- name: Enforce Release Channel Tags
|
||||
if: success()
|
||||
continue-on-error: true
|
||||
run: |
|
||||
echo "Enforcing standard tags on all repos..."
|
||||
if [ "${{ inputs.dry_run }}" = "true" ]; then
|
||||
bash automation/enforce_tags.sh --dry-run || echo "Tag enforcement skipped (non-fatal)"
|
||||
else
|
||||
bash automation/enforce_tags.sh || echo "Tag enforcement had errors (non-fatal)"
|
||||
fi
|
||||
env:
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
GITEA_URL: https://git.mokoconsulting.tech
|
||||
GITEA_ORG: MokoConsulting
|
||||
|
||||
- name: Upload Sync Log
|
||||
if: always() && github.server_url == 'https://github.com'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bulk-sync-log-${{ github.run_number }}
|
||||
path: /tmp/bulk_sync.log
|
||||
retention-days: 30
|
||||
|
||||
- name: Log Summary (Gitea)
|
||||
if: always() && github.server_url != 'https://github.com'
|
||||
run: |
|
||||
if [ -f /tmp/bulk_sync.log ]; then
|
||||
echo "## Sync Log (last 20 lines)" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
tail -20 /tmp/bulk_sync.log >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
@@ -1062,3 +1062,5 @@ terraform.rc
|
||||
# but can be ignored if you want flexibility across different platforms
|
||||
# !.terraform.lock.hcl
|
||||
logs/validation/*.md
|
||||
profile.ps1
|
||||
.mcp.json
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
* BRIEF: Bulk scaffold and sync Joomla template repositories
|
||||
*
|
||||
* USAGE
|
||||
* php api/automation/bulk_joomla_template.php --scaffold --name=MokoTheme
|
||||
* php api/automation/bulk_joomla_template.php --scaffold --name=MokoTheme --client=administrator
|
||||
* php api/automation/bulk_joomla_template.php --sync --repos=MokoTheme,MokoDarkTheme
|
||||
* php api/automation/bulk_joomla_template.php --sync --all
|
||||
* php api/automation/bulk_joomla_template.php --list
|
||||
* php automation/bulk_joomla_template.php --scaffold --name=MokoTheme
|
||||
* php automation/bulk_joomla_template.php --scaffold --name=MokoTheme --client=administrator
|
||||
* php automation/bulk_joomla_template.php --sync --repos=MokoTheme,MokoDarkTheme
|
||||
* php automation/bulk_joomla_template.php --sync --all
|
||||
* php automation/bulk_joomla_template.php --list
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -717,13 +717,13 @@ class BulkJoomlaTemplate extends CLIApp
|
||||
// ── Sync updates.xml between platforms ───────────────────────────────
|
||||
|
||||
/**
|
||||
* Sync updates.xml (or update.xml) between Gitea and GitHub for Joomla repos.
|
||||
* Sync updates.xml (or updates.xml) between Gitea and GitHub for Joomla repos.
|
||||
*
|
||||
* Reads the file from both platforms, compares by latest <version> tag,
|
||||
* and pushes the newer one to the stale platform.
|
||||
*
|
||||
* Designed to be called from a CI workflow via:
|
||||
* php api/automation/bulk_joomla_template.php --sync-updates --repos=MokoCassiopeia
|
||||
* php automation/bulk_joomla_template.php --sync-updates --repos=MokoCassiopeia
|
||||
*/
|
||||
private function syncUpdatesBetweenPlatforms(string $org): int
|
||||
{
|
||||
@@ -788,7 +788,7 @@ class BulkJoomlaTemplate extends CLIApp
|
||||
$name = $repo['name'];
|
||||
$this->log("\n[{$name}]", 'INFO');
|
||||
|
||||
// Try both update.xml and updates.xml filenames
|
||||
// Try both updates.xml and updates.xml filenames
|
||||
$updateFile = $this->resolveUpdateFile($gitea, $github, $org, $name);
|
||||
if ($updateFile === null) {
|
||||
$this->log(" ⊘ No update(s).xml found on either platform", 'INFO');
|
||||
@@ -849,7 +849,7 @@ class BulkJoomlaTemplate extends CLIApp
|
||||
/**
|
||||
* Find the updates file on both platforms, return the one with the higher version.
|
||||
*
|
||||
* Checks both `updates.xml` and `update.xml` filenames.
|
||||
* Checks both `updates.xml` and `updates.xml` filenames.
|
||||
* Returns the content from the platform with the newer <version>.
|
||||
* Gitea wins ties (primary platform).
|
||||
*
|
||||
@@ -861,7 +861,7 @@ class BulkJoomlaTemplate extends CLIApp
|
||||
string $org,
|
||||
string $name
|
||||
): ?array {
|
||||
$candidates = ['updates.xml', 'update.xml'];
|
||||
$candidates = ['updates.xml', 'updates.xml'];
|
||||
$found = []; // platform => [name, content, version]
|
||||
|
||||
foreach (['gitea' => $gitea, 'github' => $github] as $platform => $adapter) {
|
||||
|
||||
+92
-13
@@ -379,6 +379,7 @@ class BulkSync extends CLIApp
|
||||
// (e.g. 54 new labels on a fresh repo), reset it so file sync proceeds.
|
||||
if (!$this->dryRun) {
|
||||
$this->ensureRepoLabels($org, $repoName);
|
||||
$this->ensureReleaseTags($org, $repoName);
|
||||
$this->api->resetCircuitBreaker();
|
||||
}
|
||||
|
||||
@@ -419,7 +420,7 @@ class BulkSync extends CLIApp
|
||||
$this->log("The bulk repository sync is failing silently because the core", 'ERROR');
|
||||
$this->log("synchronization logic has not been implemented yet.", 'ERROR');
|
||||
$this->log("", 'ERROR');
|
||||
$this->log("Location: api/lib/Enterprise/RepositorySynchronizer.php", 'ERROR');
|
||||
$this->log("Location: lib/Enterprise/RepositorySynchronizer.php", 'ERROR');
|
||||
$this->log("Method: processRepository()", 'ERROR');
|
||||
$this->log("", 'ERROR');
|
||||
$this->log("Required Implementation:", 'ERROR');
|
||||
@@ -508,7 +509,7 @@ class BulkSync extends CLIApp
|
||||
]);
|
||||
$script = basename(__FILE__);
|
||||
$this->log("💾 Checkpoint saved. To resume once the issue is resolved, run:", 'INFO');
|
||||
$this->log(" php api/automation/{$script} --resume [same flags as before]", 'INFO');
|
||||
$this->log(" php automation/{$script} --resume [same flags as before]", 'INFO');
|
||||
} catch (\Exception $e) {
|
||||
$this->log("⚠️ Failed to save interrupt checkpoint: " . $e->getMessage(), 'WARN');
|
||||
}
|
||||
@@ -958,8 +959,51 @@ class BulkSync extends CLIApp
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a tracking issue in the target repository after a successful sync.
|
||||
* Ensure standard release tags exist on the repository.
|
||||
*
|
||||
* Creates 'development', 'beta', and 'release-candidate' tags pointing
|
||||
* to the default branch HEAD if they don't already exist. These tags
|
||||
* are used by the release workflow to track stability channels.
|
||||
*/
|
||||
private function ensureReleaseTags(string $org, string $repo): void
|
||||
{
|
||||
$requiredTags = ['development', 'beta', 'release-candidate'];
|
||||
|
||||
try {
|
||||
$existingTags = $this->api->get("/repos/{$org}/{$repo}/tags", ['limit' => 50]);
|
||||
} catch (\Exception $e) {
|
||||
return; // Non-critical
|
||||
}
|
||||
|
||||
$existingNames = array_column($existingTags, 'name');
|
||||
|
||||
// Get default branch to point new tags at
|
||||
try {
|
||||
$repoInfo = $this->api->get("/repos/{$org}/{$repo}");
|
||||
$defaultBranch = $repoInfo['default_branch'] ?? 'main';
|
||||
} catch (\Exception $e) {
|
||||
$defaultBranch = 'main';
|
||||
}
|
||||
|
||||
foreach ($requiredTags as $tagName) {
|
||||
if (in_array($tagName, $existingNames, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->api->post("/repos/{$org}/{$repo}/tags", [
|
||||
'tag_name' => $tagName,
|
||||
'target' => $defaultBranch,
|
||||
'message' => "Release channel: {$tagName}",
|
||||
]);
|
||||
$this->log(" 🏷️ Created tag '{$tagName}' on {$repo}", 'INFO');
|
||||
} catch (\Exception $e) {
|
||||
// Non-critical — tag may already exist as a release tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge main into all open PR branches (except the sync branch itself).
|
||||
*
|
||||
* This ensures feature/development branches stay up to date with the
|
||||
@@ -1020,6 +1064,39 @@ class BulkSync extends CLIApp
|
||||
* MokoStandards version that was applied — giving each repo a clear audit
|
||||
* trail of what was changed and why.
|
||||
*/
|
||||
/**
|
||||
* Resolve label names to their integer IDs for the Gitea API.
|
||||
* Creates missing labels automatically.
|
||||
*
|
||||
* @param string $org Organization name
|
||||
* @param string $repo Repository name
|
||||
* @param string[] $labelNames Label names to resolve
|
||||
* @return int[] Array of label IDs
|
||||
*/
|
||||
private function resolveLabelIds(string $org, string $repo, array $labelNames): array
|
||||
{
|
||||
try {
|
||||
$existing = $this->api->get("/repos/{$org}/{$repo}/labels", ['limit' => 50]);
|
||||
} catch (\Exception $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$nameToId = [];
|
||||
foreach ($existing as $label) {
|
||||
$nameToId[$label['name']] = (int) $label['id'];
|
||||
}
|
||||
|
||||
$ids = [];
|
||||
foreach ($labelNames as $name) {
|
||||
if (isset($nameToId[$name])) {
|
||||
$ids[] = $nameToId[$name];
|
||||
}
|
||||
// Skip labels that don't exist (ensureRepoLabels creates them separately)
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
|
||||
private function createTargetRepoIssue(string $org, string $repo, int $prNumber): ?int
|
||||
{
|
||||
$now = gmdate('Y-m-d H:i:s') . ' UTC';
|
||||
@@ -1058,7 +1135,8 @@ class BulkSync extends CLIApp
|
||||
// Dedent heredoc
|
||||
$body = preg_replace('/^ /m', '', $body);
|
||||
|
||||
$labels = ['standards-update', 'mokostandards', 'type: chore', 'automation'];
|
||||
$labelNames = ['standards-update', 'mokostandards', 'type: chore', 'automation'];
|
||||
$labels = $this->resolveLabelIds($org, $repo, $labelNames);
|
||||
|
||||
try {
|
||||
// Check for an existing tracking issue (any state so we can reopen closed ones)
|
||||
@@ -1072,7 +1150,7 @@ class BulkSync extends CLIApp
|
||||
|
||||
if (!empty($existing) && isset($existing[0]['number'])) {
|
||||
$num = $existing[0]['number'];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller-moko']];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
|
||||
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
||||
$patch['state'] = 'open';
|
||||
}
|
||||
@@ -1087,7 +1165,7 @@ class BulkSync extends CLIApp
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'labels' => $labels,
|
||||
'assignees' => ['jmiller-moko'],
|
||||
'assignees' => ['jmiller'],
|
||||
]);
|
||||
$num = $issue['number'] ?? '?';
|
||||
$this->log(" 📋 Tracking issue #{$num} created in {$repo}", 'INFO');
|
||||
@@ -1211,11 +1289,12 @@ class BulkSync extends CLIApp
|
||||
'direction'=> 'desc',
|
||||
]);
|
||||
|
||||
$labels = ['sync-report', 'mokostandards', 'type: chore', 'automation'];
|
||||
$labelNames = ['sync-report', 'mokostandards', 'type: chore', 'automation'];
|
||||
$labels = $this->resolveLabelIds($org, 'MokoStandards', $labelNames);
|
||||
|
||||
if (!empty($existing) && isset($existing[0]['number'])) {
|
||||
$issueNumber = $existing[0]['number'];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller-moko']];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
|
||||
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
||||
$patch['state'] = 'open';
|
||||
}
|
||||
@@ -1229,7 +1308,7 @@ class BulkSync extends CLIApp
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'labels' => $labels,
|
||||
'assignees' => ['jmiller-moko'],
|
||||
'assignees' => ['jmiller'],
|
||||
]);
|
||||
$issueNumber = $issue['number'] ?? '?';
|
||||
$this->log("📋 Sync report issue created: {$org}/MokoStandards#{$issueNumber}", 'INFO');
|
||||
@@ -1276,7 +1355,7 @@ class BulkSync extends CLIApp
|
||||
|
||||
1. Check the local audit log or re-run with `--repos=<repo>` to see the specific error.
|
||||
2. Fix the underlying issue (API token, rate limit, branch protection, etc.).
|
||||
3. Re-run: `php api/automation/bulk_sync.php --org={$org} --repos=<repo> --force --yes`
|
||||
3. Re-run: `php automation/bulk_sync.php --org={$org} --repos=<repo> --force --yes`
|
||||
4. Close this issue once all repos are synced successfully.
|
||||
|
||||
---
|
||||
@@ -1296,7 +1375,7 @@ class BulkSync extends CLIApp
|
||||
|
||||
if (!empty($existing) && isset($existing[0]['number'])) {
|
||||
$num = $existing[0]['number'];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller-moko']];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
|
||||
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
||||
$patch['state'] = 'open';
|
||||
}
|
||||
@@ -1306,8 +1385,8 @@ class BulkSync extends CLIApp
|
||||
$issue = $this->api->post("/repos/{$org}/MokoStandards/issues", [
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'labels' => ['sync-failure'],
|
||||
'assignees' => ['jmiller-moko'],
|
||||
'labels' => $this->resolveLabelIds($org, 'MokoStandards', ['sync-failure']),
|
||||
'assignees' => ['jmiller'],
|
||||
]);
|
||||
$num = $issue['number'] ?? '?';
|
||||
$this->log("🚨 Failure issue created: {$org}/MokoStandards#{$num}", 'WARN');
|
||||
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# enforce_tags.sh — Ensure all repos have the 5 standard release channel tags
|
||||
#
|
||||
# Standard tags: development, alpha, beta, release-candidate, stable
|
||||
# Also removes non-standard tags (keeps vXX production tags)
|
||||
#
|
||||
# Usage:
|
||||
# GA_TOKEN=xxx ./enforce_tags.sh [--dry-run] [--repos repo1,repo2]
|
||||
#
|
||||
# Called by: bulk-repo-sync.yml, infrastructure-tests/mirror-check.yml
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||
ORG="${GITEA_ORG:-MokoConsulting}"
|
||||
TOKEN="${GA_TOKEN:?GA_TOKEN required}"
|
||||
DRY_RUN=false
|
||||
FILTER_REPOS=""
|
||||
|
||||
STANDARD_TAGS=("development" "alpha" "beta" "release-candidate" "stable")
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--repos) FILTER_REPOS="$2"; shift 2 ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
api() {
|
||||
local method="$1" path="$2" data="${3:-}"
|
||||
local args=(-sf -H "Authorization: token $TOKEN" -H "Content-Type: application/json" -X "$method")
|
||||
[[ -n "$data" ]] && args+=(-d "$data")
|
||||
curl "${args[@]}" "$GITEA_URL/api/v1$path" 2>/dev/null
|
||||
}
|
||||
|
||||
# Get repos
|
||||
REPOS=""
|
||||
for page in 1 2 3; do
|
||||
BATCH=$(api GET "/orgs/$ORG/repos?limit=50&page=$page" | python3 -c "
|
||||
import sys,json
|
||||
for r in json.load(sys.stdin):
|
||||
if not r.get(empty) and not r.get(archived):
|
||||
print(r[name])
|
||||
" 2>/dev/null)
|
||||
[[ -z "$BATCH" ]] && break
|
||||
REPOS="$REPOS $BATCH"
|
||||
done
|
||||
|
||||
# Filter if specified
|
||||
if [[ -n "$FILTER_REPOS" ]]; then
|
||||
FILTERED=""
|
||||
IFS=, read -ra FILTER_ARR <<< "$FILTER_REPOS"
|
||||
for repo in $REPOS; do
|
||||
for f in "${FILTER_ARR[@]}"; do
|
||||
[[ "$repo" == "$f" ]] && FILTERED="$FILTERED $repo"
|
||||
done
|
||||
done
|
||||
REPOS="$FILTERED"
|
||||
fi
|
||||
|
||||
TOTAL=$(echo $REPOS | wc -w)
|
||||
ADDED=0
|
||||
DELETED=0
|
||||
ERRORS=0
|
||||
|
||||
echo "Enforcing tags on $TOTAL repos (dry_run=$DRY_RUN)"
|
||||
|
||||
for repo in $REPOS; do
|
||||
TAGS=$(api GET "/repos/$ORG/$repo/tags?limit=50" | python3 -c "import sys,json; print( .join(t[name] for t in json.load(sys.stdin)))" 2>/dev/null)
|
||||
MAIN_SHA=$(api GET "/repos/$ORG/$repo/branches/main" | python3 -c "import sys,json; print(json.load(sys.stdin)[commit][id])" 2>/dev/null)
|
||||
[[ -z "$MAIN_SHA" ]] && continue
|
||||
|
||||
# Add missing standard tags
|
||||
for st in "${STANDARD_TAGS[@]}"; do
|
||||
if ! echo " $TAGS " | grep -q " $st "; then
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo " [DRY] ADD $repo: $st"
|
||||
else
|
||||
STATUS=$(api POST "/repos/$ORG/$repo/tags" "{\"tag_name\":\"$st\",\"target\":\"$MAIN_SHA\"}" | python3 -c "import sys,json; print(ok)" 2>/dev/null || echo "err")
|
||||
[[ "$STATUS" == "ok" ]] && ADDED=$((ADDED + 1)) || ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Remove non-standard tags
|
||||
for t in $TAGS; do
|
||||
IS_STD=false
|
||||
for st in "${STANDARD_TAGS[@]}"; do [[ "$t" == "$st" ]] && IS_STD=true; done
|
||||
# Keep vXX production tags
|
||||
if [[ "$t" =~ ^v[0-9]{1,3}$ ]]; then IS_STD=true; fi
|
||||
|
||||
if [[ "$IS_STD" == "false" ]]; then
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo " [DRY] DEL $repo: $t"
|
||||
else
|
||||
# Delete release first if exists
|
||||
api DELETE "/repos/$ORG/$repo/releases/tags/$t" > /dev/null 2>&1 || true
|
||||
api DELETE "/repos/$ORG/$repo/tags/$t" > /dev/null 2>&1
|
||||
DELETED=$((DELETED + 1))
|
||||
echo " DEL $repo: $t"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo "Done: $ADDED added, $DELETED deleted, $ERRORS errors (dry_run=$DRY_RUN)"
|
||||
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Enrich XML .mokostandards manifests with repo-specific build, deploy, and script details.
|
||||
*
|
||||
* Runs AFTER push_mokostandards_xml.php. Clones each repo, inspects its contents,
|
||||
* and updates the manifest with discovered build/deploy/scripts config.
|
||||
*
|
||||
* Usage:
|
||||
* php automation/enrich_mokostandards_xml.php [--dry-run] [--repo NAME] [--skip NAME,NAME]
|
||||
*
|
||||
* Note: This script uses proc_open for shell commands. All arguments are escaped
|
||||
* via escapeshellarg(). No user-supplied input reaches the shell unescaped.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoEnterprise\MokoStandardsParser;
|
||||
|
||||
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
|
||||
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
|
||||
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
|
||||
|
||||
$dryRun = in_array('--dry-run', $argv, true);
|
||||
$repoFilter = null;
|
||||
$skipRepos = [];
|
||||
foreach ($argv as $i => $arg) {
|
||||
if ($arg === '--repo' && isset($argv[$i + 1])) $repoFilter = $argv[$i + 1];
|
||||
if ($arg === '--skip' && isset($argv[$i + 1])) $skipRepos = array_map('trim', explode(',', $argv[$i + 1]));
|
||||
}
|
||||
|
||||
$parser = new MokoStandardsParser();
|
||||
$tmpBase = sys_get_temp_dir() . '/moko-enrich-' . getmypid();
|
||||
|
||||
function safeExec(string $command, string $cwd = '.'): array {
|
||||
$proc = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $cwd);
|
||||
if (!is_resource($proc)) return [1, "proc_open failed"];
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
$stderr = stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]); fclose($pipes[2]);
|
||||
return [proc_close($proc), trim($stdout . "\n" . $stderr)];
|
||||
}
|
||||
|
||||
function rmTree(string $dir): void {
|
||||
if (!is_dir($dir)) return;
|
||||
$it = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
|
||||
$files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
|
||||
foreach ($files as $file) {
|
||||
if ($file->isDir()) @rmdir($file->getPathname());
|
||||
else { @chmod($file->getPathname(), 0777); @unlink($file->getPathname()); }
|
||||
}
|
||||
@rmdir($dir);
|
||||
}
|
||||
|
||||
function gitCmd(string $workDir, string ...$args): array {
|
||||
$cmd = 'git';
|
||||
foreach ($args as $a) $cmd .= ' ' . escapeshellarg($a);
|
||||
return safeExec($cmd, $workDir);
|
||||
}
|
||||
|
||||
function fetchRepos(string $url, string $org, string $token): array {
|
||||
$repos = []; $page = 1;
|
||||
do {
|
||||
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
|
||||
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: token {$token}"], CURLOPT_TIMEOUT => 30]);
|
||||
$body = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch);
|
||||
if ($code !== 200) break;
|
||||
$batch = json_decode($body, true); if (empty($batch)) break;
|
||||
$repos = array_merge($repos, $batch); $page++;
|
||||
} while (count($batch) >= 50);
|
||||
return $repos;
|
||||
}
|
||||
|
||||
function inspectRepo(string $workDir, string $platform): array {
|
||||
$enrichment = [];
|
||||
$build = [];
|
||||
|
||||
// Detect entry point
|
||||
if (is_dir("{$workDir}/src")) {
|
||||
foreach (glob("{$workDir}/src/*.xml") ?: [] as $xf) {
|
||||
$c = file_get_contents($xf);
|
||||
if (str_contains($c, '<extension') || str_contains($c, '<install')) {
|
||||
$build['entry_point'] = 'src/' . basename($xf); break;
|
||||
}
|
||||
}
|
||||
foreach (glob("{$workDir}/src/core/modules/mod*.class.php") ?: [] as $mf) {
|
||||
$build['entry_point'] = str_replace("{$workDir}/", '', $mf); break;
|
||||
}
|
||||
}
|
||||
|
||||
// composer.json
|
||||
if (file_exists("{$workDir}/composer.json")) {
|
||||
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
|
||||
$phpReq = $composer['require']['php'] ?? null;
|
||||
if ($phpReq) $build['runtime'] = "php:{$phpReq}";
|
||||
|
||||
$deps = [];
|
||||
foreach (['joomla/cms', 'joomla/framework', 'dolibarr/dolibarr'] as $pd) {
|
||||
if (isset($composer['require'][$pd])) $deps[] = ['name' => $pd, 'version' => $composer['require'][$pd], 'type' => 'platform'];
|
||||
}
|
||||
if (isset($composer['require']['mokoconsulting-tech/enterprise']))
|
||||
$deps[] = ['name' => 'mokoconsulting-tech/enterprise', 'version' => $composer['require']['mokoconsulting-tech/enterprise'], 'type' => 'composer'];
|
||||
if (!empty($deps)) $build['dependencies'] = $deps;
|
||||
}
|
||||
|
||||
// Artifact from Makefile
|
||||
if (file_exists("{$workDir}/Makefile")) {
|
||||
$mk = file_get_contents("{$workDir}/Makefile");
|
||||
if (preg_match('/\bdist\/(\S+\.zip)\b/', $mk, $m)) $build['artifact'] = ['format' => 'zip', 'path' => 'dist/', 'filename' => $m[1]];
|
||||
}
|
||||
|
||||
if (!empty($build)) $enrichment['build'] = $build;
|
||||
|
||||
// Deploy targets from workflows
|
||||
$targets = [];
|
||||
$wfDir = is_dir("{$workDir}/.gitea/workflows") ? "{$workDir}/.gitea/workflows" : "{$workDir}/.github/workflows";
|
||||
if (is_dir($wfDir)) {
|
||||
foreach (['deploy-dev', 'deploy-demo', 'deploy-rs'] as $dn) {
|
||||
$wf = "{$wfDir}/{$dn}.yml";
|
||||
if (!file_exists($wf)) continue;
|
||||
$wc = file_get_contents($wf);
|
||||
$t = ['name' => str_replace('deploy-', '', $dn)];
|
||||
if (str_contains($wc, 'sftp') || str_contains($wc, 'SFTP')) $t['method'] = 'sftp';
|
||||
elseif (str_contains($wc, 'rsync')) $t['method'] = 'rsync';
|
||||
if (str_contains($wc, 'src/')) $t['src_dir'] = 'src/';
|
||||
if (preg_match('/branches:\s*\n\s*-\s*["\']?([^"\'}\s]+)/', $wc, $m)) $t['branch'] = $m[1];
|
||||
$targets[] = $t;
|
||||
}
|
||||
}
|
||||
if (!empty($targets)) $enrichment['deploy'] = $targets;
|
||||
|
||||
// Scripts from Makefile + composer
|
||||
$scripts = [];
|
||||
if (file_exists("{$workDir}/Makefile")) {
|
||||
$mk = file_get_contents("{$workDir}/Makefile");
|
||||
$known = ['build'=>'build','test'=>'test','lint'=>'lint','clean'=>'build','package'=>'build','validate'=>'validate','release'=>'release'];
|
||||
if (preg_match_all('/^([a-zA-Z_-]+)\s*:/m', $mk, $matches)) {
|
||||
foreach ($matches[1] as $tgt) {
|
||||
$tl = strtolower($tgt);
|
||||
if (isset($known[$tl])) $scripts[] = ['name'=>$tl, 'phase'=>$known[$tl], 'command'=>"make {$tgt}", 'desc'=>ucfirst($tl).' via make', 'runner'=>'make'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (file_exists("{$workDir}/composer.json")) {
|
||||
$composer = json_decode(file_get_contents("{$workDir}/composer.json"), true) ?: [];
|
||||
$km = ['test'=>'test','lint'=>'lint','cs'=>'lint','phpcs'=>'lint','phpstan'=>'lint','validate'=>'validate'];
|
||||
foreach ($composer['scripts'] ?? [] as $sn => $cmd) {
|
||||
$sl = strtolower($sn);
|
||||
foreach ($km as $match => $phase) {
|
||||
if (str_contains($sl, $match)) {
|
||||
$exists = false;
|
||||
foreach ($scripts as $s) { if ($s['name'] === $sl) { $exists = true; break; } }
|
||||
if (!$exists) $scripts[] = ['name'=>$sn, 'phase'=>$phase, 'command'=>"composer run {$sn}", 'desc'=>is_string($cmd)?$cmd:"Run {$sn}", 'runner'=>'composer'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($scripts)) $enrichment['scripts'] = $scripts;
|
||||
|
||||
return $enrichment;
|
||||
}
|
||||
|
||||
function enrichManifestXml(string $xml, array $enrichment): string {
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$dom->formatOutput = true;
|
||||
if (!$dom->loadXML($xml)) return $xml;
|
||||
|
||||
$ns = MokoStandardsParser::NAMESPACE_URI;
|
||||
$root = $dom->documentElement;
|
||||
|
||||
foreach (['build', 'deploy', 'scripts'] as $tag) {
|
||||
$toRemove = [];
|
||||
$existing = $root->getElementsByTagNameNS($ns, $tag);
|
||||
for ($i = 0; $i < $existing->length; $i++) $toRemove[] = $existing->item($i);
|
||||
foreach ($toRemove as $node) $root->removeChild($node);
|
||||
}
|
||||
|
||||
if (!empty($enrichment['build'])) {
|
||||
$build = $dom->createElementNS($ns, 'build');
|
||||
$b = $enrichment['build'];
|
||||
foreach (['language', 'runtime'] as $f) { if (isset($b[$f])) $build->appendChild($dom->createElementNS($ns, $f, htmlspecialchars($b[$f], ENT_XML1))); }
|
||||
if (isset($b['package_type'])) $build->appendChild($dom->createElementNS($ns, 'package-type', htmlspecialchars($b['package_type'], ENT_XML1)));
|
||||
if (isset($b['entry_point'])) $build->appendChild($dom->createElementNS($ns, 'entry-point', htmlspecialchars($b['entry_point'], ENT_XML1)));
|
||||
if (isset($b['artifact'])) {
|
||||
$art = $dom->createElementNS($ns, 'artifact');
|
||||
foreach (['format','path','filename'] as $af) { if (isset($b['artifact'][$af])) $art->appendChild($dom->createElementNS($ns, $af, htmlspecialchars($b['artifact'][$af], ENT_XML1))); }
|
||||
$build->appendChild($art);
|
||||
}
|
||||
if (isset($b['dependencies'])) {
|
||||
$deps = $dom->createElementNS($ns, 'dependencies');
|
||||
foreach ($b['dependencies'] as $d) {
|
||||
$req = $dom->createElementNS($ns, 'requires', '');
|
||||
$req->setAttribute('name', $d['name']);
|
||||
if (isset($d['version'])) $req->setAttribute('version', $d['version']);
|
||||
if (isset($d['type'])) $req->setAttribute('type', $d['type']);
|
||||
$deps->appendChild($req);
|
||||
}
|
||||
$build->appendChild($deps);
|
||||
}
|
||||
$root->appendChild($build);
|
||||
}
|
||||
|
||||
if (!empty($enrichment['deploy'])) {
|
||||
$deploy = $dom->createElementNS($ns, 'deploy');
|
||||
foreach ($enrichment['deploy'] as $t) {
|
||||
$target = $dom->createElementNS($ns, 'target');
|
||||
$target->setAttribute('name', $t['name']);
|
||||
$target->appendChild($dom->createElementNS($ns, 'host', '${{ secrets.' . strtoupper($t['name']) . '_HOST }}'));
|
||||
$target->appendChild($dom->createElementNS($ns, 'path', '${{ secrets.' . strtoupper($t['name']) . '_PATH }}'));
|
||||
if (isset($t['method'])) $target->appendChild($dom->createElementNS($ns, 'method', $t['method']));
|
||||
if (isset($t['branch'])) $target->appendChild($dom->createElementNS($ns, 'branch', htmlspecialchars($t['branch'], ENT_XML1)));
|
||||
if (isset($t['src_dir'])) $target->appendChild($dom->createElementNS($ns, 'src-dir', htmlspecialchars($t['src_dir'], ENT_XML1)));
|
||||
$deploy->appendChild($target);
|
||||
}
|
||||
$root->appendChild($deploy);
|
||||
}
|
||||
|
||||
if (!empty($enrichment['scripts'])) {
|
||||
$scriptsEl = $dom->createElementNS($ns, 'scripts');
|
||||
foreach ($enrichment['scripts'] as $s) {
|
||||
$script = $dom->createElementNS($ns, 'script');
|
||||
$script->setAttribute('name', $s['name']);
|
||||
if (isset($s['phase'])) $script->setAttribute('phase', $s['phase']);
|
||||
$script->appendChild($dom->createElementNS($ns, 'command', htmlspecialchars($s['command'], ENT_XML1)));
|
||||
if (isset($s['desc'])) $script->appendChild($dom->createElementNS($ns, 'description', htmlspecialchars($s['desc'], ENT_XML1)));
|
||||
if (isset($s['runner'])) $script->appendChild($dom->createElementNS($ns, 'runner', htmlspecialchars($s['runner'], ENT_XML1)));
|
||||
$scriptsEl->appendChild($script);
|
||||
}
|
||||
$root->appendChild($scriptsEl);
|
||||
}
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────────
|
||||
echo "=== MokoStandards XML Manifest Enrichment ===\n";
|
||||
echo "Mode: " . ($dryRun ? "DRY RUN" : "LIVE") . "\n";
|
||||
if (!empty($skipRepos)) echo "Skipping: " . implode(', ', $skipRepos) . "\n";
|
||||
echo "\n";
|
||||
|
||||
if (empty($token)) { fprintf(STDERR, "ERROR: GA_TOKEN required\n"); exit(1); }
|
||||
|
||||
$repos = fetchRepos($giteaUrl, $giteaOrg, $token);
|
||||
echo "Found " . count($repos) . " repositories\n\n";
|
||||
|
||||
$stats = ['enriched' => 0, 'skipped' => 0, 'failed' => 0];
|
||||
|
||||
foreach ($repos as $repo) {
|
||||
$name = $repo['name'];
|
||||
if ($repoFilter && $name !== $repoFilter) continue;
|
||||
if (in_array($name, $skipRepos, true)) { echo " {$name} ... SKIP (excluded)\n"; $stats['skipped']++; continue; }
|
||||
if ($repo['archived'] ?? false) { $stats['skipped']++; continue; }
|
||||
|
||||
$defaultBranch = $repo['default_branch'] ?? 'main';
|
||||
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
|
||||
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
|
||||
|
||||
echo " {$name} ... ";
|
||||
|
||||
$workDir = "{$tmpBase}/{$name}";
|
||||
@mkdir($workDir, 0755, true);
|
||||
[$ret] = safeExec('git clone --depth 1 --branch ' . escapeshellarg($defaultBranch) . ' ' . escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir));
|
||||
if ($ret !== 0) { echo "FAIL (clone)\n"; $stats['failed']++; continue; }
|
||||
|
||||
$manifestPath = "{$workDir}/.gitea/.mokostandards";
|
||||
if (!file_exists($manifestPath) || !str_contains(file_get_contents($manifestPath), '<mokostandards')) {
|
||||
echo "SKIP (no XML manifest)\n"; $stats['skipped']++; rmTree($workDir); continue;
|
||||
}
|
||||
|
||||
$existingXml = file_get_contents($manifestPath);
|
||||
$platform = $parser->extractPlatform($existingXml) ?? 'default-repository';
|
||||
$enrichment = inspectRepo($workDir, $platform);
|
||||
|
||||
if (!isset($enrichment['build'])) $enrichment['build'] = [];
|
||||
$enrichment['build']['language'] = $enrichment['build']['language'] ?? $repo['language'] ?? MokoStandardsParser::platformLanguage($platform);
|
||||
$enrichment['build']['package_type'] = $enrichment['build']['package_type'] ?? MokoStandardsParser::platformPackageType($platform);
|
||||
|
||||
$enrichedXml = enrichManifestXml($existingXml, $enrichment);
|
||||
$dc = count($enrichment['deploy'] ?? []);
|
||||
$sc = count($enrichment['scripts'] ?? []);
|
||||
$details = "deploy={$dc} scripts={$sc}";
|
||||
|
||||
if ($dryRun) { echo "WOULD ENRICH [{$details}]\n"; $stats['enriched']++; rmTree($workDir); continue; }
|
||||
|
||||
file_put_contents($manifestPath, $enrichedXml);
|
||||
gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
|
||||
gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
|
||||
gitCmd($workDir, 'add', '.gitea/.mokostandards');
|
||||
|
||||
[$cr, $co] = gitCmd($workDir, 'commit', '-m', "chore: enrich .mokostandards with build/deploy/scripts\n\nAuto-detected: {$details}");
|
||||
if ($cr !== 0) { echo "SKIP (no diff)\n"; $stats['skipped']++; rmTree($workDir); continue; }
|
||||
|
||||
[$pr] = gitCmd($workDir, 'push', 'origin', $defaultBranch);
|
||||
if ($pr !== 0) { echo "FAIL (push)\n"; $stats['failed']++; }
|
||||
else { echo "ENRICHED [{$details}]\n"; $stats['enriched']++; }
|
||||
|
||||
rmTree($workDir);
|
||||
}
|
||||
|
||||
@rmdir($tmpBase);
|
||||
echo "\n=== Summary ===\nEnriched: {$stats['enriched']}\nSkipped: {$stats['skipped']}\nFailed: {$stats['failed']}\n";
|
||||
@@ -15,10 +15,10 @@
|
||||
* BRIEF: Migrate repositories from GitHub to self-hosted Gitea instance
|
||||
*
|
||||
* USAGE
|
||||
* php api/automation/migrate_to_gitea.php --dry-run
|
||||
* php api/automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods
|
||||
* php api/automation/migrate_to_gitea.php --exclude MokoStandards --skip-archived
|
||||
* php api/automation/migrate_to_gitea.php --resume
|
||||
* php automation/migrate_to_gitea.php --dry-run
|
||||
* php automation/migrate_to_gitea.php --repos MokoCRM MokoDoliMods
|
||||
* php automation/migrate_to_gitea.php --exclude MokoStandards --skip-archived
|
||||
* php automation/migrate_to_gitea.php --resume
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -358,7 +358,7 @@ class PushFiles extends CLIApp
|
||||
$prBody = $this->buildPRBody($entries);
|
||||
$pr = $this->adapter->createPullRequest(
|
||||
$org, $repo, $prTitle, $branch, $defaultBranch, $prBody,
|
||||
['assignees' => ['jmiller-moko']]
|
||||
['assignees' => ['jmiller']]
|
||||
);
|
||||
$prNumber = $pr['number'] ?? null;
|
||||
$this->log(" 📋 PR #{$prNumber} created", 'INFO');
|
||||
@@ -512,7 +512,7 @@ class PushFiles extends CLIApp
|
||||
|
||||
if (!empty($existing) && isset($existing[0]['number'])) {
|
||||
$num = $existing[0]['number'];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller-moko']];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
|
||||
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
||||
$patch['state'] = 'open';
|
||||
}
|
||||
@@ -526,7 +526,7 @@ class PushFiles extends CLIApp
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'labels' => $labels,
|
||||
'assignees' => ['jmiller-moko'],
|
||||
'assignees' => ['jmiller'],
|
||||
]);
|
||||
$num = $issue['number'] ?? null;
|
||||
$this->log(" 📋 Tracking issue #{$num} created in {$repo}", 'INFO');
|
||||
@@ -590,7 +590,7 @@ class PushFiles extends CLIApp
|
||||
|
||||
1. Check the output above for the specific error per repo.
|
||||
2. Fix the underlying issue (API token, branch permissions, file path, etc.).
|
||||
3. Re-run: `php api/automation/push_files.php --org={$org} --repos=<repo> --files=<files> --yes`
|
||||
3. Re-run: `php automation/push_files.php --org={$org} --repos=<repo> --files=<files> --yes`
|
||||
4. Close this issue once resolved.
|
||||
|
||||
---
|
||||
@@ -610,7 +610,7 @@ class PushFiles extends CLIApp
|
||||
|
||||
if (!empty($existing) && isset($existing[0]['number'])) {
|
||||
$num = $existing[0]['number'];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller-moko']];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
|
||||
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
||||
$patch['state'] = 'open';
|
||||
}
|
||||
@@ -621,7 +621,7 @@ class PushFiles extends CLIApp
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'labels' => ['push-failure'],
|
||||
'assignees' => ['jmiller-moko'],
|
||||
'assignees' => ['jmiller'],
|
||||
]);
|
||||
$num = $issue['number'] ?? '?';
|
||||
$this->log("🚨 Failure issue created: {$org}/MokoStandards#{$num}", 'WARN');
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* Push XML .mokostandards manifest to all governed repositories.
|
||||
*
|
||||
* Uses git SSH to bypass the Gitea reverse-proxy WAF that blocks
|
||||
* API requests to paths containing ".gitea".
|
||||
*
|
||||
* Usage:
|
||||
* php automation/push_mokostandards_xml.php [--dry-run] [--repo NAME] [--force]
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use MokoEnterprise\MokoStandardsParser;
|
||||
|
||||
// ── Configuration ────────────────────────────────────────────────────────
|
||||
$giteaUrl = rtrim(getenv('GITEA_URL') ?: 'https://git.mokoconsulting.tech', '/');
|
||||
$giteaOrg = getenv('GITEA_ORG') ?: 'MokoConsulting';
|
||||
$token = getenv('GA_TOKEN') ?: getenv('GH_TOKEN') ?: '';
|
||||
$sshBase = 'ssh://gitea@git.mokoconsulting.tech:2222';
|
||||
|
||||
// ── CLI args ─────────────────────────────────────────────────────────────
|
||||
$dryRun = in_array('--dry-run', $argv, true);
|
||||
$force = in_array('--force', $argv, true);
|
||||
$repoFilter = null;
|
||||
$skipRepos = [];
|
||||
foreach ($argv as $i => $arg) {
|
||||
if ($arg === '--repo' && isset($argv[$i + 1])) {
|
||||
$repoFilter = $argv[$i + 1];
|
||||
}
|
||||
if ($arg === '--skip' && isset($argv[$i + 1])) {
|
||||
$skipRepos = array_map('trim', explode(',', $argv[$i + 1]));
|
||||
}
|
||||
}
|
||||
|
||||
$parser = new MokoStandardsParser();
|
||||
$tmpBase = sys_get_temp_dir() . '/moko-manifest-push-' . getmypid();
|
||||
|
||||
// ── Platform detection heuristics (mirrors RepositorySynchronizer) ───────
|
||||
$CRM_PLATFORM_REPOS = ['MokoDolibarr', 'MokoDoliMods'];
|
||||
|
||||
function detectPlatform(array $repo): string {
|
||||
global $CRM_PLATFORM_REPOS;
|
||||
$name = $repo['name'] ?? '';
|
||||
$nameLower = strtolower($name);
|
||||
$description = strtolower($repo['description'] ?? '');
|
||||
$topics = $repo['topics'] ?? [];
|
||||
|
||||
if (in_array($name, $CRM_PLATFORM_REPOS, true)) return 'crm-platform';
|
||||
if (in_array('dolibarr-platform', $topics)) return 'crm-platform';
|
||||
if (in_array('joomla-template', $topics)) return 'joomla-template';
|
||||
if (in_array('joomla', $topics) || in_array('joomla-extension', $topics)) return 'waas-component';
|
||||
if (in_array('dolibarr', $topics) || in_array('dolibarr-module', $topics)) return 'crm-module';
|
||||
|
||||
if (str_contains($nameLower, 'template') && (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'tpl'))) return 'joomla-template';
|
||||
if (str_contains($nameLower, 'joomla') || str_contains($nameLower, 'waas')) return 'waas-component';
|
||||
if (str_contains($nameLower, 'doli') || str_contains($nameLower, 'crm')) return 'crm-module';
|
||||
|
||||
if (str_contains($description, 'joomla template')) return 'joomla-template';
|
||||
if (str_contains($description, 'joomla') || str_contains($description, 'component')) return 'waas-component';
|
||||
if (str_contains($description, 'dolibarr') || str_contains($description, 'module')) return 'crm-module';
|
||||
|
||||
if (str_contains($nameLower, 'standard')) return 'standards-repository';
|
||||
return 'default-repository';
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe shell execution — uses proc_open with explicit arguments to avoid injection.
|
||||
* @return array{int, string}
|
||||
*/
|
||||
function safeExec(string $command, string $cwd = '.'): array {
|
||||
$proc = proc_open(
|
||||
$command,
|
||||
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
|
||||
$pipes,
|
||||
$cwd
|
||||
);
|
||||
if (!is_resource($proc)) {
|
||||
return [1, "proc_open failed for: {$command}"];
|
||||
}
|
||||
$stdout = stream_get_contents($pipes[1]);
|
||||
$stderr = stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
$code = proc_close($proc);
|
||||
return [$code, trim($stdout . "\n" . $stderr)];
|
||||
}
|
||||
|
||||
/** Recursively remove a directory (cross-platform). */
|
||||
function rmTree(string $dir): void {
|
||||
if (!is_dir($dir)) return;
|
||||
$it = new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS);
|
||||
$files = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);
|
||||
foreach ($files as $file) {
|
||||
if ($file->isDir()) {
|
||||
@rmdir($file->getPathname());
|
||||
} else {
|
||||
// Clear read-only flag (git objects on Windows)
|
||||
@chmod($file->getPathname(), 0777);
|
||||
@unlink($file->getPathname());
|
||||
}
|
||||
}
|
||||
@rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a git command safely in a given working directory.
|
||||
* @return array{int, string}
|
||||
*/
|
||||
function gitCmd(string $workDir, string ...$args): array {
|
||||
$cmd = 'git';
|
||||
foreach ($args as $a) {
|
||||
$cmd .= ' ' . escapeshellarg($a);
|
||||
}
|
||||
return safeExec($cmd, $workDir);
|
||||
}
|
||||
|
||||
// ── Fetch all repos via API ──────────────────────────────────────────────
|
||||
function fetchRepos(string $url, string $org, string $token): array {
|
||||
$repos = [];
|
||||
$page = 1;
|
||||
do {
|
||||
$ch = curl_init("{$url}/api/v1/orgs/{$org}/repos?page={$page}&limit=50");
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => ["Authorization: token {$token}"],
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
]);
|
||||
$body = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($code !== 200) {
|
||||
fprintf(STDERR, "API error (HTTP %d) fetching repos page %d\n", $code, $page);
|
||||
break;
|
||||
}
|
||||
|
||||
$batch = json_decode($body, true);
|
||||
if (empty($batch)) break;
|
||||
$repos = array_merge($repos, $batch);
|
||||
$page++;
|
||||
} while (count($batch) >= 50);
|
||||
|
||||
return $repos;
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────────────────────────────
|
||||
echo "=== MokoStandards XML Manifest Push ===\n";
|
||||
echo "Org: {$giteaOrg}\n";
|
||||
echo "Mode: " . ($dryRun ? "DRY RUN" : "LIVE") . "\n";
|
||||
if ($repoFilter) echo "Filter: {$repoFilter}\n";
|
||||
echo "\n";
|
||||
|
||||
if (empty($token)) {
|
||||
fprintf(STDERR, "ERROR: GA_TOKEN or GH_TOKEN environment variable required\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$repos = fetchRepos($giteaUrl, $giteaOrg, $token);
|
||||
echo "Found " . count($repos) . " repositories\n\n";
|
||||
|
||||
$stats = ['created' => 0, 'updated' => 0, 'skipped' => 0, 'failed' => 0];
|
||||
|
||||
foreach ($repos as $repo) {
|
||||
$name = $repo['name'];
|
||||
if ($repoFilter && $name !== $repoFilter) continue;
|
||||
if (in_array($name, $skipRepos, true)) {
|
||||
echo " SKIP {$name} (excluded)\n";
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
if ($repo['archived'] ?? false) {
|
||||
echo " SKIP {$name} (archived)\n";
|
||||
$stats['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$platform = detectPlatform($repo);
|
||||
$defaultBranch = $repo['default_branch'] ?? 'main';
|
||||
// Prefer HTTPS with token (SSH port 2222 may be blocked); fall back to SSH
|
||||
$httpsUrl = $repo['clone_url'] ?? "{$giteaUrl}/{$giteaOrg}/{$name}.git";
|
||||
// Embed token in HTTPS URL for push auth
|
||||
$authedUrl = preg_replace('#^https://#', "https://gitea-actions:{$token}@", $httpsUrl);
|
||||
|
||||
echo " {$name} [{$platform}] ... ";
|
||||
|
||||
// Generate XML manifest
|
||||
$xmlContent = $parser->generate([
|
||||
'name' => $name,
|
||||
'org' => $giteaOrg,
|
||||
'platform' => $platform,
|
||||
'standards_version' => '04.07.00',
|
||||
'description' => $repo['description'] ?? '',
|
||||
'license' => 'GPL-3.0-or-later',
|
||||
'topics' => $repo['topics'] ?? [],
|
||||
'language' => $repo['language'] ?? MokoStandardsParser::platformLanguage($platform),
|
||||
'package_type' => MokoStandardsParser::platformPackageType($platform),
|
||||
'last_synced' => date('c'),
|
||||
]);
|
||||
|
||||
if ($dryRun) {
|
||||
echo "WOULD WRITE ({$platform})\n";
|
||||
$stats['created']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Clone shallow via HTTPS (token-authed)
|
||||
$workDir = "{$tmpBase}/{$name}";
|
||||
@mkdir($workDir, 0755, true);
|
||||
|
||||
[$ret, $out] = safeExec(
|
||||
'git clone --depth 1 --branch ' . escapeshellarg($defaultBranch) . ' '
|
||||
. escapeshellarg($authedUrl) . ' ' . escapeshellarg($workDir)
|
||||
);
|
||||
if ($ret !== 0) {
|
||||
echo "FAIL (clone)\n";
|
||||
fprintf(STDERR, " %s\n", $out);
|
||||
$stats['failed']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if already XML and up-to-date
|
||||
$manifestPath = "{$workDir}/.gitea/.mokostandards";
|
||||
$existingIsXml = file_exists($manifestPath) && str_contains(file_get_contents($manifestPath), '<mokostandards');
|
||||
if ($existingIsXml && !$force) {
|
||||
$existingPlatform = $parser->extractPlatform(file_get_contents($manifestPath));
|
||||
if ($existingPlatform === $platform) {
|
||||
echo "SKIP (already XML)\n";
|
||||
$stats['skipped']++;
|
||||
rmTree($workDir);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Write manifest
|
||||
@mkdir("{$workDir}/.gitea", 0755, true);
|
||||
file_put_contents($manifestPath, $xmlContent);
|
||||
|
||||
// Delete legacy files if present
|
||||
$legacyDeleted = [];
|
||||
foreach (['.mokostandards', '.github/.mokostandards'] as $legacy) {
|
||||
$legacyPath = "{$workDir}/{$legacy}";
|
||||
if (file_exists($legacyPath)) {
|
||||
unlink($legacyPath);
|
||||
$legacyDeleted[] = $legacy;
|
||||
}
|
||||
}
|
||||
|
||||
// Commit
|
||||
$isNew = !$existingIsXml;
|
||||
$commitMsg = $isNew
|
||||
? 'chore: add XML .mokostandards manifest'
|
||||
: 'chore: update .mokostandards to XML format';
|
||||
if (!empty($legacyDeleted)) {
|
||||
$commitMsg .= "\n\nRemoved legacy: " . implode(', ', $legacyDeleted);
|
||||
}
|
||||
|
||||
gitCmd($workDir, 'config', 'user.name', 'gitea-actions[bot]');
|
||||
gitCmd($workDir, 'config', 'user.email', 'gitea-actions[bot]@git.mokoconsulting.tech');
|
||||
gitCmd($workDir, 'add', '.gitea/.mokostandards');
|
||||
foreach ($legacyDeleted as $lf) {
|
||||
gitCmd($workDir, 'add', $lf);
|
||||
}
|
||||
|
||||
[$commitRet, $commitOut] = gitCmd($workDir, 'commit', '-m', $commitMsg);
|
||||
if ($commitRet !== 0 && str_contains($commitOut, 'nothing to commit')) {
|
||||
echo "SKIP (no changes)\n";
|
||||
$stats['skipped']++;
|
||||
rmTree($workDir);
|
||||
continue;
|
||||
}
|
||||
if ($commitRet !== 0) {
|
||||
echo "FAIL (commit)\n";
|
||||
fprintf(STDERR, " %s\n", $commitOut);
|
||||
$stats['failed']++;
|
||||
rmTree($workDir);
|
||||
continue;
|
||||
}
|
||||
|
||||
[$pushRet, $pushOut] = gitCmd($workDir, 'push', 'origin', $defaultBranch);
|
||||
if ($pushRet !== 0) {
|
||||
echo "FAIL (push)\n";
|
||||
fprintf(STDERR, " %s\n", $pushOut);
|
||||
$stats['failed']++;
|
||||
} else {
|
||||
$action = $isNew ? 'CREATED' : 'UPDATED';
|
||||
echo "{$action}\n";
|
||||
$stats[$isNew ? 'created' : 'updated']++;
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
rmTree($workDir);
|
||||
}
|
||||
|
||||
// Cleanup tmp base
|
||||
@rmdir($tmpBase);
|
||||
|
||||
echo "\n=== Summary ===\n";
|
||||
echo "Created: {$stats['created']}\n";
|
||||
echo "Updated: {$stats['updated']}\n";
|
||||
echo "Skipped: {$stats['skipped']}\n";
|
||||
echo "Failed: {$stats['failed']}\n";
|
||||
@@ -15,9 +15,9 @@
|
||||
* BRIEF: Gracefully retire a governed repository — archive, close issues/PRs, remove sync def
|
||||
*
|
||||
* USAGE
|
||||
* php api/cli/archive_repo.php --repo MokoOldModule
|
||||
* php api/cli/archive_repo.php --repo MokoOldModule --dry-run
|
||||
* php api/cli/archive_repo.php --repo MokoOldModule --skip-close # Archive only, keep issues open
|
||||
* php cli/archive_repo.php --repo MokoOldModule
|
||||
* php cli/archive_repo.php --repo MokoOldModule --dry-run
|
||||
* php cli/archive_repo.php --repo MokoOldModule --skip-close # Archive only, keep issues open
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -143,7 +143,7 @@ if (!$dryRun) {
|
||||
"## Repository Archived\n\n**Repository:** `{$org}/{$repoName}`\n**Archived:** {$now}\n**Platform:** {$platformName}\n**Sync definition removed:** yes\n\n---\n*Auto-created by `archive_repo.php`*\n",
|
||||
[
|
||||
'labels' => ['type: chore', 'automation', 'archived'],
|
||||
'assignees' => ['jmiller-moko'],
|
||||
'assignees' => ['jmiller'],
|
||||
]
|
||||
);
|
||||
if (isset($issue['number'])) { echo " Archival record: MokoStandards#{$issue['number']}\n"; }
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
* BRIEF: Create baseline GitHub Projects for repositories with standard fields and views
|
||||
*
|
||||
* USAGE
|
||||
* php api/cli/create_project.php --repo MokoCRM # Auto-detect type, create project
|
||||
* php api/cli/create_project.php --repo MokoCRM --type dolibarr # Force type
|
||||
* php api/cli/create_project.php --org mokoconsulting-tech --all # All repos without projects
|
||||
* php api/cli/create_project.php --repo MokoCRM --dry-run # Preview without changes
|
||||
* php cli/create_project.php --repo MokoCRM # Auto-detect type, create project
|
||||
* php cli/create_project.php --repo MokoCRM --type dolibarr # Force type
|
||||
* php cli/create_project.php --org mokoconsulting-tech --all # All repos without projects
|
||||
* php cli/create_project.php --repo MokoCRM --dry-run # Preview without changes
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -385,7 +385,7 @@ function createProject(
|
||||
updateProjectV2(input: {
|
||||
projectId: $projectId,
|
||||
shortDescription: $shortDescription,
|
||||
readme: "Managed by MokoStandards. Run `php api/cli/create_project.php` to regenerate."
|
||||
readme: "Managed by MokoStandards. Run `php cli/create_project.php` to regenerate."
|
||||
}) {
|
||||
projectV2 { id }
|
||||
}
|
||||
|
||||
+5
-5
@@ -15,9 +15,9 @@
|
||||
* BRIEF: Scaffold a new governed repository with full MokoStandards baseline
|
||||
*
|
||||
* USAGE
|
||||
* php api/cli/create_repo.php --name MokoNewModule --type dolibarr --description "My new module"
|
||||
* php api/cli/create_repo.php --name MokoNewModule --type joomla --private
|
||||
* php api/cli/create_repo.php --name MokoNewModule --type generic --dry-run
|
||||
* php cli/create_repo.php --name MokoNewModule --type dolibarr --description "My new module"
|
||||
* php cli/create_repo.php --name MokoNewModule --type joomla --private
|
||||
* php cli/create_repo.php --name MokoNewModule --type generic --dry-run
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -229,7 +229,7 @@ if (!$dryRun) {
|
||||
if (file_exists($syncScript)) {
|
||||
passthru("php " . escapeshellarg($syncScript) . " --repos " . escapeshellarg($name) . " --force --yes");
|
||||
} else {
|
||||
echo " Run manually: php api/automation/bulk_sync.php --repos {$name} --force --yes\n";
|
||||
echo " Run manually: php automation/bulk_sync.php --repos {$name} --force --yes\n";
|
||||
}
|
||||
} else {
|
||||
echo " (dry-run) would run initial sync\n";
|
||||
@@ -242,7 +242,7 @@ if (!$dryRun) {
|
||||
if (file_exists($projectScript)) {
|
||||
passthru("php " . escapeshellarg($projectScript) . " --repo " . escapeshellarg($name) . " --type " . escapeshellarg($type));
|
||||
} else {
|
||||
echo " Run manually: php api/cli/create_project.php --repo {$name} --type {$type}\n";
|
||||
echo " Run manually: php cli/create_project.php --repo {$name} --type {$type}\n";
|
||||
}
|
||||
} else {
|
||||
echo " (dry-run) would create Project\n";
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
* BRIEF: Joomla release pipeline — build ZIP+tar.gz, upload to GitHub Release, update updates.xml
|
||||
*
|
||||
* USAGE
|
||||
* php api/cli/joomla_release.php --repo MokoCassiopeia --stability stable
|
||||
* php api/cli/joomla_release.php --repo MokoCassiopeia --stability development
|
||||
* php api/cli/joomla_release.php --repo MokoCassiopeia --stability rc --dry-run
|
||||
* php api/cli/joomla_release.php --path /local/repo --stability stable
|
||||
* php cli/joomla_release.php --repo MokoCassiopeia --stability stable
|
||||
* php cli/joomla_release.php --repo MokoCassiopeia --stability development
|
||||
* php cli/joomla_release.php --repo MokoCassiopeia --stability rc --dry-run
|
||||
* php cli/joomla_release.php --path /local/repo --stability stable
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
+5
-5
@@ -13,10 +13,10 @@
|
||||
* BRIEF: Automate the MokoStandards version branch release flow
|
||||
*
|
||||
* USAGE
|
||||
* php api/cli/release.php # Release current version
|
||||
* php api/cli/release.php --bump minor # Bump minor, then release
|
||||
* php api/cli/release.php --bump major # Bump major, then release
|
||||
* php api/cli/release.php --dry-run # Preview without changes
|
||||
* php cli/release.php # Release current version
|
||||
* php cli/release.php --bump minor # Bump minor, then release
|
||||
* php cli/release.php --bump major # Bump major, then release
|
||||
* php cli/release.php --dry-run # Preview without changes
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -30,7 +30,7 @@ foreach ($argv as $i => $arg) {
|
||||
}
|
||||
|
||||
$repoRoot = dirname(__DIR__, 2);
|
||||
$syncFile = "{$repoRoot}/api/lib/Enterprise/RepositorySynchronizer.php";
|
||||
$syncFile = "{$repoRoot}/lib/Enterprise/RepositorySynchronizer.php";
|
||||
// Check both workflow directories for the bulk-repo-sync workflow
|
||||
$bulkSyncFile = file_exists("{$repoRoot}/.gitea/workflows/bulk-repo-sync.yml")
|
||||
? "{$repoRoot}/.gitea/workflows/bulk-repo-sync.yml"
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
* BRIEF: Apply branch protection rules to all repos via platform adapter
|
||||
*
|
||||
* USAGE
|
||||
* php api/cli/sync_rulesets.php # Apply to all repos
|
||||
* php api/cli/sync_rulesets.php --repo MokoCRM # Single repo
|
||||
* php api/cli/sync_rulesets.php --dry-run # Preview only
|
||||
* php api/cli/sync_rulesets.php --delete # Remove then re-apply
|
||||
* php cli/sync_rulesets.php # Apply to all repos
|
||||
* php cli/sync_rulesets.php --repo MokoCRM # Single repo
|
||||
* php cli/sync_rulesets.php --dry-run # Preview only
|
||||
* php cli/sync_rulesets.php --delete # Remove then re-apply
|
||||
*
|
||||
* NOTE: On GitHub, this creates rulesets via the rulesets API.
|
||||
* On Gitea, this creates branch_protections via the branch protection API.
|
||||
|
||||
+13
-13
@@ -13,26 +13,26 @@
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"ext-json": "*",
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-zip": "*",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"monolog/monolog": "^3.5",
|
||||
"php": ">=8.1",
|
||||
"phpseclib/phpseclib": "^3.0",
|
||||
"psr/cache": "^3.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/log": "^3.0",
|
||||
"symfony/cache": "^6.4",
|
||||
"symfony/console": "^6.4",
|
||||
"symfony/yaml": "^6.4",
|
||||
"symfony/filesystem": "^6.4",
|
||||
"symfony/process": "^6.4",
|
||||
"symfony/finder": "^6.4",
|
||||
"symfony/http-foundation": "^6.4",
|
||||
"symfony/process": "^6.4",
|
||||
"symfony/routing": "^6.4",
|
||||
"symfony/cache": "^6.4",
|
||||
"symfony/yaml": "^6.4",
|
||||
"twig/twig": "^3.8",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"psr/log": "^3.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/cache": "^3.0",
|
||||
"phpseclib/phpseclib": "^3.0",
|
||||
"ext-zip": "*"
|
||||
"vlucas/phpdotenv": "^5.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5",
|
||||
@@ -73,8 +73,8 @@
|
||||
],
|
||||
"scripts": {
|
||||
"test": "phpunit",
|
||||
"phpcs": "phpcs --standard=phpcs.xml api/",
|
||||
"phpstan": "phpstan analyse -c phpstan.neon api/",
|
||||
"phpcs": "phpcs --standard=phpcs.xml lib/ validate/ automation/",
|
||||
"phpstan": "phpstan analyse -c phpstan.neon lib/ validate/ automation/",
|
||||
"psalm": "psalm --config=psalm.xml",
|
||||
"check": [
|
||||
"@phpcs",
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Client Joomla Site Structure Definition
|
||||
* Standard repository structure for client Joomla site projects
|
||||
*
|
||||
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Version: 01.00.00
|
||||
* Schema Version: 1.0
|
||||
*/
|
||||
|
||||
locals {
|
||||
repository_structure = {
|
||||
metadata = {
|
||||
name = "Client Joomla Site"
|
||||
description = "Standard repository structure for client Joomla site projects (overrides, media, configuration)"
|
||||
repository_type = "client-site"
|
||||
platform = "mokowaas"
|
||||
last_updated = "2026-05-04T00:00:00Z"
|
||||
maintainer = "Moko Consulting"
|
||||
version = "01.00.00"
|
||||
schema_version = "1.0"
|
||||
template_repo = "MokoConsulting/MokoStandards-Template-Client"
|
||||
}
|
||||
|
||||
root_files = [
|
||||
{
|
||||
name = "README.md"
|
||||
description = "Client project documentation"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
},
|
||||
{
|
||||
name = "LICENSE"
|
||||
description = "License file (GPL-3.0-or-later)"
|
||||
required = true
|
||||
},
|
||||
{
|
||||
name = "CHANGELOG.md"
|
||||
description = "Version history and changes"
|
||||
required = true
|
||||
},
|
||||
{
|
||||
name = "SECURITY.md"
|
||||
description = "Security policy and vulnerability reporting"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "CODE_OF_CONDUCT.md"
|
||||
description = "Community code of conduct"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "CONTRIBUTING.md"
|
||||
description = "Contribution guidelines"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "Makefile"
|
||||
description = "Build automation"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "composer.json"
|
||||
description = "PHP dependency management"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
},
|
||||
{
|
||||
name = "phpstan.neon"
|
||||
description = "PHPStan static analysis config"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "codeception.yml"
|
||||
description = "Codeception test framework config"
|
||||
required = false
|
||||
always_overwrite = false
|
||||
},
|
||||
{
|
||||
name = ".editorconfig"
|
||||
description = "Editor configuration for consistent coding style"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = ".gitignore"
|
||||
description = "Git ignore patterns for client site projects"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
},
|
||||
]
|
||||
|
||||
// NOTE: Client sites do NOT have updates.xml — they are not installable extensions
|
||||
|
||||
subdirectories = [
|
||||
{
|
||||
name = "src"
|
||||
description = "Site source files — template overrides, custom code, and configuration"
|
||||
required = true
|
||||
files = []
|
||||
},
|
||||
{
|
||||
name = "src/images"
|
||||
description = "Site images — branding, headers, event photos, profiles"
|
||||
required = true
|
||||
files = []
|
||||
},
|
||||
{
|
||||
name = "src/media"
|
||||
description = "Media assets — uploaded files, documents"
|
||||
required = true
|
||||
files = []
|
||||
},
|
||||
{
|
||||
name = "docs"
|
||||
description = "Client-specific documentation — brand reference, deployment, migration plans"
|
||||
required = true
|
||||
files = []
|
||||
},
|
||||
{
|
||||
name = "scripts"
|
||||
description = "Deployment and maintenance scripts"
|
||||
required = false
|
||||
files = []
|
||||
},
|
||||
{
|
||||
name = "tests"
|
||||
description = "Acceptance and unit tests"
|
||||
required = false
|
||||
files = []
|
||||
},
|
||||
{
|
||||
name = ".gitea/workflows"
|
||||
description = "Gitea Actions CI/CD workflows (10 workflows — no update-server)"
|
||||
required = true
|
||||
files = [
|
||||
{
|
||||
name = "auto-release.yml"
|
||||
description = "Stable release on PR merge to main"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "pre-release.yml"
|
||||
description = "Manual pre-release for dev/alpha/beta/rc channels"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "ci-joomla.yml"
|
||||
description = "PHP lint, PHPStan, coding standards"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "pr-check.yml"
|
||||
description = "PR gate — validates code quality before merge"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "deploy-manual.yml"
|
||||
description = "Manual SFTP deploy to selected environment"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "repo-health.yml"
|
||||
description = "Repository health checks"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "security-audit.yml"
|
||||
description = "Dependency vulnerability scanning"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "notify.yml"
|
||||
description = "ntfy push notifications on release success or failure"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "cleanup.yml"
|
||||
description = "Weekly merged branch + old run cleanup"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
{
|
||||
name = "sync-media.yml"
|
||||
description = "Bidirectional SFTP sync for images/, files/, media/ between dev and production"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
// Per-repo variables required for sync-media.yml
|
||||
required_variables = [
|
||||
{ name = "DEV_SYNC_HOST", description = "Dev server hostname" },
|
||||
{ name = "DEV_SYNC_PORT", description = "Dev SSH port (default 22)" },
|
||||
{ name = "DEV_SYNC_USERNAME", description = "Dev server username" },
|
||||
{ name = "DEV_SYNC_PATH", description = "Base path on dev server" },
|
||||
{ name = "PROD_SYNC_HOST", description = "Production server hostname" },
|
||||
{ name = "PROD_SYNC_PORT", description = "Production SSH port (default 22)" },
|
||||
{ name = "PROD_SYNC_USERNAME", description = "Production server username" },
|
||||
{ name = "PROD_SYNC_PATH", description = "Base path on production server" },
|
||||
]
|
||||
|
||||
required_secrets = [
|
||||
{ name = "DEV_SYNC_KEY", description = "SSH private key for dev server" },
|
||||
{ name = "PROD_SYNC_KEY", description = "SSH private key for production server" },
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -167,13 +167,14 @@ EOT
|
||||
audience = "developer"
|
||||
},
|
||||
{
|
||||
name = ".mokostandards"
|
||||
extension = "yml"
|
||||
description = "MokoStandards governance attachment — links this repo back to the standards source"
|
||||
name = ".gitea/.mokostandards"
|
||||
extension = "xml"
|
||||
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/configs/mokostandards.yml.template"
|
||||
template = "managed-by-sync"
|
||||
source_type = "programmatic"
|
||||
},
|
||||
{
|
||||
name = "GOVERNANCE.md"
|
||||
|
||||
@@ -84,12 +84,13 @@ locals {
|
||||
template = "templates/configs/ftp_ignore"
|
||||
},
|
||||
{
|
||||
name = ".mokostandards"
|
||||
extension = ""
|
||||
description = "MokoStandards platform identifier"
|
||||
name = ".gitea/.mokostandards"
|
||||
extension = "xml"
|
||||
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "templates/configs/mokostandards.yml.template"
|
||||
always_overwrite = false
|
||||
template = "managed-by-sync"
|
||||
source_type = "programmatic"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -142,15 +142,24 @@
|
||||
{
|
||||
"name": ".github",
|
||||
"path": ".github",
|
||||
"description": "GitHub-specific configuration",
|
||||
"description": "Gitea/GitHub Actions configuration (Gitea reads .github/workflows natively)",
|
||||
"requirementStatus": "required",
|
||||
"purpose": "Contains GitHub Actions workflows and configuration",
|
||||
"purpose": "Contains CI/CD workflows and repository configuration. Gitea is the primary platform; GitHub is backup only.",
|
||||
"subdirectories": [
|
||||
{
|
||||
"name": "workflows",
|
||||
"path": ".github/workflows",
|
||||
"description": "GitHub Actions workflows",
|
||||
"requirementStatus": "suggested"
|
||||
"description": "CI/CD workflows (Gitea-primary, GitHub-compatible)",
|
||||
"requirementStatus": "required",
|
||||
"requiredFiles": [
|
||||
"auto-assign.yml",
|
||||
"auto-dev-issue.yml",
|
||||
"auto-release.yml",
|
||||
"branch-freeze.yml",
|
||||
"changelog-validation.yml",
|
||||
"repository-cleanup.yml",
|
||||
"sync-version-on-merge.yml"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -119,13 +119,14 @@ locals {
|
||||
template = "templates/configs/composer.generic.json"
|
||||
},
|
||||
{
|
||||
name = ".mokostandards"
|
||||
extension = "yml"
|
||||
description = "MokoStandards governance attachment — links this repo back to the standards source"
|
||||
name = ".gitea/.mokostandards"
|
||||
extension = "xml"
|
||||
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/configs/mokostandards.yml.template"
|
||||
template = "managed-by-sync"
|
||||
source_type = "programmatic"
|
||||
},
|
||||
{
|
||||
name = "GOVERNANCE.md"
|
||||
|
||||
@@ -116,12 +116,13 @@ locals {
|
||||
audience = "developer"
|
||||
},
|
||||
{
|
||||
name = ".mokostandards.yml"
|
||||
extension = "yml"
|
||||
description = "MokoStandards governance marker — identifies this repo as platform=github-private"
|
||||
name = ".gitea/.mokostandards"
|
||||
extension = "xml"
|
||||
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "templates/configs/mokostandards.yml.template"
|
||||
always_overwrite = false
|
||||
template = "managed-by-sync"
|
||||
source_type = "programmatic"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ locals {
|
||||
https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/raw/branch/main/updates.xml
|
||||
</server>
|
||||
<server type="extension" priority="2" name="{{TEMPLATE_NAME}} Update Server">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/{{REPO_NAME}}/main/updates.xml
|
||||
https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
@@ -179,7 +179,7 @@ locals {
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{TEMPLATE_SHORT_NAME}}.zip
|
||||
</downloadurl>
|
||||
<downloadurl type="full" format="zip">
|
||||
https://github.com/mokoconsulting-tech/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{TEMPLATE_SHORT_NAME}}.zip
|
||||
https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{TEMPLATE_SHORT_NAME}}.zip
|
||||
</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="[56].*"/>
|
||||
@@ -417,6 +417,83 @@ locals {
|
||||
required = false
|
||||
files = []
|
||||
},
|
||||
{
|
||||
name = ".gitea/workflows"
|
||||
description = "Gitea Actions CI/CD workflows"
|
||||
required = true
|
||||
files = [
|
||||
{
|
||||
name = "auto-release.yml"
|
||||
description = "Automated release — builds zip, creates Gitea release, updates SHA in updates.xml. Triggered by push to main (stable) or pre-release tags (development, alpha, beta, rc)"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/auto-release.yml"
|
||||
},
|
||||
{
|
||||
name = "ci-joomla.yml"
|
||||
description = "Continuous integration — PHP linting, PHPStan static analysis, coding standards checks"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/ci-joomla.yml"
|
||||
},
|
||||
{
|
||||
name = "pre-release.yml"
|
||||
description = "Manual pre-release — builds dev/alpha/beta/rc packages with patch version bump"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/pre-release.yml"
|
||||
},
|
||||
{
|
||||
name = "deploy-manual.yml"
|
||||
description = "Manual deployment — allows selecting target environment and branch for on-demand deploys"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/deploy-manual.yml"
|
||||
},
|
||||
{
|
||||
name = "repo-health.yml"
|
||||
description = "Repository health checks — validates required files, structure compliance, and standards alignment"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/repo-health.yml"
|
||||
},
|
||||
{
|
||||
name = "update-server.yml"
|
||||
description = "Update server maintenance — validates updates.xml format and ensures download URLs are reachable"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/update-server.yml"
|
||||
},
|
||||
{
|
||||
name = "pr-check.yml"
|
||||
description = "PR gate — validates PHP syntax, manifest XML, and package build before merge to main"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/pr-check.yml"
|
||||
},
|
||||
{
|
||||
name = "security-audit.yml"
|
||||
description = "Dependency vulnerability scanning — weekly schedule and on PR when lock files change"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/security-audit.yml"
|
||||
},
|
||||
{
|
||||
name = "notify.yml"
|
||||
description = "Push notifications via ntfy on release success or workflow failure"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/notify.yml"
|
||||
},
|
||||
{
|
||||
name = "cleanup.yml"
|
||||
description = "Scheduled cleanup — delete merged branches and old workflow runs weekly"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "workflows/cleanup.yml"
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,12 +199,14 @@ locals {
|
||||
audience = "developer"
|
||||
},
|
||||
{
|
||||
name = ".mokostandards"
|
||||
extension = ""
|
||||
description = "MokoStandards sync tracking file — records last sync date, version, and compliance status"
|
||||
name = ".gitea/.mokostandards"
|
||||
extension = "xml"
|
||||
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "managed-by-sync"
|
||||
source_type = "programmatic"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -83,13 +83,13 @@ locals {
|
||||
audience = "contributor"
|
||||
},
|
||||
{
|
||||
name = "update.xml"
|
||||
name = "updates.xml"
|
||||
extension = "xml"
|
||||
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
|
||||
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; managed by release workflow, never overwritten by sync"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
protected = true
|
||||
audience = "developer"
|
||||
template = "templates/joomla/update.xml.template"
|
||||
stub_content = <<-MOKO_END
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
@@ -101,10 +101,10 @@ locals {
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/raw/branch/main/update.xml
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/raw/branch/main/updates.xml
|
||||
</server>
|
||||
<server type="extension" priority="2" name="{{EXTENSION_NAME}}">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/{{REPO_NAME}}/main/update.xml
|
||||
https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/raw/branch/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
@@ -123,7 +123,7 @@ locals {
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
|
||||
</downloadurl>
|
||||
<downloadurl type="full" format="zip">
|
||||
https://github.com/mokoconsulting-tech/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
|
||||
https://git.mokoconsulting.tech/MokoConsulting/{{REPO_NAME}}/releases/download/v{{VERSION}}/{{EXTENSION_ELEMENT}}.zip
|
||||
</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="[56].*"/>
|
||||
@@ -221,13 +221,14 @@ locals {
|
||||
template = "templates/configs/composer.joomla.json"
|
||||
},
|
||||
{
|
||||
name = ".mokostandards"
|
||||
extension = "yml"
|
||||
description = "MokoStandards governance attachment — links this repo back to the standards source"
|
||||
name = ".gitea/.mokostandards"
|
||||
extension = "xml"
|
||||
description = "MokoStandards XML manifest — generated programmatically by RepositorySynchronizer::migrateMokoStandards()"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/configs/mokostandards.yml.template"
|
||||
template = "managed-by-sync"
|
||||
source_type = "programmatic"
|
||||
},
|
||||
{
|
||||
name = "GOVERNANCE.md"
|
||||
@@ -377,7 +378,7 @@ locals {
|
||||
{
|
||||
name = "update-server.md"
|
||||
extension = "md"
|
||||
description = "Joomla update server (update.xml) documentation"
|
||||
description = "Joomla update server (updates.xml) documentation"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "templates/docs/required/template-update-server-joomla.md"
|
||||
@@ -425,7 +426,7 @@ locals {
|
||||
{
|
||||
name = ".github"
|
||||
path = ".github"
|
||||
description = "GitHub-specific configuration"
|
||||
description = "Gitea/GitHub Actions configuration (Gitea reads .github/workflows natively)"
|
||||
requirement_status = "suggested"
|
||||
purpose = "Contains GitHub Actions workflows and configuration"
|
||||
files = [
|
||||
@@ -467,7 +468,7 @@ locals {
|
||||
> | Placeholder | Where to find the value |
|
||||
> |---|---|
|
||||
> | `{{REPO_NAME}}` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
||||
> | `{{REPO_URL}}` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
|
||||
> | `{{REPO_URL}}` | Full Gitea URL, e.g. `https://git.mokoconsulting.tech/MokoConsulting/<repo-name>` |
|
||||
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
|
||||
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
|
||||
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
|
||||
@@ -478,7 +479,7 @@ locals {
|
||||
|
||||
## What This Repo Is
|
||||
|
||||
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
|
||||
This is a **Moko Consulting MokoWaaS** (Joomla) repository governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). All coding standards, workflows, and policies are defined there and enforced here via bulk sync.
|
||||
|
||||
Repository URL: {{REPO_URL}}
|
||||
Extension name: **{{EXTENSION_NAME}}**
|
||||
@@ -552,13 +553,13 @@ locals {
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
|
||||
<!-- In update.xml — prepend a new <update> block for every release.
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
The version="[56].*" regex matches Joomla 5.x and 6.x. -->
|
||||
<updates>
|
||||
<update>
|
||||
@@ -582,7 +583,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required, see below)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -611,22 +612,22 @@ locals {
|
||||
|
||||
---
|
||||
|
||||
## update.xml — Required in Repo Root
|
||||
## updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="[56].*">` — Joomla treats the version value as a regex; `[56].*` matches Joomla 5.x and 6.x.
|
||||
|
||||
@@ -635,8 +636,8 @@ locals {
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
@@ -644,16 +645,16 @@ locals {
|
||||
|
||||
## GitHub Actions — Token Usage
|
||||
|
||||
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
|
||||
Every workflow must use **`secrets.GA_TOKEN`** (the Gitea API token). Use `secrets.GH_TOKEN` only for GitHub mirror operations (stable/RC releases).
|
||||
|
||||
```yaml
|
||||
# ✅ Correct
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
token: ${{ secrets.GA_TOKEN }}
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
```
|
||||
|
||||
```yaml
|
||||
@@ -666,16 +667,16 @@ locals {
|
||||
|
||||
## MokoStandards Reference
|
||||
|
||||
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards). Authoritative policies:
|
||||
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). Authoritative policies:
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
|
||||
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
||||
| [file-header-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR title/body conventions |
|
||||
| [changelog-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [joomla-development-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
||||
|
||||
---
|
||||
|
||||
@@ -714,8 +715,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -728,8 +729,8 @@ locals {
|
||||
- Never skip the FILE INFORMATION block on a new file
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
|
||||
- Use `secrets.GA_TOKEN` for Gitea operations. Use `secrets.GH_TOKEN` only for GitHub mirror (stable/RC). Never use `secrets.GITHUB_TOKEN` directly
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
MOKO_END
|
||||
},
|
||||
{
|
||||
@@ -762,7 +763,7 @@ locals {
|
||||
> | Placeholder | Where to find the value |
|
||||
> |---|---|
|
||||
> | `{{REPO_NAME}}` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
||||
> | `{{REPO_URL}}` | Full GitHub URL, e.g. `https://github.com/mokoconsulting-tech/<repo-name>` |
|
||||
> | `{{REPO_URL}}` | Full Gitea URL, e.g. `https://git.mokoconsulting.tech/MokoConsulting/<repo-name>` |
|
||||
> | `{{REPO_DESCRIPTION}}` | First paragraph of `README.md` body, or the GitHub repo description |
|
||||
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
|
||||
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
|
||||
@@ -780,7 +781,7 @@ locals {
|
||||
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
|
||||
Repository URL: {{REPO_URL}}
|
||||
|
||||
This repository is governed by [MokoStandards](https://github.com/mokoconsulting-tech/MokoStandards) — the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
|
||||
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards) — the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
|
||||
|
||||
---
|
||||
|
||||
@@ -789,7 +790,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -839,32 +840,32 @@ locals {
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `update.xml` | `<version>` in the most recent `<update>` block |
|
||||
| `updates.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# update.xml — Required in Repo Root
|
||||
# updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
|
||||
- `<targetplatform version="[56].*">` — Joomla treats the version value as a regex; `[56].*` matches Joomla 5.x and 6.x.
|
||||
|
||||
Example `update.xml` entry for a new release:
|
||||
Example `updates.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
@@ -948,16 +949,16 @@ locals {
|
||||
|
||||
# GitHub Actions — Token Usage
|
||||
|
||||
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
|
||||
Every workflow must use **`secrets.GA_TOKEN`** (the Gitea API token). Use `secrets.GH_TOKEN` only for GitHub mirror operations (stable/RC releases).
|
||||
|
||||
```yaml
|
||||
# ✅ Correct
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
token: ${{ secrets.GA_TOKEN }}
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||
```
|
||||
|
||||
```yaml
|
||||
@@ -973,8 +974,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -985,7 +986,7 @@ locals {
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
|
||||
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
@@ -999,7 +1000,7 @@ locals {
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
@@ -1010,117 +1011,109 @@ locals {
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [file-header-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
|
||||
| [changelog-standards.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [joomla-development-guide.md](https://github.com/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
||||
| [file-header-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
|
||||
| [changelog-standards.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [joomla-development-guide.md](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
||||
MOKO_END
|
||||
}
|
||||
]
|
||||
subdirectories = [
|
||||
{
|
||||
name = "workflows"
|
||||
path = ".github/workflows"
|
||||
description = "GitHub Actions workflows"
|
||||
path = ".gitea/workflows"
|
||||
description = "Gitea Actions CI/CD workflows"
|
||||
requirement_status = "required"
|
||||
files = [
|
||||
{
|
||||
name = "ci-joomla.yml"
|
||||
extension = "yml"
|
||||
description = "Joomla-specific CI workflow"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/joomla/ci-joomla.yml.template"
|
||||
},
|
||||
{
|
||||
name = "codeql-analysis.yml"
|
||||
extension = "yml"
|
||||
description = "CodeQL security analysis workflow"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/generic/codeql-analysis.yml.template"
|
||||
},
|
||||
{
|
||||
name = "standards-compliance.yml"
|
||||
extension = "yml"
|
||||
description = "MokoStandards compliance validation"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = ".github/workflows/standards-compliance.yml"
|
||||
},
|
||||
{
|
||||
name = "enterprise-firewall-setup.yml"
|
||||
extension = "yml"
|
||||
description = "Enterprise firewall configuration for trusted domain access"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/shared/enterprise-firewall-setup.yml.template"
|
||||
},
|
||||
{
|
||||
name = "deploy-dev.yml"
|
||||
extension = "yml"
|
||||
description = "SFTP deployment of src/ to the development server"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/shared/deploy-dev.yml.template"
|
||||
},
|
||||
{
|
||||
name = "deploy-demo.yml"
|
||||
extension = "yml"
|
||||
description = "SFTP deployment of src/ to the demo server on merge to main"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/shared/deploy-demo.yml.template"
|
||||
},
|
||||
{
|
||||
name = "deploy-rs.yml"
|
||||
extension = "yml"
|
||||
description = "SFTP deployment of src/ to the release staging server on merge to main"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/shared/deploy-rs.yml.template"
|
||||
},
|
||||
{
|
||||
name = "sync-version-on-merge.yml"
|
||||
extension = "yml"
|
||||
description = "Auto-bump patch version on merge and propagate to all file headers"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/shared/sync-version-on-merge.yml.template"
|
||||
},
|
||||
{
|
||||
name = "auto-release.yml"
|
||||
extension = "yml"
|
||||
description = "Auto-create GitHub Release on push to main with version from README.md"
|
||||
description = "Automated release — builds zip, creates Gitea release, updates SHA in updates.xml. Triggered by push to main (stable) or pre-release tags"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/shared/auto-release.yml.template"
|
||||
template = "workflows/auto-release.yml"
|
||||
},
|
||||
{
|
||||
name = "repository-cleanup.yml"
|
||||
name = "ci-dolibarr.yml"
|
||||
extension = "yml"
|
||||
description = "Scheduled cleanup: delete retired workflows, stale branches, old workflow runs"
|
||||
description = "Continuous integration — PHP linting, PHPStan static analysis, Dolibarr module validation"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/shared/repository-cleanup.yml.template"
|
||||
template = "workflows/ci-dolibarr.yml"
|
||||
},
|
||||
{
|
||||
name = "auto-dev-issue.yml"
|
||||
name = "publish-to-mokodolimods.yml"
|
||||
extension = "yml"
|
||||
description = "Auto-create tracking issue when a dev/** branch is pushed"
|
||||
description = "On release, copies src/ into htdocs/custom/ in mokodolimods repo and opens a PR"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/shared/auto-dev-issue.yml.template"
|
||||
template = "workflows/publish-to-mokodolimods.yml"
|
||||
},
|
||||
{
|
||||
name = "repo_health.yml"
|
||||
name = "pre-release.yml"
|
||||
extension = "yml"
|
||||
description = "Joomla-specific repository health check workflow"
|
||||
description = "Manual pre-release — builds dev/alpha/beta/rc packages with patch version bump"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "templates/workflows/joomla/repo_health.yml.template"
|
||||
template = "workflows/pre-release.yml"
|
||||
},
|
||||
{
|
||||
name = "deploy-manual.yml"
|
||||
extension = "yml"
|
||||
description = "Manual deployment — allows selecting target environment and branch for on-demand deploys"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "workflows/deploy-manual.yml"
|
||||
},
|
||||
{
|
||||
name = "repo-health.yml"
|
||||
extension = "yml"
|
||||
description = "Repository health checks — validates required files, structure compliance, and standards alignment"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "workflows/repo-health.yml"
|
||||
},
|
||||
{
|
||||
name = "update-server.yml"
|
||||
extension = "yml"
|
||||
description = "Update server maintenance — validates updates.xml format and ensures download URLs are reachable"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "workflows/update-server.yml"
|
||||
},
|
||||
{
|
||||
name = "pr-check.yml"
|
||||
extension = "yml"
|
||||
description = "PR gate — validates PHP syntax, manifest XML, and package build before merge to main"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "workflows/pr-check.yml"
|
||||
},
|
||||
{
|
||||
name = "security-audit.yml"
|
||||
extension = "yml"
|
||||
description = "Dependency vulnerability scanning — weekly schedule and on PR when lock files change"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "workflows/security-audit.yml"
|
||||
},
|
||||
{
|
||||
name = "notify.yml"
|
||||
extension = "yml"
|
||||
description = "Push notifications via ntfy on release success or workflow failure"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "workflows/notify.yml"
|
||||
},
|
||||
{
|
||||
name = "cleanup.yml"
|
||||
extension = "yml"
|
||||
description = "Scheduled cleanup — delete merged branches and old workflow runs weekly"
|
||||
requirement_status = "required"
|
||||
always_overwrite = true
|
||||
template = "workflows/cleanup.yml"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -34,7 +34,7 @@ locals {
|
||||
{ path = "SECURITY.md" action = "updated" },
|
||||
{ path = "CODE_OF_CONDUCT.md" action = "updated" },
|
||||
{ path = "CONTRIBUTING.md" action = "updated" },
|
||||
{ path = "update.xml" action = "updated" },
|
||||
{ path = "updates.xml" action = "updated" },
|
||||
{ path = "phpstan.neon" action = "updated" },
|
||||
{ path = "Makefile" action = "updated" },
|
||||
{ path = ".gitignore" action = "updated" },
|
||||
@@ -165,13 +165,13 @@ locals {
|
||||
audience = "contributor"
|
||||
},
|
||||
{
|
||||
name = "update.xml"
|
||||
name = "updates.xml"
|
||||
extension = "xml"
|
||||
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/joomla/update.xml.template"
|
||||
template = "templates/joomla/updates.xml.template"
|
||||
stub_content = <<-MOKO_END
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
@@ -183,7 +183,7 @@ locals {
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
@@ -454,7 +454,7 @@ locals {
|
||||
{
|
||||
name = "update-server.md"
|
||||
extension = "md"
|
||||
description = "Joomla update server (update.xml) documentation"
|
||||
description = "Joomla update server (updates.xml) documentation"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "templates/docs/required/template-update-server-joomla.md"
|
||||
@@ -629,13 +629,13 @@ locals {
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
|
||||
<!-- In update.xml — prepend a new <update> block for every release.
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
||||
in the XML attribute value. Joomla's update server treats the value as a
|
||||
regular expression, so \. matches a literal dot. -->
|
||||
@@ -661,7 +661,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required, see below)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -690,22 +690,22 @@ locals {
|
||||
|
||||
---
|
||||
|
||||
## update.xml — Required in Repo Root
|
||||
## updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
||||
|
||||
@@ -714,8 +714,8 @@ locals {
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
@@ -793,8 +793,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -808,7 +808,7 @@ locals {
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
MOKO_END
|
||||
},
|
||||
{
|
||||
@@ -868,7 +868,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -918,32 +918,32 @@ locals {
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `update.xml` | `<version>` in the most recent `<update>` block |
|
||||
| `updates.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# update.xml — Required in Repo Root
|
||||
# updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
|
||||
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
|
||||
|
||||
Example `update.xml` entry for a new release:
|
||||
Example `updates.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
@@ -1052,8 +1052,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -1064,7 +1064,7 @@ locals {
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
|
||||
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
@@ -1078,7 +1078,7 @@ locals {
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
|
||||
@@ -34,7 +34,7 @@ locals {
|
||||
{ path = "SECURITY.md" action = "created" },
|
||||
{ path = "CODE_OF_CONDUCT.md" action = "updated" },
|
||||
{ path = "CONTRIBUTING.md" action = "updated" },
|
||||
{ path = "update.xml" action = "updated" },
|
||||
{ path = "updates.xml" action = "updated" },
|
||||
{ path = "phpstan.neon" action = "updated" },
|
||||
{ path = "Makefile" action = "updated" },
|
||||
{ path = ".gitignore" action = "updated" },
|
||||
@@ -164,13 +164,13 @@ locals {
|
||||
audience = "contributor"
|
||||
},
|
||||
{
|
||||
name = "update.xml"
|
||||
name = "updates.xml"
|
||||
extension = "xml"
|
||||
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/joomla/update.xml.template"
|
||||
template = "templates/joomla/updates.xml.template"
|
||||
stub_content = <<-MOKO_END
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
@@ -182,7 +182,7 @@ locals {
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
@@ -453,7 +453,7 @@ locals {
|
||||
{
|
||||
name = "update-server.md"
|
||||
extension = "md"
|
||||
description = "Joomla update server (update.xml) documentation"
|
||||
description = "Joomla update server (updates.xml) documentation"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "templates/docs/required/template-update-server-joomla.md"
|
||||
@@ -628,13 +628,13 @@ locals {
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
|
||||
<!-- In update.xml — prepend a new <update> block for every release.
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
||||
in the XML attribute value. Joomla's update server treats the value as a
|
||||
regular expression, so \. matches a literal dot. -->
|
||||
@@ -660,7 +660,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required, see below)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -689,22 +689,22 @@ locals {
|
||||
|
||||
---
|
||||
|
||||
## update.xml — Required in Repo Root
|
||||
## updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
||||
|
||||
@@ -713,8 +713,8 @@ locals {
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
@@ -792,8 +792,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -807,7 +807,7 @@ locals {
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
MOKO_END
|
||||
},
|
||||
{
|
||||
@@ -867,7 +867,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -917,32 +917,32 @@ locals {
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `update.xml` | `<version>` in the most recent `<update>` block |
|
||||
| `updates.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# update.xml — Required in Repo Root
|
||||
# updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
|
||||
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
|
||||
|
||||
Example `update.xml` entry for a new release:
|
||||
Example `updates.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
@@ -1051,8 +1051,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -1063,7 +1063,7 @@ locals {
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
|
||||
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
@@ -1077,7 +1077,7 @@ locals {
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
|
||||
@@ -34,7 +34,7 @@ locals {
|
||||
{ path = "SECURITY.md" action = "updated" },
|
||||
{ path = "CODE_OF_CONDUCT.md" action = "updated" },
|
||||
{ path = "CONTRIBUTING.md" action = "updated" },
|
||||
{ path = "update.xml" action = "updated" },
|
||||
{ path = "updates.xml" action = "updated" },
|
||||
{ path = "phpstan.neon" action = "updated" },
|
||||
{ path = "Makefile" action = "updated" },
|
||||
{ path = ".gitignore" action = "updated" },
|
||||
@@ -164,13 +164,13 @@ locals {
|
||||
audience = "contributor"
|
||||
},
|
||||
{
|
||||
name = "update.xml"
|
||||
name = "updates.xml"
|
||||
extension = "xml"
|
||||
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/joomla/update.xml.template"
|
||||
template = "templates/joomla/updates.xml.template"
|
||||
stub_content = <<-MOKO_END
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
@@ -182,7 +182,7 @@ locals {
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
@@ -453,7 +453,7 @@ locals {
|
||||
{
|
||||
name = "update-server.md"
|
||||
extension = "md"
|
||||
description = "Joomla update server (update.xml) documentation"
|
||||
description = "Joomla update server (updates.xml) documentation"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "templates/docs/required/template-update-server-joomla.md"
|
||||
@@ -628,13 +628,13 @@ locals {
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
|
||||
<!-- In update.xml — prepend a new <update> block for every release.
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
||||
in the XML attribute value. Joomla's update server treats the value as a
|
||||
regular expression, so \. matches a literal dot. -->
|
||||
@@ -660,7 +660,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required, see below)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -689,22 +689,22 @@ locals {
|
||||
|
||||
---
|
||||
|
||||
## update.xml — Required in Repo Root
|
||||
## updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
||||
|
||||
@@ -713,8 +713,8 @@ locals {
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
@@ -792,8 +792,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -807,7 +807,7 @@ locals {
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
MOKO_END
|
||||
},
|
||||
{
|
||||
@@ -867,7 +867,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -917,32 +917,32 @@ locals {
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `update.xml` | `<version>` in the most recent `<update>` block |
|
||||
| `updates.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# update.xml — Required in Repo Root
|
||||
# updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
|
||||
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
|
||||
|
||||
Example `update.xml` entry for a new release:
|
||||
Example `updates.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
@@ -1051,8 +1051,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -1063,7 +1063,7 @@ locals {
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
|
||||
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
@@ -1077,7 +1077,7 @@ locals {
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -34,7 +34,7 @@ locals {
|
||||
{ path = "SECURITY.md" action = "created" },
|
||||
{ path = "CODE_OF_CONDUCT.md" action = "updated" },
|
||||
{ path = "CONTRIBUTING.md" action = "updated" },
|
||||
{ path = "update.xml" action = "updated" },
|
||||
{ path = "updates.xml" action = "updated" },
|
||||
{ path = "phpstan.neon" action = "updated" },
|
||||
{ path = "Makefile" action = "updated" },
|
||||
{ path = ".gitignore" action = "updated" },
|
||||
@@ -165,13 +165,13 @@ locals {
|
||||
audience = "contributor"
|
||||
},
|
||||
{
|
||||
name = "update.xml"
|
||||
name = "updates.xml"
|
||||
extension = "xml"
|
||||
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/joomla/update.xml.template"
|
||||
template = "templates/joomla/updates.xml.template"
|
||||
stub_content = <<-MOKO_END
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
@@ -183,7 +183,7 @@ locals {
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
@@ -454,7 +454,7 @@ locals {
|
||||
{
|
||||
name = "update-server.md"
|
||||
extension = "md"
|
||||
description = "Joomla update server (update.xml) documentation"
|
||||
description = "Joomla update server (updates.xml) documentation"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "templates/docs/required/template-update-server-joomla.md"
|
||||
@@ -629,13 +629,13 @@ locals {
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
|
||||
<!-- In update.xml — prepend a new <update> block for every release.
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
||||
in the XML attribute value. Joomla's update server treats the value as a
|
||||
regular expression, so \. matches a literal dot. -->
|
||||
@@ -661,7 +661,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required, see below)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -690,22 +690,22 @@ locals {
|
||||
|
||||
---
|
||||
|
||||
## update.xml — Required in Repo Root
|
||||
## updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
||||
|
||||
@@ -714,8 +714,8 @@ locals {
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
@@ -793,8 +793,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -808,7 +808,7 @@ locals {
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
MOKO_END
|
||||
},
|
||||
{
|
||||
@@ -868,7 +868,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -918,32 +918,32 @@ locals {
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `update.xml` | `<version>` in the most recent `<update>` block |
|
||||
| `updates.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# update.xml — Required in Repo Root
|
||||
# updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
|
||||
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
|
||||
|
||||
Example `update.xml` entry for a new release:
|
||||
Example `updates.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
@@ -1052,8 +1052,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -1064,7 +1064,7 @@ locals {
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
|
||||
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
@@ -1078,7 +1078,7 @@ locals {
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
|
||||
@@ -34,7 +34,7 @@ locals {
|
||||
{ path = "SECURITY.md" action = "created" },
|
||||
{ path = "CODE_OF_CONDUCT.md" action = "updated" },
|
||||
{ path = "CONTRIBUTING.md" action = "updated" },
|
||||
{ path = "update.xml" action = "updated" },
|
||||
{ path = "updates.xml" action = "updated" },
|
||||
{ path = "phpstan.neon" action = "updated" },
|
||||
{ path = "Makefile" action = "updated" },
|
||||
{ path = ".gitignore" action = "updated" },
|
||||
@@ -165,13 +165,13 @@ locals {
|
||||
audience = "contributor"
|
||||
},
|
||||
{
|
||||
name = "update.xml"
|
||||
name = "updates.xml"
|
||||
extension = "xml"
|
||||
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/joomla/update.xml.template"
|
||||
template = "templates/joomla/updates.xml.template"
|
||||
stub_content = <<-MOKO_END
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
@@ -183,7 +183,7 @@ locals {
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
@@ -454,7 +454,7 @@ locals {
|
||||
{
|
||||
name = "update-server.md"
|
||||
extension = "md"
|
||||
description = "Joomla update server (update.xml) documentation"
|
||||
description = "Joomla update server (updates.xml) documentation"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "templates/docs/required/template-update-server-joomla.md"
|
||||
@@ -629,13 +629,13 @@ locals {
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
|
||||
<!-- In update.xml — prepend a new <update> block for every release.
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
||||
in the XML attribute value. Joomla's update server treats the value as a
|
||||
regular expression, so \. matches a literal dot. -->
|
||||
@@ -661,7 +661,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required, see below)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -690,22 +690,22 @@ locals {
|
||||
|
||||
---
|
||||
|
||||
## update.xml — Required in Repo Root
|
||||
## updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
||||
|
||||
@@ -714,8 +714,8 @@ locals {
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
@@ -793,8 +793,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -808,7 +808,7 @@ locals {
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
MOKO_END
|
||||
},
|
||||
{
|
||||
@@ -868,7 +868,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -918,32 +918,32 @@ locals {
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `update.xml` | `<version>` in the most recent `<update>` block |
|
||||
| `updates.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# update.xml — Required in Repo Root
|
||||
# updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
|
||||
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
|
||||
|
||||
Example `update.xml` entry for a new release:
|
||||
Example `updates.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
@@ -1052,8 +1052,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -1064,7 +1064,7 @@ locals {
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
|
||||
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
@@ -1078,7 +1078,7 @@ locals {
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
|
||||
@@ -31,7 +31,7 @@ locals {
|
||||
|
||||
synced_files = [
|
||||
{ path = "LICENSE" action = "updated" },
|
||||
{ path = "update.xml" action = "updated" },
|
||||
{ path = "updates.xml" action = "updated" },
|
||||
{ path = "phpstan.neon" action = "updated" },
|
||||
{ path = "Makefile" action = "updated" },
|
||||
{ path = ".gitignore" action = "updated" },
|
||||
@@ -149,13 +149,13 @@ locals {
|
||||
audience = "contributor"
|
||||
},
|
||||
{
|
||||
name = "update.xml"
|
||||
name = "updates.xml"
|
||||
extension = "xml"
|
||||
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/joomla/update.xml.template"
|
||||
template = "templates/joomla/updates.xml.template"
|
||||
stub_content = <<-MOKO_END
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
@@ -167,7 +167,7 @@ locals {
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
@@ -605,13 +605,13 @@ locals {
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
|
||||
<!-- In update.xml — prepend a new <update> block for every release.
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
||||
in the XML attribute value. Joomla's update server treats the value as a
|
||||
regular expression, so \. matches a literal dot. -->
|
||||
@@ -637,7 +637,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required, see below)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -666,22 +666,22 @@ locals {
|
||||
|
||||
---
|
||||
|
||||
## update.xml — Required in Repo Root
|
||||
## updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
||||
|
||||
@@ -690,8 +690,8 @@ locals {
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
@@ -769,8 +769,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -784,7 +784,7 @@ locals {
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
MOKO_END
|
||||
},
|
||||
{
|
||||
@@ -844,7 +844,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -894,32 +894,32 @@ locals {
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `update.xml` | `<version>` in the most recent `<update>` block |
|
||||
| `updates.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# update.xml — Required in Repo Root
|
||||
# updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
|
||||
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
|
||||
|
||||
Example `update.xml` entry for a new release:
|
||||
Example `updates.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
@@ -1028,8 +1028,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -1040,7 +1040,7 @@ locals {
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
|
||||
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
@@ -1054,7 +1054,7 @@ locals {
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
|
||||
@@ -34,7 +34,7 @@ locals {
|
||||
{ path = "SECURITY.md" action = "updated" },
|
||||
{ path = "CODE_OF_CONDUCT.md" action = "updated" },
|
||||
{ path = "CONTRIBUTING.md" action = "updated" },
|
||||
{ path = "update.xml" action = "created" },
|
||||
{ path = "updates.xml" action = "created" },
|
||||
{ path = "phpstan.neon" action = "updated" },
|
||||
{ path = "Makefile" action = "created" },
|
||||
{ path = ".gitignore" action = "updated" },
|
||||
@@ -165,13 +165,13 @@ locals {
|
||||
audience = "contributor"
|
||||
},
|
||||
{
|
||||
name = "update.xml"
|
||||
name = "updates.xml"
|
||||
extension = "xml"
|
||||
description = "Joomla extension update server manifest — lists releases for Joomla auto-update; must be kept in sync with manifest.xml version"
|
||||
required = true
|
||||
always_overwrite = false
|
||||
audience = "developer"
|
||||
template = "templates/joomla/update.xml.template"
|
||||
template = "templates/joomla/updates.xml.template"
|
||||
stub_content = <<-MOKO_END
|
||||
<!--
|
||||
Joomla Extension Update Server XML
|
||||
@@ -183,7 +183,7 @@ locals {
|
||||
The manifest.xml in this repository must reference this file:
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
|
||||
@@ -454,7 +454,7 @@ locals {
|
||||
{
|
||||
name = "update-server.md"
|
||||
extension = "md"
|
||||
description = "Joomla update server (update.xml) documentation"
|
||||
description = "Joomla update server (updates.xml) documentation"
|
||||
required = true
|
||||
always_overwrite = true
|
||||
template = "templates/docs/required/template-update-server-joomla.md"
|
||||
@@ -629,13 +629,13 @@ locals {
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `update.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
The version in `README.md` **must always match** the `<version>` tag in `manifest.xml` and the latest entry in `updates.xml`. The `make release` command / release workflow updates all three automatically.
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — must match README.md version -->
|
||||
<version>01.02.04</version>
|
||||
|
||||
<!-- In update.xml — prepend a new <update> block for every release.
|
||||
<!-- In updates.xml — prepend a new <update> block for every release.
|
||||
Note: the backslash in version="4\.[0-9]+" is a literal backslash character
|
||||
in the XML attribute value. Joomla's update server treats the value as a
|
||||
regular expression, so \. matches a literal dot. -->
|
||||
@@ -661,7 +661,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required, see below)
|
||||
├── updates.xml # Update server manifest (root — required, see below)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -690,22 +690,22 @@ locals {
|
||||
|
||||
---
|
||||
|
||||
## update.xml — Required in Repo Root
|
||||
## updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
`updates.xml` **must exist at the repository root**. It is the Joomla update server manifest that allows Joomla installations to check for new versions of this extension.
|
||||
|
||||
The `manifest.xml` must reference it via:
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release must prepend a new `<update>` block at the top of `update.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- Every release must prepend a new `<update>` block at the top of `updates.xml` — old entries must be preserved below.
|
||||
- The `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and the version in `README.md`.
|
||||
- The `<downloadurl>` must be a publicly accessible direct download link (GitHub Releases asset URL).
|
||||
- `<targetplatform name="joomla" version="4\.[0-9]+">` — the backslash is a **literal backslash character** in the XML attribute value; Joomla's update-server parser treats the value as a regular expression, so `\.` matches a literal dot and `[0-9]+` matches one or more digits. Do not double-escape it.
|
||||
|
||||
@@ -714,8 +714,8 @@ locals {
|
||||
## manifest.xml Rules
|
||||
|
||||
- Lives at the repo root as `manifest.xml` (not inside `site/` or `admin/`).
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `update.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `update.xml`.
|
||||
- `<version>` tag must be kept in sync with `README.md` version and `updates.xml`.
|
||||
- Must include `<updateservers>` block pointing to this repo's `updates.xml`.
|
||||
- Must include `<files folder="site">` and `<administration>` sections.
|
||||
- Joomla 4.x requires `<namespace path="src">Moko\{{EXTENSION_NAME}}</namespace>` for namespaced extensions.
|
||||
|
||||
@@ -793,8 +793,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed manifest.xml | Update `update.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `update.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed manifest.xml | Update `updates.xml` version; bump README.md version |
|
||||
| New release | Prepend `<update>` block to `updates.xml`; update CHANGELOG.md; bump README.md version |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -808,7 +808,7 @@ locals {
|
||||
- Never add `defined('_JEXEC') or die;` to CLI scripts or model tests — only to web-accessible PHP files
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `update.xml` version, and `README.md` version go out of sync
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
MOKO_END
|
||||
},
|
||||
{
|
||||
@@ -868,7 +868,7 @@ locals {
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── update.xml # Update server manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
@@ -918,32 +918,32 @@ locals {
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `update.xml` | `<version>` in the most recent `<update>` block |
|
||||
| `updates.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# update.xml — Required in Repo Root
|
||||
# updates.xml — Required in Repo Root
|
||||
|
||||
`update.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}}">
|
||||
{{REPO_URL}}/raw/main/update.xml
|
||||
{{REPO_URL}}/raw/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `update.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloadurl>` must be a publicly accessible GitHub Releases asset URL.
|
||||
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
|
||||
|
||||
Example `update.xml` entry for a new release:
|
||||
Example `updates.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
@@ -1052,8 +1052,8 @@ locals {
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed `manifest.xml` | Sync version to `update.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `update.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
@@ -1064,7 +1064,7 @@ locals {
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `update.xml`, and `README.md` versions diverge.**
|
||||
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
@@ -1078,7 +1078,7 @@ locals {
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `update.xml` updated with new entry
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Repository Sync Tracking Definition: mokoconsulting-tech/joomla-api-mcp
|
||||
*
|
||||
* Auto-generated by MokoStandards bulk sync on 2026-04-23T00:00:00+00:00
|
||||
* Platform : default-repository
|
||||
* Description: MCP server for Joomla Web Services API operations
|
||||
*
|
||||
* DO NOT EDIT MANUALLY — this file is regenerated on every successful sync.
|
||||
* To change what gets synced, edit api/definitions/default/default-repository.tf
|
||||
* and re-run the bulk-repo-sync workflow.
|
||||
*/
|
||||
|
||||
locals {
|
||||
sync_record = {
|
||||
metadata = {
|
||||
repo = "mokoconsulting-tech/joomla-api-mcp"
|
||||
default_branch = "main"
|
||||
detected_platform = "default-repository"
|
||||
description = "MCP server for Joomla Web Services API operations"
|
||||
sync_timestamp = "2026-04-23T00:00:00+00:00"
|
||||
source_repo = "mokoconsulting-tech/MokoStandards"
|
||||
base_definition = "api/definitions/default/default-repository.tf"
|
||||
}
|
||||
|
||||
sync_stats = {
|
||||
total_files = 0
|
||||
created_files = 0
|
||||
updated_files = 0
|
||||
skipped_files = 0
|
||||
}
|
||||
|
||||
synced_files = []
|
||||
|
||||
skipped_files = [
|
||||
{ path = "README.md" reason = "README — never overwritten" },
|
||||
{ path = "CHANGELOG.md" reason = "CHANGELOG — never overwritten" },
|
||||
{ path = "CONTRIBUTING.md" reason = "Preserved (always_overwrite=false)" },
|
||||
{ path = "SECURITY.md" reason = "Preserved (always_overwrite=false)" },
|
||||
{ path = "CODE_OF_CONDUCT.md" reason = "Preserved (always_overwrite=false)" },
|
||||
{ path = "ROADMAP.md" reason = "Preserved (always_overwrite=false)" },
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,9 @@
|
||||
* changes — only XML manifest changes require a Joomla reinstall.
|
||||
*
|
||||
* USAGE
|
||||
* php api/deploy/deploy-joomla.php --path . --config /tmp/sftp-config.json
|
||||
* php api/deploy/deploy-joomla.php --path . --config /tmp/sftp-config.json --dry-run
|
||||
* php api/deploy/deploy-joomla.php --path . --env dev
|
||||
* php deploy/deploy-joomla.php --path . --config /tmp/sftp-config.json
|
||||
* php deploy/deploy-joomla.php --path . --config /tmp/sftp-config.json --dry-run
|
||||
* php deploy/deploy-joomla.php --path . --env dev
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -96,25 +96,25 @@ CONFIG FORMAT
|
||||
|
||||
EXAMPLES
|
||||
# Dry-run preview of dev deployment
|
||||
php api/deploy/deploy-sftp.php --env dev --dry-run --verbose
|
||||
php deploy/deploy-sftp.php --env dev --dry-run --verbose
|
||||
|
||||
# Deploy to dev server
|
||||
php api/deploy/deploy-sftp.php --path /repos/mymodule --env dev
|
||||
php deploy/deploy-sftp.php --path /repos/mymodule --env dev
|
||||
|
||||
# Deploy to release/production server
|
||||
php api/deploy/deploy-sftp.php --path /repos/mymodule --env rs
|
||||
php deploy/deploy-sftp.php --path /repos/mymodule --env rs
|
||||
|
||||
# Use a different source directory
|
||||
php api/deploy/deploy-sftp.php --env dev --src-dir htdocs
|
||||
php deploy/deploy-sftp.php --env dev --src-dir htdocs
|
||||
|
||||
# Explicit config with encrypted key
|
||||
php api/deploy/deploy-sftp.php \
|
||||
php deploy/deploy-sftp.php \
|
||||
--path /repos/mymodule \
|
||||
--env rs \
|
||||
--key-passphrase "my passphrase"
|
||||
|
||||
# Quiet mode (errors only)
|
||||
php api/deploy/deploy-sftp.php --env dev --quiet
|
||||
php deploy/deploy-sftp.php --env dev --quiet
|
||||
|
||||
EXIT CODES
|
||||
0 All files uploaded successfully
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# Workflow Standards
|
||||
|
||||
> Canonical reference for Gitea Actions CI/CD workflows across all Moko Consulting repositories.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Template Repos (canonical source) → Production Repos (synced copies)
|
||||
───────────────────────────────────── ──────────────────────────────────
|
||||
MokoStandards-Template-Joomla → MokoOnyx, MokoCassiopeia, MokoJGDPC, etc.
|
||||
MokoStandards-Template-Dolibarr → MokoCRM, MokoDoliForm, MokoDoliAuth, etc.
|
||||
MokoStandards-Template-Generic → MokoISOUpdatePortable, etc.
|
||||
MokoStandards-Template-Client → client-clarksvillefurs, client-kiddieland
|
||||
```
|
||||
|
||||
**MokoOnyx** is the living reference implementation for Joomla workflows. Template repos are the **single source of truth** for workflow content. The MokoStandards-API repo does NOT store workflow templates — its sync engine (`RepositorySynchronizer.php`) clones template repos at runtime to get the latest workflows.
|
||||
|
||||
### How Sync Works
|
||||
|
||||
```
|
||||
bulk-repo-sync.yml (API repo)
|
||||
→ RepositorySynchronizer.php detects platform type
|
||||
→ Clones the matching template repo to /tmp/
|
||||
→ Copies .gitea/workflows/*.yml from template → target repo
|
||||
```
|
||||
|
||||
No workflow files are stored in the API repo. This prevents drift.
|
||||
|
||||
## Template Repos
|
||||
|
||||
| Repo | Purpose | Types |
|
||||
|------|---------|-------|
|
||||
| `MokoStandards-Template-Joomla` | All Joomla extension types in one repo | plugin, template, module, component, package, library |
|
||||
| `MokoStandards-Template-Dolibarr` | Dolibarr module scaffold | — |
|
||||
| `MokoStandards-Template-Generic` | Non-platform projects | — |
|
||||
| `MokoStandards-Template-Client` | Client Joomla sites with media sync | — |
|
||||
|
||||
## Standard Workflow Suite
|
||||
|
||||
### Joomla Repositories (10 workflows)
|
||||
|
||||
| Workflow | Trigger | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `auto-release.yml` | PR merge to main (src/ changes) | Stable release: zip, Gitea release, version bump, updates.xml |
|
||||
| `pre-release.yml` | Manual dispatch | Dev/alpha/beta/rc: patch bump, zip, pre-release |
|
||||
| `ci-joomla.yml` | PRs to main | PHP lint, PHPStan, coding standards |
|
||||
| `pr-check.yml` | PRs to main | Gate: manifest XML validation, build test |
|
||||
| `deploy-manual.yml` | Manual dispatch | SFTP deploy to selected environment |
|
||||
| `repo-health.yml` | Weekly schedule / manual | Structure compliance, required files |
|
||||
| `update-server.yml` | Weekly schedule / manual | Validate updates.xml format + download URLs |
|
||||
| `security-audit.yml` | Weekly + PR (lock file changes) | Dependency vulnerability scanning |
|
||||
| `notify.yml` | Workflow completion | ntfy push on release success or failure |
|
||||
| `cleanup.yml` | Weekly (Sunday 03:00 UTC) | Delete merged branches + old workflow runs |
|
||||
|
||||
### Dolibarr Repositories (11 workflows)
|
||||
|
||||
Same as Joomla except:
|
||||
- `ci-dolibarr.yml` replaces `ci-joomla.yml` (Dolibarr-specific validation)
|
||||
- `publish-to-mokodolimods.yml` added (copies src/ to mokodolimods on release)
|
||||
|
||||
### Generic Repositories (9 workflows)
|
||||
|
||||
Same as Joomla minus `ci-joomla.yml` (no platform-specific CI).
|
||||
|
||||
### Client Repositories (10 workflows)
|
||||
|
||||
Same as Joomla minus `update-server.yml` (no updates.xml — clients are sites, not extensions), plus:
|
||||
- `sync-media.yml` — Bidirectional SFTP sync for `images/`, `files/`, `media/` between dev and production (every 6 hours + manual dispatch)
|
||||
|
||||
**Per-client repo variables required for sync:**
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `DEV_SYNC_HOST` | Dev server hostname |
|
||||
| `DEV_SYNC_PORT` | Dev SSH port (default 22) |
|
||||
| `DEV_SYNC_USERNAME` | Dev server user |
|
||||
| `DEV_SYNC_PATH` | Base path on dev |
|
||||
| `PROD_SYNC_HOST` | Production server hostname |
|
||||
| `PROD_SYNC_PORT` | Production SSH port (default 22) |
|
||||
| `PROD_SYNC_USERNAME` | Production server user |
|
||||
| `PROD_SYNC_PATH` | Base path on production |
|
||||
|
||||
**Per-client repo secrets:** `DEV_SYNC_KEY`, `PROD_SYNC_KEY`
|
||||
|
||||
## Release Model
|
||||
|
||||
```
|
||||
Feature branch → PR → merge to main → auto-release.yml (STABLE)
|
||||
↓
|
||||
pre-release.yml (manual dispatch for dev/alpha/beta/rc)
|
||||
```
|
||||
|
||||
- **Stable releases** trigger automatically on PR merge to main (with `src/` changes)
|
||||
- **Pre-releases** (dev, alpha, beta, rc) are manual via workflow_dispatch
|
||||
- All releases overwrite the previous release for that channel (no history accumulation)
|
||||
- Higher releases cascade-delete lower ones (stable deletes all pre-releases, rc deletes beta+alpha+dev, etc.)
|
||||
|
||||
### Version Bump Policy
|
||||
|
||||
| Trigger | Bump | Example |
|
||||
|---------|------|---------|
|
||||
| Stable (PR merge to main) | **Minor** — reset patch to 00 | `03.00.07` → `03.01.00` |
|
||||
| Pre-release (manual) | **Patch** | `03.00.07` → `03.00.08` |
|
||||
| Patch rollover (99→00) | Auto-bump minor | `03.00.99` → `03.01.00` |
|
||||
| Minor rollover (99→00) | Auto-bump major | `03.99.00` → `04.00.00` |
|
||||
|
||||
## Org-Level Configuration
|
||||
|
||||
These secrets and variables are set at the MokoConsulting org level and available to all repos:
|
||||
|
||||
### Secrets
|
||||
| Name | Purpose |
|
||||
|------|---------|
|
||||
| `GA_TOKEN` | Gitea API token for releases, branch operations |
|
||||
| `GH_TOKEN` | GitHub token for mirrors |
|
||||
| `DEPLOY_SSH_KEY` | Universal SSH key for SFTP deploys |
|
||||
| `DEV_SSH_KEY` | Dev server SSH key |
|
||||
| `DEMO_FTP_KEY` | Demo server SFTP key |
|
||||
|
||||
### Variables
|
||||
| Name | Value | Purpose |
|
||||
|------|-------|---------|
|
||||
| `NTFY_URL` | `https://ntfy.mokoconsulting.tech` | Notification server |
|
||||
| `NTFY_TOPIC` | `gitea-releases` | Default notification topic |
|
||||
| `DEV_SSH_HOST` | `dev.mokoconsulting.tech` | Dev server hostname |
|
||||
| `DEV_SSH_PORT` | `22` | Dev server SSH port |
|
||||
| `DEV_SSH_USERNAME` | `mokoconsulting_dev` | Dev server username |
|
||||
| `DEMO_FTP_HOST` | `demo.mokoconsulting.tech` | Demo server hostname |
|
||||
| `DEMO_FTP_PORT` | `22` | Demo server port |
|
||||
| `DEMO_FTP_USERNAME` | `mokoconsulting_demo` | Demo server username |
|
||||
|
||||
## Syncing Workflows
|
||||
|
||||
To update workflows across all repos from the canonical template:
|
||||
|
||||
```bash
|
||||
# Joomla repos — sync from unified template
|
||||
for REPO in MokoOnyx MokoCassiopeia MokoJGDPC MokoJoomHero MokoJoomTOS MokoWaaS MokoWaaSAnnounce MokoDPCalendarAPI; do
|
||||
cd /a/$REPO
|
||||
rm -f .gitea/workflows/*.yml
|
||||
cp /a/MokoStandards-Template-Joomla/.gitea/workflows/*.yml .gitea/workflows/
|
||||
git add .gitea/workflows/ && git commit -m "chore: sync workflows" && git push
|
||||
done
|
||||
|
||||
# Dolibarr repos — sync from Dolibarr template
|
||||
for REPO in MokoCRM MokoDoliForm MokoDoliAuth MokoDolibarr ...; do
|
||||
cd /a/$REPO
|
||||
rm -f .gitea/workflows/*.yml
|
||||
cp /a/MokoStandards-Template-Dolibarr/.gitea/workflows/*.yml .gitea/workflows/
|
||||
git add .gitea/workflows/ && git commit -m "chore: sync workflows" && git push
|
||||
done
|
||||
|
||||
# Client repos — sync from Client template
|
||||
for REPO in client-clarksvillefurs client-kiddieland; do
|
||||
cd /a/$REPO
|
||||
rm -f .gitea/workflows/*.yml
|
||||
cp /a/MokoStandards-Template-Client/.gitea/workflows/*.yml .gitea/workflows/
|
||||
git add .gitea/workflows/ && git commit -m "chore: sync workflows" && git push
|
||||
done
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-05-02 | Initial standardization: 10-workflow Joomla suite from MokoOnyx |
|
||||
| 2026-05-02 | Added pre-release.yml for manual dev/alpha/beta/rc builds |
|
||||
| 2026-05-02 | Removed auto-deploy (deploy is manual only) |
|
||||
| 2026-05-02 | Modernized Dolibarr/Generic/Client templates to match |
|
||||
| 2026-05-02 | Added workflows to all 22 Dolibarr production repos |
|
||||
| 2026-05-02 | Moved canonical source from API repo to template repos |
|
||||
| 2026-05-02 | Added sync-media.yml to Client template (bidirectional SFTP) |
|
||||
| 2026-05-02 | Deployed workflows to client repos (clarksvillefurs, kiddieland) |
|
||||
| 2026-05-02 | Consolidated 6 Joomla template repos → `MokoStandards-Template-Joomla` |
|
||||
| 2026-05-02 | Deleted individual template repos (Plugin, Template, Module, Component, Package, Library) |
|
||||
| 2026-05-02 | Cascade delete: higher releases auto-delete lower pre-release channels |
|
||||
| 2026-05-02 | Release naming: includes extension element name (e.g. "mokodpcalendarapi 03.00.00 (stable)") |
|
||||
| 2026-05-02 | Stable releases overwrite (not append) |
|
||||
| 2026-05-04 | Removed updates.xml + update-server.yml from client repos (sites, not extensions) |
|
||||
| 2026-05-04 | Added client-site.tf definition in MokoStandards-API |
|
||||
| 2026-05-05 | Version policy: stable=minor bump, pre-release=patch bump (was major/patch) |
|
||||
@@ -53,5 +53,5 @@ php api/automation/push_files.php --repos MokoCRM --files "LICENSE" --yes --no-i
|
||||
- Creates a PR with the pushed files (unless `--direct` is used)
|
||||
- Creates/updates a tracking issue in each target repo
|
||||
- Cross-links the tracking issue to the PR in the Development sidebar
|
||||
- Assigns `jmiller-moko` to both PR and issue
|
||||
- Assigns `jmiller` to both PR and issue
|
||||
- Applies standard labels: `standards-update`, `mokostandards`, `type: chore`, `automation`
|
||||
|
||||
@@ -78,8 +78,8 @@ The `custom/` directory is auto-created by the cleanup workflow if it doesn't ex
|
||||
|
||||
### Authorization
|
||||
|
||||
- **Schedule:** Always authorized (runs as github-actions[bot])
|
||||
- **Manual dispatch:** `jmiller-moko` and `github-actions[bot]` always authorized; others need admin/maintain role
|
||||
- **Schedule:** Always authorized (runs as gitea-actions[bot])
|
||||
- **Manual dispatch:** `jmiller` and `gitea-actions[bot]` always authorized; others need admin/maintain role
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# FILE INFORMATION
|
||||
DEFGROUP: MokoStandards.Documentation
|
||||
INGROUP: MokoStandards.ClientRepos
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
PATH: /docs/client-repos.md
|
||||
VERSION: 04.06.00
|
||||
BRIEF: Standards and conventions for client-facing Joomla site repositories
|
||||
-->
|
||||
|
||||
[](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)
|
||||
|
||||
# Client Repository Standards
|
||||
|
||||
## Overview
|
||||
|
||||
Client repos (`client-*`) contain Joomla site customizations for individual clients — templates, overrides, custom CSS, module configurations, and deployment scripts. They follow the **Joomla** workflow profile but have additional rules around privacy, branding, and deployment.
|
||||
|
||||
## Naming Convention
|
||||
|
||||
```
|
||||
client-{clientname}
|
||||
```
|
||||
|
||||
- Lowercase, hyphenated client identifier
|
||||
- No version numbers in the repo name
|
||||
- Examples: `client-clarksvillefurs`, `client-kiddieland`, `client-vexcreations`
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
client-{name}/
|
||||
├── .github/ # Workflows, CLAUDE.md, CODEOWNERS
|
||||
│ ├── workflows/ # Standard Joomla workflows (from template)
|
||||
│ ├── CLAUDE.md # AI assistant context for this client
|
||||
│ └── CODEOWNERS # jmiller owns all files
|
||||
├── src/ # Joomla extension source
|
||||
│ ├── templates/ # Template overrides
|
||||
│ ├── modules/ # Custom modules
|
||||
│ ├── plugins/ # Custom plugins
|
||||
│ └── media/ # CSS, JS, images
|
||||
├── docs/ # Client-specific documentation
|
||||
│ ├── INSTALLATION.md # Deployment instructions
|
||||
│ └── update-server.md # Update server configuration
|
||||
├── scripts/ # Build and deployment scripts
|
||||
├── updates.xml # Joomla update server manifest
|
||||
├── composer.json # Dependencies
|
||||
├── CHANGELOG.md # Version history
|
||||
├── README.md # Client overview (no sensitive details)
|
||||
├── LICENSE # GPL-3.0-or-later
|
||||
└── Makefile # Build targets
|
||||
```
|
||||
|
||||
## Privacy Rules
|
||||
|
||||
Client repos are **private** by default. These rules are non-negotiable:
|
||||
|
||||
1. **No client credentials** in the repo — passwords, API keys, FTP credentials, database passwords go in environment variables or secure secrets, never in code or config files
|
||||
2. **No PII** — no customer names, emails, addresses, phone numbers in code or comments
|
||||
3. **No client-specific hostnames** in committed configs — use environment variables or `sftp-config.json.template` patterns with placeholders
|
||||
4. **README.md is generic** — describes the extension type, not the client's business details
|
||||
5. **CLAUDE.md may reference the client** by name for AI context, but must not include credentials
|
||||
|
||||
## Workflows
|
||||
|
||||
Client repos use the **Joomla workflow profile** from `MokoStandards-Template-Client`:
|
||||
|
||||
- `auto-assign.yml` — Auto-assign issues/PRs to jmiller
|
||||
- `auto-dev-issue.yml` — Create tracking issue on dev branch push
|
||||
- `auto-release.yml` — Create GitHub Release on main merge
|
||||
- `changelog-validation.yml` — Validate CHANGELOG.md format
|
||||
- `ci-joomla.yml` — Joomla manifest validation, XML lint
|
||||
- `codeql-analysis.yml` — Security scanning
|
||||
- `copilot-agent.yml` — Copilot agent configuration
|
||||
- `deploy-manual.yml` — Manual FTP deploy for testing
|
||||
- `enterprise-firewall-setup.yml` — Copilot firewall rules
|
||||
- `project-setup.yml` — Initialize project structure
|
||||
- `repo_health.yml` — Repository health checks
|
||||
- `repository-cleanup.yml` — Clean up stale branches/artifacts
|
||||
- `standards-compliance.yml` — MokoStandards validation
|
||||
- `sync-version-on-merge.yml` — Version tracking
|
||||
- `update-docs.yml` — Auto-update documentation indexes
|
||||
- `update-server.yml` — Update `updates.xml` on release
|
||||
|
||||
## Release Tags
|
||||
|
||||
Standard 5-tag system applies:
|
||||
|
||||
- `development` — Dev builds
|
||||
- `alpha` — Internal testing
|
||||
- `beta` — Client preview / UAT
|
||||
- `release-candidate` — Final QA before production
|
||||
- `stable` — Production release
|
||||
|
||||
## Update Server
|
||||
|
||||
Client repos use the standard dual update server with **Gitea as priority 1**:
|
||||
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{ExtName} (Gitea)">
|
||||
https://git.mokoconsulting.tech/MokoConsulting/client-{name}/raw/branch/main/updates.xml
|
||||
</server>
|
||||
<server type="extension" priority="2" name="{ExtName} (GitHub)">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/client-{name}/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
Client sites are deployed via:
|
||||
|
||||
1. **Joomla updater** — site pulls from `updates.xml` (preferred for production)
|
||||
2. **Manual FTP** — via `deploy-manual.yml` workflow dispatch (for dev/staging)
|
||||
3. **Direct install** — download ZIP from GitHub Release and install via Joomla admin
|
||||
|
||||
## Creating a New Client Repo
|
||||
|
||||
1. Create from template: `MokoStandards-Template-Client`
|
||||
2. Name it `client-{clientname}` (lowercase, hyphenated)
|
||||
3. Set repo to **private**
|
||||
4. Set up secrets: `GA_TOKEN`, `GH_TOKEN`
|
||||
5. Configure push mirror to GitHub
|
||||
6. Apply branch protection on `main`
|
||||
7. Update `README.md`, `composer.json`, `CLAUDE.md` with client-specific context
|
||||
8. Run initial bulk sync to pull latest standards
|
||||
|
||||
## Differences from Standard Joomla Repos
|
||||
|
||||
- **Visibility**: Always private (standard Joomla repos can be public)
|
||||
- **Template**: `MokoStandards-Template-Client` (not `Template-Joomla-*`)
|
||||
- **Extra workflows**: `project-setup.yml`, `update-docs.yml`, `copilot-agent.yml`
|
||||
- **SFTP config**: `sftp-config.json.template` with credential placeholders
|
||||
- **Branding**: Client-specific (not Moko Consulting)
|
||||
- **Deployment**: Joomla updater + manual FTP (standard repos use updater only)
|
||||
@@ -0,0 +1,243 @@
|
||||
# `.mokostandards` File Specification
|
||||
|
||||
> **Version:** 1.0
|
||||
> **Status:** Active
|
||||
> **Schema:** [`mokostandards-schema.xsd`](mokostandards-schema.xsd)
|
||||
> **Last Updated:** 2026-05-02
|
||||
|
||||
## Overview
|
||||
|
||||
The `.mokostandards` file is the **repository manifest** for every repo governed by [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards). It lives at `.gitea/.mokostandards` (no file extension) and uses **XML format** internally.
|
||||
|
||||
The file serves three purposes:
|
||||
|
||||
1. **Identity** — declares the repo's name, organization, description, license, and topics.
|
||||
2. **Governance binding** — ties the repo to a specific MokoStandards platform definition, enabling the bulk sync to know which files, workflows, and templates to enforce.
|
||||
3. **Repo-specific configuration** — captures build/deploy targets, automation scripts, and per-repo sync overrides so that tooling (CI, `make`, `composer run`) can operate without guessing.
|
||||
|
||||
## Location
|
||||
|
||||
```
|
||||
.gitea/.mokostandards ← primary (Gitea-hosted repos)
|
||||
```
|
||||
|
||||
Legacy locations (`.mokostandards` at repo root, `.github/.mokostandards`) are auto-migrated by the bulk sync into `.gitea/.mokostandards`.
|
||||
|
||||
## Format
|
||||
|
||||
The file is well-formed XML with no `.xml` extension. It uses the namespace `https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API` and is validated against `mokostandards-schema.xsd`.
|
||||
|
||||
## Sections
|
||||
|
||||
### `<identity>` — Required
|
||||
|
||||
| Element | Required | Description |
|
||||
|---------------|----------|-------------|
|
||||
| `<name>` | yes | Repository name (e.g. `MokoCRM`) |
|
||||
| `<org>` | yes | Organization slug (e.g. `MokoConsulting`) |
|
||||
| `<description>` | no | Human-readable project description |
|
||||
| `<license>` | no | License name; optional `spdx` attribute for the SPDX identifier |
|
||||
| `<topics>` | no | Container for `<topic>` elements — Gitea/GitHub topics |
|
||||
|
||||
### `<governance>` — Required
|
||||
|
||||
| Element | Required | Description |
|
||||
|----------------------|----------|-------------|
|
||||
| `<platform>` | yes | Platform slug — must match a `definitions/default/*.tf` file. One of: `default-repository`, `crm-module`, `crm-platform`, `generic-repository`, `github-private-repository`, `joomla-template`, `standards-repository`, `waas-component` |
|
||||
| `<standards-version>` | yes | MokoStandards version that last synced this repo (e.g. `04.07.00`) |
|
||||
| `<standards-source>` | yes | URL to the MokoStandards repo |
|
||||
| `<last-synced>` | no | ISO 8601 timestamp of last bulk sync |
|
||||
|
||||
### `<build>` — Optional
|
||||
|
||||
| Element | Required | Description |
|
||||
|-----------------|----------|-------------|
|
||||
| `<language>` | no | Primary language (`PHP`, `JavaScript`, `CSS`, etc.) |
|
||||
| `<runtime>` | no | Runtime version requirement (e.g. `php:>=8.1`) |
|
||||
| `<package-type>`| no | Package format (`composer`, `npm`, `joomla-extension`, `dolibarr-module`) |
|
||||
| `<entry-point>` | no | Main entry file relative to repo root |
|
||||
| `<artifact>` | no | Build output: `<format>`, `<path>`, `<filename>` |
|
||||
| `<dependencies>` | no | Container for `<requires name="" version="" type="">` elements |
|
||||
|
||||
### `<deploy>` — Optional
|
||||
|
||||
Contains one or more `<target>` elements:
|
||||
|
||||
| Attribute/Element | Required | Description |
|
||||
|-------------------|----------|-------------|
|
||||
| `@name` | yes | Target name (`dev`, `demo`, `staging`, `production`) |
|
||||
| `@enabled` | no | Boolean, default `true` |
|
||||
| `<host>` | yes | Hostname or secret reference (e.g. `${{ secrets.DEV_HOST }}`) |
|
||||
| `<path>` | yes | Remote deployment path |
|
||||
| `<method>` | no | One of: `sftp`, `rsync`, `scp`, `composer`, `webhook` |
|
||||
| `<branch>` | no | Branch that triggers this deploy |
|
||||
| `<src-dir>` | no | Local source directory to deploy (default: `src/`) |
|
||||
|
||||
### `<scripts>` — Optional
|
||||
|
||||
Contains one or more `<script>` elements:
|
||||
|
||||
| Attribute/Element | Required | Description |
|
||||
|-------------------|----------|-------------|
|
||||
| `@name` | yes | Script identifier (e.g. `build`, `test`, `lint`, `package`) |
|
||||
| `@phase` | no | Lifecycle phase: `pre-build`, `build`, `post-build`, `test`, `lint`, `pre-deploy`, `post-deploy`, `release`, `validate` |
|
||||
| `<command>` | yes | Shell command to execute |
|
||||
| `<description>` | no | Human-readable purpose |
|
||||
| `<runner>` | no | Execution context (`make`, `composer`, `bash`, `php`) |
|
||||
|
||||
### `<overrides>` — Optional
|
||||
|
||||
Per-repo exceptions to the platform definition:
|
||||
|
||||
| Element | Description |
|
||||
|--------------------|-------------|
|
||||
| `<skip-files>` | `<file>` elements listing paths that bulk sync should NOT overwrite |
|
||||
| `<skip-workflows>` | `<file>` elements listing workflow filenames to skip |
|
||||
| `<extra-secrets>` | `<secret name="" required="" scope="">` for repo-specific secrets beyond the platform default |
|
||||
|
||||
## Minimal Example
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mokostandards xmlns="https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API"
|
||||
schema-version="1.0">
|
||||
<identity>
|
||||
<name>MokoTesting</name>
|
||||
<org>MokoConsulting</org>
|
||||
</identity>
|
||||
<governance>
|
||||
<platform>default-repository</platform>
|
||||
<standards-version>04.07.00</standards-version>
|
||||
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/MokoStandards</standards-source>
|
||||
</governance>
|
||||
</mokostandards>
|
||||
```
|
||||
|
||||
## Full Example (WaaS Component)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mokostandards xmlns="https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API"
|
||||
schema-version="1.0">
|
||||
|
||||
<identity>
|
||||
<name>MokoJoomTOS</name>
|
||||
<org>MokoConsulting</org>
|
||||
<description>A component to present a site's Terms of Service and privacy policy even through offline.</description>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
<topics>
|
||||
<topic>joomla</topic>
|
||||
<topic>system-plugin</topic>
|
||||
<topic>terms-of-service</topic>
|
||||
<topic>waas</topic>
|
||||
</topics>
|
||||
</identity>
|
||||
|
||||
<governance>
|
||||
<platform>waas-component</platform>
|
||||
<standards-version>04.07.00</standards-version>
|
||||
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/MokoStandards</standards-source>
|
||||
<last-synced>2026-05-01T12:00:00Z</last-synced>
|
||||
</governance>
|
||||
|
||||
<build>
|
||||
<language>PHP</language>
|
||||
<runtime>php:>=8.1</runtime>
|
||||
<package-type>joomla-extension</package-type>
|
||||
<entry-point>src/mokojoomtos.xml</entry-point>
|
||||
<artifact>
|
||||
<format>zip</format>
|
||||
<path>dist/</path>
|
||||
<filename>plg_system_mokojoomtos.zip</filename>
|
||||
</artifact>
|
||||
<dependencies>
|
||||
<requires name="joomla/cms" version=">=5.0" type="platform" />
|
||||
<requires name="mokoconsulting-tech/enterprise" version="*" type="composer" />
|
||||
</dependencies>
|
||||
</build>
|
||||
|
||||
<deploy>
|
||||
<target name="dev" enabled="true">
|
||||
<host>${{ secrets.DEV_HOST }}</host>
|
||||
<path>/var/www/dev/plugins/system/mokojoomtos</path>
|
||||
<method>sftp</method>
|
||||
<branch>dev/**</branch>
|
||||
<src-dir>src/</src-dir>
|
||||
</target>
|
||||
<target name="demo">
|
||||
<host>${{ secrets.DEMO_HOST }}</host>
|
||||
<path>/var/www/demo/plugins/system/mokojoomtos</path>
|
||||
<method>sftp</method>
|
||||
<branch>main</branch>
|
||||
<src-dir>src/</src-dir>
|
||||
</target>
|
||||
</deploy>
|
||||
|
||||
<scripts>
|
||||
<script name="build" phase="build">
|
||||
<command>make build</command>
|
||||
<description>Build the extension zip package</description>
|
||||
<runner>make</runner>
|
||||
</script>
|
||||
<script name="lint" phase="lint">
|
||||
<command>vendor/bin/phpcs --standard=Joomla src/</command>
|
||||
<description>Run PHP_CodeSniffer with Joomla coding standard</description>
|
||||
<runner>composer</runner>
|
||||
</script>
|
||||
<script name="test" phase="test">
|
||||
<command>vendor/bin/phpunit</command>
|
||||
<description>Run PHPUnit test suite</description>
|
||||
<runner>composer</runner>
|
||||
</script>
|
||||
<script name="validate" phase="validate">
|
||||
<command>vendor/bin/moko-validate</command>
|
||||
<description>Validate MokoStandards compliance</description>
|
||||
<runner>composer</runner>
|
||||
</script>
|
||||
</scripts>
|
||||
|
||||
<overrides>
|
||||
<skip-files>
|
||||
<file>composer.json</file>
|
||||
</skip-files>
|
||||
<extra-secrets>
|
||||
<secret name="JOOMLA_API_TOKEN" required="true" scope="repository" />
|
||||
</extra-secrets>
|
||||
</overrides>
|
||||
|
||||
</mokostandards>
|
||||
```
|
||||
|
||||
## Migration from Legacy Format
|
||||
|
||||
The old format was a single YAML-like line:
|
||||
|
||||
```
|
||||
platform: default-repository
|
||||
```
|
||||
|
||||
The bulk sync will:
|
||||
1. Read the legacy value
|
||||
2. Generate a new XML `.mokostandards` with the detected platform
|
||||
3. Commit the replacement file to `.gitea/.mokostandards`
|
||||
4. Delete the old file from legacy locations (root, `.github/`)
|
||||
|
||||
## Validation
|
||||
|
||||
Repos are validated against `mokostandards-schema.xsd` during:
|
||||
- Bulk sync (`automation/bulk_sync.php`)
|
||||
- Standards compliance workflow (`.gitea/workflows/standards-compliance.yml`)
|
||||
- Local validation via `vendor/bin/moko-validate`
|
||||
|
||||
A missing or invalid `.mokostandards` file is a **compliance failure**.
|
||||
|
||||
## Tooling Integration
|
||||
|
||||
| Tool | How it uses `.mokostandards` |
|
||||
|------|------------------------------|
|
||||
| **bulk_sync.php** | Reads `<governance><platform>` to select the correct definition `.tf` file; updates `<last-synced>` on success |
|
||||
| **enforce_tags.sh** | Reads `<identity><name>` for tag naming |
|
||||
| **deploy-*.yml** | Reads `<deploy><target>` for host, path, method |
|
||||
| **Makefile** | Can source `<scripts>` for consistent `make` targets |
|
||||
| **moko-validate** | Validates the XML against the XSD and checks required fields |
|
||||
| **detectPlatform()** | Falls back to name/topic heuristics only when `.mokostandards` is missing or unparseable |
|
||||
@@ -0,0 +1,273 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
FILE INFORMATION
|
||||
DEFGROUP: MokoStandards.Schema
|
||||
INGROUP: MokoStandards.Governance
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
PATH: /docs/standards/mokostandards-schema.xsd
|
||||
VERSION: 04.07.00
|
||||
BRIEF: XML Schema Definition for the .mokostandards repository manifest file
|
||||
-->
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:moko="https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API"
|
||||
targetNamespace="https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API"
|
||||
elementFormDefault="qualified"
|
||||
version="1.0">
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
ROOT ELEMENT
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
<xs:element name="mokostandards" type="moko:MokoStandardsType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Root element of the .mokostandards repository manifest.
|
||||
Every governed repository MUST contain this file at .gitea/.mokostandards
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
|
||||
<xs:complexType name="MokoStandardsType">
|
||||
<xs:all>
|
||||
<xs:element name="identity" type="moko:IdentityType" />
|
||||
<xs:element name="governance" type="moko:GovernanceType" />
|
||||
<xs:element name="build" type="moko:BuildType" minOccurs="0" />
|
||||
<xs:element name="deploy" type="moko:DeployType" minOccurs="0" />
|
||||
<xs:element name="scripts" type="moko:ScriptsType" minOccurs="0" />
|
||||
<xs:element name="overrides" type="moko:OverridesType" minOccurs="0" />
|
||||
</xs:all>
|
||||
<xs:attribute name="schema-version" type="xs:string" use="required" fixed="1.0" />
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
IDENTITY — who is this repo?
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
<xs:complexType name="IdentityType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Repository identity metadata. Provides authoritative repo-level
|
||||
information consumed by sync tools, CI, and documentation generators.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:all>
|
||||
<xs:element name="name" type="xs:string" />
|
||||
<xs:element name="org" type="xs:string" />
|
||||
<xs:element name="description" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="license" type="moko:LicenseType" minOccurs="0" />
|
||||
<xs:element name="topics" type="moko:TopicsType" minOccurs="0" />
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="LicenseType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>SPDX license identifier for the repository</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute name="spdx" type="xs:string" use="optional" />
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="TopicsType">
|
||||
<xs:sequence>
|
||||
<xs:element name="topic" type="xs:string" maxOccurs="unbounded" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
GOVERNANCE — how does MokoStandards manage this repo?
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
<xs:complexType name="GovernanceType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Binds this repository to a MokoStandards platform definition and
|
||||
tracks the governance source and version.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:all>
|
||||
<xs:element name="platform" type="moko:PlatformEnum" />
|
||||
<xs:element name="standards-version" type="xs:string" />
|
||||
<xs:element name="standards-source" type="xs:anyURI" />
|
||||
<xs:element name="last-synced" type="xs:dateTime" minOccurs="0" />
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="PlatformEnum">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Platform slug — must match a .tf file in definitions/default/.
|
||||
Controls which structure definition and workflows are synced.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="default-repository" />
|
||||
<xs:enumeration value="crm-module" />
|
||||
<xs:enumeration value="crm-platform" />
|
||||
<xs:enumeration value="generic-repository" />
|
||||
<xs:enumeration value="github-private-repository" />
|
||||
<xs:enumeration value="joomla-template" />
|
||||
<xs:enumeration value="standards-repository" />
|
||||
<xs:enumeration value="waas-component" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
BUILD — how is this repo built/packaged?
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
<xs:complexType name="BuildType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Build and packaging configuration. Describes the toolchain,
|
||||
entry points, and artifact outputs for this repository.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:all>
|
||||
<xs:element name="language" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="runtime" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="package-type" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="entry-point" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="artifact" type="moko:ArtifactType" minOccurs="0" />
|
||||
<xs:element name="dependencies" type="moko:DependenciesType" minOccurs="0" />
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ArtifactType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Describes the build output artifact (zip, phar, etc.)</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:all>
|
||||
<xs:element name="format" type="xs:string" />
|
||||
<xs:element name="path" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="filename" type="xs:string" minOccurs="0" />
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="DependenciesType">
|
||||
<xs:sequence>
|
||||
<xs:element name="requires" type="moko:DependencyType" minOccurs="0" maxOccurs="unbounded" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="DependencyType">
|
||||
<xs:attribute name="name" type="xs:string" use="required" />
|
||||
<xs:attribute name="version" type="xs:string" use="optional" />
|
||||
<xs:attribute name="type" type="xs:string" use="optional" />
|
||||
</xs:complexType>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
DEPLOY — where does this repo get deployed?
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
<xs:complexType name="DeployType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Deployment targets. Each target maps to a CI workflow and
|
||||
defines the connection method and remote path.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:sequence>
|
||||
<xs:element name="target" type="moko:DeployTargetType" maxOccurs="unbounded" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="DeployTargetType">
|
||||
<xs:all>
|
||||
<xs:element name="host" type="xs:string" />
|
||||
<xs:element name="path" type="xs:string" />
|
||||
<xs:element name="method" type="moko:DeployMethodEnum" minOccurs="0" />
|
||||
<xs:element name="branch" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="src-dir" type="xs:string" minOccurs="0" />
|
||||
</xs:all>
|
||||
<xs:attribute name="name" type="xs:string" use="required" />
|
||||
<xs:attribute name="enabled" type="xs:boolean" use="optional" default="true" />
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="DeployMethodEnum">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="sftp" />
|
||||
<xs:enumeration value="rsync" />
|
||||
<xs:enumeration value="scp" />
|
||||
<xs:enumeration value="composer" />
|
||||
<xs:enumeration value="webhook" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
SCRIPTS — repo-specific automation hooks
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
<xs:complexType name="ScriptsType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Repo-specific scripts and automation hooks.
|
||||
Each script element defines a named command that CI or
|
||||
developers can invoke via `make`, `composer run`, or directly.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:sequence>
|
||||
<xs:element name="script" type="moko:ScriptType" maxOccurs="unbounded" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="ScriptType">
|
||||
<xs:all>
|
||||
<xs:element name="command" type="xs:string" />
|
||||
<xs:element name="description" type="xs:string" minOccurs="0" />
|
||||
<xs:element name="runner" type="xs:string" minOccurs="0" />
|
||||
</xs:all>
|
||||
<xs:attribute name="name" type="xs:string" use="required" />
|
||||
<xs:attribute name="phase" type="moko:ScriptPhaseEnum" use="optional" />
|
||||
</xs:complexType>
|
||||
|
||||
<xs:simpleType name="ScriptPhaseEnum">
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="pre-build" />
|
||||
<xs:enumeration value="build" />
|
||||
<xs:enumeration value="post-build" />
|
||||
<xs:enumeration value="test" />
|
||||
<xs:enumeration value="lint" />
|
||||
<xs:enumeration value="pre-deploy" />
|
||||
<xs:enumeration value="post-deploy" />
|
||||
<xs:enumeration value="release" />
|
||||
<xs:enumeration value="validate" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════════
|
||||
OVERRIDES — per-repo sync overrides
|
||||
════════════════════════════════════════════════════════════════════ -->
|
||||
<xs:complexType name="OverridesType">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
Per-repo overrides for the bulk sync process.
|
||||
Allows a repository to skip specific synced files or
|
||||
opt out of certain governance features without forking
|
||||
the entire platform definition.
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:all>
|
||||
<xs:element name="skip-files" type="moko:FileListType" minOccurs="0" />
|
||||
<xs:element name="skip-workflows" type="moko:FileListType" minOccurs="0" />
|
||||
<xs:element name="extra-secrets" type="moko:SecretsListType" minOccurs="0" />
|
||||
</xs:all>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="FileListType">
|
||||
<xs:sequence>
|
||||
<xs:element name="file" type="xs:string" maxOccurs="unbounded" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SecretsListType">
|
||||
<xs:sequence>
|
||||
<xs:element name="secret" type="moko:SecretType" maxOccurs="unbounded" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="SecretType">
|
||||
<xs:attribute name="name" type="xs:string" use="required" />
|
||||
<xs:attribute name="required" type="xs:boolean" use="optional" default="true" />
|
||||
<xs:attribute name="scope" type="xs:string" use="optional" />
|
||||
</xs:complexType>
|
||||
|
||||
</xs:schema>
|
||||
+19
-18
@@ -5,6 +5,7 @@
|
||||
**Status**: Active | **Version**: 04.00.04 | **Effective**: 2026-01-07
|
||||
|
||||
## Overview
|
||||
> **Important (v2):** All workflows MUST be in `.gitea/workflows/` only. Gitea Actions does not run workflows from `.github/workflows/`. Having files in `.github/workflows/` creates ghost queued runs that block the runner. Releases use stream-based git tags (`stable`, `release-candidate`, `beta`, `alpha`, `development`). See [Release System](./release-system.md) for cascade logic, SHA-256 rules, and auto-detection.
|
||||
|
||||
This document provides comprehensive documentation for MokoStandards workflow templates. These templates provide standardized CI/CD configurations that ensure consistency, security, and compliance across all Moko Consulting repositories.
|
||||
|
||||
@@ -44,20 +45,20 @@ To adopt MokoStandards workflows in your repository:
|
||||
|
||||
```bash
|
||||
# Copy universal build workflow
|
||||
cp templates/workflows/build.yml.template .github/workflows/build.yml
|
||||
cp templates/workflows/build.yml.template .gitea/workflows/build.yml
|
||||
|
||||
# Copy release management workflow
|
||||
cp templates/workflows/release-cycle.yml.template .github/workflows/release.yml
|
||||
cp templates/workflows/release-cycle.yml.template .gitea/workflows/release.yml
|
||||
|
||||
# Copy security scanning workflows
|
||||
cp templates/workflows/generic/codeql-analysis.yml .github/workflows/
|
||||
cp templates/workflows/generic/dependency-review.yml.template .github/workflows/dependency-review.yml
|
||||
cp templates/workflows/generic/codeql-analysis.yml .gitea/workflows/
|
||||
cp templates/workflows/generic/dependency-review.yml.template .gitea/workflows/dependency-review.yml
|
||||
|
||||
# Copy standards compliance workflow
|
||||
cp templates/workflows/standards-compliance.yml.template .github/workflows/standards-compliance.yml
|
||||
cp templates/workflows/standards-compliance.yml.template .gitea/workflows/standards-compliance.yml
|
||||
|
||||
# Optional: Copy cache management workflow
|
||||
cp templates/workflows/flush-actions-cache.yml.template .github/workflows/flush-actions-cache.yml
|
||||
cp templates/workflows/flush-actions-cache.yml.template .gitea/workflows/flush-actions-cache.yml
|
||||
```
|
||||
|
||||
Then customize the workflows for your project as needed.
|
||||
@@ -87,7 +88,7 @@ on:
|
||||
workflow_dispatch:
|
||||
```
|
||||
|
||||
**Usage**: Copy to `.github/workflows/build.yml` and customize as needed.
|
||||
**Usage**: Copy to `.gitea/workflows/build.yml` and customize as needed.
|
||||
|
||||
See [Build System Documentation](../build-system/README.md) for details on the Makefile precedence system.
|
||||
|
||||
@@ -127,7 +128,7 @@ on:
|
||||
required: true
|
||||
```
|
||||
|
||||
**Usage**: Copy to `.github/workflows/release.yml` for automated release management.
|
||||
**Usage**: Copy to `.gitea/workflows/release.yml` for automated release management.
|
||||
|
||||
See [Release Management Documentation](../release-management/README.md) for complete release procedures.
|
||||
|
||||
@@ -151,7 +152,7 @@ on:
|
||||
branches: [main, dev/**, rc/**]
|
||||
```
|
||||
|
||||
**Usage**: Copy to `.github/workflows/dependency-review.yml` to enable dependency scanning on PRs.
|
||||
**Usage**: Copy to `.gitea/workflows/dependency-review.yml` to enable dependency scanning on PRs.
|
||||
|
||||
### 4. Standards Compliance (`standards-compliance.yml.template`)
|
||||
|
||||
@@ -177,7 +178,7 @@ on:
|
||||
workflow_dispatch:
|
||||
```
|
||||
|
||||
**Usage**: Copy to `.github/workflows/standards-compliance.yml` to enable compliance checks.
|
||||
**Usage**: Copy to `.gitea/workflows/standards-compliance.yml` to enable compliance checks.
|
||||
|
||||
### 5. Flush Actions Cache (`flush-actions-cache.yml.template`)
|
||||
|
||||
@@ -213,7 +214,7 @@ on:
|
||||
- Clear dependency caches after major updates (Composer, npm)
|
||||
- Preview cache usage with dry-run mode
|
||||
|
||||
**Usage**: Copy to `.github/workflows/flush-actions-cache.yml` to enable cache management.
|
||||
**Usage**: Copy to `.gitea/workflows/flush-actions-cache.yml` to enable cache management.
|
||||
|
||||
See [flush_actions_cache.py documentation](/docs/api/maintenance/flush-actions-cache-py.md) for detailed script usage.
|
||||
|
||||
@@ -221,7 +222,7 @@ See [flush_actions_cache.py documentation](/docs/api/maintenance/flush-actions-c
|
||||
|
||||
**Location**: `templates/workflows/generic/codeql-analysis.yml`
|
||||
|
||||
Security scanning with GitHub's CodeQL engine (also available in `.github/workflows/`).
|
||||
Security scanning with GitHub's CodeQL engine (also available in `.gitea/workflows/`).
|
||||
|
||||
See section below for complete details.
|
||||
|
||||
@@ -231,7 +232,7 @@ The following templates are organized by platform in `templates/workflows/`.
|
||||
|
||||
### 1. CI Template (`ci.yml`)
|
||||
|
||||
**Location**: `.github/workflows/ci.yml` (MokoStandards root)
|
||||
**Location**: `.gitea/workflows/ci.yml` (MokoStandards root)
|
||||
|
||||
Continuous Integration workflow that enforces repository standards through automated validation.
|
||||
|
||||
@@ -255,11 +256,11 @@ on:
|
||||
branches: [main, dev/**, rc/**, version/**]
|
||||
```
|
||||
|
||||
**Usage**: Copy from MokoStandards `.github/workflows/ci.yml` to your repository.
|
||||
**Usage**: Copy from MokoStandards `.gitea/workflows/ci.yml` to your repository.
|
||||
|
||||
### 2. CodeQL Analysis Template (`codeql-analysis.yml`)
|
||||
|
||||
**Location**: `.github/workflows/codeql-analysis.yml` (MokoStandards root)
|
||||
**Location**: `.gitea/workflows/codeql-analysis.yml` (MokoStandards root)
|
||||
|
||||
Security scanning workflow using GitHub's CodeQL engine for vulnerability detection.
|
||||
|
||||
@@ -282,7 +283,7 @@ on:
|
||||
workflow_dispatch:
|
||||
```
|
||||
|
||||
**Usage**: Copy from MokoStandards `.github/workflows/codeql-analysis.yml` to your repository.
|
||||
**Usage**: Copy from MokoStandards `.gitea/workflows/codeql-analysis.yml` to your repository.
|
||||
|
||||
### 3. Dependency Review
|
||||
|
||||
@@ -429,7 +430,7 @@ Workflows use automatic project type detection based on file presence. See [Proj
|
||||
2. **Copy workflow files**:
|
||||
```bash
|
||||
mkdir -p .github/workflows
|
||||
cp /path/to/MokoStandards/templates/workflows/joomla/ci.yml .github/workflows/
|
||||
cp /path/to/MokoStandards/templates/workflows/joomla/ci.yml .gitea/workflows/
|
||||
```
|
||||
|
||||
3. **Customize for your project**:
|
||||
@@ -440,7 +441,7 @@ Workflows use automatic project type detection based on file presence. See [Proj
|
||||
|
||||
4. **Commit and enable**:
|
||||
```bash
|
||||
git add .github/workflows/
|
||||
git add .gitea/workflows/
|
||||
git commit -m "Add MokoStandards workflows"
|
||||
git push
|
||||
```
|
||||
|
||||
@@ -56,7 +56,7 @@ Joomla repos do **not** use FTP deploy. Distribution is via GitHub Release ZIPs.
|
||||
## Triggers
|
||||
|
||||
- Push to `main` or `master`
|
||||
- Skips commits by `github-actions[bot]` and commits with `[skip ci]`
|
||||
- Skips commits by `gitea-actions[bot]` and commits with `[skip ci]`
|
||||
|
||||
## Version Lifecycle
|
||||
|
||||
@@ -88,6 +88,7 @@ Each stability level has its own GitHub Release tag:
|
||||
- No minor or patch production tags are created
|
||||
- Pre-release tags are updated in-place per stability level
|
||||
|
||||
## Stream Tags (v2)Releases use stream-based git tags, NOT version numbers:- `stable` — production release- `release-candidate` — RC testing- `beta` — feature-complete stability testing- `alpha` — early testing- `development` — unstable dev buildsTo trigger a release, push the appropriate stream tag: `git tag -f stable && git push origin stable --force`### Cascade LogicEach stability level cascades to all lower levels in updates.xml:- **stable** → updates development, alpha, beta, rc, stable- **rc** → updates development, alpha, beta, rc- **beta** → updates development, alpha, beta- **alpha** → updates development, alpha- **development** → updates development only### SHA-256 Rules- Never leave `<sha256></sha256>` empty — Joomla fails checksum verification on empty tags- Omit the `<sha256>` tag entirely if no hash is available- Always set SHA when building a package### creationDateAlways update `<creationDate>` whenever version is bumped — in the manifest AND in updates.xml.### Auto-DetectionThe release workflow (`release.yml`) is fully generic:- `GITEA_REPO` derived from `github.event.repository.name`- `EXT_ELEMENT` auto-detected from the Joomla manifest `<element>` tag- Falls back to manifest filename, then repo name (lowercased)- No per-repo customization needed### Version History (Stable Releases)Stable releases keep up to 5 previous versions in the Gitea release body.
|
||||
## Requirements
|
||||
|
||||
- `secrets.GH_TOKEN` with `contents: write` permission
|
||||
|
||||
@@ -41,7 +41,7 @@ Push to main
|
||||
## Triggers
|
||||
|
||||
- Push to `main` or `master`
|
||||
- Skips commits by `github-actions[bot]` and commits with `[skip ci]`
|
||||
- Skips commits by `gitea-actions[bot]` and commits with `[skip ci]`
|
||||
- Skips if tag + branch already exist (idempotent)
|
||||
|
||||
## What Each Step Does
|
||||
@@ -76,6 +76,7 @@ Extracts release notes from CHANGELOG.md (section matching the version heading)
|
||||
| Merge to main | `main` | Real version | Updated | Yes |
|
||||
| Version bump | `main` | Auto-incremented | Updated | Next push |
|
||||
|
||||
## Stream Tags (v2)Releases use stream-based git tags (`stable`, `release-candidate`, `beta`, `alpha`, `development`), NOT version numbers. To trigger a release, push the appropriate stream tag: `git tag -f stable && git push origin stable --force`### Cascade LogicEach stability level cascades to all lower levels in updates.xml:- **stable** → updates development, alpha, beta, rc, stable- **rc** → updates development, alpha, beta, rc- **beta** → updates development, alpha, beta- **alpha** → updates development, alpha- **development** → updates development only### SHA-256 Rules- Never leave `<sha256></sha256>` empty — Joomla fails checksum verification on empty tags- Omit the `<sha256>` tag entirely if no hash is available- Always set SHA when building a package### Auto-DetectionThe release workflow is fully generic:- `GITEA_REPO` derived from `github.event.repository.name`- `EXT_ELEMENT` auto-detected from the Joomla manifest `<element>` tag- Falls back to manifest filename, then repo name (lowercased)- No per-repo customization needed
|
||||
## Related Workflows
|
||||
|
||||
| Workflow | Role |
|
||||
|
||||
@@ -84,7 +84,7 @@ The **Bulk Repository Sync** workflow is MokoStandards' automated system for dep
|
||||
|
||||
### Workflow Location
|
||||
|
||||
- **File**: `.github/workflows/bulk-repo-sync.yml`
|
||||
- **File**: `.gitea/workflows/bulk-repo-sync.yml`
|
||||
- **Script**: `api/automation/bulk_sync.php`
|
||||
- **Version**: 5.0 (Rebuilt using Enterprise library)
|
||||
- **Status**: ✅ **ENTERPRISE READY** - Fully integrated with enterprise security, audit logging, and metrics
|
||||
@@ -316,7 +316,7 @@ locals {
|
||||
|
||||
exclude_files = [
|
||||
{
|
||||
path = ".github/workflows/custom-ci.yml"
|
||||
path = ".gitea/workflows/custom-ci.yml"
|
||||
reason = "Custom CI workflow with special requirements"
|
||||
}
|
||||
]
|
||||
@@ -425,34 +425,34 @@ The bulk sync workflow synchronizes the following file types:
|
||||
|
||||
#### 2. **Universal Workflows** (All Repositories)
|
||||
|
||||
- `.github/workflows/build.yml` - Build workflow
|
||||
- `.github/workflows/ci.yml` - CI validation workflow
|
||||
- `.gitea/workflows/build.yml` - Build workflow
|
||||
- `.gitea/workflows/ci.yml` - CI validation workflow
|
||||
|
||||
#### 3. **Platform-Specific Workflows**
|
||||
|
||||
**Terraform Repositories**:
|
||||
- `.github/workflows/terraform-ci.yml` - Terraform CI
|
||||
- `.github/workflows/terraform-deploy.yml` - Terraform deployment
|
||||
- `.github/workflows/terraform-drift.yml` - Drift detection
|
||||
- `.gitea/workflows/terraform-ci.yml` - Terraform CI
|
||||
- `.gitea/workflows/terraform-deploy.yml` - Terraform deployment
|
||||
- `.gitea/workflows/terraform-drift.yml` - Drift detection
|
||||
|
||||
**Dolibarr Repositories**:
|
||||
- `.github/workflows/release.yml` - Dolibarr release workflow
|
||||
- `.github/workflows/sync-changelogs.yml` - Changelog sync
|
||||
- `.gitea/workflows/release.yml` - Dolibarr release workflow
|
||||
- `.gitea/workflows/sync-changelogs.yml` - Changelog sync
|
||||
|
||||
**Joomla Repositories**:
|
||||
- `.github/workflows/release.yml` - Joomla release workflow
|
||||
- `.github/workflows/repo-health.yml` - Repository health checks
|
||||
- `.gitea/workflows/release.yml` - Joomla release workflow
|
||||
- `.gitea/workflows/repo-health.yml` - Repository health checks
|
||||
|
||||
**Generic Repositories**:
|
||||
- `.github/workflows/code-quality.yml` - Code quality checks
|
||||
- `.github/workflows/codeql-analysis.yml` - Security scanning
|
||||
- `.github/workflows/repo-health.yml` - Health checks
|
||||
- `.gitea/workflows/code-quality.yml` - Code quality checks
|
||||
- `.gitea/workflows/codeql-analysis.yml` - Security scanning
|
||||
- `.gitea/workflows/repo-health.yml` - Health checks
|
||||
|
||||
#### 4. **Reusable Workflows** (All Repositories)
|
||||
|
||||
- `.github/workflows/reusable-build.yml` - Reusable build workflow
|
||||
- `.github/workflows/reusable-release.yml` - Reusable release workflow
|
||||
- `.github/workflows/reusable-project-detector.yml` - Project detection
|
||||
- `.gitea/workflows/reusable-build.yml` - Reusable build workflow
|
||||
- `.gitea/workflows/reusable-release.yml` - Reusable release workflow
|
||||
- `.gitea/workflows/reusable-project-detector.yml` - Project detection
|
||||
- Additional reusable workflows based on platform
|
||||
|
||||
#### 5. **Validation Scripts** (All Repositories)
|
||||
@@ -726,7 +726,7 @@ Sync PRs respect branch protection:
|
||||
|
||||
**Updating the Workflow**:
|
||||
|
||||
1. Modify `.github/workflows/bulk-repo-sync.yml`
|
||||
1. Modify `.gitea/workflows/bulk-repo-sync.yml`
|
||||
2. Test changes with manual trigger on test repository
|
||||
3. Commit and push to MokoStandards
|
||||
4. Next scheduled run uses updated workflow
|
||||
|
||||
@@ -87,7 +87,7 @@ python3 api/maintenance/release_version.py --version 05.01.00 --update-files --c
|
||||
|
||||
### Update Changelog Workflow
|
||||
|
||||
**File**: `.github/workflows/changelog_update.yml`
|
||||
**File**: `.gitea/workflows/changelog_update.yml`
|
||||
|
||||
**Trigger**: Manual workflow dispatch
|
||||
|
||||
@@ -111,7 +111,7 @@ python3 api/maintenance/release_version.py --version 05.01.00 --update-files --c
|
||||
|
||||
### Version Release Workflow
|
||||
|
||||
**File**: `.github/workflows/version_release.yml`
|
||||
**File**: `.gitea/workflows/version_release.yml`
|
||||
|
||||
**Trigger**: Manual workflow dispatch
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ At least one of `DEMO_FTP_KEY` or `DEMO_FTP_PASSWORD` must be set.
|
||||
|
||||
## Behaviour
|
||||
|
||||
1. **Permission check** — `jmiller-moko` and `github-actions[bot]` are always authorized; other actors need `admin` or `maintain` role
|
||||
1. **Permission check** — `jmiller` and `gitea-actions[bot]` are always authorized; other actors need `admin` or `maintain` role
|
||||
2. **Skip on chore/ branches** — PRs from `chore/` branches do not trigger deployment
|
||||
3. **Skip if DEMO_FTP_SUFFIX not set** — repos without the variable are silently skipped
|
||||
4. **Clear remote folder** — always clears the remote destination before uploading
|
||||
|
||||
@@ -55,7 +55,7 @@ The tracking system consists of three main components:
|
||||
- Documentation updates
|
||||
- Changelog updates
|
||||
- Version management
|
||||
3. Assigns issues to `copilot` and `jmiller-moko`
|
||||
3. Assigns issues to `copilot` and `jmiller`
|
||||
4. Adds labels: `automation`, `version-management`, `dev-branch`
|
||||
|
||||
### 2. RC Branch Issue Creation (Automatic)
|
||||
@@ -73,14 +73,14 @@ The tracking system consists of three main components:
|
||||
- Release notes preparation
|
||||
- Final verification and sign-off
|
||||
3. Creates a **draft GitHub Release** for the major version (`vXX`)
|
||||
4. Assigns issues to `copilot` and `jmiller-moko`
|
||||
4. Assigns issues to `copilot` and `jmiller`
|
||||
5. Adds labels: `automation`, `version-management`, `rc-branch`
|
||||
|
||||
The draft release is later published by `auto-release.yml` when the RC merges to main.
|
||||
|
||||
### 2. Enterprise Issue Manager Workflow
|
||||
|
||||
**File**: `.github/workflows/enterprise-issue-manager.yml`
|
||||
**File**: `.gitea/workflows/enterprise-issue-manager.yml`
|
||||
|
||||
**Purpose**: Coordinates pull requests with dev branch tracking issues throughout the PR lifecycle.
|
||||
|
||||
@@ -207,7 +207,7 @@ No action required. When a PR is merged to main:
|
||||
# Workflow automatically:
|
||||
# 1. Creates dev/04.01 branch
|
||||
# 2. Creates tracking issue #456
|
||||
# 3. Assigns to copilot and jmiller-moko
|
||||
# 3. Assigns to copilot and jmiller
|
||||
```
|
||||
|
||||
### Manual Issue Creation
|
||||
@@ -262,7 +262,7 @@ The launch checklist aligns with the [Copilot Pre-Merge Checklist Policy](../pol
|
||||
### Standards Compliance Workflow
|
||||
|
||||
The "Standards Compliance" checklist section integrates with:
|
||||
- `.github/workflows/standards-compliance.yml`
|
||||
- `.gitea/workflows/standards-compliance.yml`
|
||||
- File header validation
|
||||
- Formatting standards checks
|
||||
|
||||
@@ -279,10 +279,10 @@ The "Security Scanning" checklist section integrates with:
|
||||
|
||||
Default assignees for tracking issues:
|
||||
- `copilot` (GitHub Copilot agent)
|
||||
- `jmiller-moko` (organization owner — GitHub assignees must be a user account, not an org)
|
||||
- `jmiller` (organization owner — GitHub assignees must be a user account, not an org)
|
||||
|
||||
To change assignees:
|
||||
- **Auto-created issues**: Edit `.github/workflows/auto-create-dev-branch.yml` line 289
|
||||
- **Auto-created issues**: Edit `.gitea/workflows/auto-create-dev-branch.yml` line 289
|
||||
- **Manual template**: Edit `.github/ISSUE_TEMPLATE/dev-branch-tracking.md` line 6
|
||||
|
||||
### Labels
|
||||
@@ -293,7 +293,7 @@ Default labels for tracking issues:
|
||||
- `dev-branch` - Development branch marker
|
||||
|
||||
To change labels:
|
||||
- **Auto-created issues**: Edit `.github/workflows/auto-create-dev-branch.yml` line 288
|
||||
- **Auto-created issues**: Edit `.gitea/workflows/auto-create-dev-branch.yml` line 288
|
||||
- **Manual template**: Edit `.github/ISSUE_TEMPLATE/dev-branch-tracking.md` line 5
|
||||
|
||||
### Branch Patterns
|
||||
@@ -304,7 +304,7 @@ The system tracks branches matching:
|
||||
- `beta/*` - Beta testing branches (optional stage)
|
||||
- `rc/*` - Release candidate branches
|
||||
|
||||
To modify patterns, edit `.github/workflows/enterprise-issue-manager.yml` line 143.
|
||||
To modify patterns, edit `.gitea/workflows/enterprise-issue-manager.yml` line 143.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
@@ -376,8 +376,8 @@ To modify patterns, edit `.github/workflows/enterprise-issue-manager.yml` line 1
|
||||
## Related Documentation
|
||||
|
||||
- [Copilot Pre-Merge Checklist Policy](../policy/copilot-pre-merge-checklist.md)
|
||||
- [Auto-Create Dev Branch Workflow](../../.github/workflows/auto-create-dev-branch.yml)
|
||||
- [Enterprise Issue Manager Workflow](../../.github/workflows/enterprise-issue-manager.yml)
|
||||
- [Auto-Create Dev Branch Workflow](../../.gitea/workflows/auto-create-dev-branch.yml)
|
||||
- [Enterprise Issue Manager Workflow](../../.gitea/workflows/enterprise-issue-manager.yml)
|
||||
- [Dev Branch Tracking Template](../../.github/ISSUE_TEMPLATE/dev-branch-tracking.md)
|
||||
- [Workflow Architecture](./workflow-architecture.md)
|
||||
- [Contributing Guide](../../CONTRIBUTING.md)
|
||||
|
||||
@@ -28,7 +28,7 @@ The `deploy-dev.yml` workflow pushes the contents of `src/` to a development ser
|
||||
- A pull request targeting those branches is **merged** (skips `chore/` branches)
|
||||
- Triggered manually via workflow dispatch
|
||||
|
||||
**Access control:** `jmiller-moko` and `github-actions[bot]` are always authorized. Other actors need **admin** or **maintain** role.
|
||||
**Access control:** `jmiller` and `gitea-actions[bot]` are always authorized. Other actors need **admin** or **maintain** role.
|
||||
|
||||
**Skips when:** `DEV_FTP_SUFFIX` variable is not set, or the branch starts with `chore/`.
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ Joomla repos use GitHub Release ZIPs via `auto-release.yml` + `updates.xml`. No
|
||||
- [Changelog Management](./changelog-management.md) — automated changelog generation
|
||||
- [Changelog Validation](./changelog-validation.md) — validates CHANGELOG.md format on PR
|
||||
|
||||
## Client Repositories
|
||||
|
||||
- [Client Repo Standards](../client-repos.md) — naming, privacy, structure, deployment for client-* repos
|
||||
|
||||
## CI & Compliance Workflows
|
||||
|
||||
- [CI Joomla](./ci-joomla.md) — Joomla-specific CI checks (manifest validation, language files, XML lint)
|
||||
|
||||
@@ -58,6 +58,7 @@ Each stability level has its own GitHub Release tag:
|
||||
| `vXX` | Stable production | Stable ZIPs (major only, e.g., `v04`) |
|
||||
|
||||
- The `vXX` production tag is **major only** — one release per major version
|
||||
## Workflow Location (v2)All workflows MUST be in `.gitea/workflows/` only. Gitea Actions does not run workflows from `.github/workflows/`. Having files in `.github/workflows/` creates ghost queued runs that block the runner.## Stream TagsReleases use stream-based git tags, NOT version numbers:- `stable` — production release- `release-candidate` — RC testing- `beta` — feature-complete stability testing- `alpha` — early testing- `development` — unstable dev buildsTo trigger a release, push the appropriate stream tag: `git tag -f stable && git push origin stable --force`## Cascade LogicEach stability level cascades to all lower levels in updates.xml:- **stable** → updates development, alpha, beta, rc, stable- **rc** → updates development, alpha, beta, rc- **beta** → updates development, alpha, beta- **alpha** → updates development, alpha- **development** → updates development only## SHA-256 Rules- Never leave `<sha256></sha256>` empty — Joomla fails checksum verification on empty tags- Omit the `<sha256>` tag entirely if no hash is available- Always set SHA when building a package## creationDateAlways update `<creationDate>` whenever version is bumped — in the manifest AND in updates.xml.## Auto-DetectionThe release workflow (`release.yml`) is fully generic:- `GITEA_REPO` derived from `github.event.repository.name`- `EXT_ELEMENT` auto-detected from the Joomla manifest `<element>` tag- Falls back to manifest filename, then repo name (lowercased)- No per-repo customization needed## Version History (Stable)Stable releases keep up to 5 previous versions in the Gitea release body.
|
||||
- All minor+patch versions **append** release notes and ZIP assets to the same `vXX` release
|
||||
- No minor or patch production tags are created
|
||||
- Example: versions `04.01.01`, `04.02.00`, `04.03.05` all update the same `v04` release
|
||||
@@ -133,7 +134,7 @@ To prevent automatic release creation, include `[skip ci]` in your commit messag
|
||||
git commit -m "docs: update README [skip ci]"
|
||||
```
|
||||
|
||||
Commits by `github-actions[bot]` are also skipped automatically (e.g., auto-bump commits).
|
||||
Commits by `gitea-actions[bot]` are also skipped automatically (e.g., auto-bump commits).
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -187,9 +188,9 @@ Commits by `github-actions[bot]` are also skipped automatically (e.g., auto-bump
|
||||
|
||||
## Configuration Files
|
||||
|
||||
- `.github/workflows/auto-release.yml` - Main release workflow (platform-aware)
|
||||
- `.github/workflows/update-server.yml` - Joomla updates.xml generation for dev/alpha/beta/rc
|
||||
- `.github/workflows/changelog-validation.yml` - CHANGELOG.md format validation
|
||||
- `.gitea/workflows/auto-release.yml` - Main release workflow (platform-aware)
|
||||
- `.gitea/workflows/update-server.yml` - Joomla updates.xml generation for dev/alpha/beta/rc
|
||||
- `.gitea/workflows/changelog-validation.yml` - CHANGELOG.md format validation
|
||||
- `.mokostandards` - Platform configuration (`platform: joomla|dolibarr|generic`)
|
||||
- `README.md` - VERSION field (single source of truth)
|
||||
- `CHANGELOG.md` - Release notes source
|
||||
|
||||
@@ -11,7 +11,7 @@ The `reserve-dolibarr-module-id.yml` workflow automates the reservation of Dolib
|
||||
## Quick Links
|
||||
|
||||
- **[Module Registry](../development/crm/module-registry.md)** - Official Dolibarr module number registry
|
||||
- **[Run Workflow](../../.github/workflows/reserve-dolibarr-module-id.yml)** - Reserve a module ID now
|
||||
- **[Run Workflow](../../.gitea/workflows/reserve-dolibarr-module-id.yml)** - Reserve a module ID now
|
||||
- **[Development Guide](../guide/crm/dolibarr-development-guide.md)** - CRM development guide
|
||||
- **[Development Standards](../policy/crm/development-standards.md)** - Coding standards
|
||||
|
||||
@@ -27,7 +27,7 @@ The `reserve-dolibarr-module-id.yml` workflow automates the reservation of Dolib
|
||||
|
||||
### Workflow Location
|
||||
|
||||
**File**: `.github/workflows/reserve-dolibarr-module-id.yml`
|
||||
**File**: `.gitea/workflows/reserve-dolibarr-module-id.yml`
|
||||
**Trigger**: Manual (workflow_dispatch)
|
||||
**Permissions**: `contents: write`, `pull-requests: write`
|
||||
**Repository**: [https://git.mokoconsulting.tech/MokoConsulting/MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards)
|
||||
@@ -202,7 +202,7 @@ Updates `override.config.tf` to protect the workflow file:
|
||||
|
||||
```hcl
|
||||
{
|
||||
path = ".github/workflows/reserve-dolibarr-module-id.yml"
|
||||
path = ".gitea/workflows/reserve-dolibarr-module-id.yml"
|
||||
reason = "Dolibarr module ID reservation workflow"
|
||||
},
|
||||
```
|
||||
|
||||
@@ -26,14 +26,14 @@ MokoStandards provides seven reusable GitHub Actions workflows that enable consi
|
||||
# Basic quality check
|
||||
jobs:
|
||||
quality:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-php-quality.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-php-quality.yml@main
|
||||
|
||||
# Type-aware build and release
|
||||
build:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-build.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-build.yml@main
|
||||
|
||||
release:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-release.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-release.yml@main
|
||||
with:
|
||||
version: '1.0.0'
|
||||
```
|
||||
@@ -68,7 +68,7 @@ Runs comprehensive PHP code quality checks using PHPCS, PHPStan, and Psalm with
|
||||
```yaml
|
||||
jobs:
|
||||
quality:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-php-quality.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-php-quality.yml@main
|
||||
with:
|
||||
php-versions: '["8.1", "8.2"]'
|
||||
tools: '["phpcs", "phpstan", "psalm"]'
|
||||
@@ -93,7 +93,7 @@ Matrix testing for Joomla extensions across PHP and Joomla versions with PHPUnit
|
||||
```yaml
|
||||
jobs:
|
||||
test:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-joomla-testing.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-joomla-testing.yml@main
|
||||
with:
|
||||
php-versions: '["8.1", "8.2"]'
|
||||
joomla-versions: '["4.4", "5.0", "5.1"]'
|
||||
@@ -118,7 +118,7 @@ Repository standards validation with configurable profiles (basic, full, strict)
|
||||
```yaml
|
||||
jobs:
|
||||
validate:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-ci-validation.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-ci-validation.yml@main
|
||||
with:
|
||||
profile: 'full'
|
||||
validate-security: true
|
||||
@@ -149,7 +149,7 @@ Automatically cleans up stale and merged branches with configurable exclusion pa
|
||||
```yaml
|
||||
jobs:
|
||||
cleanup:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-branch-cleanup.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-branch-cleanup.yml@main
|
||||
with:
|
||||
stale-days: 90
|
||||
delete-merged: true
|
||||
@@ -178,7 +178,7 @@ jobs:
|
||||
```yaml
|
||||
jobs:
|
||||
cleanup:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-branch-cleanup.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-branch-cleanup.yml@main
|
||||
with:
|
||||
stale-days: 60
|
||||
delete-merged: true
|
||||
@@ -200,7 +200,7 @@ Automatically detects project type and provides outputs for downstream workflows
|
||||
```yaml
|
||||
jobs:
|
||||
detect:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-project-detector.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-project-detector.yml@main
|
||||
```
|
||||
|
||||
**Outputs:**
|
||||
@@ -217,7 +217,7 @@ Universal build workflow that adapts to project type with automatic dependency m
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-build.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-build.yml@main
|
||||
with:
|
||||
php-version: '8.1'
|
||||
node-version: '20.x'
|
||||
@@ -243,7 +243,7 @@ Creates releases with type-specific packaging and marketplace support.
|
||||
```yaml
|
||||
jobs:
|
||||
release:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-release.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-release.yml@main
|
||||
with:
|
||||
version: '1.0.0'
|
||||
prerelease: false
|
||||
@@ -272,7 +272,7 @@ Multi-environment deployment with type-specific logic and health checks.
|
||||
```yaml
|
||||
jobs:
|
||||
deploy:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-deploy.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-deploy.yml@main
|
||||
with:
|
||||
environment: staging
|
||||
deployment-method: rsync
|
||||
@@ -326,11 +326,11 @@ permissions:
|
||||
jobs:
|
||||
# Auto-detect project type
|
||||
detect:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-project-detector.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-project-detector.yml@main
|
||||
|
||||
# Validate code
|
||||
validate:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-ci-validation.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-ci-validation.yml@main
|
||||
with:
|
||||
profile: 'full'
|
||||
|
||||
@@ -338,7 +338,7 @@ jobs:
|
||||
quality:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.has-php == 'true'
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-php-quality.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-php-quality.yml@main
|
||||
with:
|
||||
php-versions: '["8.1", "8.2"]'
|
||||
|
||||
@@ -346,7 +346,7 @@ jobs:
|
||||
test:
|
||||
needs: [detect, quality]
|
||||
if: needs.detect.outputs.project-type == 'joomla'
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-joomla-testing.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-joomla-testing.yml@main
|
||||
with:
|
||||
php-versions: '["8.1", "8.2"]'
|
||||
coverage: true
|
||||
@@ -356,13 +356,13 @@ jobs:
|
||||
# Build (works for all types)
|
||||
build:
|
||||
needs: [detect, validate]
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-build.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-build.yml@main
|
||||
|
||||
# Deploy to staging
|
||||
deploy-staging:
|
||||
needs: build
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-deploy.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-deploy.yml@main
|
||||
with:
|
||||
environment: staging
|
||||
deployment-method: rsync
|
||||
@@ -372,7 +372,7 @@ jobs:
|
||||
release:
|
||||
needs: [detect, build]
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-release.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-release.yml@main
|
||||
with:
|
||||
version: ${{ github.ref_name }}
|
||||
|
||||
@@ -380,7 +380,7 @@ jobs:
|
||||
deploy-production:
|
||||
needs: release
|
||||
if: github.event_name == 'release'
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-deploy.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-deploy.yml@main
|
||||
with:
|
||||
environment: production
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
@@ -398,19 +398,19 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-ci-validation.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-ci-validation.yml@main
|
||||
with:
|
||||
profile: 'full'
|
||||
|
||||
quality:
|
||||
needs: validate
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-php-quality.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-php-quality.yml@main
|
||||
with:
|
||||
php-versions: '["8.1", "8.2"]'
|
||||
|
||||
test:
|
||||
needs: quality
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-joomla-testing.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-joomla-testing.yml@main
|
||||
with:
|
||||
coverage: true
|
||||
secrets:
|
||||
@@ -425,17 +425,17 @@ jobs:
|
||||
|
||||
**Recommended:** Pin to main branch for automatic updates
|
||||
```yaml
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-php-quality.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-php-quality.yml@main
|
||||
```
|
||||
|
||||
**Stable:** Pin to specific tag
|
||||
```yaml
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-php-quality.yml@v1.0.0
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-php-quality.yml@v1.0.0
|
||||
```
|
||||
|
||||
**Maximum Stability:** Pin to commit SHA
|
||||
```yaml
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-php-quality.yml@abc1234
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-php-quality.yml@abc1234
|
||||
```
|
||||
|
||||
### Secret Management
|
||||
@@ -461,14 +461,14 @@ jobs:
|
||||
# Dev branches: basic validation
|
||||
validate-dev:
|
||||
if: startsWith(github.ref, 'refs/heads/dev/')
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-ci-validation.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-ci-validation.yml@main
|
||||
with:
|
||||
profile: 'basic'
|
||||
|
||||
# Main branch: strict validation
|
||||
validate-main:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-ci-validation.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-ci-validation.yml@main
|
||||
with:
|
||||
profile: 'strict'
|
||||
fail-on-warnings: true
|
||||
@@ -479,7 +479,7 @@ jobs:
|
||||
## Troubleshooting
|
||||
|
||||
### Workflow Not Found
|
||||
**Error:** `Unable to resolve action MokoConsulting/MokoStandards/.github/workflows/...`
|
||||
**Error:** `Unable to resolve action MokoConsulting/MokoStandards/.gitea/workflows/...`
|
||||
|
||||
**Solution:** Ensure calling repository has access to MokoStandards. For private repositories, configure proper access permissions.
|
||||
|
||||
@@ -536,7 +536,7 @@ jobs:
|
||||
# After: Reusable workflow
|
||||
jobs:
|
||||
quality:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-php-quality.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-php-quality.yml@main
|
||||
with:
|
||||
phpcs-standard: 'PSR12'
|
||||
tools: '["phpcs"]'
|
||||
|
||||
@@ -55,7 +55,7 @@ These workflows are synced to every governed repository via `bulk_sync.php`. The
|
||||
|
||||
**Requirements:** `secrets.GH_TOKEN` with write access
|
||||
|
||||
**Skips:** Commits by `github-actions[bot]`, commits with `[skip ci]`
|
||||
**Skips:** Commits by `gitea-actions[bot]`, commits with `[skip ci]`
|
||||
|
||||
---
|
||||
|
||||
@@ -77,7 +77,7 @@ These workflows are synced to every governed repository via `bulk_sync.php`. The
|
||||
**On patch releases (patch != 00):**
|
||||
7. Updates the existing minor release (appends patch notes, updates title)
|
||||
|
||||
**Protection:** Workflow files are protected by CODEOWNERS (requires @jmiller-moko review) and a file path restriction ruleset. Only bypass actors can modify `.github/workflows/`.
|
||||
**Protection:** Workflow files are protected by CODEOWNERS (requires @jmiller review) and a file path restriction ruleset. Only bypass actors can modify `.gitea/workflows/`.
|
||||
|
||||
| Version | What happens |
|
||||
|---------|-------------|
|
||||
@@ -108,7 +108,7 @@ See dedicated docs:
|
||||
|
||||
### Common Features (both deploy workflows)
|
||||
|
||||
- **Permission check:** `jmiller-moko` and `github-actions[bot]` hardcoded as authorized; others need `admin`/`maintain` role
|
||||
- **Permission check:** `jmiller` and `gitea-actions[bot]` hardcoded as authorized; others need `admin`/`maintain` role
|
||||
- **Chore skip:** PRs from `chore/` branches do not deploy
|
||||
- **Suffix required:** `{ENV}_FTP_SUFFIX` must be set or deployment is skipped
|
||||
- **Clear before upload:** Remote folder is always cleared before uploading
|
||||
@@ -143,7 +143,7 @@ See [update-server.md](update-server.md) for the full update server specificatio
|
||||
|
||||
**Trigger:** Branch creation matching `dev/**` or `rc/**`
|
||||
|
||||
**What it does:** Auto-creates a tracking issue when a new `dev/**` or `rc/**` branch is pushed. Assigns `jmiller-moko`.
|
||||
**What it does:** Auto-creates a tracking issue when a new `dev/**` or `rc/**` branch is pushed. Assigns `jmiller`.
|
||||
|
||||
| Branch type | Title prefix | Label |
|
||||
|-------------|-------------|-------|
|
||||
@@ -161,7 +161,7 @@ Skips if an issue for that version already exists.
|
||||
**What it does (always):**
|
||||
- Deletes 25 retired workflow files
|
||||
- Checks for version drift across all files
|
||||
- Creates `.github/workflows/custom/` directory if missing
|
||||
- Creates `.gitea/workflows/custom/` directory if missing
|
||||
|
||||
**What it does (toggleable):**
|
||||
- Reset labels to 58-label standard set (manual only, off by default)
|
||||
@@ -175,10 +175,10 @@ Skips if an issue for that version already exists.
|
||||
|
||||
## Custom Workflows
|
||||
|
||||
Every governed repo has a `.github/workflows/custom/` directory that is **never touched by sync or cleanup**. Place repo-specific workflows here:
|
||||
Every governed repo has a `.gitea/workflows/custom/` directory that is **never touched by sync or cleanup**. Place repo-specific workflows here:
|
||||
|
||||
```
|
||||
.github/workflows/
|
||||
.gitea/workflows/
|
||||
├── deploy-dev.yml ← Synced (overwritten on sync)
|
||||
├── auto-release.yml ← Synced (overwritten on sync)
|
||||
├── repository-cleanup.yml ← Synced (overwritten on sync)
|
||||
|
||||
@@ -126,7 +126,7 @@ cp templates/docs/required/SECURITY.md ./
|
||||
**Remediation**:
|
||||
```bash
|
||||
# Fix YAML formatting
|
||||
yamllint --format auto .github/workflows/*.yml
|
||||
yamllint --format auto .gitea/workflows/*.yml
|
||||
|
||||
# Fix Python formatting
|
||||
black scripts/**/*.py
|
||||
@@ -186,7 +186,7 @@ phpcs --standard=PSR12 src/
|
||||
- All version references match the canonical version in `composer.json`
|
||||
- Checks 39+ files including:
|
||||
- Documentation (README.md, CHANGELOG.md, CONTRIBUTING.md)
|
||||
- Workflows (.github/workflows/*.yml)
|
||||
- Workflows (.gitea/workflows/*.yml)
|
||||
- PHP source files (src/**/*.php)
|
||||
- Configuration files
|
||||
|
||||
@@ -229,7 +229,7 @@ python3 api/maintenance/validate_script_registry.py --priority critical
|
||||
python3 api/maintenance/generate_script_registry.py --update
|
||||
|
||||
# Or use auto-update workflow
|
||||
# .github/workflows/auto-update-sha.yml
|
||||
# .gitea/workflows/auto-update-sha.yml
|
||||
```
|
||||
|
||||
### 9. Enterprise Readiness Check ⭐ NEW (Informational)
|
||||
@@ -349,7 +349,7 @@ php api/validate/check_enterprise_readiness.php --verbose
|
||||
php api/validate/check_repo_health.php --verbose
|
||||
|
||||
# Lint YAML files
|
||||
yamllint .github/workflows/*.yml
|
||||
yamllint .gitea/workflows/*.yml
|
||||
|
||||
# Format Python code
|
||||
black --check scripts/**/*.py
|
||||
|
||||
@@ -205,7 +205,7 @@ The Create Sub-Issue workflow provides automation for:
|
||||
|
||||
- [Issue Management Configuration](.github/issue-management-config.yml)
|
||||
- [Sub-Task Template](.github/ISSUE_TEMPLATE/sub-task.yml)
|
||||
- [Create Sub-Issue Workflow](.github/workflows/create-sub-issue.yml)
|
||||
- [Create Sub-Issue Workflow](.gitea/workflows/create-sub-issue.yml)
|
||||
- [GitHub Issues Documentation](https://docs.github.com/en/issues)
|
||||
|
||||
## Support
|
||||
|
||||
+118
-33
@@ -33,10 +33,44 @@ Joomla's `updates.xml` contains **multiple `<update>` entries simultaneously**
|
||||
| Release Candidate | `<tag>rc</tag>` | Sites set to RC or lower | `update-server.yml` on `rc/**` push |
|
||||
| Beta | `<tag>beta</tag>` | Sites set to Beta or lower | `update-server.yml` on `beta/**` push |
|
||||
| Alpha | `<tag>alpha</tag>` | Sites set to Alpha or lower | `update-server.yml` on `alpha/**` push |
|
||||
| Development | `<tag>development</tag>` | Sites set to Development | `update-server.yml` on `dev/**` push |
|
||||
| Development | `<tag>development</tag>` | Sites set to Development | `update-server.yml` on `dev` or `dev/**` push |
|
||||
|
||||
**Note**: Alpha and beta are optional stages. Not every release cycle will have alpha/beta entries.
|
||||
|
||||
### `update-server.yml` Trigger Behavior
|
||||
|
||||
The `update-server.yml` workflow triggers on **both direct pushes and PR merges** to the following branches:
|
||||
|
||||
- `dev` (bare branch — no sub-path required)
|
||||
- `dev/**` (versioned dev branches like `dev/02.01`)
|
||||
- `alpha/**`
|
||||
- `beta/**`
|
||||
- `rc/**`
|
||||
|
||||
Previously, the workflow only triggered on PR merges. The addition of push triggers ensures that direct commits to these branches (e.g., CI-generated version bumps, automated fixes) also update the `updates.xml` entries.
|
||||
|
||||
### Cascade Release Channels
|
||||
|
||||
Each stability level writes its own channel **and all lower channels** to `updates.xml`. This ensures Joomla sites on any "Minimum Stability" setting always see the latest available release:
|
||||
|
||||
| Release Stream | Channels written to updates.xml |
|
||||
|---------------|-------------------------------|
|
||||
| development | `development` |
|
||||
| alpha | `development`, `alpha` |
|
||||
| beta | `development`, `alpha`, `beta` |
|
||||
| rc | `development`, `alpha`, `beta`, `rc` |
|
||||
| stable | `development`, `alpha`, `beta`, `rc`, `stable` |
|
||||
|
||||
Without cascade, a site set to "Development" minimum stability would only see `<tag>development</tag>` entries and would miss stable releases entirely. The cascade ensures stable releases are visible to all sites regardless of their minimum stability setting.
|
||||
|
||||
For full cascade documentation, see [Cascade Release Channels](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards/src/branch/main/docs/release-management/cascade-channels.md).
|
||||
|
||||
### Sync to Main
|
||||
|
||||
Since Joomla sites read `updates.xml` from the `main` branch, the `update-server.yml` workflow **syncs `updates.xml` to `main` via the Gitea API** after building on non-main branches. This ensures pre-release channel entries (dev, alpha, beta, rc) are visible to sites checking for updates, without requiring a PR merge to main.
|
||||
|
||||
Previously, `update-server.yml` only committed `updates.xml` to the current branch, so Joomla sites never saw dev/alpha/beta/rc releases until they were merged to main.
|
||||
|
||||
### How Joomla Filters Updates
|
||||
|
||||
Joomla's update system reads all `<update>` entries from the XML file but only presents entries whose `<tag>` matches the site's minimum stability threshold:
|
||||
@@ -66,7 +100,7 @@ dev → [alpha] → [beta] → rc → version/XX → main → dev
|
||||
optional optional (integration) (production) (feedback)
|
||||
```
|
||||
|
||||
- **`dev/**`**: Active development. Update files tagged as `development`.
|
||||
- **`dev` or `dev/**`**: Active development. Update files tagged as `development`.
|
||||
- **`alpha/**`**: *(Optional)* Early internal testing. Update files tagged as `alpha`. Can be skipped.
|
||||
- **`beta/**`**: *(Optional)* Broader external testing. Update files tagged as `beta`. Can be skipped.
|
||||
- **`rc/**`**: Release candidate. Update files tagged as `rc`. RC branches deploy to dev server for final testing.
|
||||
@@ -95,93 +129,101 @@ development
|
||||
|
||||
The module descriptor's `url_last_version` should point to:
|
||||
```
|
||||
https://raw.githubusercontent.com/{org}/{repo}/main/update.txt
|
||||
https://git.mokoconsulting.tech/MokoConsulting/{repo}/raw/branch/main/update.txt
|
||||
```
|
||||
|
||||
## Joomla: `updates.xml` (Multi-Entry)
|
||||
|
||||
The `updates.xml` file contains **up to five stability entries at once** (one per stability level). Joomla reads the entire file and filters by the site's minimum stability setting.
|
||||
|
||||
### Platform Distribution
|
||||
|
||||
| Release Type | Gitea Release | GitHub Release | Download URLs |
|
||||
|-------------|---------------|----------------|---------------|
|
||||
| **Stable** | Yes | Yes (via mirror) | Dual (Gitea + GitHub) |
|
||||
| **RC** | Yes | No | Single (Gitea only) |
|
||||
| **Beta** | Yes | No | Single (Gitea only) |
|
||||
| **Alpha** | Yes | No | Single (Gitea only) |
|
||||
| **Development** | Yes | No | Single (Gitea only) |
|
||||
|
||||
Pre-release builds stay on Gitea for internal testing. Only stable releases are mirrored to GitHub.
|
||||
|
||||
### Complete Multi-Entry Example
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<updates>
|
||||
<!-- Stable entry: visible to all sites (default minimum stability) -->
|
||||
<!-- Stable: dual download (Gitea + GitHub), visible to all sites -->
|
||||
<update>
|
||||
<name>My Extension</name>
|
||||
<description>My Extension stable release</description>
|
||||
<element>com_myextension</element>
|
||||
<type>component</type>
|
||||
<version>01.02.03</version>
|
||||
<client>site</client>
|
||||
<tags>
|
||||
<tag>stable</tag>
|
||||
</tags>
|
||||
<infourl title="My Extension">https://github.com/org/repo/releases/tag/v01</infourl>
|
||||
<infourl title="My Extension">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/releases</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://github.com/org/repo/releases/download/v01/com_myextension-01.02.03.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/releases/download/v01/com_myextension-01.02.03.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://github.com/mokoconsulting-tech/MyExtension/releases/download/v01/com_myextension-01.02.03.zip</downloadurl>
|
||||
</downloads>
|
||||
<sha256>abc123...full-hash-here</sha256>
|
||||
<targetplatform name="joomla" version="((4\.[3-9])|(5\.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
|
||||
<!-- RC entry: visible to sites with minimum stability = RC or lower -->
|
||||
<!-- RC: Gitea only, visible to sites with minimum stability = RC or lower -->
|
||||
<update>
|
||||
<name>My Extension</name>
|
||||
<description>My Extension release candidate</description>
|
||||
<element>com_myextension</element>
|
||||
<type>component</type>
|
||||
<version>01.03.01-rc</version>
|
||||
<client>site</client>
|
||||
<tags>
|
||||
<tag>rc</tag>
|
||||
</tags>
|
||||
<infourl title="My Extension">https://github.com/org/repo/tree/rc/01.03.01</infourl>
|
||||
<infourl title="My Extension">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/src/branch/rc</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://github.com/org/repo/archive/refs/heads/rc/01.03.01.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/releases/download/rc/com_myextension-01.03.01-rc.zip</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="((4\.[3-9])|(5\.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
|
||||
<!-- Beta entry: visible to sites with minimum stability = Beta or lower -->
|
||||
<!-- Beta: Gitea only, visible to sites with minimum stability = Beta or lower -->
|
||||
<update>
|
||||
<name>My Extension</name>
|
||||
<description>My Extension beta build</description>
|
||||
<element>com_myextension</element>
|
||||
<type>component</type>
|
||||
<version>01.03.01-beta</version>
|
||||
<client>site</client>
|
||||
<tags>
|
||||
<tag>beta</tag>
|
||||
</tags>
|
||||
<infourl title="My Extension">https://github.com/org/repo/tree/beta/01.03.01</infourl>
|
||||
<infourl title="My Extension">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/src/branch/beta</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://github.com/org/repo/archive/refs/heads/beta/01.03.01.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/releases/download/beta/com_myextension-01.03.01-beta.zip</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="((4\.[3-9])|(5\.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
|
||||
<!-- Alpha entry: visible to sites with minimum stability = Alpha or lower -->
|
||||
<!-- Alpha: Gitea only, visible to sites with minimum stability = Alpha or lower -->
|
||||
<update>
|
||||
<name>My Extension</name>
|
||||
<description>My Extension alpha build</description>
|
||||
<element>com_myextension</element>
|
||||
<type>component</type>
|
||||
<version>01.03.01-alpha</version>
|
||||
<client>site</client>
|
||||
<tags>
|
||||
<tag>alpha</tag>
|
||||
</tags>
|
||||
<infourl title="My Extension">https://github.com/org/repo/tree/alpha/01.03.01</infourl>
|
||||
<infourl title="My Extension">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/src/branch/alpha</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://github.com/org/repo/archive/refs/heads/alpha/01.03.01.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/releases/download/alpha/com_myextension-01.03.01-alpha.zip</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="((4\.[3-9])|(5\.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -195,13 +237,12 @@ The `updates.xml` file contains **up to five stability entries at once** (one pe
|
||||
<element>com_myextension</element>
|
||||
<type>component</type>
|
||||
<version>01.04.00-dev</version>
|
||||
<client>site</client>
|
||||
<tags>
|
||||
<tag>development</tag>
|
||||
</tags>
|
||||
<infourl title="My Extension">https://github.com/org/repo/tree/dev/01.04</infourl>
|
||||
<infourl title="My Extension">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/src/branch/dev</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">https://github.com/org/repo/archive/refs/heads/dev/01.04.zip</downloadurl>
|
||||
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MyExtension/releases/download/development/com_myextension-01.04.00-dev.zip</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="((4\.[3-9])|(5\.[0-9]))" />
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
@@ -215,12 +256,14 @@ The `updates.xml` file contains **up to five stability entries at once** (one pe
|
||||
| Workflow | Trigger | Entry written |
|
||||
|----------|---------|---------------|
|
||||
| `auto-release.yml` | Push to `main` | `<tag>stable</tag>` — writes the stable entry with SHA-256 hash of the ZIP |
|
||||
| `update-server.yml` | Push to `rc/**` | `<tag>rc</tag>` — adds/updates the RC entry |
|
||||
| `update-server.yml` | Push to `beta/**` | `<tag>beta</tag>` — adds/updates the beta entry |
|
||||
| `update-server.yml` | Push to `alpha/**` | `<tag>alpha</tag>` — adds/updates the alpha entry |
|
||||
| `update-server.yml` | Push to `dev/**` | `<tag>development</tag>` — adds/updates the development entry |
|
||||
| `update-server.yml` | Push to `rc/**` | `<tag>rc</tag>` — adds/updates the RC entry (+ cascaded lower channels) |
|
||||
| `update-server.yml` | Push to `beta/**` | `<tag>beta</tag>` — adds/updates the beta entry (+ cascaded lower channels) |
|
||||
| `update-server.yml` | Push to `alpha/**` | `<tag>alpha</tag>` — adds/updates the alpha entry (+ cascaded lower channels) |
|
||||
| `update-server.yml` | Push to `dev` or `dev/**` | `<tag>development</tag>` — adds/updates the development entry |
|
||||
|
||||
The `auto-release.yml` workflow writes the stable entry and preserves any existing pre-release entries. The `update-server.yml` workflow writes only its specific entry (rc, beta, alpha, or dev) and preserves the others.
|
||||
The `auto-release.yml` workflow writes the stable entry and preserves any existing pre-release entries. The `update-server.yml` workflow writes its specific entry (and cascaded lower channels) and preserves the others.
|
||||
|
||||
**Important**: All `update-server.yml` runs also sync the updated `updates.xml` to `main` via the Gitea API, since Joomla sites read the update server XML from the `main` branch.
|
||||
|
||||
### XML Elements
|
||||
|
||||
@@ -271,32 +314,40 @@ The `<updateservers>` tag tells Joomla where to check for updates. Both servers
|
||||
- Uploads ZIP to the `vXX` major release on GitHub
|
||||
- Computes SHA-256 hash of the ZIP
|
||||
- Writes/updates the `<tag>stable</tag>` entry in `updates.xml` with version, download URL, and SHA-256
|
||||
- Cascades to all 5 stability channels
|
||||
- Preserves any existing rc/dev entries in the file
|
||||
- Commits updated `updates.xml` to main
|
||||
|
||||
2. **On RC push** (`update-server.yml` → rc/** branches):
|
||||
- Writes/updates the `<tag>rc</tag>` entry in `updates.xml`
|
||||
- Download URL points to the branch archive ZIP
|
||||
- Cascades to `rc`, `beta`, `alpha`, and `development` channels
|
||||
- Download URL points to the Gitea release ZIP
|
||||
- Preserves all other stability entries
|
||||
- Commits updated `updates.xml` to the rc branch
|
||||
- **Syncs `updates.xml` to `main` via Gitea API**
|
||||
|
||||
3. **On beta push** (`update-server.yml` → beta/** branches):
|
||||
- Writes/updates the `<tag>beta</tag>` entry in `updates.xml`
|
||||
- Download URL points to the branch archive ZIP
|
||||
- Cascades to `beta`, `alpha`, and `development` channels
|
||||
- Download URL points to the Gitea release ZIP
|
||||
- Preserves all other stability entries
|
||||
- Commits updated `updates.xml` to the beta branch
|
||||
- **Syncs `updates.xml` to `main` via Gitea API**
|
||||
|
||||
4. **On alpha push** (`update-server.yml` → alpha/** branches):
|
||||
- Writes/updates the `<tag>alpha</tag>` entry in `updates.xml`
|
||||
- Download URL points to the branch archive ZIP
|
||||
- Cascades to `alpha` and `development` channels
|
||||
- Download URL points to the Gitea release ZIP
|
||||
- Preserves all other stability entries
|
||||
- Commits updated `updates.xml` to the alpha branch
|
||||
- **Syncs `updates.xml` to `main` via Gitea API**
|
||||
|
||||
5. **On dev push** (`update-server.yml` → dev/** branches):
|
||||
5. **On dev push** (`update-server.yml` → `dev` or `dev/**` branches):
|
||||
- Writes/updates the `<tag>development</tag>` entry in `updates.xml`
|
||||
- Download URL points to the branch archive ZIP
|
||||
- Download URL points to the Gitea release ZIP
|
||||
- Preserves all other stability entries
|
||||
- Commits updated `updates.xml` to the dev branch
|
||||
- **Syncs `updates.xml` to `main` via Gitea API**
|
||||
|
||||
### Dolibarr (update.txt)
|
||||
|
||||
@@ -331,3 +382,37 @@ Branch protection rulesets (applied via `sync_rulesets.php`):
|
||||
- **ALPHA**: prevents deletion, non-fast-forward
|
||||
- **BETA**: prevents deletion, non-fast-forward
|
||||
- **RC**: prevents deletion, non-fast-forward
|
||||
|
||||
## Update Server Priority
|
||||
|
||||
Joomla manifest `<updateservers>` entries MUST follow this priority order:
|
||||
|
||||
| Priority | Server | URL pattern |
|
||||
|----------|--------|-------------|
|
||||
| **1 (primary)** | Gitea | `https://git.mokoconsulting.tech/MokoConsulting/{REPO}/raw/branch/main/updates.xml` |
|
||||
| **2 (fallback)** | GitHub | `https://raw.githubusercontent.com/mokoconsulting-tech/{REPO}/main/updates.xml` |
|
||||
|
||||
### Why Gitea first
|
||||
|
||||
1. **Gitea is the source of truth** — all CI/CD runs on Gitea, releases are created here first
|
||||
2. **GitHub is a push mirror** — it may lag behind by minutes or hours
|
||||
3. **Self-hosted control** — Gitea is under our infrastructure, GitHub is third-party
|
||||
4. **Availability** — if GitHub has an outage, Joomla sites still get updates from Gitea
|
||||
|
||||
### Manifest example
|
||||
|
||||
```xml
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="ExtensionName Update Server (Gitea)">
|
||||
https://git.mokoconsulting.tech/MokoConsulting/RepoName/raw/branch/main/updates.xml
|
||||
</server>
|
||||
<server type="extension" priority="2" name="ExtensionName Update Server (GitHub)">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/RepoName/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
### Enforcement
|
||||
|
||||
The `enforce_tags.sh` script and `repo_health.yml` workflow validate this ordering.
|
||||
Repos with GitHub as priority 1 will be flagged as non-compliant.
|
||||
|
||||
@@ -32,6 +32,7 @@ BRIEF: Workflow architecture, hierarchy, and design patterns
|
||||
# Workflow Architecture
|
||||
|
||||
## Overview
|
||||
> **Important (v2):** All workflows MUST be in `.gitea/workflows/` only. Gitea Actions does not run workflows from `.github/workflows/`. Having files in `.github/workflows/` creates ghost queued runs that block the runner.
|
||||
|
||||
This document explains the workflow architecture used across Moko Consulting repositories, including the hierarchy, design patterns, reusable workflow patterns, and decision-making processes for workflow selection.
|
||||
|
||||
@@ -68,7 +69,7 @@ Moko Consulting uses a three-tier architecture for GitHub Actions workflows:
|
||||
↓ (called by)
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Tier 3: Local Workflows │
|
||||
│ Location: Individual repository .github/workflows/ │
|
||||
│ Location: Individual repository .gitea/workflows/ │
|
||||
│ Visibility: Matches repository visibility │
|
||||
│ Purpose: Repository-specific automation │
|
||||
│ Examples: Project builds, tests, custom deployments │
|
||||
@@ -77,7 +78,7 @@ Moko Consulting uses a three-tier architecture for GitHub Actions workflows:
|
||||
|
||||
### Tier 1: Organization-Wide Reusable Workflows
|
||||
|
||||
**Location**: `mokoconsulting-tech/.github-private/.github/workflows/`
|
||||
**Location**: `mokoconsulting-tech/.github-private/.gitea/workflows/`
|
||||
|
||||
**Characteristics**:
|
||||
- Private and secure
|
||||
@@ -103,7 +104,7 @@ Moko Consulting uses a three-tier architecture for GitHub Actions workflows:
|
||||
|
||||
### Tier 2: Public Reusable Workflows
|
||||
|
||||
**Location**: `MokoConsulting/MokoStandards/.github/workflows/`
|
||||
**Location**: `MokoConsulting/MokoStandards/.gitea/workflows/`
|
||||
|
||||
**Characteristics**:
|
||||
- Public and community-accessible
|
||||
@@ -130,7 +131,7 @@ Moko Consulting uses a three-tier architecture for GitHub Actions workflows:
|
||||
|
||||
### Tier 3: Local Workflows
|
||||
|
||||
**Location**: Individual repository `.github/workflows/`
|
||||
**Location**: Individual repository `.gitea/workflows/`
|
||||
|
||||
**Characteristics**:
|
||||
- Repository-specific
|
||||
@@ -160,17 +161,17 @@ Automatically detect project type and execute appropriate build/test strategy.
|
||||
```yaml
|
||||
jobs:
|
||||
detect:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-project-detector.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-project-detector.yml@main
|
||||
|
||||
build-joomla:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.project-type == 'joomla'
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-joomla-testing.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-joomla-testing.yml@main
|
||||
|
||||
build-generic:
|
||||
needs: detect
|
||||
if: needs.detect.outputs.project-type == 'generic'
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-build.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-build.yml@main
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
@@ -186,18 +187,18 @@ Build complex workflows by composing simple reusable workflows.
|
||||
jobs:
|
||||
# Step 1: Validate code
|
||||
validate:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-ci-validation.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-ci-validation.yml@main
|
||||
|
||||
# Step 2: Build if validation passes
|
||||
build:
|
||||
needs: validate
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-build.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-build.yml@main
|
||||
|
||||
# Step 3: Deploy if build passes
|
||||
deploy:
|
||||
needs: build
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-deploy.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-deploy.yml@main
|
||||
secrets: inherit
|
||||
```
|
||||
|
||||
@@ -218,7 +219,7 @@ jobs:
|
||||
matrix:
|
||||
php-version: ['7.4', '8.0', '8.1', '8.2']
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-php-quality.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-php-quality.yml@main
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
```
|
||||
@@ -236,13 +237,13 @@ Deploy to different environments based on branch or tag.
|
||||
jobs:
|
||||
deploy-staging:
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-deploy.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-deploy.yml@main
|
||||
with:
|
||||
environment: staging
|
||||
|
||||
deploy-production:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-deploy.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-deploy.yml@main
|
||||
with:
|
||||
environment: production
|
||||
```
|
||||
@@ -275,7 +276,7 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
uses: MokoConsulting/MokoStandards/.github/workflows/reusable-deploy.yml@main
|
||||
uses: MokoConsulting/MokoStandards/.gitea/workflows/reusable-deploy.yml@main
|
||||
with:
|
||||
environment: ${{ inputs.environment }}
|
||||
dry-run: ${{ inputs.dry-run }}
|
||||
@@ -458,9 +459,9 @@ jobs:
|
||||
```yaml
|
||||
jobs:
|
||||
validate:
|
||||
uses: org/repo/.github/workflows/reusable-validate.yml@main
|
||||
uses: org/repo/.gitea/workflows/reusable-validate.yml@main
|
||||
build:
|
||||
uses: org/repo/.github/workflows/reusable-build.yml@main
|
||||
uses: org/repo/.gitea/workflows/reusable-build.yml@main
|
||||
```
|
||||
|
||||
❌ **Bad**: Duplicate logic
|
||||
|
||||
@@ -89,7 +89,7 @@ All secrets are configured at **organization level** with inheritance:
|
||||
```yaml
|
||||
jobs:
|
||||
deploy:
|
||||
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable/deploy-staging.yml@main
|
||||
uses: mokoconsulting-tech/.github-private/.gitea/workflows/reusable/deploy-staging.yml@main
|
||||
secrets: inherit # All org secrets automatically available
|
||||
```
|
||||
|
||||
@@ -175,7 +175,7 @@ env:
|
||||
|
||||
**Migration Type:** Convert to reusable workflow in `.github-private`
|
||||
|
||||
**Proposed Location:** `.github-private/.github/workflows/reusable/php-quality.yml`
|
||||
**Proposed Location:** `.github-private/.gitea/workflows/reusable/php-quality.yml`
|
||||
|
||||
**Migration Complexity:** ⭐⭐⭐ Medium
|
||||
- Requires PHP tool setup and configuration
|
||||
@@ -283,7 +283,7 @@ on:
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable/php-quality.yml@main
|
||||
uses: mokoconsulting-tech/.github-private/.gitea/workflows/reusable/php-quality.yml@main
|
||||
with:
|
||||
php-versions: '["7.4", "8.0", "8.1", "8.2"]'
|
||||
tools: '["phpcs", "phpstan", "psalm"]'
|
||||
@@ -460,7 +460,7 @@ outputs:
|
||||
|
||||
**Migration Type:** Convert to reusable workflow in `.github-private`
|
||||
|
||||
**Proposed Location:** `.github-private/.github/workflows/reusable/release-pipeline.yml`
|
||||
**Proposed Location:** `.github-private/.gitea/workflows/reusable/release-pipeline.yml`
|
||||
|
||||
**Migration Complexity:** ⭐⭐⭐⭐⭐ Very High
|
||||
- Complex multi-stage process
|
||||
@@ -577,7 +577,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable/release-pipeline.yml@main
|
||||
uses: mokoconsulting-tech/.github-private/.gitea/workflows/reusable/release-pipeline.yml@main
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
platform: 'joomla'
|
||||
@@ -742,7 +742,7 @@ outputs:
|
||||
|
||||
**Migration Type:** Convert to reusable workflow in `.github-private`
|
||||
|
||||
**Proposed Location:** `.github-private/.github/workflows/reusable/deploy-staging.yml`
|
||||
**Proposed Location:** `.github-private/.gitea/workflows/reusable/deploy-staging.yml`
|
||||
|
||||
**Migration Complexity:** ⭐⭐⭐⭐ High
|
||||
- Requires secure credential handling
|
||||
@@ -869,7 +869,7 @@ on:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable/deploy-staging.yml@main
|
||||
uses: mokoconsulting-tech/.github-private/.gitea/workflows/reusable/deploy-staging.yml@main
|
||||
with:
|
||||
environment: staging
|
||||
health-check-url: 'https://staging.example.com/health'
|
||||
@@ -1015,7 +1015,7 @@ outputs:
|
||||
|
||||
**Migration Type:** Convert to reusable workflow in `.github-private`
|
||||
|
||||
**Proposed Location:** `.github-private/.github/workflows/reusable/joomla-testing.yml`
|
||||
**Proposed Location:** `.github-private/.gitea/workflows/reusable/joomla-testing.yml`
|
||||
|
||||
**Migration Complexity:** ⭐⭐⭐⭐ High
|
||||
- Complex test matrix (PHP × Joomla versions)
|
||||
@@ -1134,7 +1134,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable/joomla-testing.yml@main
|
||||
uses: mokoconsulting-tech/.github-private/.gitea/workflows/reusable/joomla-testing.yml@main
|
||||
with:
|
||||
php-versions: '["7.4", "8.0", "8.1", "8.2"]'
|
||||
joomla-versions: '["4.4", "5.0", "5.1"]'
|
||||
@@ -1408,7 +1408,7 @@ Workflows use `secrets: inherit` to access all organization secrets:
|
||||
```yaml
|
||||
jobs:
|
||||
quality:
|
||||
uses: mokoconsulting-tech/.github-private/.github/workflows/reusable/php-quality.yml@main
|
||||
uses: mokoconsulting-tech/.github-private/.gitea/workflows/reusable/php-quality.yml@main
|
||||
secrets: inherit # Automatically inherits all org secrets
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
* PATH: /lib/CliBase.php
|
||||
* VERSION: 04.06.00
|
||||
* BRIEF: Standalone base CLI class for api/ scripts that do not use CliFramework
|
||||
* BRIEF: Standalone base CLI class for scripts that do not use CliFramework
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
* PATH: /lib/Common.php
|
||||
* VERSION: 04.06.00
|
||||
* BRIEF: Common utility functions for api/ scripts
|
||||
* BRIEF: Common utility functions for scripts
|
||||
* NOTE: Version format used throughout is zero-padded semver: XX.YY.ZZ (e.g. 04.00.04).
|
||||
* All version regex patterns enforce exactly two digits per component by design.
|
||||
*/
|
||||
|
||||
@@ -92,7 +92,7 @@ class EnterpriseReadinessValidator
|
||||
];
|
||||
|
||||
foreach ($required as $library) {
|
||||
$phpFile = "{$path}/api/lib/Enterprise/{$library}.php";
|
||||
$phpFile = "{$path}/lib/Enterprise/{$library}.php";
|
||||
$this->addResult(
|
||||
"Enterprise library: {$library}",
|
||||
file_exists($phpFile),
|
||||
|
||||
@@ -273,6 +273,22 @@ class GiteaAdapter implements GitPlatformAdapter
|
||||
string $body = '',
|
||||
array $options = []
|
||||
): array {
|
||||
// Gitea expects label IDs (int64), not names. Resolve if needed.
|
||||
if (!empty($options['labels']) && is_string($options['labels'][0] ?? null)) {
|
||||
$labelNames = $options['labels'];
|
||||
$existing = $this->listLabels($org, $repo);
|
||||
$nameToId = [];
|
||||
foreach ($existing as $label) {
|
||||
$nameToId[$label['name']] = $label['id'];
|
||||
}
|
||||
$options['labels'] = [];
|
||||
foreach ($labelNames as $name) {
|
||||
if (isset($nameToId[$name])) {
|
||||
$options['labels'][] = $nameToId[$name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$data = array_merge([
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
<?php
|
||||
/**
|
||||
* 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: MokoStandards.Enterprise
|
||||
* INGROUP: MokoStandards
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
* PATH: /lib/Enterprise/MokoStandardsParser.php
|
||||
* VERSION: 04.07.00
|
||||
* BRIEF: Parser for the XML-based .mokostandards repository manifest
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MokoEnterprise;
|
||||
|
||||
use DOMDocument;
|
||||
use SimpleXMLElement;
|
||||
|
||||
/**
|
||||
* MokoStandards Parser
|
||||
*
|
||||
* Reads, writes, and validates the .mokostandards repository manifest.
|
||||
* The file uses XML format (no file extension) and lives at .gitea/.mokostandards.
|
||||
*
|
||||
* @package MokoStandards\Enterprise
|
||||
* @version 04.07.00
|
||||
*/
|
||||
class MokoStandardsParser
|
||||
{
|
||||
public const SCHEMA_VERSION = '1.0';
|
||||
public const NAMESPACE_URI = 'https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API';
|
||||
public const STANDARDS_SOURCE = 'https://git.mokoconsulting.tech/MokoConsulting/MokoStandards';
|
||||
|
||||
/** Valid platform slugs — must match definitions/default/*.tf filenames. */
|
||||
public const VALID_PLATFORMS = [
|
||||
'default-repository',
|
||||
'crm-module',
|
||||
'crm-platform',
|
||||
'generic-repository',
|
||||
'github-private-repository',
|
||||
'joomla-template',
|
||||
'standards-repository',
|
||||
'waas-component',
|
||||
];
|
||||
|
||||
/**
|
||||
* Parse a .mokostandards XML string into a structured array.
|
||||
*
|
||||
* @param string $xmlContent Raw XML content of .mokostandards
|
||||
* @return array{
|
||||
* identity: array{name: string, org: string, description?: string, license?: string, license_spdx?: string, topics?: list<string>},
|
||||
* governance: array{platform: string, standards_version: string, standards_source: string, last_synced?: string},
|
||||
* build?: array,
|
||||
* deploy?: array,
|
||||
* scripts?: array,
|
||||
* overrides?: array
|
||||
* }
|
||||
* @throws \RuntimeException If XML is invalid or missing required elements
|
||||
*/
|
||||
public function parse(string $xmlContent): array
|
||||
{
|
||||
libxml_use_internal_errors(true);
|
||||
$xml = simplexml_load_string($xmlContent);
|
||||
|
||||
if ($xml === false) {
|
||||
$errors = libxml_get_errors();
|
||||
libxml_clear_errors();
|
||||
$msg = !empty($errors) ? $errors[0]->message : 'Unknown XML parse error';
|
||||
throw new \RuntimeException("Invalid .mokostandards XML: " . trim($msg));
|
||||
}
|
||||
|
||||
// Register namespace for XPath
|
||||
$xml->registerXPathNamespace('m', self::NAMESPACE_URI);
|
||||
|
||||
$result = [
|
||||
'schema_version' => (string) ($xml['schema-version'] ?? self::SCHEMA_VERSION),
|
||||
'identity' => $this->parseIdentity($xml),
|
||||
'governance' => $this->parseGovernance($xml),
|
||||
];
|
||||
|
||||
if (isset($xml->build)) {
|
||||
$result['build'] = $this->parseBuild($xml->build);
|
||||
}
|
||||
if (isset($xml->deploy)) {
|
||||
$result['deploy'] = $this->parseDeploy($xml->deploy);
|
||||
}
|
||||
if (isset($xml->scripts)) {
|
||||
$result['scripts'] = $this->parseScripts($xml->scripts);
|
||||
}
|
||||
if (isset($xml->overrides)) {
|
||||
$result['overrides'] = $this->parseOverrides($xml->overrides);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse content, returning null on failure instead of throwing.
|
||||
*
|
||||
* @param string $content Raw file content (XML or legacy YAML-like)
|
||||
* @return array|null Parsed data or null if unparseable
|
||||
*/
|
||||
public function tryParse(string $content): ?array
|
||||
{
|
||||
// Try XML first
|
||||
if (str_contains($content, '<mokostandards')) {
|
||||
try {
|
||||
return $this->parse($content);
|
||||
} catch (\RuntimeException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Try legacy YAML-like format (e.g. "platform: default-repository")
|
||||
return $this->parseLegacy($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the legacy single-line YAML-like format.
|
||||
*
|
||||
* @param string $content e.g. "platform: default-repository\n"
|
||||
* @return array|null Minimal parsed structure or null
|
||||
*/
|
||||
public function parseLegacy(string $content): ?array
|
||||
{
|
||||
$platform = null;
|
||||
$fields = [];
|
||||
|
||||
foreach (explode("\n", $content) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || str_starts_with($line, '#')) {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('/^(\w[\w_-]*)\s*:\s*"?([^"]*)"?\s*$/', $line, $m)) {
|
||||
$fields[$m[1]] = $m[2];
|
||||
}
|
||||
}
|
||||
|
||||
$platform = $fields['platform'] ?? null;
|
||||
if ($platform === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'schema_version' => '0.0', // legacy marker
|
||||
'identity' => [
|
||||
'name' => $fields['governed_repo'] ?? '',
|
||||
'org' => '',
|
||||
],
|
||||
'governance' => [
|
||||
'platform' => $platform,
|
||||
'standards_version' => $fields['standards_version'] ?? '',
|
||||
'standards_source' => $fields['standards_source'] ?? '',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract just the platform slug from any .mokostandards content (XML or legacy).
|
||||
*
|
||||
* @param string $content Raw file content
|
||||
* @return string|null Platform slug or null if unreadable
|
||||
*/
|
||||
public function extractPlatform(string $content): ?string
|
||||
{
|
||||
$data = $this->tryParse($content);
|
||||
return $data['governance']['platform'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate XML .mokostandards content for a repository.
|
||||
*
|
||||
* @param array $params {
|
||||
* @type string $name Repository name (required)
|
||||
* @type string $org Organization (required)
|
||||
* @type string $platform Platform slug (required)
|
||||
* @type string $standards_version MokoStandards version
|
||||
* @type string $description Repo description
|
||||
* @type string $license SPDX license identifier
|
||||
* @type list<string> $topics Repo topics
|
||||
* @type string $language Primary language
|
||||
* @type string $runtime Runtime requirement
|
||||
* @type string $package_type Package format
|
||||
* @type string $entry_point Main entry file
|
||||
* @type string $last_synced ISO 8601 timestamp
|
||||
* }
|
||||
* @return string Well-formed XML content
|
||||
*/
|
||||
public function generate(array $params): string
|
||||
{
|
||||
$name = $params['name'] ?? '';
|
||||
$org = $params['org'] ?? '';
|
||||
$platform = $params['platform'] ?? 'default-repository';
|
||||
$version = $params['standards_version'] ?? '';
|
||||
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
$dom->formatOutput = true;
|
||||
|
||||
// Add comment header
|
||||
$dom->appendChild($dom->createComment(
|
||||
"\n MokoStandards Repository Manifest\n"
|
||||
. " Auto-generated by MokoStandards bulk sync.\n"
|
||||
. " Manual edits to <governance> and <last-synced> may be overwritten.\n"
|
||||
. " See: docs/standards/mokostandards-file-spec.md\n"
|
||||
));
|
||||
|
||||
// Root element
|
||||
$root = $dom->createElementNS(self::NAMESPACE_URI, 'mokostandards');
|
||||
$root->setAttribute('schema-version', self::SCHEMA_VERSION);
|
||||
$dom->appendChild($root);
|
||||
|
||||
// <identity>
|
||||
$identity = $dom->createElement('identity');
|
||||
$identity->appendChild($dom->createElement('name', $this->xmlEscape($name)));
|
||||
$identity->appendChild($dom->createElement('org', $this->xmlEscape($org)));
|
||||
|
||||
if (!empty($params['description'])) {
|
||||
$identity->appendChild($dom->createElement('description', $this->xmlEscape($params['description'])));
|
||||
}
|
||||
|
||||
if (!empty($params['license'])) {
|
||||
$license = $dom->createElement('license', $this->xmlEscape($this->licenseLabel($params['license'])));
|
||||
$license->setAttribute('spdx', $params['license']);
|
||||
$identity->appendChild($license);
|
||||
}
|
||||
|
||||
if (!empty($params['topics'])) {
|
||||
$topics = $dom->createElement('topics');
|
||||
foreach ($params['topics'] as $topic) {
|
||||
$topics->appendChild($dom->createElement('topic', $this->xmlEscape($topic)));
|
||||
}
|
||||
$identity->appendChild($topics);
|
||||
}
|
||||
|
||||
$root->appendChild($identity);
|
||||
|
||||
// <governance>
|
||||
$governance = $dom->createElement('governance');
|
||||
$governance->appendChild($dom->createElement('platform', $this->xmlEscape($platform)));
|
||||
$governance->appendChild($dom->createElement('standards-version', $this->xmlEscape($version)));
|
||||
$governance->appendChild($dom->createElement('standards-source', self::STANDARDS_SOURCE));
|
||||
|
||||
if (!empty($params['last_synced'])) {
|
||||
$governance->appendChild($dom->createElement('last-synced', $params['last_synced']));
|
||||
}
|
||||
|
||||
$root->appendChild($governance);
|
||||
|
||||
// <build> (optional)
|
||||
if (!empty($params['language']) || !empty($params['runtime']) || !empty($params['package_type']) || !empty($params['entry_point'])) {
|
||||
$build = $dom->createElement('build');
|
||||
if (!empty($params['language'])) {
|
||||
$build->appendChild($dom->createElement('language', $this->xmlEscape($params['language'])));
|
||||
}
|
||||
if (!empty($params['runtime'])) {
|
||||
$build->appendChild($dom->createElement('runtime', $this->xmlEscape($params['runtime'])));
|
||||
}
|
||||
if (!empty($params['package_type'])) {
|
||||
$build->appendChild($dom->createElement('package-type', $this->xmlEscape($params['package_type'])));
|
||||
}
|
||||
if (!empty($params['entry_point'])) {
|
||||
$build->appendChild($dom->createElement('entry-point', $this->xmlEscape($params['entry_point'])));
|
||||
}
|
||||
$root->appendChild($build);
|
||||
}
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate XML content against the XSD schema.
|
||||
*
|
||||
* @param string $xmlContent Raw XML content
|
||||
* @param string|null $xsdPath Path to the XSD file (auto-detected if null)
|
||||
* @return array{valid: bool, errors: list<string>}
|
||||
*/
|
||||
public function validate(string $xmlContent, ?string $xsdPath = null): array
|
||||
{
|
||||
if ($xsdPath === null) {
|
||||
$xsdPath = dirname(dirname(__DIR__)) . '/docs/standards/mokostandards-schema.xsd';
|
||||
}
|
||||
|
||||
if (!file_exists($xsdPath)) {
|
||||
return ['valid' => false, 'errors' => ["XSD schema not found: {$xsdPath}"]];
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$dom = new DOMDocument();
|
||||
$dom->loadXML($xmlContent);
|
||||
|
||||
$valid = $dom->schemaValidate($xsdPath);
|
||||
$errors = [];
|
||||
|
||||
if (!$valid) {
|
||||
foreach (libxml_get_errors() as $error) {
|
||||
$errors[] = "Line {$error->line}: " . trim($error->message);
|
||||
}
|
||||
}
|
||||
|
||||
libxml_clear_errors();
|
||||
|
||||
return ['valid' => $valid, 'errors' => $errors];
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// Private parsing helpers
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
private function parseIdentity(SimpleXMLElement $xml): array
|
||||
{
|
||||
$id = $xml->identity ?? null;
|
||||
if ($id === null) {
|
||||
throw new \RuntimeException('.mokostandards: missing required <identity> element');
|
||||
}
|
||||
|
||||
$result = [
|
||||
'name' => (string) ($id->name ?? ''),
|
||||
'org' => (string) ($id->org ?? ''),
|
||||
];
|
||||
|
||||
if ($result['name'] === '') {
|
||||
throw new \RuntimeException('.mokostandards: <identity><name> is required');
|
||||
}
|
||||
|
||||
if (isset($id->description)) {
|
||||
$result['description'] = (string) $id->description;
|
||||
}
|
||||
if (isset($id->license)) {
|
||||
$result['license'] = (string) $id->license;
|
||||
$spdx = (string) ($id->license['spdx'] ?? '');
|
||||
if ($spdx !== '') {
|
||||
$result['license_spdx'] = $spdx;
|
||||
}
|
||||
}
|
||||
if (isset($id->topics)) {
|
||||
$result['topics'] = [];
|
||||
foreach ($id->topics->topic as $topic) {
|
||||
$result['topics'][] = (string) $topic;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseGovernance(SimpleXMLElement $xml): array
|
||||
{
|
||||
$gov = $xml->governance ?? null;
|
||||
if ($gov === null) {
|
||||
throw new \RuntimeException('.mokostandards: missing required <governance> element');
|
||||
}
|
||||
|
||||
$result = [
|
||||
'platform' => (string) ($gov->platform ?? ''),
|
||||
'standards_version' => (string) ($gov->{'standards-version'} ?? ''),
|
||||
'standards_source' => (string) ($gov->{'standards-source'} ?? ''),
|
||||
];
|
||||
|
||||
if ($result['platform'] === '') {
|
||||
throw new \RuntimeException('.mokostandards: <governance><platform> is required');
|
||||
}
|
||||
|
||||
if (isset($gov->{'last-synced'})) {
|
||||
$result['last_synced'] = (string) $gov->{'last-synced'};
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseBuild(SimpleXMLElement $build): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
foreach (['language', 'runtime', 'entry-point'] as $field) {
|
||||
if (isset($build->$field)) {
|
||||
$key = str_replace('-', '_', $field);
|
||||
$result[$key] = (string) $build->$field;
|
||||
}
|
||||
}
|
||||
if (isset($build->{'package-type'})) {
|
||||
$result['package_type'] = (string) $build->{'package-type'};
|
||||
}
|
||||
|
||||
if (isset($build->artifact)) {
|
||||
$result['artifact'] = [];
|
||||
foreach (['format', 'path', 'filename'] as $f) {
|
||||
if (isset($build->artifact->$f)) {
|
||||
$result['artifact'][$f] = (string) $build->artifact->$f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($build->dependencies)) {
|
||||
$result['dependencies'] = [];
|
||||
foreach ($build->dependencies->requires as $req) {
|
||||
$dep = ['name' => (string) ($req['name'] ?? '')];
|
||||
if (isset($req['version'])) {
|
||||
$dep['version'] = (string) $req['version'];
|
||||
}
|
||||
if (isset($req['type'])) {
|
||||
$dep['type'] = (string) $req['type'];
|
||||
}
|
||||
$result['dependencies'][] = $dep;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function parseDeploy(SimpleXMLElement $deploy): array
|
||||
{
|
||||
$targets = [];
|
||||
foreach ($deploy->target as $target) {
|
||||
$t = [
|
||||
'name' => (string) ($target['name'] ?? ''),
|
||||
'enabled' => ((string) ($target['enabled'] ?? 'true')) !== 'false',
|
||||
'host' => (string) ($target->host ?? ''),
|
||||
'path' => (string) ($target->path ?? ''),
|
||||
];
|
||||
if (isset($target->method)) {
|
||||
$t['method'] = (string) $target->method;
|
||||
}
|
||||
if (isset($target->branch)) {
|
||||
$t['branch'] = (string) $target->branch;
|
||||
}
|
||||
if (isset($target->{'src-dir'})) {
|
||||
$t['src_dir'] = (string) $target->{'src-dir'};
|
||||
}
|
||||
$targets[] = $t;
|
||||
}
|
||||
return ['targets' => $targets];
|
||||
}
|
||||
|
||||
private function parseScripts(SimpleXMLElement $scripts): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($scripts->script as $script) {
|
||||
$s = [
|
||||
'name' => (string) ($script['name'] ?? ''),
|
||||
'command' => (string) ($script->command ?? ''),
|
||||
];
|
||||
if (isset($script['phase'])) {
|
||||
$s['phase'] = (string) $script['phase'];
|
||||
}
|
||||
if (isset($script->description)) {
|
||||
$s['description'] = (string) $script->description;
|
||||
}
|
||||
if (isset($script->runner)) {
|
||||
$s['runner'] = (string) $script->runner;
|
||||
}
|
||||
$result[] = $s;
|
||||
}
|
||||
return ['scripts' => $result];
|
||||
}
|
||||
|
||||
private function parseOverrides(SimpleXMLElement $overrides): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
if (isset($overrides->{'skip-files'})) {
|
||||
$result['skip_files'] = [];
|
||||
foreach ($overrides->{'skip-files'}->file as $file) {
|
||||
$result['skip_files'][] = (string) $file;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($overrides->{'skip-workflows'})) {
|
||||
$result['skip_workflows'] = [];
|
||||
foreach ($overrides->{'skip-workflows'}->file as $file) {
|
||||
$result['skip_workflows'][] = (string) $file;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($overrides->{'extra-secrets'})) {
|
||||
$result['extra_secrets'] = [];
|
||||
foreach ($overrides->{'extra-secrets'}->secret as $secret) {
|
||||
$s = ['name' => (string) ($secret['name'] ?? '')];
|
||||
if (isset($secret['required'])) {
|
||||
$s['required'] = ((string) $secret['required']) !== 'false';
|
||||
}
|
||||
if (isset($secret['scope'])) {
|
||||
$s['scope'] = (string) $secret['scope'];
|
||||
}
|
||||
$result['extra_secrets'][] = $s;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string for XML element content.
|
||||
*/
|
||||
private function xmlEscape(string $value): string
|
||||
{
|
||||
return htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Map SPDX identifier to a human-readable license label.
|
||||
*/
|
||||
private function licenseLabel(string $spdx): string
|
||||
{
|
||||
return match ($spdx) {
|
||||
'GPL-3.0-or-later' => 'GNU General Public License v3',
|
||||
'GPL-2.0-or-later' => 'GNU General Public License v2',
|
||||
'MIT' => 'MIT License',
|
||||
'Apache-2.0' => 'Apache License 2.0',
|
||||
'BSD-3-Clause' => 'BSD 3-Clause License',
|
||||
'LGPL-3.0-or-later' => 'GNU Lesser General Public License v3',
|
||||
default => $spdx,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a platform slug to its default package type.
|
||||
*/
|
||||
public static function platformPackageType(string $platform): string
|
||||
{
|
||||
return match ($platform) {
|
||||
'crm-module', 'crm-platform' => 'dolibarr-module',
|
||||
'waas-component' => 'joomla-extension',
|
||||
'joomla-template' => 'joomla-extension',
|
||||
'standards-repository' => 'composer',
|
||||
default => 'composer',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a platform slug to its default primary language.
|
||||
*/
|
||||
public static function platformLanguage(string $platform): string
|
||||
{
|
||||
return match ($platform) {
|
||||
'crm-module', 'crm-platform' => 'PHP',
|
||||
'waas-component', 'joomla-template' => 'PHP',
|
||||
'standards-repository' => 'PHP',
|
||||
default => 'PHP',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -39,12 +39,13 @@ class RepositorySynchronizer
|
||||
private const VERSION_BRANCH = 'version/' . self::STANDARDS_MAJOR;
|
||||
private const SYNC_BRANCH = 'chore/sync-mokostandards-v' . self::STANDARDS_MINOR;
|
||||
|
||||
private ApiClient $apiClient;
|
||||
private GitPlatformAdapter $adapter;
|
||||
private AuditLogger $logger;
|
||||
private MetricsCollector $metrics;
|
||||
private CheckpointManager $checkpoints;
|
||||
private DefinitionParser $definitionParser;
|
||||
private ApiClient $apiClient;
|
||||
private GitPlatformAdapter $adapter;
|
||||
private AuditLogger $logger;
|
||||
private MetricsCollector $metrics;
|
||||
private CheckpointManager $checkpoints;
|
||||
private DefinitionParser $definitionParser;
|
||||
private MokoStandardsParser $manifestParser;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -70,6 +71,7 @@ class RepositorySynchronizer
|
||||
$this->metrics = $metrics;
|
||||
$this->checkpoints = $checkpoints ?? new CheckpointManager('.checkpoints');
|
||||
$this->definitionParser = $definitionParser ?? new DefinitionParser();
|
||||
$this->manifestParser = new MokoStandardsParser();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +168,7 @@ class RepositorySynchronizer
|
||||
{
|
||||
$this->logger->logInfo("Starting synchronization for {$org}/{$repo}");
|
||||
|
||||
// Resolve repo root (two levels up from this file: Enterprise/ → lib/ → api/ → root)
|
||||
// Resolve repo root (three levels up from this file: Enterprise/ → lib/ → root)
|
||||
// API repo root (definitions, sync code)
|
||||
$repoRoot = dirname(dirname(__DIR__));
|
||||
// MokoStandards repo root (templates, configs)
|
||||
@@ -195,7 +197,16 @@ class RepositorySynchronizer
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->logInfo("Loaded " . count($filesToSync) . " sync entries from definition for {$platform}");
|
||||
$defCount = count($filesToSync) - count($sharedFiles);
|
||||
$sharedAdded = count($filesToSync) - $defCount;
|
||||
$sharedTotal = count($sharedFiles);
|
||||
$this->logger->logInfo("Loaded " . count($filesToSync) . " sync entries for {$platform} (def={$defCount}, shared={$sharedAdded}/{$sharedTotal} added, " . ($sharedTotal - $sharedAdded) . " deduped)");
|
||||
// Log shared workflow destinations for debugging
|
||||
foreach ($sharedFiles as $sf) {
|
||||
$dest = $sf['destination'] ?? '?';
|
||||
$added = !isset($seen[$dest]) ? 'ADDED' : 'DEDUPED';
|
||||
$this->logger->logInfo(" shared: {$dest} [{$added}]");
|
||||
}
|
||||
|
||||
if (empty($filesToSync)) {
|
||||
$this->logger->logWarning("No syncable entries found in definition for platform '{$platform}', skipping {$repo}");
|
||||
@@ -215,7 +226,8 @@ class RepositorySynchronizer
|
||||
}
|
||||
|
||||
// Create PR with file updates driven by the definition
|
||||
$result = $this->createSyncPR($org, $repo, $platform, $filesToSync, $standardsRoot, $force);
|
||||
// Use API repo root ($repoRoot) — templates live here, not in $standardsRoot
|
||||
$result = $this->createSyncPR($org, $repo, $platform, $filesToSync, $repoRoot, $force);
|
||||
$prNumber = $result['number'] ?? null;
|
||||
$summary = $result['summary'] ?? [];
|
||||
|
||||
@@ -390,7 +402,65 @@ HCL;
|
||||
/** Repos that are the full Dolibarr platform, not individual modules. */
|
||||
private const CRM_PLATFORM_REPOS = ['MokoDolibarr', 'MokoDoliMods'];
|
||||
|
||||
/**
|
||||
* Detect platform from the .mokostandards manifest (authoritative), falling
|
||||
* back to name/topic/description heuristics when the manifest is missing or
|
||||
* unparseable.
|
||||
*/
|
||||
private function detectPlatform(array $repoInfo): string
|
||||
{
|
||||
$org = $repoInfo['full_name'] ? explode('/', $repoInfo['full_name'])[0] : '';
|
||||
$name = $repoInfo['name'] ?? '';
|
||||
|
||||
// ── 1. Try reading the XML .mokostandards manifest ────────────��─
|
||||
$manifestPlatform = $this->readManifestPlatform($org, $name);
|
||||
if ($manifestPlatform !== null) {
|
||||
$this->logger->logInfo("Platform for {$name} from .mokostandards manifest: {$manifestPlatform}");
|
||||
return $manifestPlatform;
|
||||
}
|
||||
|
||||
// ── 2. Fallback: heuristic detection ────────────────────────────
|
||||
return $this->detectPlatformByHeuristics($repoInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the platform slug from the remote .mokostandards manifest.
|
||||
* Checks .gitea/.mokostandards, .github/.mokostandards, and root .mokostandards.
|
||||
*
|
||||
* @return string|null Platform slug or null if not found/parseable
|
||||
*/
|
||||
private function readManifestPlatform(string $org, string $repo): ?string
|
||||
{
|
||||
$metaDir = $this->adapter->getMetadataDir();
|
||||
$paths = [
|
||||
"{$metaDir}/.mokostandards",
|
||||
'.mokostandards',
|
||||
];
|
||||
if ($metaDir === '.gitea') {
|
||||
$paths[] = '.github/.mokostandards';
|
||||
}
|
||||
|
||||
foreach ($paths as $path) {
|
||||
try {
|
||||
$file = $this->adapter->getFileContents($org, $repo, $path);
|
||||
$content = base64_decode($file['content'] ?? '');
|
||||
$platform = $this->manifestParser->extractPlatform($content);
|
||||
if ($platform !== null && in_array($platform, MokoStandardsParser::VALID_PLATFORMS, true)) {
|
||||
return $platform;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic platform detection from repo name, topics, and description.
|
||||
* Used as fallback when .mokostandards manifest is missing or unparseable.
|
||||
*/
|
||||
private function detectPlatformByHeuristics(array $repoInfo): string
|
||||
{
|
||||
$name = $repoInfo['name'] ?? '';
|
||||
$nameLower = strtolower($name);
|
||||
@@ -438,7 +508,7 @@ HCL;
|
||||
if (str_contains($description, 'dolibarr') || str_contains($description, 'module')) {
|
||||
return 'crm-module';
|
||||
}
|
||||
|
||||
|
||||
// Default
|
||||
return 'default-repository';
|
||||
}
|
||||
@@ -493,8 +563,8 @@ HCL;
|
||||
// Ensure composer.json requires mokoconsulting-tech/enterprise (default branch only)
|
||||
$this->ensureComposerEnterprise($org, $repo, $defaultBranch, $summary);
|
||||
|
||||
// Migrate .mokostandards (default branch only)
|
||||
$this->migrateMokoStandards($org, $repo, $defaultBranch, $summary);
|
||||
// Migrate .mokostandards to XML manifest (default branch only)
|
||||
$this->migrateMokoStandards($org, $repo, $defaultBranch, $platform, $repoInfo, $summary);
|
||||
|
||||
if (count($summary['copied']) === 0) {
|
||||
$this->logger->logWarning("No files were created/updated for {$repo}");
|
||||
@@ -509,7 +579,7 @@ HCL;
|
||||
try {
|
||||
$issueData = $this->adapter->createIssue($org, $repo, $issueTitle, $issueBody, [
|
||||
'labels' => ['mokostandards', 'type: chore', 'automation'],
|
||||
'assignees' => ['jmiller-moko'],
|
||||
'assignees' => ['jmiller'],
|
||||
]);
|
||||
$issueNumber = $issueData['number'] ?? null;
|
||||
$this->logger->logInfo("Created tracking issue #{$issueNumber} — " . count($summary['copied']) . " files synced directly to {$defaultBranch}");
|
||||
@@ -598,7 +668,12 @@ HCL;
|
||||
$isReadme = $basename === 'readme.md';
|
||||
$isChangelog = in_array($basename, ['changelog.md', 'changelog'], true);
|
||||
$isProtected = $isReadme || $isChangelog;
|
||||
$canOverwrite = !$isProtected && ($force || $entry['always_overwrite']) && !($entry['protected'] ?? false);
|
||||
// Protected files are NEVER overwritten, even with --force
|
||||
if ($entry['protected'] ?? false) {
|
||||
$summary['skipped'][] = ['file' => $targetPath, 'reason' => 'Protected — never overwritten'];
|
||||
continue;
|
||||
}
|
||||
$canOverwrite = !$isProtected && ($force || $entry['always_overwrite']);
|
||||
|
||||
if ($isReadme) {
|
||||
$summary['skipped'][] = ['file' => $targetPath, 'reason' => 'README — never overwritten'];
|
||||
@@ -691,65 +766,229 @@ HCL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate .mokostandards from repo root to .github/.mokostandards.
|
||||
* Deletes the root file after copying to .github/.
|
||||
* Migrate .mokostandards to the platform metadata dir (.gitea/ or .github/)
|
||||
* and convert legacy YAML-like format to the new XML manifest.
|
||||
*
|
||||
* Handles:
|
||||
* 1. Location migration: root or .github/ → .gitea/.mokostandards
|
||||
* 2. Format migration: legacy "platform: xxx" → XML manifest
|
||||
* 3. Update existing XML: refresh <governance><last-synced> timestamp
|
||||
*/
|
||||
private function migrateMokoStandards(string $org, string $repo, string $branchName, array &$summary): void
|
||||
{
|
||||
$metaDir = $this->adapter->getMetadataDir();
|
||||
private function migrateMokoStandards(
|
||||
string $org,
|
||||
string $repo,
|
||||
string $branchName,
|
||||
string $platform,
|
||||
array $repoInfo,
|
||||
array &$summary
|
||||
): void {
|
||||
$metaDir = $this->adapter->getMetadataDir();
|
||||
$targetPath = "{$metaDir}/.mokostandards";
|
||||
|
||||
// Check if .mokostandards exists in root
|
||||
try {
|
||||
$rootFile = $this->adapter->getFileContents($org, $repo, '.mokostandards', $branchName);
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
return; // Doesn't exist in root — nothing to migrate
|
||||
// ── Collect existing files from all legacy locations ─────────
|
||||
$legacySources = ['.mokostandards'];
|
||||
if ($metaDir === '.gitea') {
|
||||
$legacySources[] = '.github/.mokostandards';
|
||||
}
|
||||
|
||||
// Check if already exists in metadata dir
|
||||
$existsInMetaDir = false;
|
||||
try {
|
||||
$this->adapter->getFileContents($org, $repo, $targetPath, $branchName);
|
||||
$existsInMetaDir = true;
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
}
|
||||
|
||||
$content = base64_decode($rootFile['content'] ?? '');
|
||||
$rootSha = $rootFile['sha'] ?? '';
|
||||
|
||||
if (!$existsInMetaDir) {
|
||||
// Copy to metadata dir
|
||||
$legacyFiles = []; // path => ['content' => raw, 'sha' => sha]
|
||||
foreach ($legacySources as $path) {
|
||||
try {
|
||||
$this->adapter->createOrUpdateFile(
|
||||
$org, $repo, $targetPath, $content,
|
||||
"chore: migrate .mokostandards to {$metaDir}/",
|
||||
null, $branchName
|
||||
);
|
||||
$this->logger->logInfo("Migrated .mokostandards → {$targetPath}");
|
||||
$summary['copied'][] = ['file' => $targetPath, 'action' => 'migrated from root'];
|
||||
$file = $this->adapter->getFileContents($org, $repo, $path, $branchName);
|
||||
$legacyFiles[$path] = [
|
||||
'content' => base64_decode($file['content'] ?? ''),
|
||||
'sha' => $file['sha'] ?? '',
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if target already exists in metadata dir
|
||||
$existingTarget = null;
|
||||
try {
|
||||
$file = $this->adapter->getFileContents($org, $repo, $targetPath, $branchName);
|
||||
$existingTarget = [
|
||||
'content' => base64_decode($file['content'] ?? ''),
|
||||
'sha' => $file['sha'] ?? '',
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
}
|
||||
|
||||
// ── Determine the best existing content to work from ────────
|
||||
$currentContent = $existingTarget['content'] ?? null;
|
||||
if ($currentContent === null) {
|
||||
// Pick from legacy sources (first found)
|
||||
foreach ($legacyFiles as $data) {
|
||||
$currentContent = $data['content'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Generate the new XML manifest ───────────────────────────
|
||||
$xmlContent = $this->generateMokoStandardsXml(
|
||||
$org,
|
||||
$repo,
|
||||
$platform,
|
||||
$repoInfo,
|
||||
$currentContent
|
||||
);
|
||||
|
||||
// ── Write to target path ────────────────────────────────────
|
||||
$targetSha = $existingTarget['sha'] ?? null;
|
||||
$isNew = $existingTarget === null;
|
||||
$needsUpdate = $isNew || $existingTarget['content'] !== $xmlContent;
|
||||
|
||||
if ($needsUpdate) {
|
||||
$action = $isNew ? 'create' : 'update';
|
||||
$commitMsg = $isNew
|
||||
? "chore: add XML .mokostandards manifest to {$metaDir}/"
|
||||
: "chore: update .mokostandards manifest (XML format)";
|
||||
|
||||
try {
|
||||
$this->adapter->createOrUpdateFile(
|
||||
$org, $repo, $targetPath, $xmlContent,
|
||||
$commitMsg, $targetSha, $branchName
|
||||
);
|
||||
$this->logger->logInfo(ucfirst($action) . "d XML .mokostandards → {$targetPath}");
|
||||
$summary['copied'][] = ['file' => $targetPath, 'action' => "{$action}d (XML manifest)"];
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
$this->logger->logWarning("Could not {$action} .mokostandards: " . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete from root
|
||||
if (!empty($rootSha)) {
|
||||
// ── Delete legacy source files ──────────────────────────────
|
||||
foreach ($legacyFiles as $path => $data) {
|
||||
if ($path === $targetPath || empty($data['sha'])) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$this->adapter->deleteFile(
|
||||
$org, $repo, '.mokostandards', $rootSha,
|
||||
"chore: remove .mokostandards from root (moved to {$metaDir}/)",
|
||||
$org, $repo, $path, $data['sha'],
|
||||
"chore: remove legacy {$path} (replaced by {$targetPath})",
|
||||
$branchName
|
||||
);
|
||||
$this->logger->logInfo("Deleted root .mokostandards");
|
||||
$this->logger->logInfo("Deleted legacy {$path}");
|
||||
} catch (Exception $e) {
|
||||
$this->adapter->getApiClient()->resetCircuitBreaker();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an XML .mokostandards manifest for a repository.
|
||||
*
|
||||
* If existing content is valid XML, preserves user-edited sections
|
||||
* (build, deploy, scripts, overrides) and only refreshes governance metadata.
|
||||
*
|
||||
* @param string $org Organization name
|
||||
* @param string $repo Repository name
|
||||
* @param string $platform Detected platform slug
|
||||
* @param array $repoInfo Gitea API repo object
|
||||
* @param string|null $existingContent Current .mokostandards content (XML or legacy)
|
||||
* @return string Well-formed XML content
|
||||
*/
|
||||
private function generateMokoStandardsXml(
|
||||
string $org,
|
||||
string $repo,
|
||||
string $platform,
|
||||
array $repoInfo,
|
||||
?string $existingContent
|
||||
): string {
|
||||
$params = [
|
||||
'name' => $repoInfo['name'] ?? $repo,
|
||||
'org' => $org,
|
||||
'platform' => $platform,
|
||||
'standards_version' => self::STANDARDS_VERSION,
|
||||
'description' => $repoInfo['description'] ?? '',
|
||||
'license' => 'GPL-3.0-or-later',
|
||||
'topics' => $repoInfo['topics'] ?? [],
|
||||
'language' => $repoInfo['language'] ?? MokoStandardsParser::platformLanguage($platform),
|
||||
'package_type' => MokoStandardsParser::platformPackageType($platform),
|
||||
'last_synced' => date('c'),
|
||||
];
|
||||
|
||||
// If existing content is already valid XML, try to preserve user sections
|
||||
if ($existingContent !== null && str_contains($existingContent, '<mokostandards')) {
|
||||
try {
|
||||
$existing = $this->manifestParser->parse($existingContent);
|
||||
|
||||
// Preserve user-edited build, deploy, scripts, overrides by re-emitting
|
||||
// the existing XML with only governance fields refreshed.
|
||||
// For now, we use the simple generate() which creates identity + governance + build.
|
||||
// User-managed sections (deploy, scripts, overrides) are preserved by doing
|
||||
// a targeted replacement of governance fields in the existing XML.
|
||||
return $this->refreshGovernanceInXml(
|
||||
$existingContent,
|
||||
$platform,
|
||||
self::STANDARDS_VERSION,
|
||||
date('c')
|
||||
);
|
||||
} catch (\RuntimeException $e) {
|
||||
// Existing XML is broken — regenerate from scratch
|
||||
$this->logger->logInfo("Existing .mokostandards XML invalid, regenerating: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this->manifestParser->generate($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh only the <governance> fields in an existing XML .mokostandards,
|
||||
* preserving all other sections (build, deploy, scripts, overrides).
|
||||
*/
|
||||
private function refreshGovernanceInXml(
|
||||
string $xml,
|
||||
string $platform,
|
||||
string $standardsVersion,
|
||||
string $lastSynced
|
||||
): string {
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->preserveWhiteSpace = true;
|
||||
$dom->formatOutput = true;
|
||||
|
||||
if (!$dom->loadXML($xml)) {
|
||||
// If parsing fails, return as-is
|
||||
return $xml;
|
||||
}
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$xpath->registerNamespace('m', MokoStandardsParser::NAMESPACE_URI);
|
||||
|
||||
// Update <platform>
|
||||
$nodes = $xpath->query('//m:governance/m:platform');
|
||||
if ($nodes->length > 0) {
|
||||
$nodes->item(0)->textContent = $platform;
|
||||
}
|
||||
|
||||
// Update <standards-version>
|
||||
$nodes = $xpath->query('//m:governance/m:standards-version');
|
||||
if ($nodes->length > 0) {
|
||||
$nodes->item(0)->textContent = $standardsVersion;
|
||||
}
|
||||
|
||||
// Update or create <last-synced>
|
||||
$nodes = $xpath->query('//m:governance/m:last-synced');
|
||||
if ($nodes->length > 0) {
|
||||
$nodes->item(0)->textContent = $lastSynced;
|
||||
} else {
|
||||
$govNodes = $xpath->query('//m:governance');
|
||||
if ($govNodes->length > 0) {
|
||||
$lastSyncedEl = $dom->createElementNS(
|
||||
MokoStandardsParser::NAMESPACE_URI,
|
||||
'last-synced'
|
||||
);
|
||||
$lastSyncedEl->textContent = $lastSynced;
|
||||
$govNodes->item(0)->appendChild($lastSyncedEl);
|
||||
}
|
||||
}
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
private function ensureComposerEnterprise(string $org, string $repo, string $branchName, array &$summary): void
|
||||
{
|
||||
try {
|
||||
@@ -764,6 +1003,11 @@ HCL;
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't add self-referencing dependency — skip if this IS the enterprise package
|
||||
if (($json['name'] ?? '') === 'mokoconsulting-tech/enterprise') {
|
||||
return;
|
||||
}
|
||||
|
||||
$expectedConstraint = 'dev-' . self::VERSION_BRANCH;
|
||||
|
||||
// Check if enterprise package is already required with correct constraint
|
||||
@@ -798,50 +1042,51 @@ HCL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Template repo mapping — canonical source for each platform's workflows.
|
||||
* The sync engine clones these at runtime to get the latest workflow files.
|
||||
*/
|
||||
private const TEMPLATE_REPOS = [
|
||||
'joomla' => 'MokoConsulting/MokoStandards-Template-Joomla',
|
||||
'dolibarr' => 'MokoConsulting/MokoStandards-Template-Dolibarr',
|
||||
'generic' => 'MokoConsulting/MokoStandards-Template-Generic',
|
||||
'client' => 'MokoConsulting/MokoStandards-Template-Client',
|
||||
];
|
||||
|
||||
private function getSharedWorkflows(string $platform, string $repoRoot): array
|
||||
{
|
||||
$root = rtrim($repoRoot, '/');
|
||||
$wfDir = $this->adapter->getWorkflowDir();
|
||||
|
||||
$shared = [
|
||||
['templates/workflows/shared/enterprise-firewall-setup.yml.template', "{$wfDir}/enterprise-firewall-setup.yml"],
|
||||
['templates/workflows/shared/sync-version-on-merge.yml.template', "{$wfDir}/sync-version-on-merge.yml"],
|
||||
['templates/workflows/shared/repository-cleanup.yml.template', "{$wfDir}/repository-cleanup.yml"],
|
||||
['templates/workflows/shared/auto-dev-issue.yml.template', "{$wfDir}/auto-dev-issue.yml"],
|
||||
['templates/workflows/shared/branch-freeze.yml.template', "{$wfDir}/branch-freeze.yml"],
|
||||
['templates/workflows/shared/auto-assign.yml.template', "{$wfDir}/auto-assign.yml"],
|
||||
['templates/workflows/shared/changelog-validation.yml.template', "{$wfDir}/changelog-validation.yml"],
|
||||
['.github/workflows/standards-compliance.yml', "{$wfDir}/standards-compliance.yml"],
|
||||
];
|
||||
// Determine which template repo to source from
|
||||
$templateType = match (true) {
|
||||
in_array($platform, ['crm-module', 'crm-platform']) => 'dolibarr',
|
||||
in_array($platform, ['waas-component', 'joomla-template']) => 'joomla',
|
||||
str_starts_with($platform, 'client') => 'client',
|
||||
default => 'generic',
|
||||
};
|
||||
|
||||
// CodeQL is GitHub-only; on Gitea, Trivy replaces it
|
||||
if ($this->adapter->getPlatformName() === 'github') {
|
||||
$shared[] = ['.github/workflows/codeql-analysis.yml', "{$wfDir}/codeql-analysis.yml"];
|
||||
// Clone template repo to tmp if not already cached
|
||||
$templateRepo = self::TEMPLATE_REPOS[$templateType];
|
||||
$cacheDir = sys_get_temp_dir() . '/mokostandards-sync/' . basename($templateRepo);
|
||||
|
||||
if (!is_dir($cacheDir)) {
|
||||
$gitUrl = $this->adapter->getCloneUrl($templateRepo);
|
||||
$this->logger->logInfo("Cloning template: {$templateRepo} → {$cacheDir}");
|
||||
$cloneResult = $this->adapter->cloneRepo($templateRepo, $cacheDir, ['depth' => 1]);
|
||||
if (!$cloneResult) {
|
||||
throw new \RuntimeException("Failed to clone template repo: {$templateRepo}");
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-specific workflows
|
||||
if ($platform === 'crm-module') {
|
||||
$shared[] = ['templates/workflows/shared/deploy-dev.yml.template', "{$wfDir}/deploy-dev.yml"];
|
||||
$shared[] = ['templates/workflows/shared/deploy-demo.yml.template', "{$wfDir}/deploy-demo.yml"];
|
||||
$shared[] = ['templates/workflows/dolibarr/auto-release.yml.template', "{$wfDir}/auto-release.yml"];
|
||||
$shared[] = ['templates/workflows/dolibarr/ci-dolibarr.yml.template', "{$wfDir}/ci-dolibarr.yml"];
|
||||
$shared[] = ['templates/workflows/dolibarr/publish-to-mokodolimods.yml.template', "{$wfDir}/publish-to-mokodolimods.yml"];
|
||||
$shared[] = ['templates/workflows/dolibarr/repo_health.yml.template', "{$wfDir}/repo_health.yml"];
|
||||
} elseif ($platform === 'crm-platform') {
|
||||
$shared[] = ['templates/workflows/shared/deploy-dev.yml.template', "{$wfDir}/deploy-dev.yml"];
|
||||
$shared[] = ['templates/workflows/shared/deploy-demo.yml.template', "{$wfDir}/deploy-demo.yml"];
|
||||
$shared[] = ['templates/workflows/dolibarr/auto-release.yml.template', "{$wfDir}/auto-release.yml"];
|
||||
$shared[] = ['templates/workflows/dolibarr/ci-dolibarr.yml.template', "{$wfDir}/ci-dolibarr.yml"];
|
||||
} elseif ($platform === 'waas-component' || $platform === 'joomla-template') {
|
||||
$shared[] = ['templates/workflows/joomla/auto-release.yml.template', "{$wfDir}/auto-release.yml"];
|
||||
$shared[] = ['templates/workflows/joomla/update-server.yml.template', "{$wfDir}/update-server.yml"];
|
||||
$shared[] = ['templates/workflows/joomla/ci-joomla.yml.template', "{$wfDir}/ci-joomla.yml"];
|
||||
$shared[] = ['templates/workflows/joomla/repo_health.yml.template', "{$wfDir}/repo_health.yml"];
|
||||
$shared[] = ['templates/workflows/joomla/deploy-manual.yml.template', "{$wfDir}/deploy-manual.yml"];
|
||||
} else {
|
||||
$shared[] = ['templates/workflows/shared/deploy-dev.yml.template', "{$wfDir}/deploy-dev.yml"];
|
||||
$shared[] = ['templates/workflows/shared/deploy-demo.yml.template', "{$wfDir}/deploy-demo.yml"];
|
||||
$shared[] = ['templates/workflows/shared/auto-release.yml.template', "{$wfDir}/auto-release.yml"];
|
||||
// Read all .yml files from the template's .gitea/workflows/
|
||||
$sourceDir = "{$cacheDir}/.gitea/workflows";
|
||||
$shared = [];
|
||||
|
||||
if (is_dir($sourceDir)) {
|
||||
foreach (glob("{$sourceDir}/*.yml") as $file) {
|
||||
$basename = basename($file);
|
||||
$shared[] = [$file, "{$wfDir}/{$basename}"];
|
||||
}
|
||||
}
|
||||
|
||||
// CODEOWNERS — GitHub only; Gitea doesn't enforce it
|
||||
@@ -868,8 +1113,7 @@ HCL;
|
||||
|
||||
// Always create a custom/ subdirectory under the workflow dir with a README
|
||||
// so repos have a safe place for custom workflows that sync won't touch.
|
||||
$entries = [
|
||||
[
|
||||
$entries[] = [
|
||||
'inline_content' => "# Custom Workflows\n\nPlace repo-specific workflows here.\n\n"
|
||||
. "- **Never overwritten** by MokoStandards bulk sync\n"
|
||||
. "- **Never deleted** by the repository-cleanup workflow\n"
|
||||
@@ -877,7 +1121,6 @@ HCL;
|
||||
. "Synced workflows live in the parent `{$wfDir}/` directory.\n",
|
||||
'destination' => "{$wfDir}/custom/README.md",
|
||||
'always_overwrite' => false,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($shared as [$source, $dest]) {
|
||||
@@ -903,6 +1146,77 @@ HCL;
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Required .gitignore entries that MUST exist in every governed repo.
|
||||
* The sync validates these exist (appending if missing) without
|
||||
* overwriting custom entries. Repos can add their own patterns freely.
|
||||
*/
|
||||
private const REQUIRED_GITIGNORE_ENTRIES = [
|
||||
// Secrets & environment
|
||||
'.env',
|
||||
'.env.local',
|
||||
'.env.*.local',
|
||||
'secrets/',
|
||||
'*.secrets.*',
|
||||
|
||||
// Sublime Text project files
|
||||
'*.sublime-project',
|
||||
'*.sublime-workspace',
|
||||
'*.sublime-settings',
|
||||
|
||||
// SFTP config (Sublime SFTP, VS Code SFTP, etc.)
|
||||
'sftp-config*.json',
|
||||
'sftp-config.json.template',
|
||||
'sftp-settings.json',
|
||||
|
||||
// IDE / editor
|
||||
'.idea/',
|
||||
'.vscode/*',
|
||||
'.claude/',
|
||||
'*.code-workspace',
|
||||
|
||||
// OS cruft
|
||||
'.DS_Store',
|
||||
'Thumbs.db',
|
||||
|
||||
// Task tracking
|
||||
'TODO.md',
|
||||
|
||||
// Vendor / dependencies
|
||||
'/vendor/',
|
||||
'node_modules/',
|
||||
|
||||
// Logs
|
||||
'*.log',
|
||||
];
|
||||
|
||||
/**
|
||||
* Validate that required .gitignore entries exist in a repo.
|
||||
* Returns array of missing entries, empty if all present.
|
||||
*
|
||||
* @param string $existingContent Current .gitignore content from repo
|
||||
* @return array<string> Missing required entries
|
||||
*/
|
||||
public function validateGitignoreEntries(string $existingContent): array
|
||||
{
|
||||
$existingLines = array_map('trim', explode("\n", $existingContent));
|
||||
$existingSet = [];
|
||||
foreach ($existingLines as $line) {
|
||||
if ($line !== '' && !str_starts_with($line, '#')) {
|
||||
$existingSet[$line] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$missing = [];
|
||||
foreach (self::REQUIRED_GITIGNORE_ENTRIES as $entry) {
|
||||
if (!isset($existingSet[$entry])) {
|
||||
$missing[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
private function mergeGitConfigFile(string $existing, string $template): string
|
||||
{
|
||||
$existingLines = array_map('rtrim', explode("\n", $existing));
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
* INGROUP: MokoStandards
|
||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
* PATH: /lib/plugins/Joomla/UpdateXmlGenerator.php
|
||||
* VERSION: 04.06.00
|
||||
* VERSION: 04.07.00
|
||||
* BRIEF: Generates and updates Joomla extension updates.xml files
|
||||
*/
|
||||
|
||||
@@ -124,6 +124,20 @@ class UpdateXmlGenerator
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map numeric client ID to Joomla client name
|
||||
*
|
||||
* @param string $clientId Numeric client ID
|
||||
* @return string Client name for updates.xml
|
||||
*/
|
||||
private function resolveClientName(string $clientId): string
|
||||
{
|
||||
return match ($clientId) {
|
||||
'1' => 'administrator',
|
||||
default => 'site',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an update entry to the XML document
|
||||
*
|
||||
@@ -145,8 +159,22 @@ class UpdateXmlGenerator
|
||||
$this->addElement($dom, $update, 'description', $release['description'] ?? '');
|
||||
$this->addElement($dom, $update, 'element', $this->element);
|
||||
$this->addElement($dom, $update, 'type', $this->extensionType);
|
||||
|
||||
// Folder (for plugins)
|
||||
if (!empty($release['folder'])) {
|
||||
$this->addElement($dom, $update, 'folder', $release['folder']);
|
||||
}
|
||||
|
||||
// Client — always emit for correct extension matching
|
||||
$this->addElement($dom, $update, 'client', $this->resolveClientName($this->clientId));
|
||||
|
||||
$this->addElement($dom, $update, 'version', $release['version']);
|
||||
|
||||
// Creation date
|
||||
if (!empty($release['creation_date'])) {
|
||||
$this->addElement($dom, $update, 'creationDate', $release['creation_date']);
|
||||
}
|
||||
|
||||
// Joomla target platform
|
||||
$infourl = $this->addElement($dom, $update, 'infourl', $release['infourl'] ?? '');
|
||||
if (!empty($release['infourl'])) {
|
||||
@@ -161,6 +189,37 @@ class UpdateXmlGenerator
|
||||
$downloadUrl->setAttribute('type', 'full');
|
||||
$downloadUrl->setAttribute('format', 'zip');
|
||||
|
||||
// Checksums
|
||||
if (!empty($release['sha256'])) {
|
||||
$this->addElement($dom, $update, 'sha256', $release['sha256']);
|
||||
}
|
||||
|
||||
if (!empty($release['sha384'])) {
|
||||
$this->addElement($dom, $update, 'sha384', $release['sha384']);
|
||||
}
|
||||
|
||||
if (!empty($release['sha512'])) {
|
||||
$this->addElement($dom, $update, 'sha512', $release['sha512']);
|
||||
}
|
||||
|
||||
// Tags
|
||||
if (!empty($release['tags'])) {
|
||||
$tags = $dom->createElement('tags');
|
||||
$update->appendChild($tags);
|
||||
foreach ($release['tags'] as $tag) {
|
||||
$this->addElement($dom, $tags, 'tag', $tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Maintainer information
|
||||
if (!empty($release['maintainer'])) {
|
||||
$this->addElement($dom, $update, 'maintainer', $release['maintainer']);
|
||||
}
|
||||
|
||||
if (!empty($release['maintainer_url'])) {
|
||||
$this->addElement($dom, $update, 'maintainerurl', $release['maintainer_url']);
|
||||
}
|
||||
|
||||
// Target platform
|
||||
if (!empty($release['target_platform'])) {
|
||||
$targetPlatform = $dom->createElement('targetplatform');
|
||||
@@ -174,42 +233,6 @@ class UpdateXmlGenerator
|
||||
$this->addElement($dom, $update, 'php_minimum', $release['php_minimum']);
|
||||
}
|
||||
|
||||
// Optional: Tags
|
||||
if (!empty($release['tags'])) {
|
||||
$tags = $dom->createElement('tags');
|
||||
$update->appendChild($tags);
|
||||
foreach ($release['tags'] as $tag) {
|
||||
$this->addElement($dom, $tags, 'tag', $tag);
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: Maintainer information
|
||||
if (!empty($release['maintainer'])) {
|
||||
$this->addElement($dom, $update, 'maintainer', $release['maintainer']);
|
||||
}
|
||||
|
||||
if (!empty($release['maintainer_url'])) {
|
||||
$this->addElement($dom, $update, 'maintainerurl', $release['maintainer_url']);
|
||||
}
|
||||
|
||||
// Optional: Client (site or administrator)
|
||||
if ($this->clientId !== '0') {
|
||||
$this->addElement($dom, $update, 'client', $this->clientId);
|
||||
}
|
||||
|
||||
// Optional: Checksums
|
||||
if (!empty($release['sha256'])) {
|
||||
$this->addElement($dom, $update, 'sha256', $release['sha256']);
|
||||
}
|
||||
|
||||
if (!empty($release['sha384'])) {
|
||||
$this->addElement($dom, $update, 'sha384', $release['sha384']);
|
||||
}
|
||||
|
||||
if (!empty($release['sha512'])) {
|
||||
$this->addElement($dom, $update, 'sha512', $release['sha512']);
|
||||
}
|
||||
|
||||
// Add to updates element
|
||||
if ($prepend && $updates->firstChild) {
|
||||
$updates->insertBefore($update, $updates->firstChild);
|
||||
@@ -312,6 +335,11 @@ class UpdateXmlGenerator
|
||||
}
|
||||
}
|
||||
|
||||
// Warn if <client> is missing
|
||||
if ($update->getElementsByTagName('client')->length === 0) {
|
||||
$errors[] = "Missing <client> tag — Joomla may not match this update to the installed extension";
|
||||
}
|
||||
|
||||
// Check for download URL
|
||||
$downloads = $update->getElementsByTagName('downloads');
|
||||
if ($downloads->length > 0) {
|
||||
|
||||
@@ -34,7 +34,7 @@ use MokoEnterprise\PlatformAdapterFactory;
|
||||
* pinned commit SHA. Already-pinned references (40-char hex SHA) are left untouched.
|
||||
*
|
||||
* Usage:
|
||||
* php api/maintenance/pin_action_shas.php [--dry-run] [--verbose] [--help]
|
||||
* php maintenance/pin_action_shas.php [--dry-run] [--verbose] [--help]
|
||||
*
|
||||
* Environment:
|
||||
* GH_TOKEN Personal access token for GitHub API calls.
|
||||
@@ -90,7 +90,7 @@ class ActionShaPinner
|
||||
private function showHelp(): void
|
||||
{
|
||||
echo <<<'HELP'
|
||||
Usage: php api/maintenance/pin_action_shas.php [OPTIONS]
|
||||
Usage: php maintenance/pin_action_shas.php [OPTIONS]
|
||||
|
||||
Pins GitHub Actions to immutable commit SHAs in all .github/workflows/*.yml
|
||||
files. Already-pinned references (40-character commit SHA) are skipped.
|
||||
@@ -106,10 +106,10 @@ Environment:
|
||||
|
||||
Examples:
|
||||
# Preview all changes
|
||||
GH_TOKEN=ghp_xxx php api/maintenance/pin_action_shas.php --dry-run --verbose
|
||||
GH_TOKEN=ghp_xxx php maintenance/pin_action_shas.php --dry-run --verbose
|
||||
|
||||
# Apply changes
|
||||
GH_TOKEN=ghp_xxx php api/maintenance/pin_action_shas.php
|
||||
GH_TOKEN=ghp_xxx php maintenance/pin_action_shas.php
|
||||
|
||||
HELP;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
* BRIEF: Generate a live inventory dashboard of all governed repos as a GitHub issue
|
||||
*
|
||||
* USAGE
|
||||
* php api/maintenance/repo_inventory.php # Generate and post dashboard
|
||||
* php api/maintenance/repo_inventory.php --dry-run # Preview only
|
||||
* php api/maintenance/repo_inventory.php --json # JSON output to stdout
|
||||
* php maintenance/repo_inventory.php # Generate and post dashboard
|
||||
* php maintenance/repo_inventory.php --dry-run # Preview only
|
||||
* php maintenance/repo_inventory.php --json # JSON output to stdout
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -212,14 +212,14 @@ if (!$dryRun) {
|
||||
if (!empty($existing[0]['number'])) {
|
||||
$num = $existing[0]['number'];
|
||||
ghApi('PATCH', "repos/{$org}/MokoStandards/issues/{$num}", [
|
||||
'title' => $title, 'body' => $body, 'state' => 'open', 'assignees' => ['jmiller-moko'],
|
||||
'title' => $title, 'body' => $body, 'state' => 'open', 'assignees' => ['jmiller'],
|
||||
], $token);
|
||||
echo "Updated inventory issue #{$num}\n";
|
||||
} else {
|
||||
[$_, $issue] = ghApi('POST', "repos/{$org}/MokoStandards/issues", [
|
||||
'title' => $title, 'body' => $body,
|
||||
'labels' => ['inventory', 'type: chore', 'automation'],
|
||||
'assignees' => ['jmiller-moko'],
|
||||
'assignees' => ['jmiller'],
|
||||
], $token);
|
||||
echo "Created inventory issue #{$issue['number']}\n";
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
* BRIEF: Audit FTP secrets and variables across all governed repos — report missing or stale
|
||||
*
|
||||
* USAGE
|
||||
* php api/maintenance/rotate_secrets.php --all # Audit all repos
|
||||
* php api/maintenance/rotate_secrets.php --repo MokoCRM # Single repo
|
||||
* php api/maintenance/rotate_secrets.php --all --json # JSON output
|
||||
* php api/maintenance/rotate_secrets.php --all --create-issue # Post results as issue
|
||||
* php maintenance/rotate_secrets.php --all # Audit all repos
|
||||
* php maintenance/rotate_secrets.php --repo MokoCRM # Single repo
|
||||
* php maintenance/rotate_secrets.php --all --json # JSON output
|
||||
* php maintenance/rotate_secrets.php --all --create-issue # Post results as issue
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
@@ -204,12 +204,12 @@ if ($createIssue && $issueCount > 0) {
|
||||
[$_, $existing] = ghApi('GET', "repos/{$org}/MokoStandards/issues?labels=secret-audit&state=all&per_page=1&sort=created&direction=desc", null, $token);
|
||||
if (!empty($existing[0]['number'])) {
|
||||
$num = $existing[0]['number'];
|
||||
ghApi('PATCH', "repos/{$org}/MokoStandards/issues/{$num}", ['title' => "audit: FTP secrets — {$issueCount} issues", 'body' => $body, 'state' => 'open', 'assignees' => ['jmiller-moko']], $token);
|
||||
ghApi('PATCH', "repos/{$org}/MokoStandards/issues/{$num}", ['title' => "audit: FTP secrets — {$issueCount} issues", 'body' => $body, 'state' => 'open', 'assignees' => ['jmiller']], $token);
|
||||
if (!$jsonOut) { echo "Updated audit issue #{$num}\n"; }
|
||||
} else {
|
||||
[$_, $issue] = ghApi('POST', "repos/{$org}/MokoStandards/issues", [
|
||||
'title' => "audit: FTP secrets — {$issueCount} issues", 'body' => $body,
|
||||
'labels' => ['secret-audit', 'type: chore', 'automation'], 'assignees' => ['jmiller-moko'],
|
||||
'labels' => ['secret-audit', 'type: chore', 'automation'], 'assignees' => ['jmiller'],
|
||||
], $token);
|
||||
if (!$jsonOut) { echo "Created audit issue #{$issue['number']}\n"; }
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ declare(strict_types=1);
|
||||
*/
|
||||
class ScriptRegistryUpdater
|
||||
{
|
||||
private const REGISTRY_PATH = 'api/.script-registry.json';
|
||||
private const REGISTRY_PATH = '.script-registry.json';
|
||||
|
||||
private bool $dryRun = false;
|
||||
private bool $verbose = false;
|
||||
|
||||
@@ -435,15 +435,15 @@ class UpdateVersionFromReadme extends CliFramework
|
||||
"",
|
||||
"1. Run the sync script locally:",
|
||||
" ```bash",
|
||||
" php api/maintenance/update_version_from_readme.php --path . --dry-run",
|
||||
" php api/maintenance/update_version_from_readme.php --path .",
|
||||
" php maintenance/update_version_from_readme.php --path . --dry-run",
|
||||
" php maintenance/update_version_from_readme.php --path .",
|
||||
" ```",
|
||||
"2. Inspect any files still flagged — they may use a non-standard VERSION format.",
|
||||
"3. Update them manually to match `VERSION: {$version}`.",
|
||||
"4. Commit and push — this issue will be closed automatically on the next successful sync.",
|
||||
"",
|
||||
"---",
|
||||
"*Automatically created by [update_version_from_readme.php](api/maintenance/update_version_from_readme.php)*",
|
||||
"*Automatically created by [update_version_from_readme.php](maintenance/update_version_from_readme.php)*",
|
||||
]);
|
||||
|
||||
try {
|
||||
@@ -458,7 +458,7 @@ class UpdateVersionFromReadme extends CliFramework
|
||||
|
||||
if (!empty($existing[0]['number'])) {
|
||||
$num = (int) $existing[0]['number'];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller-moko']];
|
||||
$patch = ['title' => $title, 'body' => $body, 'assignees' => ['jmiller']];
|
||||
if (($existing[0]['state'] ?? 'open') === 'closed') {
|
||||
$patch['state'] = 'open';
|
||||
}
|
||||
@@ -472,7 +472,7 @@ class UpdateVersionFromReadme extends CliFramework
|
||||
'title' => $title,
|
||||
'body' => $body,
|
||||
'labels' => $labels,
|
||||
'assignees' => ['jmiller-moko'],
|
||||
'assignees' => ['jmiller'],
|
||||
]);
|
||||
$this->log('✅ Created issue #' . ($issue['number'] ?? '?') . " in {$repo}");
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ Icon?
|
||||
.idea/
|
||||
.settings/
|
||||
.claude/
|
||||
.claude-worktree*/
|
||||
.vscode/*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/settings.json.example
|
||||
|
||||
@@ -46,6 +46,7 @@ Icon?
|
||||
.idea/
|
||||
.settings/
|
||||
.claude/
|
||||
.claude-worktree*/
|
||||
.vscode/*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/settings.json.example
|
||||
|
||||
@@ -46,6 +46,7 @@ Icon?
|
||||
.idea/
|
||||
.settings/
|
||||
.claude/
|
||||
.claude-worktree*/
|
||||
.vscode/*
|
||||
!.vscode/tasks.json
|
||||
!.vscode/settings.json.example
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
FILE INFORMATION
|
||||
DEFGROUP: MokoStandards.Templates.Config
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||
PATH: /templates/configs/mokostandards.xml.template
|
||||
VERSION: 04.07.00
|
||||
BRIEF: XML manifest template — synced to .gitea/.mokostandards in every governed repository
|
||||
NOTE: This template is a reference only. The bulk sync generates XML via MokoStandardsParser::generate().
|
||||
|
||||
MokoStandards Repository Manifest
|
||||
Auto-generated by MokoStandards bulk sync.
|
||||
Manual edits to <governance> and <last-synced> may be overwritten.
|
||||
See: docs/standards/mokostandards-file-spec.md
|
||||
-->
|
||||
<mokostandards xmlns="https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API"
|
||||
schema-version="1.0">
|
||||
|
||||
<identity>
|
||||
<name>{{REPO_NAME}}</name>
|
||||
<org>{{org}}</org>
|
||||
<description>{{REPO_DESCRIPTION}}</description>
|
||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||
</identity>
|
||||
|
||||
<governance>
|
||||
<platform>{{platform}}</platform>
|
||||
<standards-version>{{standards_version}}</standards-version>
|
||||
<standards-source>https://git.mokoconsulting.tech/MokoConsulting/MokoStandards</standards-source>
|
||||
</governance>
|
||||
|
||||
<build>
|
||||
<language>{{PRIMARY_LANGUAGE}}</language>
|
||||
</build>
|
||||
|
||||
</mokostandards>
|
||||
@@ -294,3 +294,8 @@ Before opening a PR, verify:
|
||||
| [merge-strategy.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
|
||||
| [changelog-standards.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [joomla-development-guide.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
||||
|
||||
### Update Server Priority
|
||||
|
||||
In the manifest XML `<updateservers>` block, Gitea MUST be priority 1 and GitHub priority 2.
|
||||
Never set GitHub as the primary update server — Gitea is the source of truth.
|
||||
|
||||
+38
-38
@@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# CODEOWNERS — require approval from jmiller-moko for protected paths
|
||||
# CODEOWNERS — require approval from jmiller for protected paths
|
||||
# Synced from MokoStandards. Do not edit manually.
|
||||
#
|
||||
# Changes to these paths require review from the listed owners before merge.
|
||||
@@ -9,47 +9,47 @@
|
||||
# unauthorized modifications to workflows, configs, and governance files.
|
||||
|
||||
# ── Synced workflows (managed by MokoStandards — do not edit manually) ────
|
||||
/.github/workflows/deploy-dev.yml @jmiller-moko
|
||||
/.github/workflows/deploy-demo.yml @jmiller-moko
|
||||
/.github/workflows/deploy-manual.yml @jmiller-moko
|
||||
/.github/workflows/auto-release.yml @jmiller-moko
|
||||
/.github/workflows/auto-dev-issue.yml @jmiller-moko
|
||||
/.github/workflows/auto-assign.yml @jmiller-moko
|
||||
/.github/workflows/sync-version-on-merge.yml @jmiller-moko
|
||||
/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko
|
||||
/.github/workflows/repository-cleanup.yml @jmiller-moko
|
||||
/.github/workflows/standards-compliance.yml @jmiller-moko
|
||||
/.github/workflows/codeql-analysis.yml @jmiller-moko
|
||||
/.github/workflows/repo_health.yml @jmiller-moko
|
||||
/.github/workflows/ci-joomla.yml @jmiller-moko
|
||||
/.github/workflows/update-server.yml @jmiller-moko
|
||||
/.github/workflows/deploy-manual.yml @jmiller-moko
|
||||
/.github/workflows/ci-dolibarr.yml @jmiller-moko
|
||||
/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko
|
||||
/.github/workflows/changelog-validation.yml @jmiller-moko
|
||||
/.github/workflows/branch-freeze.yml @jmiller-moko
|
||||
/.github/workflows/deploy-dev.yml @jmiller
|
||||
/.github/workflows/deploy-demo.yml @jmiller
|
||||
/.github/workflows/deploy-manual.yml @jmiller
|
||||
/.github/workflows/auto-release.yml @jmiller
|
||||
/.github/workflows/auto-dev-issue.yml @jmiller
|
||||
/.github/workflows/auto-assign.yml @jmiller
|
||||
/.github/workflows/sync-version-on-merge.yml @jmiller
|
||||
/.github/workflows/enterprise-firewall-setup.yml @jmiller
|
||||
/.github/workflows/repository-cleanup.yml @jmiller
|
||||
/.github/workflows/standards-compliance.yml @jmiller
|
||||
/.github/workflows/codeql-analysis.yml @jmiller
|
||||
/.github/workflows/repo_health.yml @jmiller
|
||||
/.github/workflows/ci-joomla.yml @jmiller
|
||||
/.github/workflows/update-server.yml @jmiller
|
||||
/.github/workflows/deploy-manual.yml @jmiller
|
||||
/.github/workflows/ci-dolibarr.yml @jmiller
|
||||
/.github/workflows/publish-to-mokodolimods.yml @jmiller
|
||||
/.github/workflows/changelog-validation.yml @jmiller
|
||||
/.github/workflows/branch-freeze.yml @jmiller
|
||||
# Custom workflows in .github/workflows/ not listed above are repo-owned.
|
||||
|
||||
# ── GitHub configuration ─────────────────────────────────────────────────
|
||||
/.github/ISSUE_TEMPLATE/ @jmiller-moko
|
||||
/.github/CODEOWNERS @jmiller-moko
|
||||
/.github/copilot.yml @jmiller-moko
|
||||
/.github/copilot-instructions.md @jmiller-moko
|
||||
/.github/CLAUDE.md @jmiller-moko
|
||||
/.github/.mokostandards @jmiller-moko
|
||||
/.github/ISSUE_TEMPLATE/ @jmiller
|
||||
/.github/CODEOWNERS @jmiller
|
||||
/.github/copilot.yml @jmiller
|
||||
/.github/copilot-instructions.md @jmiller
|
||||
/.github/CLAUDE.md @jmiller
|
||||
/.github/.mokostandards @jmiller
|
||||
|
||||
# ── Build and config files ───────────────────────────────────────────────
|
||||
/composer.json @jmiller-moko
|
||||
/phpstan.neon @jmiller-moko
|
||||
/Makefile @jmiller-moko
|
||||
/.ftpignore @jmiller-moko
|
||||
/.gitignore @jmiller-moko
|
||||
/.gitattributes @jmiller-moko
|
||||
/.editorconfig @jmiller-moko
|
||||
/composer.json @jmiller
|
||||
/phpstan.neon @jmiller
|
||||
/Makefile @jmiller
|
||||
/.ftpignore @jmiller
|
||||
/.gitignore @jmiller
|
||||
/.gitattributes @jmiller
|
||||
/.editorconfig @jmiller
|
||||
|
||||
# ── Governance documents ─────────────────────────────────────────────────
|
||||
/LICENSE @jmiller-moko
|
||||
/CONTRIBUTING.md @jmiller-moko
|
||||
/SECURITY.md @jmiller-moko
|
||||
/GOVERNANCE.md @jmiller-moko
|
||||
/CODE_OF_CONDUCT.md @jmiller-moko
|
||||
/LICENSE @jmiller
|
||||
/CONTRIBUTING.md @jmiller
|
||||
/SECURITY.md @jmiller
|
||||
/GOVERNANCE.md @jmiller
|
||||
/CODE_OF_CONDUCT.md @jmiller
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Dolibarr Module ID Request
|
||||
about: Request a unique module ID for a Dolibarr module
|
||||
title: '[MODULE ID] '
|
||||
labels: ['dolibarr', 'module-id-request', 'admin']
|
||||
assignees: ['jmiller-moko']
|
||||
assignees: ['jmiller']
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Firewall Request
|
||||
about: Request firewall rule changes or access to external resources
|
||||
title: '[FIREWALL] [Resource Name] - [Brief Description]'
|
||||
labels: ['firewall-request', 'infrastructure', 'security']
|
||||
assignees: ['jmiller-moko']
|
||||
assignees: ['jmiller']
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Question
|
||||
about: Ask a question about usage, features, or best practices
|
||||
title: '[QUESTION] '
|
||||
labels: ['question']
|
||||
assignees: ['jmiller-moko']
|
||||
assignees: ['jmiller']
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: License Request
|
||||
about: Request an organization license for Sublime Text
|
||||
title: '[LICENSE REQUEST] Sublime Text - [Your Name]'
|
||||
labels: ['license-request', 'admin']
|
||||
assignees: ['jmiller-moko']
|
||||
assignees: ['jmiller']
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: Version Bump
|
||||
about: Request or track a version change
|
||||
title: '[VERSION] '
|
||||
labels: 'version, type: version'
|
||||
assignees: 'jmiller-moko'
|
||||
assignees: 'jmiller'
|
||||
---
|
||||
|
||||
## Version Change
|
||||
|
||||
@@ -305,3 +305,6 @@ Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `d
|
||||
- Never hardcode version numbers in body text — update `README.md` and let automation propagate
|
||||
- Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows — always use `secrets.GH_TOKEN`
|
||||
- Never let `manifest.xml` version, `updates.xml` version, and `README.md` version go out of sync
|
||||
|
||||
## Update Server Priority
|
||||
In <updateservers>, Gitea must be priority 1 and GitHub priority 2. Never reverse this.
|
||||
|
||||
@@ -39,7 +39,7 @@ updates:
|
||||
reviewers:
|
||||
- "mokoconsulting-tech/maintainers"
|
||||
assignees:
|
||||
- "jmiller-moko"
|
||||
- "jmiller"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
@@ -66,7 +66,7 @@ updates:
|
||||
# reviewers:
|
||||
# - "mokoconsulting-tech/maintainers"
|
||||
# assignees:
|
||||
# - "jmiller-moko"
|
||||
# - "jmiller"
|
||||
# groups:
|
||||
# github-actions-templates:
|
||||
# patterns:
|
||||
@@ -92,7 +92,7 @@ updates:
|
||||
reviewers:
|
||||
- "mokoconsulting-tech/maintainers"
|
||||
assignees:
|
||||
- "jmiller-moko"
|
||||
- "jmiller"
|
||||
groups:
|
||||
composer-dependencies:
|
||||
patterns:
|
||||
@@ -118,7 +118,7 @@ updates:
|
||||
# reviewers:
|
||||
# - "mokoconsulting-tech/maintainers"
|
||||
# assignees:
|
||||
# - "jmiller-moko"
|
||||
# - "jmiller"
|
||||
# groups:
|
||||
# npm-dependencies:
|
||||
# patterns:
|
||||
@@ -144,7 +144,7 @@ updates:
|
||||
# reviewers:
|
||||
# - "mokoconsulting-tech/maintainers"
|
||||
# assignees:
|
||||
# - "jmiller-moko"
|
||||
# - "jmiller"
|
||||
# groups:
|
||||
# python-dependencies:
|
||||
# patterns:
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
<!--
|
||||
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: MokoStandards.Templates.GitHub
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards
|
||||
PATH: /templates/github/CLAUDE.dolibarr.md.template
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: Claude AI assistant context template for Dolibarr/MokoCRM module repositories
|
||||
NOTE: Synced to .github/CLAUDE.md in all Dolibarr/CRM repos via bulk sync.
|
||||
Tokens replaced at sync time: {{REPO_NAME}}, {{REPO_URL}}, {{MODULE_NAME}},
|
||||
{{MODULE_CLASS}}, {{MODULE_ID}}, {{REPO_DESCRIPTION}}
|
||||
-->
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **🔧 AI Self-Update Required on First Use**
|
||||
>
|
||||
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
|
||||
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
|
||||
> this repository — or this is a brand-new repo.
|
||||
>
|
||||
> **On your very first interaction in this repository, before answering any other question:**
|
||||
>
|
||||
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
|
||||
> 2. If they do, locate the values as described in the table below and replace every occurrence
|
||||
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
|
||||
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
|
||||
> through the closing `> ---` line) in both files.
|
||||
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
|
||||
> values — all set."*
|
||||
>
|
||||
> | Placeholder | Where to find the value |
|
||||
> |---|---|
|
||||
> | `{{REPO_NAME}}` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
||||
> | `{{REPO_URL}}` | Full GitHub URL, e.g. `https://git.mokoconsulting.tech/mokoconsulting-tech/<repo-name>` |
|
||||
> | `{{REPO_DESCRIPTION}}` | First paragraph of `README.md` body, or the GitHub repo description |
|
||||
> | `{{MODULE_NAME}}` | The module name as used in Dolibarr (lowercase, e.g. `mymodule`) — from the `langs/en_US/*.lang` filename or `$this->rights_class` in the module descriptor |
|
||||
> | `{{MODULE_CLASS}}` | PascalCase module class name (e.g. `MyModule`) — from the `src/core/modules/mod*.class.php` filename |
|
||||
> | `{{MODULE_ID}}` | The `$this->numero` value in `src/core/modules/mod*.class.php`; check [module-registry.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/development/crm/module-registry.md) if creating a new module |
|
||||
>
|
||||
> ---
|
||||
|
||||
# What This Repo Is
|
||||
|
||||
**{{REPO_NAME}}** is a Moko Consulting **MokoCRM** (Dolibarr) module repository.
|
||||
|
||||
{{REPO_DESCRIPTION}}
|
||||
|
||||
Module name: **{{MODULE_NAME}}**
|
||||
Module class: **{{MODULE_CLASS}}**
|
||||
Module ID: **{{MODULE_ID}}** *(unique, immutable — registered in [module-registry.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/development/crm/module-registry.md))*
|
||||
Repository URL: {{REPO_URL}}
|
||||
|
||||
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards) — the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
|
||||
|
||||
---
|
||||
|
||||
# Repo Structure
|
||||
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── src/ # Module source (deployed to Dolibarr)
|
||||
│ ├── index.php # REQUIRED — accessforbidden() guard
|
||||
│ ├── README.md # End-user documentation
|
||||
│ ├── core/
|
||||
│ │ ├── index.php # REQUIRED — accessforbidden() guard
|
||||
│ │ └── modules/
|
||||
│ │ ├── index.php # REQUIRED — accessforbidden() guard
|
||||
│ │ └── mod{{MODULE_CLASS}}.class.php # Main module descriptor
|
||||
│ ├── langs/
|
||||
│ │ ├── index.php # REQUIRED — accessforbidden() guard
|
||||
│ │ └── en_US/
|
||||
│ │ ├── index.php # REQUIRED — accessforbidden() guard
|
||||
│ │ └── {{MODULE_NAME}}.lang
|
||||
│ ├── sql/ # Database schema
|
||||
│ │ └── index.php # REQUIRED — accessforbidden() guard
|
||||
│ ├── class/ # PHP class files
|
||||
│ │ └── index.php # REQUIRED — accessforbidden() guard
|
||||
│ └── lib/ # Library files
|
||||
│ └── index.php # REQUIRED — accessforbidden() guard
|
||||
├── docs/ # Technical documentation
|
||||
├── scripts/ # Build and maintenance scripts
|
||||
├── tests/ # Test suite
|
||||
│ ├── unit/
|
||||
│ └── integration/
|
||||
├── .github/
|
||||
│ ├── workflows/ # CI/CD workflows (synced from MokoStandards)
|
||||
│ ├── copilot-instructions.md
|
||||
│ └── CLAUDE.md # This file
|
||||
├── README.md # Version source of truth
|
||||
├── CHANGELOG.md
|
||||
├── CONTRIBUTING.md
|
||||
├── LICENSE # GPL-3.0-or-later
|
||||
└── Makefile # Build automation
|
||||
```
|
||||
|
||||
**Every directory inside `src/` MUST have an `index.php`** that either contains live code or calls `accessforbidden()`. Standard guard template (adjust the relative fallback path to match directory depth from `htdocs/`):
|
||||
|
||||
```php
|
||||
<?php
|
||||
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* FILE INFORMATION / BRIEF: Directory access guard
|
||||
*/
|
||||
// Adjust relative path below per depth: mymodule/subdir/ uses ../../../main.inc.php
|
||||
$res = 0;
|
||||
if (!$res && !empty($_SERVER["DOCUMENT_ROOT"])) {
|
||||
$res = @include $_SERVER["DOCUMENT_ROOT"]."/main.inc.php";
|
||||
}
|
||||
if (!$res && file_exists("../../../main.inc.php")) {
|
||||
$res = @include "../../../main.inc.php";
|
||||
}
|
||||
if (!$res) { die("Include of main fails"); }
|
||||
accessforbidden();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Primary Language
|
||||
|
||||
**PHP** (≥ 8.1) is the primary language for this Dolibarr module. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
|
||||
|
||||
---
|
||||
|
||||
# Version Management
|
||||
|
||||
**`README.md` is the single source of truth for the repository version.**
|
||||
|
||||
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it to all `FILE INFORMATION` headers automatically on merge.
|
||||
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
|
||||
- Never hardcode a version number in body text — use the badge or FILE INFORMATION header only.
|
||||
|
||||
### Dolibarr Version Alignment
|
||||
|
||||
Two artefacts must always carry the same version:
|
||||
|
||||
| Artefact | Location |
|
||||
|----------|----------|
|
||||
| `README.md` | `FILE INFORMATION VERSION` field + badge |
|
||||
| Module descriptor | `$this->version` in `src/core/modules/mod{{MODULE_CLASS}}.class.php` |
|
||||
|
||||
---
|
||||
|
||||
# Module Descriptor Class
|
||||
|
||||
The file `src/core/modules/mod{{MODULE_CLASS}}.class.php` is the Dolibarr module descriptor. The key properties:
|
||||
|
||||
```php
|
||||
public $numero = {{MODULE_ID}}; // IMMUTABLE — never change; registered globally
|
||||
public $version = 'XX.YY.ZZ'; // Must match README.md version exactly
|
||||
```
|
||||
|
||||
**`$numero` is permanent.** It was registered in [module-registry.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/development/crm/module-registry.md) when this module was created. Changing it would break all Dolibarr installations that have this module activated.
|
||||
|
||||
Before creating a new module, always check the registry for the next available ID.
|
||||
|
||||
---
|
||||
|
||||
# File Header Requirements
|
||||
|
||||
Every new file **must** have a copyright header as its first content. JSON files, binary files, generated files, and third-party files are exempt.
|
||||
|
||||
**PHP:**
|
||||
```php
|
||||
<?php
|
||||
/* 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: {{REPO_NAME}}.Module
|
||||
* INGROUP: {{REPO_NAME}}
|
||||
* REPO: {{REPO_URL}}
|
||||
* PATH: /src/class/MyClass.php
|
||||
* VERSION: XX.YY.ZZ
|
||||
* BRIEF: One-line description of file purpose
|
||||
*/
|
||||
```
|
||||
|
||||
**Markdown / YAML / Shell:** Use the appropriate comment syntax with the same fields.
|
||||
|
||||
---
|
||||
|
||||
# Coding Standards
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Context | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| PHP class | `PascalCase` | `MyService` |
|
||||
| PHP method / function | `camelCase` | `getUserData()` |
|
||||
| PHP variable | `$snake_case` | `$module_name` |
|
||||
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_RETRIES` |
|
||||
| PHP class file | `PascalCase.php` | `ApiClient.php` |
|
||||
| PHP script file | `snake_case.php` | `check_health.php` |
|
||||
| YAML workflow | `kebab-case.yml` | `ci-dolibarr.yml` |
|
||||
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
|
||||
|
||||
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
|
||||
|
||||
## Branch Naming
|
||||
|
||||
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
|
||||
|
||||
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
|
||||
|
||||
---
|
||||
|
||||
# GitHub Actions — Token Usage
|
||||
|
||||
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
|
||||
|
||||
```yaml
|
||||
# ✅ Correct
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# ❌ Wrong — never use these
|
||||
token: ${{ github.token }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` — `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is a local-dev fallback only.
|
||||
|
||||
---
|
||||
|
||||
# Keeping Documentation Current
|
||||
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed module version | Update `$this->version` in module descriptor; bump `README.md` |
|
||||
| New library class or major feature | `CHANGELOG.md` entry under `Added` |
|
||||
| Bug fix | `CHANGELOG.md` entry under `Fixed` |
|
||||
| Breaking change | `CHANGELOG.md` entry under `Changed` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
|
||||
---
|
||||
|
||||
# What NOT to Do
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never change `$this->numero`** — the module ID is permanent and globally registered.
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
- **Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows** — always use `secrets.GH_TOKEN`.
|
||||
- **Never register a new module ID** without first consulting module-registry.md.
|
||||
- **Never let `$this->version` and `README.md` version diverge.**
|
||||
- **Never create a directory inside `src/` without adding an `index.php`** — every directory must have an `index.php` with live code or `accessforbidden()`.
|
||||
|
||||
---
|
||||
|
||||
# PR Checklist
|
||||
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] `$this->version` in module descriptor updated to match
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Every new directory inside `src/` has an `index.php` (live code or `accessforbidden()` guard)
|
||||
|
||||
---
|
||||
|
||||
# Key Policy Documents (MokoStandards)
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [file-header-standards.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
|
||||
| [changelog-standards.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [module-registry.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/development/crm/module-registry.md) | Dolibarr module ID registry — check before reserving a new ID |
|
||||
| [crm/development-standards.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/crm/development-standards.md) | MokoCRM Dolibarr module development standards |
|
||||
| [dolibarr-development-guide.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/crm/dolibarr-development-guide.md) | MokoCRM full development guide |
|
||||
@@ -1,296 +0,0 @@
|
||||
<!--
|
||||
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: MokoStandards.Templates.GitHub
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards
|
||||
PATH: /templates/github/CLAUDE.joomla.md.template
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: Claude AI assistant context template for Joomla/MokoWaaS governed repositories
|
||||
NOTE: Synced to .github/CLAUDE.md in all Joomla/WaaS repos via bulk sync.
|
||||
Tokens replaced at sync time: {{REPO_NAME}}, {{REPO_URL}}, {{EXTENSION_NAME}},
|
||||
{{EXTENSION_TYPE}}, {{EXTENSION_ELEMENT}}, {{REPO_DESCRIPTION}}
|
||||
-->
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **🔧 AI Self-Update Required on First Use**
|
||||
>
|
||||
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
|
||||
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
|
||||
> this repository — or this is a brand-new repo.
|
||||
>
|
||||
> **On your very first interaction in this repository, before answering any other question:**
|
||||
>
|
||||
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
|
||||
> 2. If they do, locate the values as described in the table below and replace every occurrence
|
||||
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
|
||||
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
|
||||
> through the closing `> ---` line) in both files.
|
||||
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
|
||||
> values — all set."*
|
||||
>
|
||||
> | Placeholder | Where to find the value |
|
||||
> |---|---|
|
||||
> | `{{REPO_NAME}}` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
||||
> | `{{REPO_URL}}` | Full GitHub URL, e.g. `https://git.mokoconsulting.tech/mokoconsulting-tech/<repo-name>` |
|
||||
> | `{{REPO_DESCRIPTION}}` | First paragraph of `README.md` body, or the GitHub repo description |
|
||||
> | `{{EXTENSION_NAME}}` | The `<name>` element in `manifest.xml` at the repository root |
|
||||
> | `{{EXTENSION_TYPE}}` | The `type` attribute of the `<extension>` tag in `manifest.xml` (`component`, `module`, `plugin`, or `template`) |
|
||||
> | `{{EXTENSION_ELEMENT}}` | The `<element>` tag in `manifest.xml`, or the filename prefix (e.g. `com_myextension`, `mod_mymodule`) |
|
||||
>
|
||||
> ---
|
||||
|
||||
# What This Repo Is
|
||||
|
||||
**{{REPO_NAME}}** is a Moko Consulting **MokoWaaS** (Joomla) extension repository.
|
||||
|
||||
{{REPO_DESCRIPTION}}
|
||||
|
||||
Extension name: **{{EXTENSION_NAME}}**
|
||||
Extension type: **{{EXTENSION_TYPE}}** (`{{EXTENSION_ELEMENT}}`)
|
||||
Repository URL: {{REPO_URL}}
|
||||
|
||||
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards) — the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
|
||||
|
||||
---
|
||||
|
||||
# Repo Structure
|
||||
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── manifest.xml # Joomla installer manifest (root — required)
|
||||
├── updates.xml # Update server manifest (root — required)
|
||||
├── site/ # Frontend (site) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
│ ├── models/
|
||||
│ └── views/
|
||||
├── admin/ # Backend (admin) code
|
||||
│ ├── controller.php
|
||||
│ ├── controllers/
|
||||
│ ├── models/
|
||||
│ ├── views/
|
||||
│ └── sql/
|
||||
├── language/ # Language INI files
|
||||
├── media/ # CSS, JS, images
|
||||
├── docs/ # Technical documentation
|
||||
├── tests/ # Test suite
|
||||
├── .github/
|
||||
│ ├── workflows/ # CI/CD workflows (synced from MokoStandards)
|
||||
│ ├── copilot-instructions.md
|
||||
│ └── CLAUDE.md # This file
|
||||
├── README.md # Version source of truth
|
||||
├── CHANGELOG.md
|
||||
├── CONTRIBUTING.md
|
||||
└── LICENSE # GPL-3.0-or-later
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Primary Language
|
||||
|
||||
**PHP** (≥ 7.4) is the primary language for this Joomla extension. YAML uses 2-space indentation. All other text files use tabs per `.editorconfig`.
|
||||
|
||||
---
|
||||
|
||||
# Version Management
|
||||
|
||||
**`README.md` is the single source of truth for the repository version.**
|
||||
|
||||
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it to all `FILE INFORMATION` headers automatically on merge.
|
||||
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
|
||||
- Never hardcode a version number in body text — use the badge or FILE INFORMATION header only.
|
||||
|
||||
### Joomla Version Alignment
|
||||
|
||||
Three files must **always have the same version**:
|
||||
|
||||
| File | Where the version lives |
|
||||
|------|------------------------|
|
||||
| `README.md` | `FILE INFORMATION` block + badge |
|
||||
| `manifest.xml` | `<version>` tag |
|
||||
| `updates.xml` | `<version>` in the most recent `<update>` block |
|
||||
|
||||
The `make release` command / release workflow syncs all three automatically.
|
||||
|
||||
---
|
||||
|
||||
# updates.xml — Required in Repo Root
|
||||
|
||||
`updates.xml` is the Joomla update server manifest. It allows Joomla installations to check for new versions of this extension via:
|
||||
|
||||
```xml
|
||||
<!-- In manifest.xml — dual-platform: Gitea primary, GitHub mirror -->
|
||||
<updateservers>
|
||||
<server type="extension" priority="1" name="{{EXTENSION_NAME}} Update Server (Gitea)">
|
||||
https://git.mokoconsulting.tech/mokoconsulting-tech/{{REPO_NAME}}/raw/branch/main/updates.xml
|
||||
</server>
|
||||
<server type="extension" priority="2" name="{{EXTENSION_NAME}} Update Server (GitHub)">
|
||||
https://raw.githubusercontent.com/mokoconsulting-tech/{{REPO_NAME}}/main/updates.xml
|
||||
</server>
|
||||
</updateservers>
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Every release prepends a new `<update>` block at the top — older entries are preserved.
|
||||
- `<version>` in `updates.xml` must exactly match `<version>` in `manifest.xml` and `README.md`.
|
||||
- `<downloads>` must include two `<downloadurl>` entries: Gitea release asset (primary) and GitHub release asset (mirror).
|
||||
- `<targetplatform version="4\.[0-9]+">` — backslash is literal (Joomla regex syntax).
|
||||
|
||||
Example `updates.xml` entry for a new release:
|
||||
```xml
|
||||
<updates>
|
||||
<update>
|
||||
<name>{{EXTENSION_NAME}}</name>
|
||||
<description>{{REPO_NAME}}</description>
|
||||
<element>{{EXTENSION_ELEMENT}}</element>
|
||||
<type>{{EXTENSION_TYPE}}</type>
|
||||
<version>01.02.04</version>
|
||||
<infourl title="Release Information">{{REPO_URL}}/releases/tag/01.02.04</infourl>
|
||||
<downloads>
|
||||
<downloadurl type="full" format="zip">
|
||||
{{REPO_URL}}/releases/download/01.02.04/{{EXTENSION_ELEMENT}}-01.02.04.zip
|
||||
</downloadurl>
|
||||
</downloads>
|
||||
<targetplatform name="joomla" version="4\.[0-9]+" />
|
||||
<php_minimum>8.2</php_minimum>
|
||||
<maintainer>Moko Consulting</maintainer>
|
||||
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||
</update>
|
||||
</updates>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# File Header Requirements
|
||||
|
||||
Every new file **must** have a copyright header as its first content. JSON files, binary files, generated files, and third-party files are exempt.
|
||||
|
||||
**PHP:**
|
||||
```php
|
||||
<?php
|
||||
/* 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: {{REPO_NAME}}.{{EXTENSION_TYPE}}
|
||||
* INGROUP: {{REPO_NAME}}
|
||||
* REPO: {{REPO_URL}}
|
||||
* PATH: /site/controllers/item.php
|
||||
* VERSION: XX.YY.ZZ
|
||||
* BRIEF: One-line description of file purpose
|
||||
*/
|
||||
|
||||
defined('_JEXEC') or die;
|
||||
```
|
||||
|
||||
**Markdown / YAML / Shell / XML:** Use the appropriate comment syntax with the same fields.
|
||||
|
||||
---
|
||||
|
||||
# Coding Standards
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Context | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| PHP class | `PascalCase` | `ItemModel` |
|
||||
| PHP method / function | `camelCase` | `getItems()` |
|
||||
| PHP variable | `$snake_case` | `$item_id` |
|
||||
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_ITEMS` |
|
||||
| PHP class file | `PascalCase.php` | `ItemModel.php` |
|
||||
| YAML workflow | `kebab-case.yml` | `ci-joomla.yml` |
|
||||
| Markdown doc | `kebab-case.md` | `installation-guide.md` |
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
|
||||
|
||||
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
|
||||
|
||||
## Branch Naming
|
||||
|
||||
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
|
||||
|
||||
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
|
||||
|
||||
---
|
||||
|
||||
# GitHub Actions — Token Usage
|
||||
|
||||
Every workflow must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
|
||||
|
||||
```yaml
|
||||
# ✅ Correct
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# ❌ Wrong — never use these
|
||||
token: ${{ github.token }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Keeping Documentation Current
|
||||
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed PHP class/method | PHPDoc block; `docs/api/` entry |
|
||||
| New or changed `manifest.xml` | Sync version to `updates.xml` and `README.md` |
|
||||
| New release | Prepend `<update>` to `updates.xml`; update `CHANGELOG.md`; bump `README.md` |
|
||||
| New or changed workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it |
|
||||
|
||||
---
|
||||
|
||||
# What NOT to Do
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never let `manifest.xml`, `updates.xml`, and `README.md` versions diverge.**
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
- **Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows** — always use `secrets.GH_TOKEN`.
|
||||
- **Never remove `defined('_JEXEC') or die;`** from web-accessible PHP files.
|
||||
|
||||
---
|
||||
|
||||
# PR Checklist
|
||||
|
||||
Before opening a PR, verify:
|
||||
|
||||
- [ ] Patch version bumped in `README.md` (e.g. `01.02.03` → `01.02.04`)
|
||||
- [ ] If this is a release: `manifest.xml` version updated; `updates.xml` updated with new entry
|
||||
- [ ] FILE INFORMATION headers updated in modified files
|
||||
- [ ] CHANGELOG.md updated
|
||||
- [ ] Tests pass
|
||||
|
||||
---
|
||||
|
||||
# Key Policy Documents (MokoStandards)
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [file-header-standards.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
|
||||
| [changelog-standards.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [joomla-development-guide.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/waas/joomla-development-guide.md) | MokoWaaS Joomla extension development guide |
|
||||
@@ -1,366 +0,0 @@
|
||||
<!--
|
||||
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: MokoStandards.Templates.GitHub
|
||||
INGROUP: MokoStandards.Templates
|
||||
REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards
|
||||
PATH: /templates/github/CLAUDE.md.template
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: Standard CLAUDE.md template for Moko Consulting governed repositories
|
||||
NOTE: Synced to .github/CLAUDE.md in all governed repositories via bulk sync.
|
||||
Tokens replaced at sync time: {{REPO_NAME}}, {{REPO_URL}}, {{PRIMARY_LANGUAGE}}, {{PLATFORM_TYPE}}, {{REPO_DESCRIPTION}}
|
||||
-->
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **🔧 AI Self-Update Required on First Use**
|
||||
>
|
||||
> This file may contain unfilled `{{TOKEN}}` placeholders. The MokoStandards bulk-sync system
|
||||
> replaces them automatically at sync time; if you can still see them, sync has not yet run for
|
||||
> this repository — or this is a brand-new repo.
|
||||
>
|
||||
> **On your very first interaction in this repository, before answering any other question:**
|
||||
>
|
||||
> 1. Check whether any `{{TOKEN}}` placeholders remain in this file.
|
||||
> 2. If they do, locate the values as described in the table below and replace every occurrence
|
||||
> in **both** `.github/copilot-instructions.md` and `.github/CLAUDE.md`.
|
||||
> 3. After replacing all tokens, **delete this entire notice block** (from `> [!IMPORTANT]`
|
||||
> through the closing `> ---` line) in both files.
|
||||
> 4. Tell the developer: *"I've self-updated the AI context files with this repository's
|
||||
> values — all set."*
|
||||
>
|
||||
> | Placeholder | Where to find the value |
|
||||
> |---|---|
|
||||
> | `{{REPO_NAME}}` | The GitHub repository name (visible in the URL, `README.md` heading, or `git remote -v`) |
|
||||
> | `{{REPO_URL}}` | Full GitHub URL, e.g. `https://git.mokoconsulting.tech/mokoconsulting-tech/<repo-name>` |
|
||||
> | `{{REPO_DESCRIPTION}}` | First paragraph of `README.md` body, or the GitHub repo description |
|
||||
> | `{{PRIMARY_LANGUAGE}}` | The dominant programming language (check file extensions in the repository) |
|
||||
> | `{{PLATFORM_TYPE}}` | The project type: `PHP library`, `Joomla extension`, `Dolibarr module`, `WaaS site`, etc. — infer from repo structure |
|
||||
>
|
||||
> ---
|
||||
|
||||
# What This Repo Is
|
||||
|
||||
**{{REPO_NAME}}** is a Moko Consulting **{{PLATFORM_TYPE}}** repository.
|
||||
|
||||
{{REPO_DESCRIPTION}}
|
||||
|
||||
Repository URL: {{REPO_URL}}
|
||||
|
||||
This repository is governed by [MokoStandards](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards) — the single source of truth for coding standards, file-header policies, GitHub Actions workflows, and Terraform configuration templates across all Moko Consulting repositories.
|
||||
|
||||
---
|
||||
|
||||
# Repo Structure
|
||||
|
||||
```
|
||||
{{REPO_NAME}}/
|
||||
├── src/ # Primary source code
|
||||
├── docs/ # Documentation
|
||||
├── tests/ # Test suite
|
||||
├── .github/
|
||||
│ ├── workflows/ # CI/CD workflows (synced from MokoStandards)
|
||||
│ ├── ISSUE_TEMPLATE/ # Issue templates (synced from MokoStandards)
|
||||
│ ├── copilot-instructions.md # GitHub Copilot custom instructions
|
||||
│ ├── CLAUDE.md # This file — Claude AI assistant context
|
||||
│ └── override.tf # Repository-specific health-check overrides
|
||||
├── README.md # Project overview — version source of truth
|
||||
├── CHANGELOG.md # Version history
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
└── LICENSE # GPL-3.0-or-later
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Primary Language
|
||||
|
||||
**{{PRIMARY_LANGUAGE}}** is the primary language for this repository.
|
||||
|
||||
YAML uses 2-space indentation (spaces, not tabs). All other text files use tabs per `.editorconfig`.
|
||||
|
||||
---
|
||||
|
||||
# Composer Package (PHP repositories)
|
||||
|
||||
This repository requires the MokoStandards enterprise library. The package is installed from the private GitHub VCS source.
|
||||
|
||||
`composer.json` must contain:
|
||||
|
||||
```json
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"type": "vcs",
|
||||
"url": "https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"mokoconsulting/mokostandards": "^4.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Install or update with:
|
||||
|
||||
```bash
|
||||
composer install # first time
|
||||
composer update mokoconsulting/mokostandards # upgrade
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# PHP Script Pattern
|
||||
|
||||
All PHP scripts must extend `MokoStandards\Enterprise\CliFramework` — **never** use a standalone class or the legacy `CliBase`.
|
||||
|
||||
```php
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/* 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: {{REPO_NAME}}.Scripts
|
||||
* INGROUP: {{REPO_NAME}}
|
||||
* REPO: {{REPO_URL}}
|
||||
* PATH: /api/my_script.php
|
||||
* VERSION: XX.YY.ZZ
|
||||
* BRIEF: One-line description of what this script does
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use MokoStandards\Enterprise\CliFramework;
|
||||
|
||||
class MyScript extends CliFramework
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription('One-line description of what this script does');
|
||||
$this->addArgument('--path', 'Repository root path', '.');
|
||||
$this->addArgument('--dry-run', 'Preview changes without writing', false);
|
||||
}
|
||||
|
||||
protected function run(): int
|
||||
{
|
||||
$path = $this->getArgument('--path');
|
||||
$dryRun = (bool) $this->getArgument('--dry-run');
|
||||
|
||||
// implementation …
|
||||
$this->log('INFO', "Processing: {$path}");
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
$script = new MyScript('my_script', 'One-line description of what this script does');
|
||||
exit($script->execute());
|
||||
```
|
||||
|
||||
**CliFramework interface summary:**
|
||||
|
||||
| Member | Purpose |
|
||||
|--------|---------|
|
||||
| `configure(): void` | Abstract — register arguments with `addArgument()` |
|
||||
| `run(): int` | Abstract — main script logic; return the exit code |
|
||||
| `initialize(): void` | Optional hook — runs after arg-parse, before `run()` |
|
||||
| `execute(array $argv = []): int` | **Public entry point** — call this at the bottom; it calls `configure()`, parses argv, then calls `run()` |
|
||||
| `addArgument(string $name, string $desc, mixed $default)` | Register a CLI argument |
|
||||
| `getArgument(string $name): mixed` | Read a parsed or default argument value |
|
||||
| `log(string $level, string $message)` | Structured log — levels: INFO SUCCESS WARNING ERROR DEBUG |
|
||||
| `error(string $message, int $code = 1): never` | Log error and exit |
|
||||
| `$this->dryRun` | `true` when `--dry-run` is passed |
|
||||
| `$this->verbose` | `true` when `--verbose` / `-v` is passed |
|
||||
|
||||
**Forbidden patterns in PHP:**
|
||||
|
||||
```php
|
||||
// ❌ Wrong — legacy base class, not namespaced
|
||||
class MyScript extends CliBase { … }
|
||||
|
||||
// ❌ Wrong — standalone class with no framework
|
||||
class MyScript { public function run() { … } }
|
||||
|
||||
// ❌ Wrong — method names and entry-point transposed
|
||||
protected function execute(): int { … } // should be run()
|
||||
exit($script->run()); // should be execute()
|
||||
|
||||
// ✅ Correct
|
||||
class MyScript extends CliFramework {
|
||||
protected function configure(): void { … }
|
||||
protected function run(): int { … }
|
||||
}
|
||||
$script = new MyScript('name', 'description');
|
||||
exit($script->execute());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Version Management
|
||||
|
||||
**`README.md` is the single source of truth for the repository version.**
|
||||
|
||||
- **Bump the patch version on every PR** — increment `XX.YY.ZZ` (e.g. `01.02.03` → `01.02.04`) in `README.md` before opening the PR; the `sync-version-on-merge` workflow propagates it to all badges and `FILE INFORMATION` headers automatically on merge to `main`.
|
||||
- The `VERSION: XX.YY.ZZ` field in the `README.md` `FILE INFORMATION` block governs all other version references.
|
||||
- Update `README.md` only — the `sync-version-on-merge` workflow propagates it to all badges and `FILE INFORMATION` headers automatically on merge to `main`.
|
||||
- Version format is zero-padded semver: `XX.YY.ZZ` (e.g. `01.02.03`).
|
||||
- Never hardcode a version number in body text — use the badge or FILE INFORMATION header only.
|
||||
|
||||
---
|
||||
|
||||
# File Header Requirements
|
||||
|
||||
Every new file **must** have a copyright header as its first content. JSON files, binary files, generated files, and third-party files are exempt.
|
||||
|
||||
## Minimal header
|
||||
|
||||
**PHP:**
|
||||
```php
|
||||
<?php
|
||||
/* 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: {{REPO_NAME}}.Module
|
||||
* INGROUP: {{REPO_NAME}}
|
||||
* REPO: {{REPO_URL}}
|
||||
* PATH: /src/MyClass.php
|
||||
* VERSION: XX.YY.ZZ
|
||||
* BRIEF: One-line description of file purpose
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
```
|
||||
|
||||
**Markdown:**
|
||||
```markdown
|
||||
<!--
|
||||
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: {{REPO_NAME}}.Documentation
|
||||
INGROUP: {{REPO_NAME}}
|
||||
REPO: {{REPO_URL}}
|
||||
PATH: /docs/guide/example.md
|
||||
VERSION: XX.YY.ZZ
|
||||
BRIEF: One-line description of file purpose
|
||||
-->
|
||||
```
|
||||
|
||||
**YAML / Shell:** Use `#` comments with the same fields. JSON files are exempt.
|
||||
|
||||
---
|
||||
|
||||
# Coding Standards
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Context | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| PHP class | `PascalCase` | `MyService` |
|
||||
| PHP method / function | `camelCase` | `getUserData()` |
|
||||
| PHP variable | `$snake_case` | `$user_id` |
|
||||
| PHP constant | `UPPER_SNAKE_CASE` | `MAX_RETRIES` |
|
||||
| PHP class file | `PascalCase.php` | `UserService.php` |
|
||||
| PHP script file | `snake_case.php` | `check_health.php` |
|
||||
| YAML workflow | `kebab-case.yml` | `code-quality.yml` |
|
||||
| Markdown doc | `kebab-case.md` | `coding-style-guide.md` |
|
||||
|
||||
## Commit Messages
|
||||
|
||||
Format: `<type>(<scope>): <subject>` — imperative, lower-case subject, no trailing period.
|
||||
|
||||
Valid types: `feat` · `fix` · `docs` · `chore` · `ci` · `refactor` · `style` · `test` · `perf` · `revert` · `build`
|
||||
|
||||
## Branch Naming
|
||||
|
||||
Format: `<prefix>/<MAJOR.MINOR.PATCH>[/description]`
|
||||
|
||||
Approved prefixes: `dev/` · `rc/` · `version/` · `patch/` · `copilot/` · `dependabot/`
|
||||
|
||||
---
|
||||
|
||||
# GitHub Actions — Token Usage
|
||||
|
||||
Every workflow in this repository must use **`secrets.GH_TOKEN`** (the org-level Personal Access Token).
|
||||
|
||||
```yaml
|
||||
# ✅ Correct — always use GH_TOKEN
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# ❌ Wrong — never use these
|
||||
token: ${{ github.token }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
PHP scripts read the token with: `getenv('GH_TOKEN') ?: getenv('GITHUB_TOKEN')` — `GH_TOKEN` is always preferred; `GITHUB_TOKEN` is a local-dev fallback only.
|
||||
|
||||
---
|
||||
|
||||
# Keeping Documentation Current
|
||||
|
||||
Whenever you make code changes, update the corresponding documentation in the same commit or PR. Do not leave docs stale.
|
||||
|
||||
| Change type | Documentation to update |
|
||||
|-------------|------------------------|
|
||||
| New or renamed public PHP method | PHPDoc block on the method; `docs/api/` index for that class |
|
||||
| New or changed CLI script argument | Script's own `--help` text; `docs/api/` or equivalent |
|
||||
| New or changed GitHub Actions workflow | `docs/workflows/<workflow-name>.md` |
|
||||
| New or changed policy | Corresponding file under `docs/policy/` |
|
||||
| New library class or major feature | `CHANGELOG.md` entry under `Added` |
|
||||
| Bug fix | `CHANGELOG.md` entry under `Fixed` |
|
||||
| Breaking change | `CHANGELOG.md` entry under `Changed`; update `CONTRIBUTING.md` if contributor steps change |
|
||||
| Any modified file | Update the `VERSION` field in that file's `FILE INFORMATION` block |
|
||||
| **Every PR** | **Bump the patch version** — increment `XX.YY.ZZ` in `README.md`; `sync-version-on-merge` propagates it to all headers and badges on merge |
|
||||
|
||||
If your code change makes any existing doc sentence false or incomplete, fix the doc before closing the PR.
|
||||
|
||||
---
|
||||
|
||||
# What NOT to Do
|
||||
|
||||
- **Never commit directly to `main`** — all changes go through a PR.
|
||||
- **Never hardcode version numbers** in body text — update `README.md` and let automation propagate.
|
||||
- **Never skip the FILE INFORMATION block** on a new source file.
|
||||
- **Never use bare `catch (\Throwable $e) {}`** — always log or re-throw.
|
||||
- **Never mix tabs and spaces** within a file — follow `.editorconfig`.
|
||||
- **Never use `github.token` or `secrets.GITHUB_TOKEN` in workflows** — always use `secrets.GH_TOKEN`.
|
||||
- **Never extend `CliBase` in PHP scripts** — extend `MokoStandards\Enterprise\CliFramework` instead.
|
||||
- **Never use `exit($script->run())`** — the correct entry point is `exit($script->execute())`.
|
||||
|
||||
---
|
||||
|
||||
# Key Policy Documents (MokoStandards)
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| [file-header-standards.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/file-header-standards.md) | Copyright-header rules for every file type |
|
||||
| [coding-style-guide.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/coding-style-guide.md) | Naming and formatting conventions |
|
||||
| [branching-strategy.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/branching-strategy.md) | Branch naming, hierarchy, and release workflow |
|
||||
| [merge-strategy.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/merge-strategy.md) | Squash-merge policy and PR conventions |
|
||||
| [changelog-standards.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/changelog-standards.md) | How and when to update CHANGELOG.md |
|
||||
| [scripting-standards.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/policy/scripting-standards.md) | PHP script requirements and CliFramework usage |
|
||||
| [package-installation.md](https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards/blob/main/docs/guide/package-installation.md) | Installing `mokoconsulting/mokostandards` via Composer |
|
||||
@@ -1,55 +0,0 @@
|
||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# CODEOWNERS — require approval from jmiller-moko for protected paths
|
||||
# Synced from MokoStandards. Do not edit manually.
|
||||
#
|
||||
# Changes to these paths require review from the listed owners before merge.
|
||||
# Combined with branch protection (require PR reviews), this prevents
|
||||
# unauthorized modifications to workflows, configs, and governance files.
|
||||
|
||||
# ── Synced workflows (managed by MokoStandards — do not edit manually) ────
|
||||
/.github/workflows/deploy-dev.yml @jmiller-moko
|
||||
/.github/workflows/deploy-demo.yml @jmiller-moko
|
||||
/.github/workflows/deploy-manual.yml @jmiller-moko
|
||||
/.github/workflows/auto-release.yml @jmiller-moko
|
||||
/.github/workflows/auto-dev-issue.yml @jmiller-moko
|
||||
/.github/workflows/auto-assign.yml @jmiller-moko
|
||||
/.github/workflows/sync-version-on-merge.yml @jmiller-moko
|
||||
/.github/workflows/enterprise-firewall-setup.yml @jmiller-moko
|
||||
/.github/workflows/repository-cleanup.yml @jmiller-moko
|
||||
/.github/workflows/standards-compliance.yml @jmiller-moko
|
||||
/.github/workflows/codeql-analysis.yml @jmiller-moko
|
||||
/.github/workflows/repo_health.yml @jmiller-moko
|
||||
/.github/workflows/ci-joomla.yml @jmiller-moko
|
||||
/.github/workflows/update-server.yml @jmiller-moko
|
||||
/.github/workflows/deploy-manual.yml @jmiller-moko
|
||||
/.github/workflows/ci-dolibarr.yml @jmiller-moko
|
||||
/.github/workflows/publish-to-mokodolimods.yml @jmiller-moko
|
||||
/.github/workflows/changelog-validation.yml @jmiller-moko
|
||||
/.github/workflows/branch-freeze.yml @jmiller-moko
|
||||
# Custom workflows in .github/workflows/ not listed above are repo-owned.
|
||||
|
||||
# ── GitHub configuration ─────────────────────────────────────────────────
|
||||
/.github/ISSUE_TEMPLATE/ @jmiller-moko
|
||||
/.github/CODEOWNERS @jmiller-moko
|
||||
/.github/copilot.yml @jmiller-moko
|
||||
/.github/copilot-instructions.md @jmiller-moko
|
||||
/.github/CLAUDE.md @jmiller-moko
|
||||
/.github/.mokostandards @jmiller-moko
|
||||
|
||||
# ── Build and config files ───────────────────────────────────────────────
|
||||
/composer.json @jmiller-moko
|
||||
/phpstan.neon @jmiller-moko
|
||||
/Makefile @jmiller-moko
|
||||
/.ftpignore @jmiller-moko
|
||||
/.gitignore @jmiller-moko
|
||||
/.gitattributes @jmiller-moko
|
||||
/.editorconfig @jmiller-moko
|
||||
|
||||
# ── Governance documents ─────────────────────────────────────────────────
|
||||
/LICENSE @jmiller-moko
|
||||
/CONTRIBUTING.md @jmiller-moko
|
||||
/SECURITY.md @jmiller-moko
|
||||
/GOVERNANCE.md @jmiller-moko
|
||||
/CODE_OF_CONDUCT.md @jmiller-moko
|
||||
@@ -1,51 +0,0 @@
|
||||
# CODEOWNERS Template
|
||||
#
|
||||
# This file defines code ownership for automatic review assignment.
|
||||
# Copy to .github/CODEOWNERS (remove .template suffix) and customize.
|
||||
#
|
||||
# Syntax:
|
||||
# pattern owner(s)
|
||||
#
|
||||
# Examples:
|
||||
# * @org/default-team
|
||||
# /docs/ @org/docs-team
|
||||
# *.js @org/frontend-team
|
||||
# /src/security/ @org/security-team
|
||||
#
|
||||
# Last matching pattern takes precedence.
|
||||
# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Default owners for everything in the repo
|
||||
# These owners will be requested for review when someone opens a PR
|
||||
# Unless a later match takes precedence
|
||||
* @mokoconsulting-tech/maintainers
|
||||
|
||||
# Documentation
|
||||
/docs/ @mokoconsulting-tech/docs-team
|
||||
/README.md @mokoconsulting-tech/docs-team
|
||||
/CHANGELOG.md @mokoconsulting-tech/docs-team
|
||||
/CONTRIBUTING.md @mokoconsulting-tech/docs-team
|
||||
|
||||
# GitHub configuration
|
||||
/.github/ @mokoconsulting-tech/devops-team
|
||||
/.github/workflows/ @mokoconsulting-tech/devops-team
|
||||
|
||||
# Security-related files
|
||||
/SECURITY.md @mokoconsulting-tech/security-team
|
||||
/docs/policy/security-*.md @mokoconsulting-tech/security-team
|
||||
/.github/workflows/security-*.yml @mokoconsulting-tech/security-team
|
||||
|
||||
# API and automation
|
||||
/api/ @mokoconsulting-tech/automation-team
|
||||
|
||||
# Build system
|
||||
/Makefiles/ @mokoconsulting-tech/build-team
|
||||
|
||||
# Policies (require policy owner approval)
|
||||
/docs/policy/ @mokoconsulting-tech/policy-owners
|
||||
|
||||
# Templates
|
||||
/templates/ @mokoconsulting-tech/template-maintainers
|
||||
|
||||
# Schemas
|
||||
/schemas/ @mokoconsulting-tech/architecture-team
|
||||
@@ -1,110 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,48 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
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-tech/MokoStandards
|
||||
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
|
||||
@@ -1,52 +0,0 @@
|
||||
---
|
||||
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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user