feat(workflows): Universal auto-release with .moko-platform detection
Joomla: Repo Health / Access control (push) Successful in 1s
Joomla: Repo Health / Release configuration (push) Failing after 3s
Joomla: Repo Health / Scripts governance (push) Successful in 3s
Joomla: Repo Health / Repository health (push) Failing after 3s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 3s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 4s
Universal: Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been skipped
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been skipped
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been skipped
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 3s
Joomla: Repo Health / Access control (pull_request) Successful in 1s
Joomla: Repo Health / Release configuration (pull_request) Failing after 3s
Joomla: Repo Health / Scripts governance (pull_request) Successful in 2s
Joomla: Repo Health / Repository health (pull_request) Failing after 3s

- Replace Joomla-specific auto-release with Universal: Build & Release
- Add .moko-platform file (joomla) for platform detection
- Rename all workflows with Universal:/Joomla: prefixes
- Smart case/esac branching for joomla|dolibarr|generic platforms
- Add Dolibarr post-release version reset support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan Miller
2026-05-11 22:03:53 -05:00
parent 06c6ebb2b7
commit f8a3028f0f
14 changed files with 214 additions and 157 deletions
+112 -56
View File
@@ -6,35 +6,24 @@
# DEFGROUP: Gitea.Workflow
# INGROUP: MokoStandards.Release
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
# PATH: /templates/workflows/joomla/auto-release.yml.template
# VERSION: 04.06.00
# BRIEF: Joomla build & release — ZIP package, updates.xml, SHA-256 checksum
# PATH: /templates/workflows/universal/auto-release.yml.template
# VERSION: 05.00.00
# BRIEF: Universal build & release — detects platform from .moko-platform
#
# +========================================================================+
# | BUILD & RELEASE PIPELINE (JOOMLA) |
# | UNIVERSAL BUILD & RELEASE PIPELINE |
# +========================================================================+
# | |
# | Triggers on push to main (skips bot commits + [skip ci]): |
# | Reads .moko-platform (joomla|dolibarr|generic) to branch logic. |
# | |
# | Every push: |
# | 1. Read version from README.md |
# | 3. Set platform version (Joomla <version>) |
# | 4. Update [VERSION: XX.YY.ZZ] badges in markdown files |
# | 5. Write updates.xml (Joomla update server XML) |
# | 6. Create git tag vXX.YY.ZZ |
# | 7a. Patch: update existing Gitea Release for this minor |
# | 8. Build ZIP, upload asset, write SHA-256 to updates.xml |
# | |
# | Every version change: archives main -> version/XX.YY branch |
# | All patches release (including 00). Patch 00/01 = full pipeline. |
# | First release only (patch == 01): |
# | 7b. Create new Gitea Release |
# | |
# | GitHub mirror: stable/rc releases only (continue-on-error) |
# | Platform-specific: |
# | joomla: XML manifest, updates.xml, type-prefixed packages |
# | dolibarr: mod*.class.php, update.txt, dev version reset |
# | generic: README-only, no update stream |
# | |
# +========================================================================+
name: Build & Release
name: "Universal: Build & Release"
on:
pull_request:
@@ -85,6 +74,20 @@ jobs:
cd /tmp/mokostandards-api
composer install --no-dev --no-interaction --quiet
# -- PLATFORM DETECTION ---------------------------------------------------
- name: Detect platform
id: platform
run: |
PLATFORM=$(cat .moko-platform 2>/dev/null | tr -d '[:space:]')
[ -z "$PLATFORM" ] && PLATFORM="generic"
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
echo "Platform detected: ${PLATFORM}"
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
echo "manifest=${MANIFEST}" >> "$GITHUB_OUTPUT"
echo "mod_file=${MOD_FILE}" >> "$GITHUB_OUTPUT"
# -- STEP 1: Read version -----------------------------------------------
- name: "Step 1: Read version from README.md"
id: version
@@ -143,13 +146,26 @@ jobs:
# Update README.md
sed -i "s/VERSION:[[:space:]]*${CURRENT}/VERSION: ${VERSION}/" README.md
# Update manifest
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
[ -n "$MANIFEST_VER" ] && sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
# Update platform-specific manifest
PLATFORM="${{ steps.platform.outputs.platform }}"
MANIFEST="${{ steps.platform.outputs.manifest }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
case "$PLATFORM" in
joomla)
if [ -n "$MANIFEST" ]; then
MANIFEST_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" | head -1)
[ -n "$MANIFEST_VER" ] && sed -i "s|<version>${MANIFEST_VER}</version>|<version>${VERSION}</version>|" "$MANIFEST"
sed -i "s|<creationDate>[^<]*</creationDate>|<creationDate>${TODAY}</creationDate>|" "$MANIFEST"
fi
;;
dolibarr)
if [ -n "$MOD_FILE" ]; then
sed -i "s/\$this->version = '[^']*'/\$this->version = '${VERSION}'/" "$MOD_FILE"
fi
echo "${VERSION}" > update.txt
;;
*) ;;
esac
# Promote [Unreleased] section in CHANGELOG.md to new version
if [ -f "CHANGELOG.md" ] && grep -qi "Unreleased" CHANGELOG.md; then
@@ -202,7 +218,10 @@ jobs:
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
ERRORS=0
echo "## Pre-Release Sanity Checks (Joomla)" >> $GITHUB_STEP_SUMMARY
PLATFORM="${{ steps.platform.outputs.platform }}"
MANIFEST="${{ steps.platform.outputs.manifest }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
echo "## Pre-Release Sanity Checks (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# -- Version drift check (must pass before release) --------
@@ -244,29 +263,41 @@ jobs:
echo "- Source directory present" >> $GITHUB_STEP_SUMMARY
fi
# -- Joomla: manifest version drift --------
MANIFEST=$(find . -maxdepth 2 -name "*.xml" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
if [ -n "$MANIFEST" ]; then
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
fi
# -- Joomla: XML manifest existence --------
if [ -z "$MANIFEST" ]; then
echo "- No Joomla XML manifest found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest: \`${MANIFEST}\`" >> $GITHUB_STEP_SUMMARY
# -- Joomla: extension type check --------
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
fi
# -- Platform-specific checks --------
case "$PLATFORM" in
joomla)
if [ -n "$MANIFEST" ]; then
XML_VER=$(sed -n 's/.*<version>\([^<]*\)<\/version>.*/\1/p' "$MANIFEST" 2>/dev/null | head -1)
if [ -n "$XML_VER" ] && [ "$XML_VER" != "$VERSION" ]; then
echo "- Manifest drift: \`${XML_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Manifest version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
TYPE=$(sed -n 's/.*<extension[^>]*type="\([^"]*\)".*/\1/p' "$MANIFEST" 2>/dev/null)
echo "- Extension type: ${TYPE:-unknown}" >> $GITHUB_STEP_SUMMARY
else
echo "- No Joomla XML manifest (WaaS site)" >> $GITHUB_STEP_SUMMARY
fi ;;
dolibarr)
if [ -n "$MOD_FILE" ]; then
MOD_VER=$(sed -n "s/.*\\\$this->version = '\([^']*\)'.*/\1/p" "$MOD_FILE" 2>/dev/null | head -1)
if [ -n "$MOD_VER" ] && [ "$MOD_VER" != "$VERSION" ]; then
echo "- Module drift: \`${MOD_VER}\` != \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
else
echo "- Module version: \`${VERSION}\`" >> $GITHUB_STEP_SUMMARY
fi
else
echo "- No mod*.class.php found" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi
if [ ! -f "update.txt" ]; then
echo "- Missing update.txt" >> $GITHUB_STEP_SUMMARY
ERRORS=$((ERRORS+1))
fi ;;
*) echo "- Generic platform — no manifest checks" >> $GITHUB_STEP_SUMMARY ;;
esac
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$ERRORS" -gt 0 ]; then
@@ -319,7 +350,7 @@ jobs:
done
# -- STEP 5: Write updates.xml (Joomla update server) ---------------------
- name: "Step 5: Write updates.xml"
- name: "Step 5: Write update stream"
id: updates
if: >-
steps.version.outputs.skip != 'true' &&
@@ -567,7 +598,7 @@ jobs:
echo "Release created: ${RELEASE_NAME}" >> $GITHUB_STEP_SUMMARY
# -- STEP 8: Build Joomla install ZIP + SHA-256 checksum ------------------
- name: "Step 8: Build Joomla package and update checksum"
- name: "Step 8: Build package and update checksum"
if: >-
steps.version.outputs.skip != 'true'
run: |
@@ -741,7 +772,7 @@ jobs:
fi
fi
echo "### Joomla Packages" >> $GITHUB_STEP_SUMMARY
echo "### Packages" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Package | Size | SHA-256 |" >> $GITHUB_STEP_SUMMARY
echo "|---------|------|---------|" >> $GITHUB_STEP_SUMMARY
@@ -926,11 +957,35 @@ jobs:
echo "Dev branch reset from main (keeps dev ahead after release)" >> $GITHUB_STEP_SUMMARY
# -- Dolibarr post-release: Reset dev version -----------------------------
- name: "Dolibarr: Reset dev version"
if: >-
steps.version.outputs.skip != 'true' &&
steps.platform.outputs.platform == 'dolibarr' &&
steps.platform.outputs.mod_file != ''
continue-on-error: true
run: |
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
TOKEN="${{ secrets.GA_TOKEN }}"
MOD_FILE="${{ steps.platform.outputs.mod_file }}"
ENCODED_PATH=$(echo "$MOD_FILE" | sed 's|^\./||' | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))")
FILE_RESP=$(curl -sf -H "Authorization: token ${TOKEN}" "${API_BASE}/contents/${ENCODED_PATH}?ref=dev" 2>/dev/null || true)
FILE_SHA=$(echo "$FILE_RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
FILE_CONTENT=$(echo "$FILE_RESP" | python3 -c "import sys,json,base64; print(base64.b64decode(json.load(sys.stdin).get('content','')).decode())" 2>/dev/null || true)
if [ -n "$FILE_SHA" ] && [ -n "$FILE_CONTENT" ]; then
UPDATED=$(echo "$FILE_CONTENT" | sed "s/\$this->version = '[^']*'/\$this->version = 'development'/")
ENCODED=$(echo "$UPDATED" | base64 -w0)
curl -sf -X PUT -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/contents/${ENCODED_PATH}" \
-d "$(jq -n --arg content \"$ENCODED\" --arg sha \"$FILE_SHA\" --arg msg \"chore(version): reset dev version [skip ci]\" --arg branch \"dev\" '{content:$content,sha:$sha,message:$msg,branch:$branch}')" > /dev/null 2>&1 || true
fi
# -- Summary --------------------------------------------------------------
- name: Pipeline Summary
if: always()
run: |
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
PLATFORM="${{ steps.platform.outputs.platform }}"
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
@@ -938,10 +993,11 @@ jobs:
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (Joomla)" >> $GITHUB_STEP_SUMMARY
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
+1 -1
View File
@@ -22,7 +22,7 @@
# | |
# +========================================================================+
name: Cascade Main → Dev
name: "Universal: Cascade Main → Dev"
on:
push:
+1 -1
View File
@@ -12,7 +12,7 @@
# VERSION: 04.06.00
# BRIEF: CI workflow for Joomla extensions — lint, validate, test
name: Joomla Extension CI
name: "Joomla: Extension CI"
on:
pull_request:
+1 -1
View File
@@ -10,7 +10,7 @@
# VERSION: 01.00.00
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
name: Repository Cleanup
name: "Universal: Repository Cleanup"
on:
schedule:
+1 -1
View File
@@ -10,7 +10,7 @@
# VERSION: 04.07.00
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
name: Deploy to Dev (Manual)
name: "Universal: Deploy to Dev (Manual)"
on:
workflow_dispatch:
+1 -1
View File
@@ -22,7 +22,7 @@
# | |
# +========================================================================+
name: Secret Scanning
name: "Universal: Secret Scanning"
on:
pull_request:
+1 -1
View File
@@ -10,7 +10,7 @@
# VERSION: 01.00.00
# BRIEF: Push notifications via ntfy on release success or workflow failure
name: Notifications
name: "Universal: Notifications"
on:
workflow_run:
+90 -90
View File
@@ -1,90 +1,90 @@
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Enforces branch merge policy:
# feature/* → dev only
# fix/* → dev only
# hotfix/* → dev or main (emergency)
# dev → main only
# alpha/* → dev only
# beta/* → dev only
# rc/* → main only
name: Branch Policy Check
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
check-target:
name: Verify merge target
runs-on: ubuntu-latest
steps:
- name: Check branch policy
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
alpha/*|beta/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Pre-release branches must target 'dev', not '${BASE}'"
fi
;;
rc/*)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Release candidate branches must target 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo ""
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
# SPDX-License-Identifier: GPL-3.0-or-later
#
# Enforces branch merge policy:
# feature/* → dev only
# fix/* → dev only
# hotfix/* → dev or main (emergency)
# dev → main only
# alpha/* → dev only
# beta/* → dev only
# rc/* → main only
name: "Universal: Branch Policy Check"
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
check-target:
name: Verify merge target
runs-on: ubuntu-latest
steps:
- name: Check branch policy
run: |
HEAD="${{ github.head_ref }}"
BASE="${{ github.base_ref }}"
echo "PR: ${HEAD} → ${BASE}"
ALLOWED=true
REASON=""
case "$HEAD" in
feature/*|feat/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Feature branches must target 'dev', not '${BASE}'"
fi
;;
fix/*|bugfix/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Fix branches must target 'dev', not '${BASE}'"
fi
;;
hotfix/*)
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
fi
;;
alpha/*|beta/*)
if [ "$BASE" != "dev" ]; then
ALLOWED=false
REASON="Pre-release branches must target 'dev', not '${BASE}'"
fi
;;
rc/*)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Release candidate branches must target 'main', not '${BASE}'"
fi
;;
dev)
if [ "$BASE" != "main" ]; then
ALLOWED=false
REASON="Dev branch can only merge into 'main', not '${BASE}'"
fi
;;
esac
if [ "$ALLOWED" = false ]; then
echo "::error::${REASON}"
echo ""
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
exit 1
fi
echo "Branch policy: OK (${HEAD} → ${BASE})"
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
+1 -1
View File
@@ -10,7 +10,7 @@
# VERSION: 01.00.00
# BRIEF: PR gate — validates code quality and manifest before merge to main
name: PR Check
name: "Universal: PR Check"
on:
pull_request:
+1 -1
View File
@@ -10,7 +10,7 @@
# VERSION: 01.00.00
# BRIEF: Manual pre-release — builds dev/alpha/beta/rc packages from any branch
name: Pre-Release
name: "Joomla: Pre-Release"
on:
workflow_dispatch:
+1 -1
View File
@@ -14,7 +14,7 @@
# BRIEF: Enforces repository guardrails by validating release configuration, scripts governance, tooling availability, and core repository health artifacts.
# ============================================================================
name: Repo Health
name: "Joomla: Repo Health"
concurrency:
group: repo-health-${{ github.repository }}-${{ github.ref }}
+1 -1
View File
@@ -10,7 +10,7 @@
# VERSION: 01.00.00
# BRIEF: Dependency vulnerability scanning for composer and npm packages
name: Security Audit
name: "Universal: Security Audit"
on:
schedule:
+1 -1
View File
@@ -17,7 +17,7 @@
#
# Joomla filters by user's "Minimum Stability" setting.
name: Update Joomla Update Server XML Feed
name: "Joomla: Update Server"
on:
push:
+1
View File
@@ -0,0 +1 @@
joomla