Compare commits
53 Commits
development
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 537f4539c8 | |||
| 036f8b9877 | |||
| ba22067c56 | |||
| 2c0cbc5a13 | |||
| 6beea230a8 | |||
| cd76449f79 | |||
| cd0590cee4 | |||
| 05914c0c70 | |||
| 6b752babd3 | |||
| 466eb7da3c | |||
| 2dbb285fdf | |||
| 52d67f5fb1 | |||
| 42ca6325c7 | |||
| ee060243f5 | |||
| 9bd14d3547 | |||
| b75e7ccf10 | |||
| 8fd232c959 | |||
| fde6df7398 | |||
| ec8545c7d3 | |||
| 63e87a0c4d | |||
| 12143fc4b1 | |||
| abb9238ebe | |||
| d162f2317c | |||
| c02bb54759 | |||
| 4b682c5ebd | |||
| e0b4008ac7 | |||
| bd220a7d7c | |||
| 9d79830b02 | |||
| 8610aa5fcd | |||
| 821a3398a5 | |||
| 354081d7a5 | |||
| 1ad79be9b2 | |||
| 8fb6a84e81 | |||
| 7da249ea73 | |||
| 9d628412e0 | |||
| 516f7b4832 | |||
| d35660b4cf | |||
| be05c56d29 | |||
| 608ad43242 | |||
| f92b53d24e | |||
| 6d6840c68b | |||
| 485b0cf696 | |||
| a8805d16f1 | |||
| e4dad451b4 | |||
| 42d9de47d5 | |||
| 9753088798 | |||
| b01107d6e6 | |||
| fd7ccfb927 | |||
| 2fd22c6030 | |||
| 8054c6c80b | |||
| 7c90f05e9d | |||
| 648549ea66 | |||
| 52b2d89bc5 |
@@ -1,62 +0,0 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
|
||||||
* text=auto
|
|
||||||
|
|
||||||
# PHP files
|
|
||||||
*.php text eol=lf
|
|
||||||
|
|
||||||
# XML manifests
|
|
||||||
*.xml text eol=lf
|
|
||||||
|
|
||||||
# Language files
|
|
||||||
*.ini text eol=lf
|
|
||||||
|
|
||||||
# SQL files
|
|
||||||
*.sql text eol=lf
|
|
||||||
|
|
||||||
# Shell scripts
|
|
||||||
*.sh text eol=lf
|
|
||||||
|
|
||||||
# Markdown
|
|
||||||
*.md text eol=lf
|
|
||||||
|
|
||||||
# YAML
|
|
||||||
*.yml text eol=lf
|
|
||||||
*.yaml text eol=lf
|
|
||||||
|
|
||||||
# CSS/JS
|
|
||||||
*.css text eol=lf
|
|
||||||
*.js text eol=lf
|
|
||||||
|
|
||||||
# JSON
|
|
||||||
*.json text eol=lf
|
|
||||||
|
|
||||||
# Windows scripts
|
|
||||||
*.bat text eol=crlf
|
|
||||||
*.cmd text eol=crlf
|
|
||||||
*.ps1 text eol=crlf
|
|
||||||
|
|
||||||
# Binary files
|
|
||||||
*.zip binary
|
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
*.jpeg binary
|
|
||||||
*.gif binary
|
|
||||||
*.ico binary
|
|
||||||
*.webp binary
|
|
||||||
*.woff binary
|
|
||||||
*.woff2 binary
|
|
||||||
*.ttf binary
|
|
||||||
*.eot binary
|
|
||||||
|
|
||||||
# Export ignore (not included in archives)
|
|
||||||
.mokogitea/ export-ignore
|
|
||||||
.editorconfig export-ignore
|
|
||||||
.gitattributes export-ignore
|
|
||||||
.gitignore export-ignore
|
|
||||||
.gitmessage export-ignore
|
|
||||||
CLAUDE.md export-ignore
|
|
||||||
CONTRIBUTING.md export-ignore
|
|
||||||
CODE_OF_CONDUCT.md export-ignore
|
|
||||||
Makefile export-ignore
|
|
||||||
composer.json export-ignore
|
|
||||||
phpstan.neon export-ignore
|
|
||||||
-204
@@ -1,204 +0,0 @@
|
|||||||
# ============================================================
|
|
||||||
# Local task tracking (not version controlled)
|
|
||||||
# ============================================================
|
|
||||||
TODO.md
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Environment and secrets
|
|
||||||
# ============================================================
|
|
||||||
.env
|
|
||||||
.env.local
|
|
||||||
.env.*.local
|
|
||||||
*.local.php
|
|
||||||
*.secret.php
|
|
||||||
configuration.php
|
|
||||||
configuration.*.php
|
|
||||||
configuration.local.php
|
|
||||||
conf/conf.php
|
|
||||||
conf/conf*.php
|
|
||||||
secrets/
|
|
||||||
*.secrets.*
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Logs, dumps and databases
|
|
||||||
# ============================================================
|
|
||||||
*.db
|
|
||||||
*.db-journal
|
|
||||||
*.dump
|
|
||||||
*.log
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# OS / Editor / IDE cruft
|
|
||||||
# ============================================================
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
desktop.ini
|
|
||||||
Thumbs.db:encryptable
|
|
||||||
ehthumbs.db
|
|
||||||
ehthumbs_vista.db
|
|
||||||
$RECYCLE.BIN/
|
|
||||||
System Volume Information/
|
|
||||||
*.lnk
|
|
||||||
Icon?
|
|
||||||
.idea/
|
|
||||||
.settings/
|
|
||||||
.claude/
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/settings.json.example
|
|
||||||
!.vscode/extensions.json
|
|
||||||
*.code-workspace
|
|
||||||
*.sublime*
|
|
||||||
.project
|
|
||||||
.buildpath
|
|
||||||
.classpath
|
|
||||||
*.bak
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*.tmp
|
|
||||||
*.old
|
|
||||||
*.orig
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Dev scripts and scratch
|
|
||||||
# ============================================================
|
|
||||||
TODO.md
|
|
||||||
todo*
|
|
||||||
*ffs*
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# SFTP / sync tools
|
|
||||||
# ============================================================
|
|
||||||
sftp-config*.json
|
|
||||||
sftp-config.json.template
|
|
||||||
sftp-settings.json
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Sublime SFTP / FTP sync
|
|
||||||
# ============================================================
|
|
||||||
*.sublime-project
|
|
||||||
*.sublime-workspace
|
|
||||||
*.sublime-settings
|
|
||||||
.libsass.json
|
|
||||||
*.ffs*
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Replit / cloud IDE
|
|
||||||
# ============================================================
|
|
||||||
.replit
|
|
||||||
replit.md
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Archives / release artifacts
|
|
||||||
# ============================================================
|
|
||||||
*.7z
|
|
||||||
*.rar
|
|
||||||
*.tar
|
|
||||||
*.tar.gz
|
|
||||||
*.tgz
|
|
||||||
*.zip
|
|
||||||
artifacts/
|
|
||||||
release/
|
|
||||||
releases/
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Build outputs and site generators
|
|
||||||
# ============================================================
|
|
||||||
.mkdocs-build/
|
|
||||||
.cache/
|
|
||||||
.parcel-cache/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
out/
|
|
||||||
site/
|
|
||||||
*.map
|
|
||||||
*.css.map
|
|
||||||
*.js.map
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# CI / test artifacts
|
|
||||||
# ============================================================
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
coverage/
|
|
||||||
coverage.xml
|
|
||||||
htmlcov/
|
|
||||||
junit.xml
|
|
||||||
reports/
|
|
||||||
test-results/
|
|
||||||
tests/_output/
|
|
||||||
.github/local/
|
|
||||||
.github/workflows/*.log
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Node / JavaScript
|
|
||||||
# ============================================================
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
.pnpm-store/
|
|
||||||
.yarn/
|
|
||||||
.npmrc
|
|
||||||
.eslintcache
|
|
||||||
package-lock.json
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# PHP / Composer tooling
|
|
||||||
# ============================================================
|
|
||||||
vendor/
|
|
||||||
!src/media/vendor/
|
|
||||||
composer.lock
|
|
||||||
*.phar
|
|
||||||
codeception.phar
|
|
||||||
.phpunit.result.cache
|
|
||||||
.php_cs.cache
|
|
||||||
.php-cs-fixer.cache
|
|
||||||
.phpstan.cache
|
|
||||||
.phplint-cache
|
|
||||||
phpmd-cache/
|
|
||||||
.psalm/
|
|
||||||
.rector/
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# Python
|
|
||||||
# ============================================================
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*.pyc
|
|
||||||
*$py.class
|
|
||||||
*.so
|
|
||||||
.Python
|
|
||||||
.eggs/
|
|
||||||
*.egg
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
MANIFEST
|
|
||||||
develop-eggs/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
ENV/
|
|
||||||
env/
|
|
||||||
.venv/
|
|
||||||
venv/
|
|
||||||
.pytest_cache/
|
|
||||||
.mypy_cache/
|
|
||||||
.ruff_cache/
|
|
||||||
.pyright/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
*.cover
|
|
||||||
*.coverage
|
|
||||||
hypothesis/
|
|
||||||
|
|
||||||
profile.ps1
|
|
||||||
.mcp.json
|
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.gitea/workflows/branch-protection.yml
|
||||||
|
# BRIEF: Apply standardised branch protection rules to all governed repositories
|
||||||
|
#
|
||||||
|
# +========================================================================+
|
||||||
|
# | BRANCH PROTECTION SETUP |
|
||||||
|
# +========================================================================+
|
||||||
|
# | |
|
||||||
|
# | Applies protection rules for: main, dev, rc, beta, alpha |
|
||||||
|
# | |
|
||||||
|
# | main — Require PR, block rejected reviews, no force push |
|
||||||
|
# | dev — Allow push, no force push, no delete |
|
||||||
|
# | rc — Allow push, no force push, no delete |
|
||||||
|
# | beta — Allow push, no force push, no delete |
|
||||||
|
# | alpha — Allow push, no force push, no delete |
|
||||||
|
# | |
|
||||||
|
# | jmiller has override authority on all branches. |
|
||||||
|
# | |
|
||||||
|
# +========================================================================+
|
||||||
|
|
||||||
|
name: Branch Protection Setup
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 2 * * 1' # Weekly Monday 02:00 UTC
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dry_run:
|
||||||
|
description: 'Preview mode (no changes)'
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
repos:
|
||||||
|
description: 'Comma-separated repo names (empty = all governed repos)'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ''
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://git.mokoconsulting.tech
|
||||||
|
GITEA_ORG: MokoConsulting
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
protect:
|
||||||
|
name: Apply Branch Protection Rules
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Determine target repos
|
||||||
|
id: repos
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1"
|
||||||
|
|
||||||
|
# Platform/standards/infra repos to exclude
|
||||||
|
EXCLUDE="gitea-org-config org-profile gitea-private .mokogitea-private MokoStandards moko-platform MokoTesting"
|
||||||
|
EXCLUDE="$EXCLUDE MokoStandards-Template-Client MokoStandards-Template-Dolibarr MokoStandards-Template-Generic MokoStandards-Template-Joomla MokoDoliProjTemplate"
|
||||||
|
|
||||||
|
if [ -n "${{ inputs.repos }}" ]; then
|
||||||
|
# User-specified repos
|
||||||
|
REPOS=$(echo "${{ inputs.repos }}" | tr ',' ' ')
|
||||||
|
else
|
||||||
|
# Fetch all org repos
|
||||||
|
PAGE=1
|
||||||
|
REPOS=""
|
||||||
|
while true; do
|
||||||
|
BATCH=$(curl -sS \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/orgs/${GITEA_ORG}/repos?page=${PAGE}&limit=50" \
|
||||||
|
| jq -r '.[].name // empty')
|
||||||
|
[ -z "$BATCH" ] && break
|
||||||
|
REPOS="$REPOS $BATCH"
|
||||||
|
PAGE=$((PAGE + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
# Filter out excluded repos
|
||||||
|
FILTERED=""
|
||||||
|
for REPO in $REPOS; do
|
||||||
|
SKIP=false
|
||||||
|
for EX in $EXCLUDE; do
|
||||||
|
if [ "$REPO" = "$EX" ]; then
|
||||||
|
SKIP=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$SKIP" = "false" ]; then
|
||||||
|
FILTERED="$FILTERED $REPO"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
REPOS="$FILTERED"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "repos=$REPOS" >> "$GITHUB_OUTPUT"
|
||||||
|
COUNT=$(echo "$REPOS" | wc -w)
|
||||||
|
echo "📋 Target repos (${COUNT}): $REPOS"
|
||||||
|
|
||||||
|
- name: Apply protection rules
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
|
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
||||||
|
run: |
|
||||||
|
API="${GITEA_URL}/api/v1"
|
||||||
|
REPOS="${{ steps.repos.outputs.repos }}"
|
||||||
|
|
||||||
|
SUCCESS=0
|
||||||
|
FAILED=0
|
||||||
|
SKIPPED=0
|
||||||
|
|
||||||
|
# ── Rule definitions ──────────────────────────────────────
|
||||||
|
# Only the CI bot (jmiller token) can push directly.
|
||||||
|
# All human contributors must use PRs.
|
||||||
|
# Force push disabled on all branches.
|
||||||
|
|
||||||
|
RULE_MAIN='{
|
||||||
|
"rule_name": "main",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"dismiss_stale_approvals": true,
|
||||||
|
"block_on_rejected_reviews": true,
|
||||||
|
"block_on_outdated_branch": false,
|
||||||
|
"priority": 1
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_DEV='{
|
||||||
|
"rule_name": "dev",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 2
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_RC='{
|
||||||
|
"rule_name": "rc",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 3
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_BETA='{
|
||||||
|
"rule_name": "beta",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 4
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULE_ALPHA='{
|
||||||
|
"rule_name": "alpha",
|
||||||
|
"enable_push": true,
|
||||||
|
"enable_push_whitelist": true,
|
||||||
|
"push_whitelist_usernames": ["jmiller"],
|
||||||
|
"enable_force_push": false,
|
||||||
|
"enable_force_push_allowlist": false,
|
||||||
|
"force_push_allowlist_usernames": [],
|
||||||
|
"enable_merge_whitelist": false,
|
||||||
|
"required_approvals": 0,
|
||||||
|
"block_on_rejected_reviews": false,
|
||||||
|
"priority": 5
|
||||||
|
}'
|
||||||
|
|
||||||
|
RULES=("$RULE_MAIN" "$RULE_DEV" "$RULE_RC" "$RULE_BETA" "$RULE_ALPHA")
|
||||||
|
RULE_NAMES=("main" "dev" "rc" "beta" "alpha")
|
||||||
|
|
||||||
|
# ── Apply rules to each repo ──────────────────────────────
|
||||||
|
for REPO in $REPOS; do
|
||||||
|
echo ""
|
||||||
|
echo "═══ ${REPO} ═══"
|
||||||
|
|
||||||
|
for i in "${!RULES[@]}"; do
|
||||||
|
RULE="${RULES[$i]}"
|
||||||
|
NAME="${RULE_NAMES[$i]}"
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" = "true" ]; then
|
||||||
|
echo " [DRY RUN] Would apply rule: ${NAME}"
|
||||||
|
SKIPPED=$((SKIPPED + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Delete existing rule if present (idempotent recreate)
|
||||||
|
ENCODED_NAME=$(echo "$NAME" | sed 's|/|%2F|g')
|
||||||
|
curl -sS -o /dev/null -w "" \
|
||||||
|
-X DELETE \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections/${ENCODED_NAME}" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Create rule
|
||||||
|
RESPONSE=$(curl -sS -w "\n%{http_code}" \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: token ${GA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$RULE" \
|
||||||
|
"${API}/repos/${GITEA_ORG}/${REPO}/branch_protections")
|
||||||
|
|
||||||
|
HTTP=$(echo "$RESPONSE" | tail -1)
|
||||||
|
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
if [ "$HTTP" = "201" ]; then
|
||||||
|
echo " ✅ ${NAME}"
|
||||||
|
SUCCESS=$((SUCCESS + 1))
|
||||||
|
else
|
||||||
|
echo " ❌ ${NAME} (HTTP ${HTTP}): $(echo "$BODY" | jq -r '.message // .' 2>/dev/null | head -1)"
|
||||||
|
FAILED=$((FAILED + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
echo " ✅ Success: ${SUCCESS}"
|
||||||
|
echo " ❌ Failed: ${FAILED}"
|
||||||
|
echo " ⏭️ Skipped: ${SKIPPED}"
|
||||||
|
echo "════════════════════════════════════════"
|
||||||
|
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
echo "::warning::${FAILED} rule(s) failed to apply"
|
||||||
|
fi
|
||||||
@@ -5,11 +5,9 @@
|
|||||||
-->
|
-->
|
||||||
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
<moko-platform xmlns="https://standards.mokoconsulting.tech/moko-platform/1.0" schema-version="1.0">
|
||||||
<identity>
|
<identity>
|
||||||
<name>MokoJoomOpenGraph</name>
|
<name>MokoOpenGraph</name>
|
||||||
<display-name>Package - MokoJoomOpenGraph</display-name>
|
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>Open Graph, SEO meta tags, and social sharing image management for Joomla articles and menu items</description>
|
<description>Open Graph, SEO meta tags, and social sharing image management for Joomla articles and menu items</description>
|
||||||
<version>01.00.01</version>
|
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -1,67 +1,66 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /.mokogitea/workflows/auto-bump.yml
|
# PATH: /.mokogitea/workflows/auto-bump.yml
|
||||||
# VERSION: 09.02.00
|
# VERSION: 09.02.00
|
||||||
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
# BRIEF: Auto patch-bump version on every push to dev (skips merge commits)
|
||||||
|
|
||||||
name: "Universal: Auto Version Bump"
|
name: "Universal: Auto Version Bump"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
- rc
|
- rc
|
||||||
- 'feature/**'
|
- 'feature/**'
|
||||||
- 'patch/**'
|
- 'patch/**'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump:
|
bump:
|
||||||
name: Version Bump
|
name: Version Bump
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
!contains(github.event.head_commit.message, '[skip ci]') &&
|
||||||
!contains(github.event.head_commit.message, '[skip bump]') &&
|
!contains(github.event.head_commit.message, '[skip bump]') &&
|
||||||
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
!startsWith(github.event.head_commit.message, 'Merge pull request')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
run: |
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
if ! command -v composer &> /dev/null; then
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
run: |
|
fi
|
||||||
if ! command -v composer &> /dev/null; then
|
if [ -d "/opt/moko-platform/cli" ]; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
echo "MOKO_CLI=/opt/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
fi
|
else
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
git clone --depth 1 --branch main --quiet \
|
||||||
rm -rf /tmp/moko-platform-api
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/moko-platform.git" \
|
||||||
git clone --depth 1 --branch main --quiet \
|
/tmp/moko-platform-api
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
/tmp/moko-platform-api
|
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
fi
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
|
- name: Bump version
|
||||||
- name: Bump version
|
run: |
|
||||||
run: |
|
php ${MOKO_CLI}/version_auto_bump.php \
|
||||||
php ${MOKO_CLI}/version_auto_bump.php \
|
--path . --branch "${GITHUB_REF_NAME}" \
|
||||||
--path . --branch "${GITHUB_REF_NAME}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
--repo-url "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
|
|||||||
@@ -1,270 +1,285 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/auto-release.yml.template
|
# PATH: /templates/workflows/universal/auto-release.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: Universal build & release � detects platform from manifest.xml
|
# BRIEF: Universal build & release � detects platform from manifest.xml
|
||||||
#
|
#
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
# | UNIVERSAL BUILD & RELEASE PIPELINE |
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
# | |
|
# | |
|
||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
# +========================================================================+
|
# +========================================================================+
|
||||||
|
|
||||||
name: "Universal: Build & Release"
|
name: "Universal: Build & Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, closed]
|
types: [opened, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
action:
|
action:
|
||||||
description: 'Action to perform'
|
description: 'Action to perform'
|
||||||
required: false
|
required: false
|
||||||
type: choice
|
type: choice
|
||||||
default: release
|
default: release
|
||||||
options:
|
options:
|
||||||
- release
|
- release
|
||||||
- promote-rc
|
- promote-rc
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
# ── PR Opened → Rename branch to RC and build RC release ─────────────────────
|
||||||
promote-rc:
|
promote-rc:
|
||||||
name: Promote to RC
|
name: Promote to RC
|
||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
(github.event.action == 'opened' && github.event.pull_request.merged != true) ||
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
(github.event_name == 'workflow_dispatch' && inputs.action == 'promote-rc')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if ! command -v composer &> /dev/null; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
rm -rf /tmp/moko-platform-api
|
rm -rf /tmp/moko-platform-api
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
/tmp/moko-platform-api
|
/tmp/moko-platform-api
|
||||||
cd /tmp/moko-platform-api
|
cd /tmp/moko-platform-api
|
||||||
composer install --no-dev --no-interaction --quiet
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/branch_rename.php \
|
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
--pr "${{ github.event.pull_request.number }}"
|
--pr "${{ github.event.pull_request.number }}"
|
||||||
|
|
||||||
- name: Checkout rc and configure git
|
- name: Checkout rc and configure git
|
||||||
run: |
|
run: |
|
||||||
git fetch origin rc
|
git fetch origin rc
|
||||||
git checkout rc
|
git checkout rc
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
run: |
|
run: |
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
--skip-update-stream
|
||||||
- name: Summary
|
|
||||||
if: always()
|
- name: Summary
|
||||||
run: |
|
if: always()
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
run: |
|
||||||
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Branch renamed to rc, minor bump, RC release built (updates.xml managed by Gitea Pages)" >> $GITHUB_STEP_SUMMARY
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
|
||||||
release:
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
name: Build & Release Pipeline
|
release:
|
||||||
runs-on: release
|
name: Build & Release Pipeline
|
||||||
if: >-
|
runs-on: release
|
||||||
github.event.pull_request.merged == true ||
|
if: >-
|
||||||
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
github.event.pull_request.merged == true ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.action != 'promote-rc')
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
steps:
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
- name: Checkout repository
|
||||||
with:
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
with:
|
||||||
fetch-depth: 0
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
- name: Configure git for bot pushes
|
|
||||||
run: |
|
- name: Configure git for bot pushes
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
run: |
|
||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
- name: Setup moko-platform tools
|
|
||||||
env:
|
- name: Check for merge conflict markers
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
run: |
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
if [ -n "$CONFLICTS" ]; then
|
||||||
run: |
|
echo "::error::Merge conflict markers found — aborting release"
|
||||||
# Ensure PHP + Composer are available
|
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
||||||
if ! command -v composer &> /dev/null; then
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
exit 1
|
||||||
rm -rf /tmp/moko-platform-api
|
fi
|
||||||
git clone --depth 1 --branch main --quiet \
|
echo "No conflict markers found"
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
- name: Setup moko-platform tools
|
||||||
cd /tmp/moko-platform-api
|
env:
|
||||||
composer install --no-dev --no-interaction --quiet
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
- name: "Publish stable release"
|
run: |
|
||||||
run: |
|
# Ensure PHP + Composer are available
|
||||||
php /tmp/moko-platform-api/cli/release_publish.php \
|
if ! command -v composer &> /dev/null; then
|
||||||
--path . --stability stable --bump minor --branch main \
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
rm -rf /tmp/moko-platform-api
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
git clone --depth 1 --branch main --quiet \
|
||||||
if: >-
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
steps.version.outputs.skip != 'true' &&
|
/tmp/moko-platform-api
|
||||||
secrets.GH_MIRROR_TOKEN != ''
|
cd /tmp/moko-platform-api
|
||||||
continue-on-error: true
|
composer install --no-dev --no-interaction --quiet
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
- name: "Publish stable release"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
php /tmp/moko-platform-api/cli/release_mirror.php \
|
--path . --stability stable --bump minor --branch main \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--skip-update-stream
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
|
||||||
--branch main 2>&1 || true
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
|
if: >-
|
||||||
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
steps.version.outputs.skip != 'true' &&
|
||||||
- name: "Step 10: Push main to GitHub mirror"
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
if: >-
|
continue-on-error: true
|
||||||
steps.version.outputs.skip != 'true' &&
|
run: |
|
||||||
secrets.GH_MIRROR_TOKEN != ''
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
continue-on-error: true
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
run: |
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||||
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
git fetch origin main --depth=1
|
--branch main 2>&1 || true
|
||||||
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
echo "GitHub mirror updated" >> $GITHUB_STEP_SUMMARY
|
||||||
&& echo "main branch pushed to GitHub mirror" \
|
|
||||||
|| echo "WARNING: GitHub mirror push failed"
|
# -- STEP 10: Sync main branch to GitHub mirror ----------------------------
|
||||||
|
- name: "Step 10: Push main to GitHub mirror"
|
||||||
- name: "Step 11: Delete rc branch and recreate dev from main"
|
if: >-
|
||||||
if: steps.version.outputs.skip != 'true'
|
steps.version.outputs.skip != 'true' &&
|
||||||
continue-on-error: true
|
secrets.GH_MIRROR_TOKEN != ''
|
||||||
run: |
|
continue-on-error: true
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
run: |
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
|
GH_ORG=$(echo "$GH_REPO" | cut -d/ -f1)
|
||||||
# Delete rc branch (ephemeral — created by promote-rc)
|
GH_NAME=$(echo "$GH_REPO" | cut -d/ -f2)
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
git remote add github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git" 2>/dev/null || \
|
||||||
"${API_BASE}/branches/rc" 2>/dev/null \
|
git remote set-url github "https://x-access-token:${{ secrets.GH_MIRROR_TOKEN }}@github.com/${GH_ORG}/${GH_NAME}.git"
|
||||||
&& echo "Deleted rc branch" || echo "rc branch not found"
|
git fetch origin main --depth=1
|
||||||
|
git push github origin/main:refs/heads/main --force 2>/dev/null \
|
||||||
# Delete dev branch
|
&& echo "main branch pushed to GitHub mirror" \
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
|| echo "WARNING: GitHub mirror push failed"
|
||||||
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
|
||||||
|
- name: "Step 11: Delete rc branch and recreate dev from main"
|
||||||
# Recreate dev from main (now includes version bump + changelog promotion)
|
if: steps.version.outputs.skip != 'true'
|
||||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
continue-on-error: true
|
||||||
-H "Content-Type: application/json" \
|
run: |
|
||||||
"${API_BASE}/branches" \
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
# Delete rc branch (ephemeral — created by promote-rc)
|
||||||
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
- name: "Step 12: Create version branch from main"
|
"${API_BASE}/branches/rc" 2>/dev/null \
|
||||||
if: steps.version.outputs.skip != 'true'
|
&& echo "Deleted rc branch" || echo "rc branch not found"
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
# Delete dev branch
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
"${API_BASE}/branches/dev" 2>/dev/null && echo "Deleted dev branch"
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
||||||
BRANCH_NAME="version/${VERSION}"
|
# Recreate dev from main (now includes version bump + changelog promotion)
|
||||||
MAIN_SHA=$(git rev-parse HEAD)
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
# Delete old version branch if it exists (same version re-release)
|
"${API_BASE}/branches" \
|
||||||
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
-d '{"new_branch_name":"dev","old_branch_name":"main"}' 2>/dev/null && echo "Recreated dev from main"
|
||||||
|
|
||||||
# Create version/XX.YY.ZZ from main
|
echo "Pre-release branches cleaned, dev reset from main" >> $GITHUB_STEP_SUMMARY
|
||||||
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
|
||||||
|
- name: "Step 12: Create version branch from main"
|
||||||
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
if: steps.version.outputs.skip != 'true'
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
# -- Dolibarr post-release: Reset dev version -----------------------------
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
- name: "Post-release: Reset dev version"
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
if: steps.version.outputs.skip != 'true'
|
BRANCH_NAME="version/${VERSION}"
|
||||||
continue-on-error: true
|
MAIN_SHA=$(git rev-parse HEAD)
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
# Delete old version branch if it exists (same version re-release)
|
||||||
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
curl -sf -X DELETE -H "Authorization: token ${TOKEN}" "${API_BASE}/branches/${BRANCH_NAME}" 2>/dev/null && echo "Deleted old ${BRANCH_NAME}"
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
|
||||||
--branch dev --path . 2>&1 || true
|
# Create version/XX.YY.ZZ from main
|
||||||
|
curl -sf -X POST -H "Authorization: token ${TOKEN}" -H "Content-Type: application/json" "${API_BASE}/branches" -d "{\"new_branch_name\":\"${BRANCH_NAME}\",\"old_branch_name\":\"main\"}" 2>/dev/null && echo "Created ${BRANCH_NAME} from main (${MAIN_SHA})" || echo "WARNING: ${BRANCH_NAME} creation failed"
|
||||||
# -- Summary --------------------------------------------------------------
|
|
||||||
- name: Pipeline Summary
|
echo "Version branch created: ${BRANCH_NAME} (${MAIN_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
# -- Dolibarr post-release: Reset dev version -----------------------------
|
||||||
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
- name: "Post-release: Reset dev version"
|
||||||
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
if: steps.version.outputs.skip != 'true'
|
||||||
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
continue-on-error: true
|
||||||
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
run: |
|
||||||
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
else
|
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
--branch dev --path . 2>&1 || true
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
# -- Summary --------------------------------------------------------------
|
||||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
- name: Pipeline Summary
|
||||||
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
if: always()
|
||||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
run: |
|
||||||
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
VERSION="${{ steps.bump.outputs.version || steps.version.outputs.version }}"
|
||||||
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
if [ "${{ steps.version.outputs.skip }}" = "true" ]; then
|
||||||
fi
|
echo "## Release Skipped" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "No VERSION in README.md" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "${{ steps.check.outputs.already_released }}" = "true" ]; then
|
||||||
|
echo "## Already Released — ${VERSION}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "## Build & Release Complete (${PLATFORM})" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Step | Result |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Platform | \`${PLATFORM}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Branch | \`${{ steps.version.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Tag | \`${{ steps.version.outputs.tag }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Release | [View](${GITEA_URL}/${GITEA_ORG}/${GITEA_REPO}/releases/tag/${{ steps.version.outputs.tag }}) |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /.mokogitea/workflows/branch-cleanup.yml
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Delete feature branches after PR merge
|
||||||
|
|
||||||
|
name: "Branch Cleanup"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cleanup:
|
||||||
|
name: Delete merged branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true &&
|
||||||
|
github.event.pull_request.head.ref != 'dev' &&
|
||||||
|
github.event.pull_request.head.ref != 'main'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Delete source branch
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.event.pull_request.head.ref }}"
|
||||||
|
API="${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}/api/v1/repos/${{ github.repository }}/branches"
|
||||||
|
ENCODED=$(php -r "echo rawurlencode('${BRANCH}');")
|
||||||
|
|
||||||
|
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" -X DELETE \
|
||||||
|
-H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API}/${ENCODED}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ "$STATUS" = "204" ]; then
|
||||||
|
echo "Deleted branch: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
elif [ "$STATUS" = "404" ]; then
|
||||||
|
echo "Branch already deleted: ${BRANCH}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "::warning::Failed to delete branch ${BRANCH} (HTTP ${STATUS})"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
||||||
|
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
||||||
|
# have different version numbers in templateDetails.xml / manifest.xml.
|
||||||
|
name: "Cascade Main → Dev (DISABLED)"
|
||||||
|
on: workflow_dispatch
|
||||||
|
jobs:
|
||||||
|
noop:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
||||||
@@ -43,9 +43,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Clone MokoStandards
|
- name: Clone MokoStandards
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || secrets.MOKOGITEA_TOKEN || github.token }}
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN || secrets.GA_TOKEN || github.token }}
|
||||||
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
MOKO_CLONE_HOST: ${{ secrets.GA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
run: |
|
run: |
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -124,13 +124,8 @@ jobs:
|
|||||||
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
|
echo "Manifest is well-formed XML." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check required tags
|
# Check required tags: name, version, author, namespace (Joomla 5+)
|
||||||
REQUIRED_TAGS="name version author"
|
for TAG in name version author namespace; do
|
||||||
# namespace is only required for non-package extensions
|
|
||||||
if ! grep -q 'type="package"' "$MANIFEST" 2>/dev/null; then
|
|
||||||
REQUIRED_TAGS="$REQUIRED_TAGS namespace"
|
|
||||||
fi
|
|
||||||
for TAG in $REQUIRED_TAGS; do
|
|
||||||
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
|
if ! grep -q "<${TAG}>" "$MANIFEST" 2>/dev/null; then
|
||||||
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
echo "Missing required tag: \`<${TAG}>\`" >> $GITHUB_STEP_SUMMARY
|
||||||
ERRORS=$((ERRORS + 1))
|
ERRORS=$((ERRORS + 1))
|
||||||
@@ -351,7 +346,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install \
|
composer install \
|
||||||
@@ -396,7 +391,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
env:
|
env:
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN || github.token }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GA_TOKEN || github.token }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f "composer.json" ]; then
|
if [ -f "composer.json" ]; then
|
||||||
composer install --no-interaction --prefer-dist --optimize-autoloader
|
composer install --no-interaction --prefer-dist --optimize-autoloader
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Maintenance
|
# INGROUP: MokoStandards.Maintenance
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.gitea/workflows/cleanup.yml
|
# PATH: /.gitea/workflows/cleanup.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
# BRIEF: Scheduled cleanup — delete merged branches and old workflow runs
|
||||||
@@ -33,17 +33,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.GA_TOKEN }}
|
||||||
|
|
||||||
- name: Delete merged branches
|
- name: Delete merged branches
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Merged Branch Cleanup ==="
|
echo "=== Merged Branch Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
|
||||||
# List branches via API
|
# List branches via API
|
||||||
BRANCHES=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
BRANCHES=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches?limit=50" | jq -r '.[].name')
|
"${API}/branches?limit=50" | jq -r '.[].name')
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
# Check if branch is merged into main
|
# Check if branch is merged into main
|
||||||
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
if git merge-base --is-ancestor "origin/${BRANCH}" origin/main 2>/dev/null; then
|
||||||
echo " Deleting merged branch: ${BRANCH}"
|
echo " Deleting merged branch: ${BRANCH}"
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
"${API}/branches/${BRANCH}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
fi
|
fi
|
||||||
@@ -66,20 +66,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Clean old workflow runs
|
- name: Clean old workflow runs
|
||||||
env:
|
env:
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
GA_TOKEN: ${{ secrets.GA_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
echo "=== Workflow Run Cleanup ==="
|
echo "=== Workflow Run Cleanup ==="
|
||||||
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
CUTOFF=$(date -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -v-30d +%Y-%m-%dT%H:%M:%SZ)
|
||||||
|
|
||||||
# Get old completed runs
|
# Get old completed runs
|
||||||
RUNS=$(curl -sS -H "Authorization: token ${GITEA_TOKEN}" \
|
RUNS=$(curl -sS -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs?status=completed&limit=50" | \
|
"${API}/actions/runs?status=completed&limit=50" | \
|
||||||
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
jq -r ".workflow_runs[] | select(.created_at < \"${CUTOFF}\") | .id" 2>/dev/null)
|
||||||
|
|
||||||
DELETED=0
|
DELETED=0
|
||||||
for RUN_ID in $RUNS; do
|
for RUN_ID in $RUNS; do
|
||||||
curl -sS -X DELETE -H "Authorization: token ${GITEA_TOKEN}" \
|
curl -sS -X DELETE -H "Authorization: token ${GA_TOKEN}" \
|
||||||
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
"${API}/actions/runs/${RUN_ID}" 2>/dev/null || true
|
||||||
DELETED=$((DELETED + 1))
|
DELETED=$((DELETED + 1))
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Security
|
# INGROUP: MokoStandards.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/MokoStandards-API
|
||||||
# PATH: /templates/workflows/gitleaks.yml.template
|
# PATH: /templates/workflows/gitleaks.yml.template
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
# BRIEF: Secret scanning — detect leaked credentials, API keys, and tokens
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# VERSION: 01.00.00
|
||||||
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
|
name: "Universal: Issue Branch"
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-branch:
|
||||||
|
name: Create feature branch
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create branch and comment
|
||||||
|
run: |
|
||||||
|
TOKEN="${{ secrets.GA_TOKEN }}"
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${{ github.repository }}"
|
||||||
|
ISSUE_NUM="${{ github.event.issue.number }}"
|
||||||
|
ISSUE_TITLE="${{ github.event.issue.title }}"
|
||||||
|
|
||||||
|
# Build slug from title: lowercase, replace non-alnum with dash, trim
|
||||||
|
SLUG=$(echo "${ISSUE_TITLE}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-40)
|
||||||
|
BRANCH="feature/${ISSUE_NUM}-${SLUG}"
|
||||||
|
|
||||||
|
# Check dev branch exists
|
||||||
|
DEV_EXISTS=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
"${API}/branches/dev" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "${DEV_EXISTS}" != "200" ]; then
|
||||||
|
echo "No dev branch -- skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create branch from dev
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/branches" \
|
||||||
|
-d "{\"new_branch_name\":\"${BRANCH}\",\"old_branch_name\":\"dev\"}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "${HTTP}" = "201" ]; then
|
||||||
|
echo "Created branch: ${BRANCH}"
|
||||||
|
|
||||||
|
# Comment on issue with branch link
|
||||||
|
REPO_URL="${GITEA_URL}/${{ github.repository }}"
|
||||||
|
BODY="Branch created: [\`${BRANCH}\`](${REPO_URL}/src/branch/${BRANCH})\n\n\`\`\`bash\ngit fetch origin\ngit checkout ${BRANCH}\n\`\`\`"
|
||||||
|
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/comments" \
|
||||||
|
-d "{\"body\":\"${BODY}\"}" > /dev/null 2>&1
|
||||||
|
|
||||||
|
echo "Commented on issue #${ISSUE_NUM}"
|
||||||
|
else
|
||||||
|
echo "Failed to create branch (HTTP ${HTTP}) -- may already exist"
|
||||||
|
fi
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Notifications
|
# INGROUP: MokoStandards.Notifications
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.gitea/workflows/notify.yml
|
# PATH: /.gitea/workflows/notify.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
# BRIEF: Push notifications via ntfy on release success or workflow failure
|
||||||
@@ -18,6 +18,7 @@ on:
|
|||||||
- "Joomla Build & Release"
|
- "Joomla Build & Release"
|
||||||
- "Joomla Extension CI"
|
- "Joomla Extension CI"
|
||||||
- "Deploy"
|
- "Deploy"
|
||||||
|
- "Cascade Main → Dev"
|
||||||
types:
|
types:
|
||||||
- completed
|
- completed
|
||||||
|
|
||||||
|
|||||||
+508
-219
@@ -1,219 +1,508 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 05.00.00
|
# VERSION: 09.23.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
name: "Universal: PR Check"
|
name: "Universal: PR Check"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, edited]
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── Branch Policy ──────────────────────────────────────────────────────
|
# ── Branch Policy ──────────────────────────────────────────────────────
|
||||||
branch-policy:
|
branch-policy:
|
||||||
name: Branch Policy
|
name: Branch Policy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check branch merge target
|
- name: Check branch merge target
|
||||||
run: |
|
run: |
|
||||||
HEAD="${{ github.head_ref }}"
|
HEAD="${{ github.head_ref }}"
|
||||||
BASE="${{ github.base_ref }}"
|
BASE="${{ github.base_ref }}"
|
||||||
|
|
||||||
echo "PR: ${HEAD} → ${BASE}"
|
echo "PR: ${HEAD} → ${BASE}"
|
||||||
|
|
||||||
ALLOWED=true
|
ALLOWED=true
|
||||||
REASON=""
|
REASON=""
|
||||||
|
|
||||||
case "$HEAD" in
|
case "$HEAD" in
|
||||||
feature/*|feat/*)
|
feature/*|feat/*)
|
||||||
if [ "$BASE" != "dev" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Feature branches must target 'dev', not '${BASE}'"
|
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
fix/*|bugfix/*)
|
fix/*|bugfix/*)
|
||||||
if [ "$BASE" != "dev" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Fix branches must target 'dev', not '${BASE}'"
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
patch/*)
|
patch/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
hotfix/*)
|
hotfix/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
rc)
|
rc)
|
||||||
if [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
dev)
|
dev)
|
||||||
if [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "$ALLOWED" = false ]; then
|
if [ "$ALLOWED" = false ]; then
|
||||||
echo "::error::${REASON}"
|
echo "::error::${REASON}"
|
||||||
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Code Validation ────────────────────────────────────────────────────
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
validate:
|
validate:
|
||||||
name: Validate PR
|
name: Validate PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Check for merge conflict markers
|
||||||
id: platform
|
run: |
|
||||||
run: |
|
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
||||||
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
if [ -n "$CONFLICTS" ]; then
|
||||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
echo "::error::Merge conflict markers found in source files"
|
||||||
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
- name: Setup PHP
|
exit 1
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
fi
|
||||||
run: |
|
echo "No conflict markers found"
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
- name: Detect platform
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
id: platform
|
||||||
fi
|
run: |
|
||||||
|
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||||
- name: PHP syntax check
|
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||||
run: |
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
ERRORS=0
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
while IFS= read -r -d '' file; do
|
|
||||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
- name: Setup PHP
|
||||||
ERRORS=$((ERRORS + 1))
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
fi
|
run: |
|
||||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
if ! command -v php &> /dev/null; then
|
||||||
echo "PHP lint: ${ERRORS} error(s)"
|
sudo apt-get update -qq
|
||||||
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
|
fi
|
||||||
- name: Validate platform manifest
|
|
||||||
run: |
|
- name: PHP syntax check
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
case "$PLATFORM" in
|
run: |
|
||||||
joomla)
|
ERRORS=0
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
while IFS= read -r -d '' file; do
|
||||||
if [ -z "$MANIFEST" ]; then
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
echo "::warning::No Joomla manifest found (WaaS site)"
|
ERRORS=$((ERRORS + 1))
|
||||||
exit 0
|
fi
|
||||||
fi
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
echo "Manifest: ${MANIFEST}"
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
if command -v php &> /dev/null; then
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
|
||||||
fi
|
- name: Joomla JEXEC guard check
|
||||||
for ELEMENT in name version description; do
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
run: |
|
||||||
done
|
ERRORS=0
|
||||||
echo "Joomla manifest valid"
|
while IFS= read -r -d '' file; do
|
||||||
;;
|
# Skip vendor, node_modules, and index.html stub files
|
||||||
dolibarr)
|
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
||||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
# Check first 10 lines for JEXEC or JPATH guard
|
||||||
if [ -z "$MOD_FILE" ]; then
|
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
|
||||||
echo "::error::No mod*.class.php found"
|
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
||||||
exit 1
|
ERRORS=$((ERRORS + 1))
|
||||||
fi
|
fi
|
||||||
echo "Dolibarr module: ${MOD_FILE}"
|
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
;;
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
*)
|
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
||||||
echo "Generic platform — no manifest validation"
|
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
||||||
esac
|
exit 1
|
||||||
|
fi
|
||||||
- name: Check update stream format
|
echo "JEXEC guard: OK"
|
||||||
run: |
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
- name: Joomla directory listing protection
|
||||||
case "$PLATFORM" in
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
joomla)
|
run: |
|
||||||
if [ -f "updates.xml" ]; then
|
MISSING=0
|
||||||
if command -v php &> /dev/null; then
|
SOURCE_DIR="src"
|
||||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
fi
|
while IFS= read -r dir; do
|
||||||
echo "updates.xml valid"
|
if [ ! -f "${dir}/index.html" ]; then
|
||||||
fi
|
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
|
||||||
;;
|
MISSING=$((MISSING + 1))
|
||||||
dolibarr)
|
fi
|
||||||
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
|
||||||
;;
|
if [ "$MISSING" -gt 0 ]; then
|
||||||
esac
|
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
|
||||||
- name: Check changelog has unreleased entry
|
fi
|
||||||
run: |
|
echo "Directory protection: ${MISSING} missing (advisory)"
|
||||||
if [ ! -f "CHANGELOG.md" ]; then
|
|
||||||
echo "::warning::No CHANGELOG.md found"
|
- name: Joomla script file and asset checks
|
||||||
exit 0
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
fi
|
run: |
|
||||||
# Check for content under [Unreleased] section
|
ERRORS=0
|
||||||
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
[ -z "$MANIFEST" ] && exit 0
|
||||||
exit 1
|
MANIFEST_DIR=$(dirname "$MANIFEST")
|
||||||
fi
|
|
||||||
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
# Check scriptfile exists if declared
|
||||||
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
|
||||||
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
if [ -n "$SCRIPTFILE" ]; then
|
||||||
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
|
||||||
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
|
||||||
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
ERRORS=$((ERRORS + 1))
|
||||||
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
else
|
||||||
exit 1
|
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
|
||||||
fi
|
fi
|
||||||
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
fi
|
||||||
|
|
||||||
- name: Verify package source
|
# Require joomla.asset.json and validate it
|
||||||
run: |
|
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
SOURCE_DIR="src"
|
if [ -z "$ASSET_JSON" ]; then
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
echo "::error::joomla.asset.json not found — Joomla asset system is required"
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
ERRORS=$((ERRORS + 1))
|
||||||
echo "::warning::No src/ or htdocs/ directory"
|
else
|
||||||
exit 0
|
if command -v php &> /dev/null; then
|
||||||
fi
|
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
|
||||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
echo "::error::joomla.asset.json is not valid JSON"
|
||||||
echo "Source: ${FILE_COUNT} files"
|
ERRORS=$((ERRORS + 1))
|
||||||
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
}
|
||||||
|
fi
|
||||||
|
echo "joomla.asset.json: valid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate all XML files in src/ are well-formed
|
||||||
|
XML_ERRORS=0
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
while IFS= read -r -d '' xmlfile; do
|
||||||
|
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
|
||||||
|
XML_ERRORS=$((XML_ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
|
||||||
|
fi
|
||||||
|
if [ "$XML_ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::${XML_ERRORS} XML file(s) are malformed"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
else
|
||||||
|
echo "XML well-formedness: OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
echo "Joomla asset checks: OK"
|
||||||
|
|
||||||
|
- name: Validate platform manifest
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
joomla)
|
||||||
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MANIFEST" ]; then
|
||||||
|
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Manifest: ${MANIFEST}"
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
||||||
|
fi
|
||||||
|
for ELEMENT in name version description; do
|
||||||
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
|
done
|
||||||
|
# Block legacy raw/branch update server URLs on MokoGitea
|
||||||
|
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
||||||
|
if [ -n "$RAW_URLS" ]; then
|
||||||
|
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
||||||
|
echo "$RAW_URLS"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Joomla manifest valid"
|
||||||
|
;;
|
||||||
|
dolibarr)
|
||||||
|
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||||
|
if [ -z "$MOD_FILE" ]; then
|
||||||
|
echo "::error::No mod*.class.php found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Dolibarr module: ${MOD_FILE}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Generic platform — no manifest validation"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Check update stream format
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
joomla)
|
||||||
|
if [ -f "updates.xml" ]; then
|
||||||
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
||||||
|
fi
|
||||||
|
echo "updates.xml valid"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
dolibarr)
|
||||||
|
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Validate Joomla language files
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
|
||||||
|
# Require both en-GB and en-US language directories
|
||||||
|
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
if [ -z "$LANG_ROOT" ]; then
|
||||||
|
echo "No language/ directory found — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$LANG_ROOT/en-GB" ]; then
|
||||||
|
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
if [ ! -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check that en-GB and en-US have matching .ini files
|
||||||
|
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
|
||||||
|
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
|
||||||
|
[ ! -f "$GB_INI" ] && continue
|
||||||
|
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
|
||||||
|
if [ ! -f "$US_INI" ]; then
|
||||||
|
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
|
||||||
|
[ ! -f "$US_INI" ] && continue
|
||||||
|
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
|
||||||
|
if [ ! -f "$GB_INI" ]; then
|
||||||
|
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find all .ini language files
|
||||||
|
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
|
||||||
|
if [ -z "$INI_FILES" ]; then
|
||||||
|
echo "No .ini language files found"
|
||||||
|
[ "$ERRORS" -gt 0 ] && exit 1
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
|
||||||
|
|
||||||
|
for FILE in $INI_FILES; do
|
||||||
|
FNAME=$(basename "$FILE")
|
||||||
|
LINENUM=0
|
||||||
|
SEEN_KEYS=""
|
||||||
|
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
LINENUM=$((LINENUM + 1))
|
||||||
|
|
||||||
|
# Skip empty lines and comments
|
||||||
|
[ -z "$line" ] && continue
|
||||||
|
echo "$line" | grep -qE '^\s*;' && continue
|
||||||
|
echo "$line" | grep -qE '^\s*$' && continue
|
||||||
|
|
||||||
|
# Must match KEY="VALUE" format
|
||||||
|
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract key and check for duplicates
|
||||||
|
KEY=$(echo "$line" | sed 's/=.*//')
|
||||||
|
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
|
||||||
|
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
SEEN_KEYS="${SEEN_KEYS}
|
||||||
|
${KEY}"
|
||||||
|
done < "$FILE"
|
||||||
|
|
||||||
|
echo " ${FILE}: checked ${LINENUM} lines"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cross-check en-GB vs en-US key consistency
|
||||||
|
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
|
||||||
|
for GB_FILE in "$GB_DIR"/*.ini; do
|
||||||
|
[ ! -f "$GB_FILE" ] && continue
|
||||||
|
FNAME=$(basename "$GB_FILE")
|
||||||
|
US_FILE="$US_DIR/$FNAME"
|
||||||
|
[ ! -f "$US_FILE" ] && continue
|
||||||
|
|
||||||
|
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
|
||||||
|
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
|
||||||
|
|
||||||
|
# Keys in en-GB but not en-US
|
||||||
|
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_US" ]; then
|
||||||
|
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
|
||||||
|
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Keys in en-US but not en-GB
|
||||||
|
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
||||||
|
if [ -n "$MISSING_GB" ]; then
|
||||||
|
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
|
||||||
|
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
|
||||||
|
WARNINGS=$((WARNINGS + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### Language File Validation"
|
||||||
|
echo "| Metric | Count |"
|
||||||
|
echo "|---|---|"
|
||||||
|
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
|
||||||
|
echo "| Errors | ${ERRORS} |"
|
||||||
|
echo "| Warnings | ${WARNINGS} |"
|
||||||
|
} >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [ "$ERRORS" -gt 0 ]; then
|
||||||
|
echo "::error::Language validation failed with ${ERRORS} error(s)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Language files: OK (${WARNINGS} warning(s))"
|
||||||
|
|
||||||
|
- name: Check changelog has unreleased entry
|
||||||
|
run: |
|
||||||
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
|
echo "::warning::No CHANGELOG.md found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
# Check for content under [Unreleased] section
|
||||||
|
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
||||||
|
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
||||||
|
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
||||||
|
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
||||||
|
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
||||||
|
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||||
|
|
||||||
|
- name: Verify package source
|
||||||
|
run: |
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
|
echo "Source: ${FILE_COUNT} files"
|
||||||
|
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||||
|
|
||||||
|
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
||||||
|
pre-release:
|
||||||
|
name: Build RC Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger RC pre-release
|
||||||
|
env:
|
||||||
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
BRANCH: ${{ github.head_ref }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
# ── Issue Reporter ──────────────────────────────────────────────────────
|
||||||
|
report-issues:
|
||||||
|
name: Report Issues
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [branch-policy, validate]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
needs.validate.result == 'failure'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
sparse-checkout: automation/ci-issue-reporter.sh
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: "File issue for PR validation failure"
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
run: |
|
||||||
|
chmod +x automation/ci-issue-reporter.sh
|
||||||
|
./automation/ci-issue-reporter.sh \
|
||||||
|
--gate "PR Validation" \
|
||||||
|
--workflow "PR Check" \
|
||||||
|
--severity error \
|
||||||
|
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
||||||
|
|||||||
@@ -7,28 +7,12 @@
|
|||||||
# INGROUP: moko-platform.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 06.00.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Pre-release pipeline — builds dev/alpha/beta/rc packages, updates update server
|
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||||
#
|
|
||||||
# Consolidates the former pre-release.yml + update-server.yml into one workflow.
|
|
||||||
# Triggers:
|
|
||||||
# - Push to dev/alpha/beta/rc branches (src/ or htdocs/ changes)
|
|
||||||
# - PR merged into dev (any source branch)
|
|
||||||
# - Manual dispatch with stability choice
|
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- 'dev'
|
|
||||||
- 'dev/**'
|
|
||||||
- 'alpha/**'
|
|
||||||
- 'beta/**'
|
|
||||||
- 'rc/**'
|
|
||||||
paths:
|
|
||||||
- 'src/**'
|
|
||||||
- 'htdocs/**'
|
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [closed]
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
@@ -49,7 +33,6 @@ permissions:
|
|||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
@@ -60,7 +43,6 @@ jobs:
|
|||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
github.event_name == 'push' ||
|
|
||||||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -74,11 +56,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if ! command -v composer &> /dev/null; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
rm -rf /tmp/moko-platform-api
|
rm -rf /tmp/moko-platform-api
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
@@ -94,25 +76,7 @@ jobs:
|
|||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
BRANCH="${{ github.ref_name }}"
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
|
|
||||||
# Configure git for bot pushes
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
|
||||||
|
|
||||||
# Determine stability from manual input, branch name, or default
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
STABILITY="${{ inputs.stability }}"
|
|
||||||
elif [[ "$BRANCH" == rc/* ]]; then
|
|
||||||
STABILITY="release-candidate"
|
|
||||||
elif [[ "$BRANCH" == beta/* ]]; then
|
|
||||||
STABILITY="beta"
|
|
||||||
elif [[ "$BRANCH" == alpha/* ]]; then
|
|
||||||
STABILITY="alpha"
|
|
||||||
else
|
|
||||||
STABILITY="development"
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$STABILITY" in
|
case "$STABILITY" in
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
@@ -121,27 +85,40 @@ jobs:
|
|||||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Read current version
|
# Read current version (bump already handled by push workflow)
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
[ -z "$VERSION" ] && VERSION="00.00.01"
|
||||||
|
|
||||||
# Strip any existing suffix before applying stability
|
# Strip any existing suffix from version before applying stability
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
# Propagate version with stability suffix to all manifest files
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Verify version consistency across all files
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Update VERSION variable with suffix
|
||||||
if [ -n "$SUFFIX" ]; then
|
if [ -n "$SUFFIX" ]; then
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Commit version bump
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||||
|
git push origin HEAD 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
# Auto-detect element via manifest_element.php
|
# Auto-detect element via manifest_element.php
|
||||||
php ${MOKO_CLI}/manifest_element.php \
|
php ${MOKO_CLI}/manifest_element.php \
|
||||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||||
--repo "${GITEA_REPO}" --github-output
|
--repo "${GITEA_REPO}" --github-output
|
||||||
|
|
||||||
|
# Read back element outputs
|
||||||
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
@@ -154,14 +131,7 @@ jobs:
|
|||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
# Commit version changes
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
git add -A
|
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
|
||||||
git push origin HEAD 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ==="
|
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: release
|
id: release
|
||||||
@@ -172,7 +142,7 @@ jobs:
|
|||||||
php ${MOKO_CLI}/release_create.php \
|
php ${MOKO_CLI}/release_create.php \
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||||
|
|
||||||
- name: Build package and upload
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
@@ -205,16 +175,21 @@ jobs:
|
|||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
${SHA_FLAG}
|
${SHA_FLAG}
|
||||||
|
|
||||||
|
# Commit and push
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git add updates.xml
|
git add updates.xml
|
||||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Sync updates.xml to all branches
|
- name: "Sync updates.xml to all branches"
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
run: |
|
run: |
|
||||||
CURRENT_BRANCH="${{ github.ref_name }}"
|
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
|
||||||
for BRANCH in main dev; do
|
for BRANCH in main dev; do
|
||||||
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
||||||
@@ -230,68 +205,17 @@ jobs:
|
|||||||
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Delete lesser pre-release channels
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
php ${MOKO_CLI}/release_cascade.php \
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
--stability "${{ steps.meta.outputs.stability }}" \
|
--stability "${{ steps.meta.outputs.stability }}" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${TOKEN}" \
|
||||||
--api-base "${API_BASE}"
|
--api-base "${API_BASE}"
|
||||||
|
|
||||||
- name: SFTP deploy to dev server
|
|
||||||
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
|
||||||
env:
|
|
||||||
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
|
||||||
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
|
||||||
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
|
||||||
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
|
||||||
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
|
||||||
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
|
||||||
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
|
||||||
run: |
|
|
||||||
ACTOR="${{ github.actor }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
|
||||||
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
|
||||||
case "$PERMISSION" in
|
|
||||||
admin|maintain|write) ;;
|
|
||||||
*)
|
|
||||||
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
|
||||||
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
|
||||||
|
|
||||||
PORT="${DEV_PORT:-22}"
|
|
||||||
REMOTE="${DEV_PATH%/}"
|
|
||||||
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
|
||||||
|
|
||||||
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
|
||||||
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
|
||||||
if [ -n "$DEV_KEY" ]; then
|
|
||||||
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
|
||||||
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
|
||||||
else
|
|
||||||
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
|
|
||||||
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
|
||||||
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
|
||||||
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
||||||
fi
|
|
||||||
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
|
||||||
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@
|
|||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Security
|
# INGROUP: MokoStandards.Security
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
# PATH: /.gitea/workflows/security-audit.yml
|
||||||
# VERSION: 01.00.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
||||||
@@ -80,19 +80,3 @@ jobs:
|
|||||||
-H "Priority: high" \
|
-H "Priority: high" \
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
-d "Security audit found vulnerabilities. Review dependency updates." \
|
||||||
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
"${NTFY_URL}/${NTFY_TOPIC}" || true
|
||||||
|
|
||||||
|
|
||||||
- name: Joomla version audit
|
|
||||||
if: always()
|
|
||||||
run: |
|
|
||||||
if [ -f "monitoring/joomla-version-audit.php" ] && [ -n "$JOOMLA_SITES" ]; then
|
|
||||||
echo "$JOOMLA_SITES" > /tmp/sites.json
|
|
||||||
php monitoring/joomla-version-audit.php --sites /tmp/sites.json || true
|
|
||||||
echo "### Joomla Version Audit" >> $GITHUB_STEP_SUMMARY
|
|
||||||
rm -f /tmp/sites.json
|
|
||||||
else
|
|
||||||
echo "Joomla audit skipped (no script or JOOMLA_SITES_JSON not configured)"
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
JOOMLA_SITES: ${{ vars.JOOMLA_SITES_JSON }}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,312 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /templates/workflows/update-server.yml
|
||||||
|
# VERSION: 05.00.00
|
||||||
|
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
||||||
|
#
|
||||||
|
# Thin wrapper around moko-platform CLI tools.
|
||||||
|
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
||||||
|
#
|
||||||
|
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
||||||
|
|
||||||
|
name: "Update Server"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Stability tag'
|
||||||
|
required: true
|
||||||
|
default: 'development'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
|
- stable
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-xml:
|
||||||
|
name: Update Server
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform 2>/dev/null || true
|
||||||
|
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
||||||
|
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve stability and bump version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.ref_name }}"
|
||||||
|
|
||||||
|
# Configure git for bot pushes
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
# Auto-bump patch version
|
||||||
|
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
||||||
|
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
||||||
|
|
||||||
|
# Strip any existing suffix before applying stability
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
|
# Determine stability from branch or manual input
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
elif [[ "$BRANCH" == rc/* ]]; then
|
||||||
|
STABILITY="rc"
|
||||||
|
elif [[ "$BRANCH" == beta/* ]]; then
|
||||||
|
STABILITY="beta"
|
||||||
|
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||||
|
STABILITY="alpha"
|
||||||
|
else
|
||||||
|
STABILITY="development"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Version suffix per stability stream
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
|
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
|
*) SUFFIX=""; TAG="stable" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Propagate version with stability suffix to all manifest files
|
||||||
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Re-read version (now includes suffix from version_set_platform)
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Commit version bump if changed
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Create release and upload package
|
||||||
|
id: package
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Create or update Gitea release
|
||||||
|
php ${MOKO_CLI}/release_create.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
|
# Build package and upload
|
||||||
|
php ${MOKO_CLI}/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
|
- name: Update updates.xml
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
echo "No updates.xml — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA_FLAG=""
|
||||||
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
${SHA_FLAG}
|
||||||
|
|
||||||
|
# Commit and push updates.xml
|
||||||
|
git add updates.xml
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Sync updates.xml to main
|
||||||
|
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import base64, json, urllib.request, sys
|
||||||
|
with open('updates.xml', 'rb') as f:
|
||||||
|
content = base64.b64encode(f.read()).decode()
|
||||||
|
payload = json.dumps({
|
||||||
|
'content': content,
|
||||||
|
'sha': '${FILE_SHA}',
|
||||||
|
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
||||||
|
'branch': 'main'
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/contents/updates.xml',
|
||||||
|
data=payload, method='PUT',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${GITEA_TOKEN}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
print('updates.xml synced to main')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SFTP deploy to dev server
|
||||||
|
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||||
|
env:
|
||||||
|
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# Permission check: admin or maintain role required
|
||||||
|
ACTOR="${{ github.actor }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||||
|
case "$PERMISSION" in
|
||||||
|
admin|maintain|write) ;;
|
||||||
|
*)
|
||||||
|
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||||
|
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
|
||||||
|
PORT="${DEV_PORT:-22}"
|
||||||
|
REMOTE="${DEV_PATH%/}"
|
||||||
|
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||||
|
if [ -n "$DEV_KEY" ]; then
|
||||||
|
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
||||||
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
||||||
|
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
+13
-33
@@ -1,44 +1,24 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
<!-- VERSION: 01.00.01 -->
|
<!-- VERSION: 01.00.00 -->
|
||||||
|
|
||||||
All notable changes to MokoJoomOpenGraph will be documented in this file.
|
All notable changes to MokoOpenGraph will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Security
|
### Removed
|
||||||
- Fix JSON-LD XSS vulnerability via `</script>` injection in content data (#34)
|
- Removed deploy-manual.yml workflow — switching to Joomla update server method for extension distribution
|
||||||
- Add ACL permission checks to Batch and ImportExport controllers (#37)
|
|
||||||
- Add CSV import file type, MIME type, size, and content_type validation (#35)
|
|
||||||
- Fix multilingual data corruption in content plugin load/save (#41)
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Site-wide default OG title and description plugin parameters
|
- Initial package structure with component, system plugin, and content plugin
|
||||||
- Discord embed color via `theme-color` meta tag (color picker in plugin config)
|
- Open Graph meta tag injection via system plugin (`onBeforeCompileHead`)
|
||||||
- LinkedIn article tags: `article:published_time`, `article:modified_time`, `article:author`
|
- Twitter/X Card meta tag support (Summary and Summary with Large Image)
|
||||||
- `og:image:width` and `og:image:height` for faster social preview rendering
|
- Per-article OG fields in the article editor
|
||||||
- `onMokoOGAfterRender` event for third-party plugin extensibility
|
- Per-menu-item OG fields in the menu item editor
|
||||||
- Joomla Web Services API for OG tags — full CRUD at `/api/v1/mokoog/tags` (#27)
|
- Auto-generation of OG tags from article title, description, and images
|
||||||
- Live social preview in article/menu editors (Facebook and Twitter/X card mockups) (#3)
|
|
||||||
- CSV import/export for bulk OG tag management (#12)
|
|
||||||
- OG image text overlay generator (#7)
|
|
||||||
- Multilingual OG tag support with per-language records (#11)
|
|
||||||
- JSON-LD structured data: Article, WebPage, BreadcrumbList schemas (#6)
|
|
||||||
- Social platform debugger quick links (Facebook, LinkedIn, Google) (#9)
|
|
||||||
- Content type adapter architecture for K2, VirtueMart, HikaShop (#5)
|
|
||||||
- WhatsApp and Telegram link preview optimization (#10)
|
|
||||||
- Category-level OG tag support (#4)
|
|
||||||
- Batch OG tag generation for existing articles (#1)
|
|
||||||
- Auto-resize OG images to 1200x630px with center crop (#2)
|
|
||||||
- SEO meta tag management: title, description, robots, canonical URL (#8)
|
|
||||||
- Per-article and per-menu-item OG fields in the editor
|
|
||||||
- Auto-generation of OG tags from article content, title, and images
|
|
||||||
- Default fallback image configuration
|
- Default fallback image configuration
|
||||||
- Admin tag manager component with filtering, search, and pagination
|
- Admin tag manager component for viewing all OG records
|
||||||
- Facebook App ID and Telegram channel support
|
- Facebook App ID support
|
||||||
- Database table `#__mokoog_tags` with multilingual unique key
|
- Database table `#__mokoog_tags` for storing custom OG data
|
||||||
|
|
||||||
### Removed
|
|
||||||
- Removed deploy-manual.yml workflow — using Joomla update server for distribution
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ This file provides guidance to Claude Code when working with this repository.
|
|||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**MokoJoomOpenGraph** -- Open Graph, Twitter Card, and social sharing meta tag management for Joomla
|
**MokoOpenGraph** -- Open Graph, Twitter Card, and social sharing meta tag management for Joomla
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -12,7 +12,7 @@ This file provides guidance to Claude Code when working with this repository.
|
|||||||
| **Language** | PHP |
|
| **Language** | PHP |
|
||||||
| **Default branch** | main |
|
| **Default branch** | main |
|
||||||
| **License** | GPL-3.0-or-later |
|
| **License** | GPL-3.0-or-later |
|
||||||
| **Wiki** | [MokoJoomOpenGraph Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/wiki) |
|
| **Wiki** | [MokoOpenGraph Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/wiki) |
|
||||||
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
|
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
|
||||||
|
|
||||||
## Common Commands
|
## Common Commands
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
# Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
We pledge to make participation in our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to a positive environment:
|
|
||||||
|
|
||||||
- Using welcoming and inclusive language
|
|
||||||
- Being respectful of differing viewpoints and experiences
|
|
||||||
- Gracefully accepting constructive criticism
|
|
||||||
- Focusing on what is best for the community
|
|
||||||
|
|
||||||
Examples of unacceptable behavior:
|
|
||||||
|
|
||||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
- Public or private harassment
|
|
||||||
- Publishing others' private information without explicit permission
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team at hello@mokoconsulting.tech. All complaints will be reviewed and investigated.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 2.1.
|
|
||||||
+161
-34
@@ -1,34 +1,161 @@
|
|||||||
# Contributing to MokoJoomOpenGraph
|
# Contributing to Moko Consulting Projects
|
||||||
|
|
||||||
Thank you for your interest in contributing to MokoJoomOpenGraph.
|
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||||
|
|
||||||
## Getting Started
|
## Branching Workflow
|
||||||
|
|
||||||
1. Fork the repository on Gitea
|
```
|
||||||
2. Create a feature branch from `dev` (`feature/your-feature`)
|
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||||
3. Make your changes following the coding standards below
|
```
|
||||||
4. Submit a pull request targeting `dev`
|
|
||||||
|
### Step by step
|
||||||
## Branch Strategy
|
|
||||||
|
1. **Create a feature branch** from `dev`:
|
||||||
- `main` — stable releases only
|
```bash
|
||||||
- `dev` — active development
|
git checkout dev && git pull
|
||||||
- `feature/*` — new features (target `dev`)
|
git checkout -b feature/my-change
|
||||||
- `fix/*` — bug fixes (target `dev`)
|
```
|
||||||
- `hotfix/*` — urgent fixes (target `dev` or `main`)
|
|
||||||
|
2. **Work and commit** on your feature branch. Push to origin.
|
||||||
## Coding Standards
|
|
||||||
|
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||||
- PHP 8.1+ required
|
|
||||||
- Follow Joomla coding standards
|
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||||
- SPDX license headers on all PHP files
|
- This automatically renames the source branch to `rc` (release candidate)
|
||||||
- Use `SubscriberInterface` for event subscription
|
- An RC pre-release is built and uploaded
|
||||||
- Use `bind() -> check() -> store()` for Table operations
|
|
||||||
|
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||||
## Reporting Issues
|
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||||
|
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||||
Report bugs and feature requests via [Issues](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/issues).
|
- When the draft PR is created, the branch is renamed to `rc`
|
||||||
|
|
||||||
## License
|
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||||
|
|
||||||
By contributing, you agree that your contributions will be licensed under GPL-3.0-or-later.
|
7. **Merging to main** triggers the stable release pipeline:
|
||||||
|
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||||
|
- Stability suffix stripped (clean version)
|
||||||
|
- Gitea release created with ZIP/tar.gz packages
|
||||||
|
- `updates.xml` updated (Joomla extensions)
|
||||||
|
- `dev` branch recreated from `main`
|
||||||
|
|
||||||
|
### Branch summary
|
||||||
|
|
||||||
|
| Branch | Purpose | Created by |
|
||||||
|
|--------|---------|-----------|
|
||||||
|
| `feature/*` | New features and fixes | Developer |
|
||||||
|
| `dev` | Integration branch | Auto-recreated after release |
|
||||||
|
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||||
|
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||||
|
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||||
|
| `main` | Stable releases | Protected, merge only |
|
||||||
|
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||||
|
|
||||||
|
### Protected branches
|
||||||
|
|
||||||
|
| Branch | Direct push | Merge via |
|
||||||
|
|--------|------------|-----------|
|
||||||
|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||||
|
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||||
|
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||||
|
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
|
| `feature/*` | Open | N/A (source branch) |
|
||||||
|
|
||||||
|
## Version Policy
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||||
|
|
||||||
|
- **XX** — Major version (breaking changes)
|
||||||
|
- **YY** — Minor version (new features, bumped on release to main)
|
||||||
|
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||||
|
|
||||||
|
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||||
|
|
||||||
|
### Stability suffixes
|
||||||
|
|
||||||
|
Each branch appends a suffix to indicate stability:
|
||||||
|
|
||||||
|
| Branch | Suffix | Example |
|
||||||
|
|--------|--------|---------|
|
||||||
|
| `main` | (none) | `02.09.00` |
|
||||||
|
| `dev` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||||
|
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||||
|
| `beta` | `-beta` | `02.09.01-beta` |
|
||||||
|
| `rc` | `-rc` | `02.09.01-rc` |
|
||||||
|
|
||||||
|
### Auto version bump
|
||||||
|
|
||||||
|
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||||
|
|
||||||
|
1. Patch version incremented
|
||||||
|
2. Stability suffix `-dev` applied
|
||||||
|
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||||
|
4. Commit created with `[skip ci]` to avoid loops
|
||||||
|
|
||||||
|
### Release version flow
|
||||||
|
|
||||||
|
Version bumps happen at specific release events:
|
||||||
|
|
||||||
|
| Event | Bump | Example |
|
||||||
|
|-------|------|---------|
|
||||||
|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||||
|
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||||
|
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||||
|
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||||
|
|
||||||
|
### Release stream copies
|
||||||
|
|
||||||
|
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||||
|
|
||||||
|
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||||
|
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||||
|
|
||||||
|
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||||
|
|
||||||
|
### Version files
|
||||||
|
|
||||||
|
The version tools update all files containing version stamps:
|
||||||
|
|
||||||
|
- `.mokogitea/manifest.xml` (canonical source)
|
||||||
|
- Joomla XML manifests (`<version>` tag)
|
||||||
|
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||||
|
- `package.json`, `pyproject.toml`
|
||||||
|
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||||
|
|
||||||
|
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||||
|
|
||||||
|
## Code Standards
|
||||||
|
|
||||||
|
- **PHP**: PSR-12, tabs for indentation
|
||||||
|
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||||
|
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||||
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
Use conventional commit format:
|
||||||
|
|
||||||
|
```
|
||||||
|
type(scope): short description
|
||||||
|
|
||||||
|
Optional body with context.
|
||||||
|
|
||||||
|
Authored-by: Moko Consulting
|
||||||
|
```
|
||||||
|
|
||||||
|
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||||
|
|
||||||
|
Special flags in commit messages:
|
||||||
|
- `[skip ci]` — skip all CI workflows
|
||||||
|
- `[skip bump]` — skip auto version bump only
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
Use the repository's issue tracker with the appropriate template.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# MokoJoomOpenGraph — Open Graph & social sharing meta tag management
|
# MokoOpenGraph — Open Graph & social sharing meta tag management
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# CONFIGURATION - Customize these for your extension
|
# CONFIGURATION - Customize these for your extension
|
||||||
|
|||||||
@@ -1,69 +1,40 @@
|
|||||||
# MokoJoomOpenGraph
|
# MokoOpenGraph
|
||||||
|
|
||||||
<!-- VERSION: 01.00.01 -->
|
<!-- VERSION: 01.00.00 -->
|
||||||
|
|
||||||
Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6.
|
Open Graph, Twitter Card, and social sharing meta tag management for Joomla 4/5/6.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
MokoJoomOpenGraph gives you full control over how your Joomla content appears when shared on Facebook, Twitter/X, LinkedIn, Discord, WhatsApp, Telegram, and other social platforms. Set custom titles, descriptions, and images per article, menu item, and category — or let the extension auto-generate them from your existing content.
|
MokoOpenGraph gives you full control over how your Joomla content appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and other social platforms. Set custom titles, descriptions, and images per article and menu item — or let the extension auto-generate them from your existing content.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### Social Meta Tags
|
- **Open Graph tags** — `og:title`, `og:description`, `og:image`, `og:url`, `og:type`, `og:site_name`
|
||||||
- **Open Graph tags** — `og:title`, `og:description`, `og:image`, `og:url`, `og:type`, `og:site_name`, `og:locale`
|
|
||||||
- **Twitter/X Cards** — Summary and Summary with Large Image card types
|
- **Twitter/X Cards** — Summary and Summary with Large Image card types
|
||||||
- **LinkedIn** — `article:published_time`, `article:modified_time`, `article:author`
|
- **Per-article control** — Custom OG fields in the article editor
|
||||||
- **Discord** — Custom embed color via `theme-color` meta tag
|
|
||||||
- **Telegram** — `telegram:channel` for link previews
|
|
||||||
- **Facebook** — `fb:app_id` support, `og:image:width`/`og:image:height` for instant previews
|
|
||||||
|
|
||||||
### Content Management
|
|
||||||
- **Per-article control** — Custom OG fields tab in the article editor
|
|
||||||
- **Per-menu-item control** — Custom OG fields in the menu item editor
|
- **Per-menu-item control** — Custom OG fields in the menu item editor
|
||||||
- **Per-category control** — Category-level OG tag overrides
|
- **Auto-generation** — Automatically builds tags from article content, title, and images
|
||||||
- **Multilingual support** — Per-language OG data with language-aware fallback
|
- **Default fallback image** — Site-wide default when no article image exists
|
||||||
- **Auto-generation** — Builds tags from article content, title, and images automatically
|
- **Admin tag manager** — View and manage all OG records from a central dashboard
|
||||||
- **Site-wide defaults** — Default OG title, description, and image for all pages
|
- **Facebook App ID** — Optional `fb:app_id` meta tag support
|
||||||
|
- **Joomla 4/5/6** — Modern DI container architecture, Joomla coding standards
|
||||||
### SEO
|
|
||||||
- **SEO title override** — Custom `<title>` tag per page
|
|
||||||
- **Meta description** — Per-page meta description control
|
|
||||||
- **Robots directive** — Per-page noindex/nofollow settings
|
|
||||||
- **Canonical URL** — Custom canonical URL overrides
|
|
||||||
- **JSON-LD structured data** — Article, WebPage, BreadcrumbList, Organization schemas
|
|
||||||
|
|
||||||
### Admin Tools
|
|
||||||
- **Tag manager dashboard** — View and manage all OG records centrally
|
|
||||||
- **Batch generation** — Auto-generate OG tags for all existing articles
|
|
||||||
- **CSV import/export** — Bulk manage OG data via CSV files
|
|
||||||
- **SEO health badges** — Visual indicators for missing descriptions, long titles, noindex
|
|
||||||
- **Debug links** — Quick links to Facebook Debugger, LinkedIn Inspector, Google Rich Results
|
|
||||||
- **Live preview** — Real-time Facebook and Twitter/X card preview in the editor
|
|
||||||
|
|
||||||
### Developer Features
|
|
||||||
- **REST API** — Full CRUD via Joomla Web Services (`/api/v1/mokoog/tags`)
|
|
||||||
- **Content type adapters** — Extensible architecture for K2, VirtueMart, HikaShop
|
|
||||||
- **Plugin event** — `onMokoOGAfterRender` for third-party plugins to add custom social tags
|
|
||||||
- **OG image generator** — Text overlay on template backgrounds with auto-resize to 1200x630
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Download the latest `pkg_mokoog-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoJoomOpenGraph/releases)
|
1. Download the latest `pkg_mokoog-*.zip` from [Releases](https://git.mokoconsulting.tech/MokoConsulting/MokoOpenGraph/releases)
|
||||||
2. In Joomla Administrator → Extensions → Install → Upload Package File
|
2. In Joomla Administrator → Extensions → Install → Upload Package File
|
||||||
3. All plugins are enabled automatically on install
|
3. The system plugin is enabled automatically on install
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Navigate to **Extensions → Plugins → System - MokoJoomOpenGraph** to configure:
|
Navigate to **Extensions → Plugins → System - MokoOpenGraph** to configure:
|
||||||
- Site name override
|
- Site name override
|
||||||
- Default OG title and description (site-wide fallback)
|
|
||||||
- Default fallback image
|
- Default fallback image
|
||||||
- Twitter Card type and @username
|
- Twitter Card type and @username
|
||||||
- Facebook App ID
|
- Facebook App ID
|
||||||
- Discord embed color
|
- Auto-generation behavior
|
||||||
- Telegram channel
|
- Description length limit
|
||||||
- Auto-generation, image resize, JSON-LD, and description length settings
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================================
|
||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Automation.CI
|
||||||
|
# INGROUP: moko-platform.Automation
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /automation/ci-issue-reporter.sh
|
||||||
|
# VERSION: 09.23.00
|
||||||
|
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
||||||
|
# Deduplicates by searching open issues with the "ci-auto" label
|
||||||
|
# whose title matches the gate. If a matching issue exists, a comment
|
||||||
|
# is appended instead of opening a duplicate.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── Defaults ────────────────────────────────────────────────────────────────
|
||||||
|
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
||||||
|
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||||
|
REPO="${GITHUB_REPOSITORY:-}"
|
||||||
|
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
||||||
|
LABEL_NAME="ci-auto"
|
||||||
|
LABEL_COLOR="#e11d48"
|
||||||
|
|
||||||
|
GATE=""
|
||||||
|
DETAILS=""
|
||||||
|
SEVERITY="error"
|
||||||
|
WORKFLOW=""
|
||||||
|
|
||||||
|
# ── Parse arguments ─────────────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
cat <<EOF
|
||||||
|
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
||||||
|
|
||||||
|
Required:
|
||||||
|
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
||||||
|
--details Human-readable failure description
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--severity "error" (default) or "warning"
|
||||||
|
--workflow Workflow name for the issue title
|
||||||
|
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
||||||
|
--run-url URL to the CI run (auto-detected from env)
|
||||||
|
--token Gitea API token (default: \$GITEA_TOKEN)
|
||||||
|
--url Gitea base URL (default: \$GITEA_URL)
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--gate) GATE="$2"; shift 2 ;;
|
||||||
|
--details) DETAILS="$2"; shift 2 ;;
|
||||||
|
--severity) SEVERITY="$2"; shift 2 ;;
|
||||||
|
--workflow) WORKFLOW="$2"; shift 2 ;;
|
||||||
|
--repo) REPO="$2"; shift 2 ;;
|
||||||
|
--run-url) RUN_URL="$2"; shift 2 ;;
|
||||||
|
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
||||||
|
--url) GITEA_URL="$2"; shift 2 ;;
|
||||||
|
-h|--help) usage ;;
|
||||||
|
*) echo "Unknown option: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
||||||
|
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
||||||
|
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
||||||
|
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
||||||
|
|
||||||
|
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
||||||
|
|
||||||
|
# ── Build title ─────────────────────────────────────────────────────────────
|
||||||
|
if [[ -n "$WORKFLOW" ]]; then
|
||||||
|
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
||||||
|
else
|
||||||
|
TITLE="[CI] ${GATE} failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Ensure label exists ─────────────────────────────────────────────────────
|
||||||
|
ensure_label() {
|
||||||
|
local exists
|
||||||
|
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$exists" == "200" ]]; then
|
||||||
|
# Check if label already exists
|
||||||
|
local found
|
||||||
|
found=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
||||||
|
|
||||||
|
if [[ -z "$found" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/labels" \
|
||||||
|
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Search for existing open issue ──────────────────────────────────────────
|
||||||
|
find_existing_issue() {
|
||||||
|
# URL-encode the gate name for the query
|
||||||
|
local query
|
||||||
|
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
||||||
|
|
||||||
|
local response
|
||||||
|
response=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
||||||
|
2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
# Extract the first matching issue number
|
||||||
|
echo "$response" \
|
||||||
|
| grep -oP '"number":\s*\K[0-9]+' \
|
||||||
|
| head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build issue body ────────────────────────────────────────────────────────
|
||||||
|
build_body() {
|
||||||
|
local severity_badge
|
||||||
|
if [[ "$SEVERITY" == "error" ]]; then
|
||||||
|
severity_badge="**Severity:** Error"
|
||||||
|
else
|
||||||
|
severity_badge="**Severity:** Warning"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<BODY
|
||||||
|
## CI Gate Failure: ${GATE}
|
||||||
|
|
||||||
|
${severity_badge}
|
||||||
|
**Workflow:** ${WORKFLOW:-unknown}
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
### Details
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
|
||||||
|
### Resolution
|
||||||
|
|
||||||
|
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
||||||
|
BODY
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Build comment body (for existing issues) ────────────────────────────────
|
||||||
|
build_comment() {
|
||||||
|
cat <<COMMENT
|
||||||
|
### CI failure recurrence
|
||||||
|
|
||||||
|
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
||||||
|
**Commit:** \`${GITHUB_SHA:0:8}\`
|
||||||
|
**Run:** [View CI run](${RUN_URL})
|
||||||
|
|
||||||
|
${DETAILS}
|
||||||
|
COMMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main ────────────────────────────────────────────────────────────────────
|
||||||
|
ensure_label
|
||||||
|
|
||||||
|
EXISTING=$(find_existing_issue)
|
||||||
|
|
||||||
|
if [[ -n "$EXISTING" ]]; then
|
||||||
|
# Append comment to existing issue
|
||||||
|
COMMENT_BODY=$(build_comment)
|
||||||
|
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
||||||
|
|
||||||
|
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${EXISTING}/comments" \
|
||||||
|
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [[ "$HTTP" == "201" ]]; then
|
||||||
|
echo "Commented on existing issue #${EXISTING}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Create new issue
|
||||||
|
ISSUE_BODY=$(build_body)
|
||||||
|
ISSUE_JSON=$(python3 -c "
|
||||||
|
import sys, json
|
||||||
|
body = sys.stdin.read()
|
||||||
|
print(json.dumps({
|
||||||
|
'title': sys.argv[1],
|
||||||
|
'body': body,
|
||||||
|
'labels': []
|
||||||
|
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
||||||
|
|
||||||
|
# Create the issue
|
||||||
|
RESPONSE=$(curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues" \
|
||||||
|
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
||||||
|
|
||||||
|
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
||||||
|
|
||||||
|
if [[ -n "$ISSUE_NUM" ]]; then
|
||||||
|
# Apply label (separate call — more reliable across Gitea versions)
|
||||||
|
LABEL_ID=$(curl -sf \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API}/labels" 2>/dev/null \
|
||||||
|
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
||||||
|
| head -1 || true)
|
||||||
|
|
||||||
|
if [[ -n "$LABEL_ID" ]]; then
|
||||||
|
curl -sf -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API}/issues/${ISSUE_NUM}/labels" \
|
||||||
|
-d "{\"labels\":[${LABEL_ID}]}" \
|
||||||
|
> /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
||||||
|
else
|
||||||
|
echo "WARNING: Failed to create issue"
|
||||||
|
echo "Response: ${RESPONSE}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
+1
-3
@@ -17,10 +17,8 @@
|
|||||||
"require-dev": {
|
"require-dev": {
|
||||||
"squizlabs/php_codesniffer": "^3.7",
|
"squizlabs/php_codesniffer": "^3.7",
|
||||||
"phpstan/phpstan": "^1.10",
|
"phpstan/phpstan": "^1.10",
|
||||||
"joomla/coding-standards": "^3.0"
|
"joomla/coding-standards": "^4.0"
|
||||||
},
|
},
|
||||||
"minimum-stability": "alpha",
|
|
||||||
"prefer-stable": true,
|
|
||||||
"config": {
|
"config": {
|
||||||
"sort-packages": true
|
"sort-packages": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
; MokoJoomOpenGraph - Package System Language File
|
; MokoOpenGraph - Package System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PKG_MOKOOG="MokoJoomOpenGraph"
|
PKG_MOKOOG="MokoOpenGraph"
|
||||||
PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more."
|
PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more."
|
||||||
PKG_MOKOOG_PHP_VERSION_ERROR="MokoJoomOpenGraph requires PHP %s or later."
|
PKG_MOKOOG_PHP_VERSION_ERROR="MokoOpenGraph requires PHP %s or later."
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
; MokoJoomOpenGraph - Package System Language File
|
; MokoOpenGraph - Package System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PKG_MOKOOG="MokoJoomOpenGraph"
|
PKG_MOKOOG="MokoOpenGraph"
|
||||||
PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more."
|
PKG_MOKOOG_DESCRIPTION="Complete Open Graph, Twitter Card, and social sharing meta tag management for Joomla. Control how every page appears when shared on Facebook, Twitter/X, LinkedIn, WhatsApp, and more."
|
||||||
PKG_MOKOOG_PHP_VERSION_ERROR="MokoJoomOpenGraph requires PHP %s or later."
|
PKG_MOKOOG_PHP_VERSION_ERROR="MokoOpenGraph requires PHP %s or later."
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage com_mokoog.api
|
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoOG\Api\Controller;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\MVC\Controller\ApiController;
|
|
||||||
|
|
||||||
class TagsController extends ApiController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The content type for JSON:API output.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $contentType = 'tags';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default view for the API.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $default_view = 'tags';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lookup an OG tag by content_type and content_id.
|
|
||||||
*
|
|
||||||
* GET /api/index.php/v1/mokoog/lookup/:content_type/:content_id
|
|
||||||
*
|
|
||||||
* @return static
|
|
||||||
*/
|
|
||||||
public function lookup(): static
|
|
||||||
{
|
|
||||||
$contentType = $this->input->getString('content_type', '');
|
|
||||||
$contentId = $this->input->getInt('content_id', 0);
|
|
||||||
|
|
||||||
if (empty($contentType) || $contentId <= 0) {
|
|
||||||
throw new \RuntimeException('content_type and content_id are required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('id'))
|
|
||||||
->from($db->quoteName('#__mokoog_tags'))
|
|
||||||
->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType))
|
|
||||||
->where($db->quoteName('content_id') . ' = ' . $contentId);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$id = $db->loadResult();
|
|
||||||
|
|
||||||
if (!$id) {
|
|
||||||
throw new \RuntimeException('OG tag not found for ' . $contentType . ':' . $contentId, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->input->set('id', $id);
|
|
||||||
|
|
||||||
return $this->displayItem();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage com_mokoog.api
|
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoOG\Api\View\Tags;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\View\JsonApiView as BaseApiView;
|
|
||||||
|
|
||||||
class JsonapiView extends BaseApiView
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The fields to render in the API response.
|
|
||||||
*
|
|
||||||
* Whitelist of fields from #__mokoog_tags that are safe to expose.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $fieldsToRenderItem = [
|
|
||||||
'id',
|
|
||||||
'content_type',
|
|
||||||
'content_id',
|
|
||||||
'og_title',
|
|
||||||
'og_description',
|
|
||||||
'og_image',
|
|
||||||
'og_type',
|
|
||||||
'seo_title',
|
|
||||||
'meta_description',
|
|
||||||
'robots',
|
|
||||||
'canonical_url',
|
|
||||||
'language',
|
|
||||||
'published',
|
|
||||||
'created',
|
|
||||||
'modified',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The fields to render in list responses.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $fieldsToRenderList = [
|
|
||||||
'id',
|
|
||||||
'content_type',
|
|
||||||
'content_id',
|
|
||||||
'og_title',
|
|
||||||
'og_description',
|
|
||||||
'og_image',
|
|
||||||
'og_type',
|
|
||||||
'seo_title',
|
|
||||||
'meta_description',
|
|
||||||
'robots',
|
|
||||||
'canonical_url',
|
|
||||||
'language',
|
|
||||||
'published',
|
|
||||||
'created',
|
|
||||||
'modified',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -70,37 +70,4 @@
|
|||||||
<option value="0">JUNPUBLISHED</option>
|
<option value="0">JUNPUBLISHED</option>
|
||||||
</field>
|
</field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="seo" label="SEO Meta Tags">
|
|
||||||
<field
|
|
||||||
name="seo_title"
|
|
||||||
type="text"
|
|
||||||
label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE"
|
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC"
|
|
||||||
filter="string"
|
|
||||||
maxlength="70"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="meta_description"
|
|
||||||
type="textarea"
|
|
||||||
label="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION"
|
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC"
|
|
||||||
filter="string"
|
|
||||||
rows="3"
|
|
||||||
maxlength="200"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="robots"
|
|
||||||
type="text"
|
|
||||||
label="PLG_CONTENT_MOKOOG_FIELD_ROBOTS"
|
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC"
|
|
||||||
filter="string"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="canonical_url"
|
|
||||||
type="url"
|
|
||||||
label="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL"
|
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC"
|
|
||||||
filter="url"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
; MokoJoomOpenGraph - Component Language File
|
; MokoOpenGraph - Component Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOOG="MokoJoomOpenGraph"
|
COM_MOKOOG="MokoOpenGraph"
|
||||||
COM_MOKOOG_TAGS_TITLE="MokoJoomOpenGraph - Tag Manager"
|
COM_MOKOOG_TAGS_TITLE="MokoOpenGraph - Tag Manager"
|
||||||
COM_MOKOOG_SUBMENU_TAGS="Tags"
|
COM_MOKOOG_SUBMENU_TAGS="Tags"
|
||||||
COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items."
|
COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items."
|
||||||
COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags"
|
COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags"
|
||||||
@@ -13,47 +13,4 @@ COM_MOKOOG_HEADING_CONTENT_TYPE="Content Type"
|
|||||||
COM_MOKOOG_HEADING_CONTENT_ID="Content ID"
|
COM_MOKOOG_HEADING_CONTENT_ID="Content ID"
|
||||||
COM_MOKOOG_HEADING_OG_TITLE="OG Title"
|
COM_MOKOOG_HEADING_OG_TITLE="OG Title"
|
||||||
COM_MOKOOG_HEADING_IMAGE="Image"
|
COM_MOKOOG_HEADING_IMAGE="Image"
|
||||||
COM_MOKOOG_HEADING_SEO="SEO"
|
|
||||||
COM_MOKOOG_HEADING_DEBUG="Debug"
|
|
||||||
COM_MOKOOG_HEADING_MODIFIED="Modified"
|
COM_MOKOOG_HEADING_MODIFIED="Modified"
|
||||||
|
|
||||||
COM_MOKOOG_SEO_OK="OK"
|
|
||||||
COM_MOKOOG_SEO_MISSING_DESC="No meta description"
|
|
||||||
COM_MOKOOG_SEO_TITLE_LONG="SEO title too long"
|
|
||||||
COM_MOKOOG_SEO_NOINDEX="noindex"
|
|
||||||
|
|
||||||
COM_MOKOOG_FIELD_CONTENT_TYPE="Content Type"
|
|
||||||
COM_MOKOOG_FIELD_CONTENT_ID="Content ID"
|
|
||||||
COM_MOKOOG_FIELD_OG_TITLE="OG Title"
|
|
||||||
COM_MOKOOG_FIELD_OG_TITLE_DESC="Custom title for social sharing."
|
|
||||||
COM_MOKOOG_FIELD_OG_DESCRIPTION="OG Description"
|
|
||||||
COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC="Custom description for social sharing."
|
|
||||||
COM_MOKOOG_FIELD_OG_IMAGE="OG Image"
|
|
||||||
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
|
||||||
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
|
||||||
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
|
||||||
|
|
||||||
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
|
||||||
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
|
||||||
COM_MOKOOG_FILTER_SELECT_TYPE="- Select Type -"
|
|
||||||
COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending"
|
|
||||||
COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending"
|
|
||||||
COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending"
|
|
||||||
COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending"
|
|
||||||
|
|
||||||
COM_MOKOOG_TOOLBAR_BATCH_GENERATE="Batch Generate"
|
|
||||||
COM_MOKOOG_BATCH_TITLE="Batch OG Tag Generation"
|
|
||||||
COM_MOKOOG_BATCH_COUNTING="Counting articles without OG tags..."
|
|
||||||
COM_MOKOOG_BATCH_NONE="All articles already have OG tags."
|
|
||||||
COM_MOKOOG_BATCH_FOUND="articles found without OG tags."
|
|
||||||
COM_MOKOOG_BATCH_PROCESSED="processed"
|
|
||||||
COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!"
|
|
||||||
COM_MOKOOG_BATCH_ERROR="Error:"
|
|
||||||
|
|
||||||
COM_MOKOOG_TOOLBAR_EXPORT="Export CSV"
|
|
||||||
COM_MOKOOG_TOOLBAR_IMPORT="Import CSV"
|
|
||||||
COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded."
|
|
||||||
COM_MOKOOG_IMPORT_INVALID_TYPE="Invalid file type. Please upload a .csv file."
|
|
||||||
COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s."
|
|
||||||
COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file."
|
|
||||||
COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped."
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Component System Language File
|
; MokoOpenGraph - Component System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOOG="MokoJoomOpenGraph"
|
COM_MOKOOG="MokoOpenGraph"
|
||||||
COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata."
|
COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata."
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
; MokoJoomOpenGraph - Component Language File
|
; MokoOpenGraph - Component Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOOG="MokoJoomOpenGraph"
|
COM_MOKOOG="MokoOpenGraph"
|
||||||
COM_MOKOOG_TAGS_TITLE="MokoJoomOpenGraph - Tag Manager"
|
COM_MOKOOG_TAGS_TITLE="MokoOpenGraph - Tag Manager"
|
||||||
COM_MOKOOG_SUBMENU_TAGS="Tags"
|
COM_MOKOOG_SUBMENU_TAGS="Tags"
|
||||||
COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items."
|
COM_MOKOOG_NO_TAGS="No Open Graph tags have been created yet. Tags are created automatically when you edit articles or menu items."
|
||||||
COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags"
|
COM_MOKOOG_TABLE_CAPTION="Table of Open Graph tags"
|
||||||
@@ -13,47 +13,4 @@ COM_MOKOOG_HEADING_CONTENT_TYPE="Content Type"
|
|||||||
COM_MOKOOG_HEADING_CONTENT_ID="Content ID"
|
COM_MOKOOG_HEADING_CONTENT_ID="Content ID"
|
||||||
COM_MOKOOG_HEADING_OG_TITLE="OG Title"
|
COM_MOKOOG_HEADING_OG_TITLE="OG Title"
|
||||||
COM_MOKOOG_HEADING_IMAGE="Image"
|
COM_MOKOOG_HEADING_IMAGE="Image"
|
||||||
COM_MOKOOG_HEADING_SEO="SEO"
|
|
||||||
COM_MOKOOG_HEADING_DEBUG="Debug"
|
|
||||||
COM_MOKOOG_HEADING_MODIFIED="Modified"
|
COM_MOKOOG_HEADING_MODIFIED="Modified"
|
||||||
|
|
||||||
COM_MOKOOG_SEO_OK="OK"
|
|
||||||
COM_MOKOOG_SEO_MISSING_DESC="No meta description"
|
|
||||||
COM_MOKOOG_SEO_TITLE_LONG="SEO title too long"
|
|
||||||
COM_MOKOOG_SEO_NOINDEX="noindex"
|
|
||||||
|
|
||||||
COM_MOKOOG_FIELD_CONTENT_TYPE="Content Type"
|
|
||||||
COM_MOKOOG_FIELD_CONTENT_ID="Content ID"
|
|
||||||
COM_MOKOOG_FIELD_OG_TITLE="OG Title"
|
|
||||||
COM_MOKOOG_FIELD_OG_TITLE_DESC="Custom title for social sharing."
|
|
||||||
COM_MOKOOG_FIELD_OG_DESCRIPTION="OG Description"
|
|
||||||
COM_MOKOOG_FIELD_OG_DESCRIPTION_DESC="Custom description for social sharing."
|
|
||||||
COM_MOKOOG_FIELD_OG_IMAGE="OG Image"
|
|
||||||
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
|
||||||
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
|
||||||
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
|
||||||
|
|
||||||
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
|
||||||
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
|
||||||
COM_MOKOOG_FILTER_SELECT_TYPE="- Select Type -"
|
|
||||||
COM_MOKOOG_HEADING_OG_TITLE_ASC="OG Title ascending"
|
|
||||||
COM_MOKOOG_HEADING_OG_TITLE_DESC="OG Title descending"
|
|
||||||
COM_MOKOOG_HEADING_MODIFIED_ASC="Modified ascending"
|
|
||||||
COM_MOKOOG_HEADING_MODIFIED_DESC="Modified descending"
|
|
||||||
|
|
||||||
COM_MOKOOG_TOOLBAR_BATCH_GENERATE="Batch Generate"
|
|
||||||
COM_MOKOOG_BATCH_TITLE="Batch OG Tag Generation"
|
|
||||||
COM_MOKOOG_BATCH_COUNTING="Counting articles without OG tags..."
|
|
||||||
COM_MOKOOG_BATCH_NONE="All articles already have OG tags."
|
|
||||||
COM_MOKOOG_BATCH_FOUND="articles found without OG tags."
|
|
||||||
COM_MOKOOG_BATCH_PROCESSED="processed"
|
|
||||||
COM_MOKOOG_BATCH_COMPLETE="Batch generation complete!"
|
|
||||||
COM_MOKOOG_BATCH_ERROR="Error:"
|
|
||||||
|
|
||||||
COM_MOKOOG_TOOLBAR_EXPORT="Export CSV"
|
|
||||||
COM_MOKOOG_TOOLBAR_IMPORT="Import CSV"
|
|
||||||
COM_MOKOOG_IMPORT_NO_FILE="No CSV file was uploaded."
|
|
||||||
COM_MOKOOG_IMPORT_INVALID_TYPE="Invalid file type. Please upload a .csv file."
|
|
||||||
COM_MOKOOG_IMPORT_FILE_TOO_LARGE="File is too large. Maximum allowed size is %s."
|
|
||||||
COM_MOKOOG_IMPORT_READ_ERROR="Could not read the uploaded CSV file."
|
|
||||||
COM_MOKOOG_IMPORT_RESULT="Import complete: %d created, %d updated, %d skipped."
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Component System Language File
|
; MokoOpenGraph - Component System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
COM_MOKOOG="MokoJoomOpenGraph"
|
COM_MOKOOG="MokoOpenGraph"
|
||||||
COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata."
|
COM_MOKOOG_DESCRIPTION="Manage Open Graph and social sharing tags for all your content. View, edit, and batch-process OG metadata."
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="component" method="upgrade">
|
<extension type="component" method="upgrade">
|
||||||
<name>com_mokoog</name>
|
<name>com_mokoog</name>
|
||||||
<version>01.00.01-dev</version>
|
<version>01.00.00</version>
|
||||||
<creationDate>2026-05-23</creationDate>
|
<creationDate>2026-05-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -42,7 +42,6 @@
|
|||||||
<filename>provider.php</filename>
|
<filename>provider.php</filename>
|
||||||
</files>
|
</files>
|
||||||
<files folder="src">
|
<files folder="src">
|
||||||
<folder>ContentType</folder>
|
|
||||||
<folder>Controller</folder>
|
<folder>Controller</folder>
|
||||||
<folder>Extension</folder>
|
<folder>Extension</folder>
|
||||||
<folder>Model</folder>
|
<folder>Model</folder>
|
||||||
@@ -69,10 +68,4 @@
|
|||||||
<menu link="option=com_mokoog&view=tags">COM_MOKOOG_SUBMENU_TAGS</menu>
|
<menu link="option=com_mokoog&view=tags">COM_MOKOOG_SUBMENU_TAGS</menu>
|
||||||
</submenu>
|
</submenu>
|
||||||
</administration>
|
</administration>
|
||||||
|
|
||||||
<api>
|
|
||||||
<files folder="api">
|
|
||||||
<folder>src</folder>
|
|
||||||
</files>
|
|
||||||
</api>
|
|
||||||
</extension>
|
</extension>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -23,7 +23,7 @@ class Com_MokoOGInstallerScript
|
|||||||
*/
|
*/
|
||||||
public function install(InstallerAdapter $parent): void
|
public function install(InstallerAdapter $parent): void
|
||||||
{
|
{
|
||||||
echo '<p>MokoJoomOpenGraph component installed successfully.</p>';
|
echo '<p>MokoOpenGraph component installed successfully.</p>';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -35,6 +35,6 @@ class Com_MokoOGInstallerScript
|
|||||||
*/
|
*/
|
||||||
public function update(InstallerAdapter $parent): void
|
public function update(InstallerAdapter $parent): void
|
||||||
{
|
{
|
||||||
echo '<p>MokoJoomOpenGraph component updated successfully.</p>';
|
echo '<p>MokoOpenGraph component updated successfully.</p>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
--
|
--
|
||||||
-- MokoJoomOpenGraph - Database Schema
|
-- MokoOpenGraph - Database Schema
|
||||||
-- Copyright (C) 2026 Moko Consulting. All rights reserved.
|
-- Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
-- License: GPL-3.0-or-later
|
-- License: GPL-3.0-or-later
|
||||||
--
|
--
|
||||||
@@ -12,15 +12,10 @@ CREATE TABLE IF NOT EXISTS `#__mokoog_tags` (
|
|||||||
`og_description` TEXT NOT NULL,
|
`og_description` TEXT NOT NULL,
|
||||||
`og_image` VARCHAR(512) NOT NULL DEFAULT '',
|
`og_image` VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
`og_type` VARCHAR(50) NOT NULL DEFAULT 'article',
|
`og_type` VARCHAR(50) NOT NULL DEFAULT 'article',
|
||||||
`seo_title` VARCHAR(70) NOT NULL DEFAULT '',
|
|
||||||
`meta_description` VARCHAR(200) NOT NULL DEFAULT '',
|
|
||||||
`robots` VARCHAR(100) NOT NULL DEFAULT '',
|
|
||||||
`canonical_url` VARCHAR(512) NOT NULL DEFAULT '',
|
|
||||||
`language` CHAR(7) NOT NULL DEFAULT '*',
|
|
||||||
`published` TINYINT(1) NOT NULL DEFAULT 1,
|
`published` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
`created` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||||
`modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
`modified` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `idx_content_lang` (`content_type`, `content_id`, `language`),
|
UNIQUE KEY `idx_content` (`content_type`, `content_id`),
|
||||||
KEY `idx_published` (`published`)
|
KEY `idx_published` (`published`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
--
|
--
|
||||||
-- MokoJoomOpenGraph - Uninstall
|
-- MokoOpenGraph - Uninstall
|
||||||
--
|
--
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `#__mokoog_tags`;
|
DROP TABLE IF EXISTS `#__mokoog_tags`;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
--
|
|
||||||
-- MokoJoomOpenGraph 01.01.00 — Add SEO meta management columns
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE `#__mokoog_tags`
|
|
||||||
ADD COLUMN `seo_title` VARCHAR(70) NOT NULL DEFAULT '' AFTER `og_type`,
|
|
||||||
ADD COLUMN `meta_description` VARCHAR(200) NOT NULL DEFAULT '' AFTER `seo_title`,
|
|
||||||
ADD COLUMN `robots` VARCHAR(100) NOT NULL DEFAULT '' AFTER `meta_description`,
|
|
||||||
ADD COLUMN `canonical_url` VARCHAR(512) NOT NULL DEFAULT '' AFTER `robots`;
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
--
|
|
||||||
-- MokoJoomOpenGraph 01.02.00 — Add multilingual OG tag support
|
|
||||||
--
|
|
||||||
|
|
||||||
ALTER TABLE `#__mokoog_tags`
|
|
||||||
ADD COLUMN `language` CHAR(7) NOT NULL DEFAULT '*' AFTER `canonical_url`;
|
|
||||||
|
|
||||||
ALTER TABLE `#__mokoog_tags`
|
|
||||||
DROP INDEX `idx_content`,
|
|
||||||
ADD UNIQUE KEY `idx_content_lang` (`content_type`, `content_id`, `language`);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage com_mokoog
|
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoOG\Administrator\ContentType;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
interface ContentTypeInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Check if this adapter can handle the given component/view.
|
|
||||||
*
|
|
||||||
* @param string $option Component option (e.g. com_virtuemart)
|
|
||||||
* @param string $view View name (e.g. productdetails)
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function canHandle(string $option, string $view): bool;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the content type identifier for database storage.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getContentType(): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the title for the content item.
|
|
||||||
*
|
|
||||||
* @param int $id Content item ID
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getTitle(int $id): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a description for the content item.
|
|
||||||
*
|
|
||||||
* @param int $id Content item ID
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getDescription(int $id): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the primary image for the content item.
|
|
||||||
*
|
|
||||||
* @param int $id Content item ID
|
|
||||||
*
|
|
||||||
* @return string Image path relative to JPATH_ROOT, or empty string
|
|
||||||
*/
|
|
||||||
public function getImage(int $id): string;
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage com_mokoog
|
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoOG\Administrator\ContentType;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
|
|
||||||
class HikaShopAdapter implements ContentTypeInterface
|
|
||||||
{
|
|
||||||
public function canHandle(string $option, string $view): bool
|
|
||||||
{
|
|
||||||
return $option === 'com_hikashop' && $view === 'product';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getContentType(): string
|
|
||||||
{
|
|
||||||
return 'com_hikashop';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTitle(int $id): string
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('product_name'))
|
|
||||||
->from($db->quoteName('#__hikashop_product'))
|
|
||||||
->where($db->quoteName('product_id') . ' = ' . $id);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
|
|
||||||
return $db->loadResult() ?: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDescription(int $id): string
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('product_description'))
|
|
||||||
->from($db->quoteName('#__hikashop_product'))
|
|
||||||
->where($db->quoteName('product_id') . ' = ' . $id);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$text = $db->loadResult() ?: '';
|
|
||||||
$text = strip_tags($text);
|
|
||||||
$text = trim(preg_replace('/\s+/', ' ', $text));
|
|
||||||
|
|
||||||
if (\strlen($text) > 160) {
|
|
||||||
$text = mb_substr($text, 0, 157) . '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getImage(int $id): string
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('f.file_path'))
|
|
||||||
->from($db->quoteName('#__hikashop_file', 'f'))
|
|
||||||
->where($db->quoteName('f.file_ref_id') . ' = ' . $id)
|
|
||||||
->where($db->quoteName('f.file_type') . ' = ' . $db->quote('product'))
|
|
||||||
->order($db->quoteName('f.file_ordering') . ' ASC');
|
|
||||||
|
|
||||||
$db->setQuery($query, 0, 1);
|
|
||||||
|
|
||||||
return $db->loadResult() ?: '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage com_mokoog
|
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoOG\Administrator\ContentType;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
|
|
||||||
class K2Adapter implements ContentTypeInterface
|
|
||||||
{
|
|
||||||
public function canHandle(string $option, string $view): bool
|
|
||||||
{
|
|
||||||
return $option === 'com_k2' && $view === 'item';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getContentType(): string
|
|
||||||
{
|
|
||||||
return 'com_k2';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTitle(int $id): string
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('title'))
|
|
||||||
->from($db->quoteName('#__k2_items'))
|
|
||||||
->where($db->quoteName('id') . ' = ' . $id);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
|
|
||||||
return $db->loadResult() ?: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDescription(int $id): string
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('introtext'))
|
|
||||||
->from($db->quoteName('#__k2_items'))
|
|
||||||
->where($db->quoteName('id') . ' = ' . $id);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$text = $db->loadResult() ?: '';
|
|
||||||
$text = strip_tags($text);
|
|
||||||
$text = trim(preg_replace('/\s+/', ' ', $text));
|
|
||||||
|
|
||||||
if (\strlen($text) > 160) {
|
|
||||||
$text = mb_substr($text, 0, 157) . '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getImage(int $id): string
|
|
||||||
{
|
|
||||||
// K2 stores images as media/k2/items/cache/{md5}_L.jpg
|
|
||||||
$imagePath = 'media/k2/items/cache/' . md5('Image' . $id) . '_L.jpg';
|
|
||||||
|
|
||||||
if (is_file(JPATH_ROOT . '/' . $imagePath)) {
|
|
||||||
return $imagePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage com_mokoog
|
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoOG\Administrator\ContentType;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
|
|
||||||
class VirtueMartAdapter implements ContentTypeInterface
|
|
||||||
{
|
|
||||||
public function canHandle(string $option, string $view): bool
|
|
||||||
{
|
|
||||||
return $option === 'com_virtuemart' && $view === 'productdetails';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getContentType(): string
|
|
||||||
{
|
|
||||||
return 'com_virtuemart';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getTitle(int $id): string
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('product_name'))
|
|
||||||
->from($db->quoteName('#__virtuemart_products_' . $this->getLangTag()))
|
|
||||||
->where($db->quoteName('virtuemart_product_id') . ' = ' . $id);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
|
|
||||||
return $db->loadResult() ?: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDescription(int $id): string
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('product_s_desc'))
|
|
||||||
->from($db->quoteName('#__virtuemart_products_' . $this->getLangTag()))
|
|
||||||
->where($db->quoteName('virtuemart_product_id') . ' = ' . $id);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$desc = $db->loadResult() ?: '';
|
|
||||||
|
|
||||||
return strip_tags($desc);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getImage(int $id): string
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('m.file_url'))
|
|
||||||
->from($db->quoteName('#__virtuemart_product_medias', 'pm'))
|
|
||||||
->join('INNER', $db->quoteName('#__virtuemart_medias', 'm') . ' ON ' . $db->quoteName('m.virtuemart_media_id') . ' = ' . $db->quoteName('pm.virtuemart_media_id'))
|
|
||||||
->where($db->quoteName('pm.virtuemart_product_id') . ' = ' . $id)
|
|
||||||
->order($db->quoteName('pm.ordering') . ' ASC');
|
|
||||||
|
|
||||||
$db->setQuery($query, 0, 1);
|
|
||||||
|
|
||||||
return $db->loadResult() ?: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the VirtueMart language table suffix.
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function getLangTag(): string
|
|
||||||
{
|
|
||||||
$lang = Factory::getLanguage()->getTag();
|
|
||||||
|
|
||||||
return strtolower(str_replace('-', '_', $lang));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage com_mokoog
|
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoOG\Administrator\Controller;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\MVC\Controller\BaseController;
|
|
||||||
use Joomla\CMS\Response\JsonResponse;
|
|
||||||
use Joomla\CMS\Session\Session;
|
|
||||||
|
|
||||||
class BatchController extends BaseController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Count the total articles eligible for batch generation.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function count(): void
|
|
||||||
{
|
|
||||||
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
|
|
||||||
|
|
||||||
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
|
|
||||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('COUNT(*)')
|
|
||||||
->from($db->quoteName('#__content', 'c'))
|
|
||||||
->leftJoin(
|
|
||||||
$db->quoteName('#__mokoog_tags', 't')
|
|
||||||
. ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content')
|
|
||||||
. ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id')
|
|
||||||
)
|
|
||||||
->where($db->quoteName('c.state') . ' = 1')
|
|
||||||
->where($db->quoteName('t.id') . ' IS NULL');
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$total = (int) $db->loadResult();
|
|
||||||
|
|
||||||
echo new JsonResponse(['total' => $total]);
|
|
||||||
|
|
||||||
Factory::getApplication()->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process a chunk of articles for batch OG generation.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function process(): void
|
|
||||||
{
|
|
||||||
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
|
|
||||||
|
|
||||||
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokoog')) {
|
|
||||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = Factory::getApplication();
|
|
||||||
$offset = $app->getInput()->getInt('offset', 0);
|
|
||||||
$limit = $app->getInput()->getInt('limit', 50);
|
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName([
|
|
||||||
'c.id', 'c.title', 'c.metadesc', 'c.introtext', 'c.fulltext', 'c.images',
|
|
||||||
]))
|
|
||||||
->from($db->quoteName('#__content', 'c'))
|
|
||||||
->leftJoin(
|
|
||||||
$db->quoteName('#__mokoog_tags', 't')
|
|
||||||
. ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content')
|
|
||||||
. ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id')
|
|
||||||
)
|
|
||||||
->where($db->quoteName('c.state') . ' = 1')
|
|
||||||
->where($db->quoteName('t.id') . ' IS NULL')
|
|
||||||
->order($db->quoteName('c.id') . ' ASC');
|
|
||||||
|
|
||||||
$db->setQuery($query, $offset, $limit);
|
|
||||||
$articles = $db->loadObjectList();
|
|
||||||
|
|
||||||
$created = 0;
|
|
||||||
$now = Factory::getDate()->toSql();
|
|
||||||
|
|
||||||
foreach ($articles as $article) {
|
|
||||||
$ogTitle = $article->title;
|
|
||||||
$ogDescription = $this->extractDescription($article);
|
|
||||||
$ogImage = $this->extractImage($article);
|
|
||||||
|
|
||||||
$record = (object) [
|
|
||||||
'content_type' => 'com_content',
|
|
||||||
'content_id' => (int) $article->id,
|
|
||||||
'og_title' => $ogTitle,
|
|
||||||
'og_description' => $ogDescription,
|
|
||||||
'og_image' => $ogImage,
|
|
||||||
'og_type' => 'article',
|
|
||||||
'seo_title' => '',
|
|
||||||
'meta_description' => $article->metadesc ?: '',
|
|
||||||
'robots' => '',
|
|
||||||
'canonical_url' => '',
|
|
||||||
'published' => 1,
|
|
||||||
'created' => $now,
|
|
||||||
'modified' => $now,
|
|
||||||
];
|
|
||||||
|
|
||||||
$db->insertObject('#__mokoog_tags', $record);
|
|
||||||
$created++;
|
|
||||||
}
|
|
||||||
|
|
||||||
echo new JsonResponse([
|
|
||||||
'created' => $created,
|
|
||||||
'offset' => $offset,
|
|
||||||
'processed' => $offset + $created,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$app->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a description from article content.
|
|
||||||
*
|
|
||||||
* @param object $article Article record
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function extractDescription(object $article): string
|
|
||||||
{
|
|
||||||
// Prefer meta description if set
|
|
||||||
if (!empty($article->metadesc)) {
|
|
||||||
return $article->metadesc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to intro text
|
|
||||||
$text = $article->introtext ?: $article->fulltext;
|
|
||||||
$text = strip_tags($text);
|
|
||||||
$text = trim(preg_replace('/\s+/', ' ', $text));
|
|
||||||
|
|
||||||
if (\strlen($text) > 160) {
|
|
||||||
$text = mb_substr($text, 0, 157) . '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
return $text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract the best image from article data.
|
|
||||||
*
|
|
||||||
* @param object $article Article record
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private function extractImage(object $article): string
|
|
||||||
{
|
|
||||||
if (!empty($article->images)) {
|
|
||||||
$images = json_decode($article->images, true);
|
|
||||||
|
|
||||||
if (!empty($images['image_fulltext'])) {
|
|
||||||
return $images['image_fulltext'];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($images['image_intro'])) {
|
|
||||||
return $images['image_intro'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage com_mokoog
|
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoOG\Administrator\Controller;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\MVC\Controller\BaseController;
|
|
||||||
use Joomla\CMS\Session\Session;
|
|
||||||
|
|
||||||
class ImportExportController extends BaseController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Maximum upload file size in bytes (2 MB).
|
|
||||||
*/
|
|
||||||
private const MAX_FILE_SIZE = 2 * 1024 * 1024;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allowed content_type patterns for import.
|
|
||||||
*/
|
|
||||||
private const CONTENT_TYPE_PATTERN = '/^[a-z][a-z0-9_.]*$/';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export all OG tags as CSV.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function export(): void
|
|
||||||
{
|
|
||||||
Session::checkToken('get') || jexit(Text::_('JINVALID_TOKEN'));
|
|
||||||
|
|
||||||
if (!Factory::getApplication()->getIdentity()->authorise('core.manage', 'com_mokoog')) {
|
|
||||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = Factory::getApplication();
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
|
|
||||||
// Join with #__content to get article titles for reference
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select([
|
|
||||||
$db->quoteName('t.content_type'),
|
|
||||||
$db->quoteName('t.content_id'),
|
|
||||||
'COALESCE(' . $db->quoteName('c.title') . ', ' . $db->quote('') . ') AS ' . $db->quoteName('article_title'),
|
|
||||||
$db->quoteName('t.og_title'),
|
|
||||||
$db->quoteName('t.og_description'),
|
|
||||||
$db->quoteName('t.og_image'),
|
|
||||||
$db->quoteName('t.og_type'),
|
|
||||||
$db->quoteName('t.seo_title'),
|
|
||||||
$db->quoteName('t.meta_description'),
|
|
||||||
$db->quoteName('t.robots'),
|
|
||||||
$db->quoteName('t.canonical_url'),
|
|
||||||
])
|
|
||||||
->from($db->quoteName('#__mokoog_tags', 't'))
|
|
||||||
->leftJoin(
|
|
||||||
$db->quoteName('#__content', 'c')
|
|
||||||
. ' ON ' . $db->quoteName('t.content_type') . ' = ' . $db->quote('com_content')
|
|
||||||
. ' AND ' . $db->quoteName('t.content_id') . ' = ' . $db->quoteName('c.id')
|
|
||||||
)
|
|
||||||
->order($db->quoteName('t.content_type') . ', ' . $db->quoteName('t.content_id'));
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$rows = $db->loadAssocList();
|
|
||||||
|
|
||||||
// Send CSV headers
|
|
||||||
$app->setHeader('Content-Type', 'text/csv; charset=utf-8');
|
|
||||||
$app->setHeader('Content-Disposition', 'attachment; filename="mokoog_tags_export.csv"');
|
|
||||||
$app->sendHeaders();
|
|
||||||
|
|
||||||
$output = fopen('php://output', 'w');
|
|
||||||
|
|
||||||
// Header row
|
|
||||||
fputcsv($output, [
|
|
||||||
'content_type', 'content_id', 'article_title',
|
|
||||||
'og_title', 'og_description', 'og_image', 'og_type',
|
|
||||||
'seo_title', 'meta_description', 'robots', 'canonical_url',
|
|
||||||
]);
|
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
fputcsv($output, $row);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose($output);
|
|
||||||
$app->close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import OG tags from uploaded CSV.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function import(): void
|
|
||||||
{
|
|
||||||
Session::checkToken() || jexit(Text::_('JINVALID_TOKEN'));
|
|
||||||
|
|
||||||
$identity = Factory::getApplication()->getIdentity();
|
|
||||||
|
|
||||||
if (!$identity->authorise('core.create', 'com_mokoog') || !$identity->authorise('core.edit', 'com_mokoog')) {
|
|
||||||
throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_ACCESS_FORBIDDEN'), 403);
|
|
||||||
}
|
|
||||||
|
|
||||||
$app = Factory::getApplication();
|
|
||||||
$input = $app->getInput();
|
|
||||||
$files = $input->files->get('jform', [], 'array');
|
|
||||||
|
|
||||||
if (empty($files['csv_file']['tmp_name'])) {
|
|
||||||
$app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_NO_FILE'), 'error');
|
|
||||||
$app->redirect('index.php?option=com_mokoog&view=tags');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$csvFile = $files['csv_file'];
|
|
||||||
|
|
||||||
// Validate file extension
|
|
||||||
$ext = strtolower(pathinfo($csvFile['name'] ?? '', PATHINFO_EXTENSION));
|
|
||||||
|
|
||||||
if ($ext !== 'csv') {
|
|
||||||
$app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_INVALID_TYPE'), 'error');
|
|
||||||
$app->redirect('index.php?option=com_mokoog&view=tags');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate MIME type
|
|
||||||
$allowedMimes = ['text/csv', 'text/plain', 'application/csv', 'application/vnd.ms-excel'];
|
|
||||||
|
|
||||||
if (!empty($csvFile['type']) && !\in_array($csvFile['type'], $allowedMimes, true)) {
|
|
||||||
$app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_INVALID_TYPE'), 'error');
|
|
||||||
$app->redirect('index.php?option=com_mokoog&view=tags');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file size
|
|
||||||
if (($csvFile['size'] ?? 0) > self::MAX_FILE_SIZE) {
|
|
||||||
$app->enqueueMessage(Text::sprintf('COM_MOKOOG_IMPORT_FILE_TOO_LARGE', '2 MB'), 'error');
|
|
||||||
$app->redirect('index.php?option=com_mokoog&view=tags');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmpFile = $csvFile['tmp_name'];
|
|
||||||
$handle = fopen($tmpFile, 'r');
|
|
||||||
|
|
||||||
if (!$handle) {
|
|
||||||
$app->enqueueMessage(Text::_('COM_MOKOOG_IMPORT_READ_ERROR'), 'error');
|
|
||||||
$app->redirect('index.php?option=com_mokoog&view=tags');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
$header = fgetcsv($handle);
|
|
||||||
$created = 0;
|
|
||||||
$updated = 0;
|
|
||||||
$skipped = 0;
|
|
||||||
$now = Factory::getDate()->toSql();
|
|
||||||
|
|
||||||
while (($row = fgetcsv($handle)) !== false) {
|
|
||||||
if (\count($row) < 7) {
|
|
||||||
$skipped++;
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$contentType = trim($row[0]);
|
|
||||||
$contentId = (int) $row[1];
|
|
||||||
// $row[2] = article_title (informational, skip)
|
|
||||||
$ogTitle = trim($row[3] ?? '');
|
|
||||||
$ogDescription = trim($row[4] ?? '');
|
|
||||||
$ogImage = trim($row[5] ?? '');
|
|
||||||
$ogType = trim($row[6] ?? 'article');
|
|
||||||
$seoTitle = trim($row[7] ?? '');
|
|
||||||
$metaDesc = trim($row[8] ?? '');
|
|
||||||
$robots = trim($row[9] ?? '');
|
|
||||||
$canonicalUrl = trim($row[10] ?? '');
|
|
||||||
|
|
||||||
if (empty($contentType) || $contentId <= 0) {
|
|
||||||
$skipped++;
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate content_type against allowed pattern
|
|
||||||
if (!preg_match(self::CONTENT_TYPE_PATTERN, $contentType)) {
|
|
||||||
$skipped++;
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing record
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('id'))
|
|
||||||
->from($db->quoteName('#__mokoog_tags'))
|
|
||||||
->where($db->quoteName('content_type') . ' = ' . $db->quote($contentType))
|
|
||||||
->where($db->quoteName('content_id') . ' = ' . $contentId);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$existingId = $db->loadResult();
|
|
||||||
|
|
||||||
$record = (object) [
|
|
||||||
'content_type' => $contentType,
|
|
||||||
'content_id' => $contentId,
|
|
||||||
'og_title' => $ogTitle,
|
|
||||||
'og_description' => $ogDescription,
|
|
||||||
'og_image' => $ogImage,
|
|
||||||
'og_type' => $ogType,
|
|
||||||
'seo_title' => $seoTitle,
|
|
||||||
'meta_description' => $metaDesc,
|
|
||||||
'robots' => $robots,
|
|
||||||
'canonical_url' => $canonicalUrl,
|
|
||||||
'published' => 1,
|
|
||||||
'modified' => $now,
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($existingId) {
|
|
||||||
$record->id = $existingId;
|
|
||||||
$db->updateObject('#__mokoog_tags', $record, 'id');
|
|
||||||
$updated++;
|
|
||||||
} else {
|
|
||||||
$record->created = $now;
|
|
||||||
$db->insertObject('#__mokoog_tags', $record);
|
|
||||||
$created++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose($handle);
|
|
||||||
|
|
||||||
$app->enqueueMessage(
|
|
||||||
Text::sprintf('COM_MOKOOG_IMPORT_RESULT', $created, $updated, $skipped),
|
|
||||||
'success'
|
|
||||||
);
|
|
||||||
$app->redirect('index.php?option=com_mokoog&view=tags');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage com_mokoog
|
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Joomla\Component\MokoOG\Administrator\Model;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\MVC\Model\AdminModel;
|
|
||||||
|
|
||||||
class TagModel extends AdminModel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the form for the item.
|
|
||||||
*
|
|
||||||
* @param array $data Form data
|
|
||||||
* @param bool $loadData Load data from state
|
|
||||||
*
|
|
||||||
* @return \Joomla\CMS\Form\Form|false
|
|
||||||
*/
|
|
||||||
public function getForm($data = [], $loadData = true)
|
|
||||||
{
|
|
||||||
$form = $this->loadForm(
|
|
||||||
'com_mokoog.tag',
|
|
||||||
'tag',
|
|
||||||
['control' => 'jform', 'load_data' => $loadData]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $form ?: false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the form data.
|
|
||||||
*
|
|
||||||
* @return object
|
|
||||||
*/
|
|
||||||
protected function loadFormData(): object
|
|
||||||
{
|
|
||||||
$data = Factory::getApplication()->getUserState('com_mokoog.edit.tag.data', []);
|
|
||||||
|
|
||||||
if (empty($data)) {
|
|
||||||
$data = $this->getItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the table class name.
|
|
||||||
*
|
|
||||||
* @param string $name Table name
|
|
||||||
* @param string $prefix Table prefix
|
|
||||||
* @param array $options Table options
|
|
||||||
*
|
|
||||||
* @return \Joomla\CMS\Table\Table
|
|
||||||
*/
|
|
||||||
public function getTable($name = 'Tag', $prefix = 'Administrator', $options = [])
|
|
||||||
{
|
|
||||||
return parent::getTable($name, $prefix, $options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -39,20 +39,6 @@ class HtmlView extends BaseHtmlView
|
|||||||
*/
|
*/
|
||||||
protected $state;
|
protected $state;
|
||||||
|
|
||||||
/**
|
|
||||||
* The filter form.
|
|
||||||
*
|
|
||||||
* @var \Joomla\CMS\Form\Form|null
|
|
||||||
*/
|
|
||||||
public $filterForm;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The active filters.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
public $activeFilters = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the view.
|
* Display the view.
|
||||||
*
|
*
|
||||||
@@ -62,11 +48,9 @@ class HtmlView extends BaseHtmlView
|
|||||||
*/
|
*/
|
||||||
public function display($tpl = null): void
|
public function display($tpl = null): void
|
||||||
{
|
{
|
||||||
$this->items = $this->get('Items');
|
$this->items = $this->get('Items');
|
||||||
$this->pagination = $this->get('Pagination');
|
$this->pagination = $this->get('Pagination');
|
||||||
$this->state = $this->get('State');
|
$this->state = $this->get('State');
|
||||||
$this->filterForm = $this->get('FilterForm');
|
|
||||||
$this->activeFilters = $this->get('ActiveFilters');
|
|
||||||
|
|
||||||
$this->addToolbar();
|
$this->addToolbar();
|
||||||
|
|
||||||
@@ -81,8 +65,6 @@ class HtmlView extends BaseHtmlView
|
|||||||
protected function addToolbar(): void
|
protected function addToolbar(): void
|
||||||
{
|
{
|
||||||
ToolbarHelper::title(Text::_('COM_MOKOOG_TAGS_TITLE'), 'bookmark');
|
ToolbarHelper::title(Text::_('COM_MOKOOG_TAGS_TITLE'), 'bookmark');
|
||||||
ToolbarHelper::custom('batch.generate', 'refresh', '', 'COM_MOKOOG_TOOLBAR_BATCH_GENERATE', false);
|
|
||||||
ToolbarHelper::custom('importexport.export', 'download', '', 'COM_MOKOOG_TOOLBAR_EXPORT', false);
|
|
||||||
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'tags.delete');
|
ToolbarHelper::deleteList('JGLOBAL_CONFIRM_DELETE', 'tags.delete');
|
||||||
ToolbarHelper::preferences('com_mokoog');
|
ToolbarHelper::preferences('com_mokoog');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage com_mokoog
|
* @subpackage com_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -14,18 +14,14 @@ use Joomla\CMS\HTML\HTMLHelper;
|
|||||||
use Joomla\CMS\Language\Text;
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\Layout\LayoutHelper;
|
use Joomla\CMS\Layout\LayoutHelper;
|
||||||
use Joomla\CMS\Router\Route;
|
use Joomla\CMS\Router\Route;
|
||||||
use Joomla\CMS\Uri\Uri;
|
|
||||||
use Joomla\CMS\Session\Session;
|
|
||||||
|
|
||||||
/** @var \Joomla\Component\MokoOG\Administrator\View\Tags\HtmlView $this */
|
/** @var \Joomla\Component\MokoOG\Administrator\View\Tags\HtmlView $this */
|
||||||
|
|
||||||
$token = Session::getFormToken();
|
|
||||||
?>
|
?>
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokoog&view=tags'); ?>" method="post" name="adminForm" id="adminForm">
|
<form action="<?php echo Route::_('index.php?option=com_mokoog&view=tags'); ?>" method="post" name="adminForm" id="adminForm">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div id="j-main-container" class="j-main-container">
|
<div id="j-main-container" class="j-main-container">
|
||||||
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
|
|
||||||
|
|
||||||
<?php if (empty($this->items)) : ?>
|
<?php if (empty($this->items)) : ?>
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
@@ -54,15 +50,9 @@ $token = Session::getFormToken();
|
|||||||
<th scope="col" class="w-10">
|
<th scope="col" class="w-10">
|
||||||
<?php echo Text::_('COM_MOKOOG_HEADING_IMAGE'); ?>
|
<?php echo Text::_('COM_MOKOOG_HEADING_IMAGE'); ?>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="w-10">
|
|
||||||
<?php echo Text::_('COM_MOKOOG_HEADING_SEO'); ?>
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="w-10">
|
<th scope="col" class="w-10">
|
||||||
<?php echo Text::_('JSTATUS'); ?>
|
<?php echo Text::_('JSTATUS'); ?>
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" class="w-10">
|
|
||||||
<?php echo Text::_('COM_MOKOOG_HEADING_DEBUG'); ?>
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="w-10">
|
<th scope="col" class="w-10">
|
||||||
<?php echo Text::_('COM_MOKOOG_HEADING_MODIFIED'); ?>
|
<?php echo Text::_('COM_MOKOOG_HEADING_MODIFIED'); ?>
|
||||||
</th>
|
</th>
|
||||||
@@ -93,50 +83,9 @@ $token = Session::getFormToken();
|
|||||||
<span class="icon-minus-circle text-muted" aria-hidden="true"></span>
|
<span class="icon-minus-circle text-muted" aria-hidden="true"></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<?php
|
|
||||||
$seoIssues = [];
|
|
||||||
|
|
||||||
if (empty($item->meta_description)) {
|
|
||||||
$seoIssues[] = Text::_('COM_MOKOOG_SEO_MISSING_DESC');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($item->seo_title) && \strlen($item->seo_title) > 60) {
|
|
||||||
$seoIssues[] = Text::_('COM_MOKOOG_SEO_TITLE_LONG');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($item->robots) && str_contains($item->robots, 'noindex')) {
|
|
||||||
$seoIssues[] = Text::_('COM_MOKOOG_SEO_NOINDEX');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($seoIssues)) : ?>
|
|
||||||
<span class="badge bg-success"><?php echo Text::_('COM_MOKOOG_SEO_OK'); ?></span>
|
|
||||||
<?php else : ?>
|
|
||||||
<?php foreach ($seoIssues as $issue) : ?>
|
|
||||||
<span class="badge bg-warning text-dark"><?php echo $issue; ?></span>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<?php echo $item->published ? Text::_('JPUBLISHED') : Text::_('JUNPUBLISHED'); ?>
|
<?php echo $item->published ? Text::_('JPUBLISHED') : Text::_('JUNPUBLISHED'); ?>
|
||||||
</td>
|
</td>
|
||||||
<td class="mokoog-debug-links">
|
|
||||||
<?php
|
|
||||||
// Build frontend URL for this content item
|
|
||||||
if ($item->content_type === 'com_content') {
|
|
||||||
$debugUrl = Uri::root() . 'index.php?option=com_content&view=article&id=' . (int) $item->content_id;
|
|
||||||
} elseif ($item->content_type === 'menu') {
|
|
||||||
$debugUrl = Uri::root() . 'index.php?Itemid=' . (int) $item->content_id;
|
|
||||||
} elseif ($item->content_type === 'com_content.category') {
|
|
||||||
$debugUrl = Uri::root() . 'index.php?option=com_content&view=category&id=' . (int) $item->content_id;
|
|
||||||
} else {
|
|
||||||
$debugUrl = Uri::root();
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
<a href="https://developers.facebook.com/tools/debug/?q=<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="Facebook Debugger" class="btn btn-sm btn-outline-primary">FB</a>
|
|
||||||
<a href="https://www.linkedin.com/post-inspector/inspect/<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="LinkedIn Inspector" class="btn btn-sm btn-outline-info">LI</a>
|
|
||||||
<a href="https://search.google.com/test/rich-results?url=<?php echo urlencode($debugUrl); ?>" target="_blank" rel="noopener" title="Google Rich Results" class="btn btn-sm btn-outline-success">G</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<?php echo HTMLHelper::_('date', $item->modified, Text::_('DATE_FORMAT_LC4')); ?>
|
<?php echo HTMLHelper::_('date', $item->modified, Text::_('DATE_FORMAT_LC4')); ?>
|
||||||
</td>
|
</td>
|
||||||
@@ -158,85 +107,3 @@ $token = Session::getFormToken();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Batch Generation Progress -->
|
|
||||||
<div id="mokoog-batch-panel" style="display:none;" class="card mt-3">
|
|
||||||
<div class="card-body">
|
|
||||||
<h4><?php echo Text::_('COM_MOKOOG_BATCH_TITLE'); ?></h4>
|
|
||||||
<div class="progress mb-2">
|
|
||||||
<div id="mokoog-batch-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%">0%</div>
|
|
||||||
</div>
|
|
||||||
<p id="mokoog-batch-status"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Intercept the batch.generate toolbar button
|
|
||||||
var origSubmitbutton = Joomla.submitbutton;
|
|
||||||
Joomla.submitbutton = function(task) {
|
|
||||||
if (task === 'batch.generate') {
|
|
||||||
mokoogBatchGenerate();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (origSubmitbutton) {
|
|
||||||
origSubmitbutton(task);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function mokoogBatchGenerate() {
|
|
||||||
var panel = document.getElementById('mokoog-batch-panel');
|
|
||||||
var bar = document.getElementById('mokoog-batch-bar');
|
|
||||||
var status = document.getElementById('mokoog-batch-status');
|
|
||||||
var token = '<?php echo $token; ?>';
|
|
||||||
var chunkSize = 50;
|
|
||||||
|
|
||||||
panel.style.display = 'block';
|
|
||||||
status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COUNTING', true); ?>';
|
|
||||||
|
|
||||||
// Step 1: Count eligible articles
|
|
||||||
fetch('index.php?option=com_mokoog&task=batch.count&format=json&' + token + '=1')
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(resp) {
|
|
||||||
var total = resp.data.total;
|
|
||||||
if (total === 0) {
|
|
||||||
bar.style.width = '100%';
|
|
||||||
bar.textContent = '100%';
|
|
||||||
bar.classList.remove('progress-bar-animated');
|
|
||||||
bar.classList.add('bg-success');
|
|
||||||
status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_NONE', true); ?>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
status.textContent = total + ' <?php echo Text::_('COM_MOKOOG_BATCH_FOUND', true); ?>';
|
|
||||||
processChunk(0, total, chunkSize, token, bar, status);
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_ERROR', true); ?> ' + err.message;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function processChunk(offset, total, chunkSize, token, bar, status) {
|
|
||||||
fetch('index.php?option=com_mokoog&task=batch.process&format=json&offset=' + offset + '&limit=' + chunkSize + '&' + token + '=1')
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(resp) {
|
|
||||||
var processed = resp.data.processed;
|
|
||||||
var pct = Math.min(100, Math.round((processed / total) * 100));
|
|
||||||
bar.style.width = pct + '%';
|
|
||||||
bar.textContent = pct + '%';
|
|
||||||
status.textContent = processed + ' / ' + total + ' <?php echo Text::_('COM_MOKOOG_BATCH_PROCESSED', true); ?>';
|
|
||||||
|
|
||||||
if (processed < total) {
|
|
||||||
processChunk(processed, total, chunkSize, token, bar, status);
|
|
||||||
} else {
|
|
||||||
bar.classList.remove('progress-bar-animated');
|
|
||||||
bar.classList.add('bg-success');
|
|
||||||
status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_COMPLETE', true); ?> ' + total + ' articles.';
|
|
||||||
setTimeout(function() { location.reload(); }, 2000);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
status.textContent = '<?php echo Text::_('COM_MOKOOG_BATCH_ERROR', true); ?> ' + err.message;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!--
|
<!--
|
||||||
* @package MokoJoomOpenGraph
|
* @package MokoOpenGraph
|
||||||
* @subpackage plg_content_mokoog
|
* @subpackage plg_content_mokoog
|
||||||
* @author Moko Consulting <hello@mokoconsulting.tech>
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
@@ -50,48 +50,5 @@
|
|||||||
<option value="video.other">Video</option>
|
<option value="video.other">Video</option>
|
||||||
</field>
|
</field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="mokoog_seo" label="PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL"
|
|
||||||
description="PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC">
|
|
||||||
<field
|
|
||||||
name="seo_title"
|
|
||||||
type="text"
|
|
||||||
label="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE"
|
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC"
|
|
||||||
filter="string"
|
|
||||||
maxlength="70"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="meta_description"
|
|
||||||
type="textarea"
|
|
||||||
label="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION"
|
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC"
|
|
||||||
filter="string"
|
|
||||||
rows="3"
|
|
||||||
maxlength="200"
|
|
||||||
/>
|
|
||||||
<field
|
|
||||||
name="robots"
|
|
||||||
type="list"
|
|
||||||
label="PLG_CONTENT_MOKOOG_FIELD_ROBOTS"
|
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC"
|
|
||||||
default=""
|
|
||||||
multiple="true"
|
|
||||||
>
|
|
||||||
<option value="">PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT</option>
|
|
||||||
<option value="noindex">noindex</option>
|
|
||||||
<option value="nofollow">nofollow</option>
|
|
||||||
<option value="nosnippet">nosnippet</option>
|
|
||||||
<option value="noarchive">noarchive</option>
|
|
||||||
<option value="noimageindex">noimageindex</option>
|
|
||||||
</field>
|
|
||||||
<field
|
|
||||||
name="canonical_url"
|
|
||||||
type="url"
|
|
||||||
label="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL"
|
|
||||||
description="PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC"
|
|
||||||
filter="url"
|
|
||||||
validate="url"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</fields>
|
</fields>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
; MokoJoomOpenGraph - Content Plugin Language File
|
; MokoOpenGraph - Content Plugin Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
@@ -13,16 +13,3 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE="OG Image"
|
|||||||
PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image."
|
PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image."
|
||||||
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type"
|
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type"
|
||||||
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page."
|
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page."
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page."
|
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE="SEO Title"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC="Custom <title> tag. 50-60 characters recommended. Leave blank to use the default page title."
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION="Meta Description"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC="Custom meta description. 150-160 characters recommended. Leave blank to use the default."
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_ROBOTS="Robots Directive"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this page. Leave blank for default (index, follow)."
|
|
||||||
PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL."
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Content Plugin System Language File
|
; MokoOpenGraph - Content Plugin System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG="Content - MokoJoomOpenGraph"
|
PLG_CONTENT_MOKOOG="Content - MokoOpenGraph"
|
||||||
PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control."
|
PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control."
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
; MokoJoomOpenGraph - Content Plugin Language File
|
; MokoOpenGraph - Content Plugin Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
@@ -13,16 +13,3 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE="OG Image"
|
|||||||
PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image."
|
PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recommended: 1200x630px. Leave blank to use the article image."
|
||||||
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type"
|
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE="OG Type"
|
||||||
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page."
|
PLG_CONTENT_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type for this page."
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC="Control search engine meta tags for this page."
|
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE="SEO Title"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_SEO_TITLE_DESC="Custom <title> tag. 50-60 characters recommended. Leave blank to use the default page title."
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION="Meta Description"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_META_DESCRIPTION_DESC="Custom meta description. 150-160 characters recommended. Leave blank to use the default."
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_ROBOTS="Robots Directive"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_ROBOTS_DESC="Search engine indexing directives for this page. Leave blank for default (index, follow)."
|
|
||||||
PLG_CONTENT_MOKOOG_ROBOTS_DEFAULT="- Use default (index, follow) -"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL="Canonical URL"
|
|
||||||
PLG_CONTENT_MOKOOG_FIELD_CANONICAL_URL_DESC="Override the canonical URL for this page. Leave blank to use the current URL."
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; MokoJoomOpenGraph - Content Plugin System Language File
|
; MokoOpenGraph - Content Plugin System Language File
|
||||||
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
; Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
; License: GPL-3.0-or-later
|
; License: GPL-3.0-or-later
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG="Content - MokoJoomOpenGraph"
|
PLG_CONTENT_MOKOOG="Content - MokoOpenGraph"
|
||||||
PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control."
|
PLG_CONTENT_MOKOOG_DESCRIPTION="Adds Open Graph fields to article and menu item edit forms for per-page social sharing control."
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
<html><body bgcolor="#FFFFFF"></body></html>
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
/**
|
|
||||||
* @package MokoJoomOpenGraph
|
|
||||||
* @subpackage plg_content_mokoog
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mokoog-preview-wrapper {
|
|
||||||
margin: 15px 0;
|
|
||||||
padding: 15px;
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-preview-heading {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-platform-label {
|
|
||||||
display: block;
|
|
||||||
color: #999;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 16px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-platform-label:first-of-type {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card {
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 500px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-fb {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-tw {
|
|
||||||
border: 1px solid #cfd9de;
|
|
||||||
border-radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-img {
|
|
||||||
height: 260px;
|
|
||||||
background: #e4e6eb center / cover no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-body {
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-top: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-tw .mokoog-card-body {
|
|
||||||
border-top-color: #cfd9de;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-domain {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #65676b;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-tw .mokoog-card-domain {
|
|
||||||
font-size: 13px;
|
|
||||||
text-transform: none;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1d2129;
|
|
||||||
margin: 3px 0 2px;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-tw .mokoog-card-title {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f1419;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #65676b;
|
|
||||||
line-height: 1.4;
|
|
||||||
max-height: 2.8em;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mokoog-card-tw .mokoog-card-desc {
|
|
||||||
font-size: 15px;
|
|
||||||
color: #536471;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user