Compare commits
49 Commits
version/01.02.00
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e3331170f | |||
| 4699686f26 | |||
| a7fe881d84 | |||
| ab02de34f4 | |||
| 64d9a97db1 | |||
| 3ba1c3ead4 | |||
| 4c091805ee | |||
| 0d4e7785a3 | |||
| 6f13a10a34 | |||
| 5f1e44e66b | |||
| 646dd23e81 | |||
| d4229fd450 | |||
| 5724a1545e | |||
| a04dbfd732 | |||
| bc06710fdd | |||
| 07b296db61 | |||
| 6a0ee812d8 | |||
| fcfa6838e5 | |||
| 908e1d3e1b | |||
| 9539bb44c2 | |||
| 5b29690d34 | |||
| 881bb0a2ae | |||
| e9b34522d3 | |||
| 9aeb588937 | |||
| 9cdc7915a3 | |||
| 72ffaded49 | |||
| 7d1a939b6a | |||
| 23f6fe12a0 | |||
| 4c1d630673 | |||
| 6a3f9c126e | |||
| ddb378a042 | |||
| 560c7458c6 | |||
| e39b617464 | |||
| 20b62b95d8 | |||
| 437a23cec2 | |||
| dac22fdcc4 | |||
| 68eab6fdb2 | |||
| b033cfe4e2 | |||
| e86bb5906b | |||
| b310ddfab2 | |||
| fa12fa5937 | |||
| b52867614c | |||
| b140bc9000 | |||
| 1a16f9ef8e | |||
| 7cdf8b4693 | |||
| d4b24fb57e | |||
| 6169716154 | |||
| 5904bea91d | |||
| 6ef4331f4c |
@@ -1,66 +1,66 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Release
|
# INGROUP: mokocli.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/mokocli
|
||||||
# PATH: /.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 mokocli tools
|
- name: Setup mokocli tools
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
if ! command -v composer &> /dev/null; then
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
fi
|
fi
|
||||||
if [ -d "/opt/mokocli/cli" ]; then
|
if [ -d "/opt/mokocli/cli" ]; then
|
||||||
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/opt/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
else
|
else
|
||||||
git clone --depth 1 --branch main --quiet \
|
git clone --depth 1 --branch main --quiet \
|
||||||
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
"https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/MokoConsulting/mokocli.git" \
|
||||||
/tmp/mokocli
|
/tmp/mokocli
|
||||||
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
cd /tmp/mokocli && composer install --no-dev --no-interaction --quiet
|
||||||
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
echo "MOKO_CLI=/tmp/mokocli/cli" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/version_auto_bump.php \
|
php ${MOKO_CLI}/version_auto_bump.php \
|
||||||
--path . --branch "${GITHUB_REF_NAME}" \
|
--path . --branch "${GITHUB_REF_NAME}" \
|
||||||
--token "${{ secrets.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"
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
# 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. |
|
||||||
# | |
|
# | |
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
# | 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"
|
||||||
|
|
||||||
@@ -30,6 +30,15 @@ on:
|
|||||||
types: [opened, closed]
|
types: [opened, closed]
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- '.mokogitea/workflows/**'
|
||||||
|
- '*.md'
|
||||||
|
- 'wiki/**'
|
||||||
|
- '.editorconfig'
|
||||||
|
- '.gitignore'
|
||||||
|
- '.gitattributes'
|
||||||
|
- '.gitmessage'
|
||||||
|
- 'LICENSE'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
action:
|
action:
|
||||||
@@ -51,7 +60,7 @@ 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
|
||||||
@@ -149,7 +158,7 @@ jobs:
|
|||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ─────────────────────────
|
||||||
release:
|
release:
|
||||||
name: Build & Release Pipeline
|
name: Build & Release Pipeline
|
||||||
runs-on: release
|
runs-on: release
|
||||||
@@ -241,11 +250,47 @@ jobs:
|
|||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
[ -z "$VERSION" ] && VERSION="00.00.00" && echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
if [[ "$PLATFORM" == joomla* ]]; then
|
||||||
|
echo "tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=stable" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "release_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
echo "branch=main" >> "$GITHUB_OUTPUT"
|
echo "branch=main" >> "$GITHUB_OUTPUT"
|
||||||
echo "Published version: ${VERSION}"
|
echo "Published version: ${VERSION}"
|
||||||
|
|
||||||
|
- name: "Create semver tag for non-Joomla repos"
|
||||||
|
id: semver
|
||||||
|
if: |
|
||||||
|
steps.version.outputs.skip != 'true' &&
|
||||||
|
!startsWith(steps.platform.outputs.platform, 'joomla')
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
SEMVER_TAG="v${VERSION}"
|
||||||
|
|
||||||
|
echo "Creating semver tag: ${SEMVER_TAG}"
|
||||||
|
|
||||||
|
# Create the git tag via API
|
||||||
|
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \
|
||||||
|
-X POST -H "Authorization: token ${TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
"${API_BASE}/tags" \
|
||||||
|
-d "{\"tag_name\":\"${SEMVER_TAG}\",\"target\":\"main\",\"message\":\"Release ${VERSION}\"}" 2>/dev/null || echo "000")
|
||||||
|
|
||||||
|
if [ "$HTTP_CODE" = "201" ] || [ "$HTTP_CODE" = "200" ]; then
|
||||||
|
echo "Created semver tag: ${SEMVER_TAG}"
|
||||||
|
elif [ "$HTTP_CODE" = "409" ]; then
|
||||||
|
echo "Semver tag ${SEMVER_TAG} already exists (skipped)"
|
||||||
|
else
|
||||||
|
echo "::warning::Failed to create semver tag ${SEMVER_TAG} (HTTP ${HTTP_CODE})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "semver_tag=${SEMVER_TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Update release notes and promote changelog
|
- name: Update release notes and promote changelog
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
name: "Publish to Composer"
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
- '[0-9]*.[0-9]*.[0-9]*'
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
name: Publish Package
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
!contains(github.event.head_commit.message, '[skip ci]') &&
|
|
||||||
!contains(github.event.head_commit.message, '[skip publish]')
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: composer install --no-dev --no-interaction --prefer-dist --quiet
|
|
||||||
|
|
||||||
- name: Determine version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION=$(php -r "echo json_decode(file_get_contents('composer.json'))->version;")
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Package version: ${VERSION}"
|
|
||||||
|
|
||||||
# Gitea Composer Registry — auto-publishes from tags
|
|
||||||
# The tag push itself registers the package at:
|
|
||||||
# https://git.mokoconsulting.tech/api/packages/MokoConsulting/composer
|
|
||||||
- name: Verify Gitea registry
|
|
||||||
run: |
|
|
||||||
echo "Gitea Composer registry auto-publishes from tags."
|
|
||||||
echo "Package available at: ${GITEA_URL}/api/packages/MokoConsulting/composer"
|
|
||||||
echo "Install: composer require mokoconsulting/mokocli"
|
|
||||||
|
|
||||||
# Packagist — notify of new version
|
|
||||||
- name: Notify Packagist
|
|
||||||
if: secrets.PACKAGIST_TOKEN != ''
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
echo "Notifying Packagist of version ${VERSION}..."
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"repository":{"url":"https://git.mokoconsulting.tech/MokoConsulting/mokocli"}}' \
|
|
||||||
"https://packagist.org/api/update-package?username=mokoconsulting&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \
|
|
||||||
&& echo "Packagist notified" \
|
|
||||||
|| echo "::warning::Packagist notification failed (package may not be registered yet)"
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
echo "## Composer Package Published" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Registry | Status |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Gitea | \`composer require mokoconsulting/mokocli:${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Packagist | \`composer require mokoconsulting/mokocli\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 01.02.00
|
# VERSION: 01.00.00
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+534
-534
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,98 +0,0 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Gitea.Workflow
|
|
||||||
# INGROUP: moko-platform.Security
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /.gitea/workflows/security-audit.yml
|
|
||||||
# VERSION: 01.00.00
|
|
||||||
# BRIEF: Dependency vulnerability scanning for composer and npm packages
|
|
||||||
|
|
||||||
name: "Universal: Security Audit"
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 6 * * 1' # Weekly on Monday at 06:00 UTC
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'composer.json'
|
|
||||||
- 'composer.lock'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
env:
|
|
||||||
NTFY_URL: ${{ vars.NTFY_URL || 'https://ntfy.mokoconsulting.tech' }}
|
|
||||||
NTFY_TOPIC: ${{ vars.NTFY_TOPIC || 'gitea-security' }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
audit:
|
|
||||||
name: Dependency Audit
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Composer audit
|
|
||||||
if: hashFiles('composer.lock') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== Composer Security Audit ==="
|
|
||||||
if ! command -v composer &> /dev/null; then
|
|
||||||
sudo apt-get update -qq
|
|
||||||
sudo apt-get install -y -qq php-cli composer >/dev/null 2>&1
|
|
||||||
fi
|
|
||||||
composer audit --format=plain 2>&1 | tee /tmp/composer-audit.txt
|
|
||||||
RESULT=$?
|
|
||||||
if [ $RESULT -ne 0 ]; then
|
|
||||||
echo "::warning::Composer vulnerabilities found"
|
|
||||||
echo "composer_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
else
|
|
||||||
echo "No known vulnerabilities in composer dependencies"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: NPM audit
|
|
||||||
if: hashFiles('package-lock.json') != ''
|
|
||||||
run: |
|
|
||||||
echo "=== NPM Security Audit ==="
|
|
||||||
npm audit --production 2>&1 | tee /tmp/npm-audit.txt || true
|
|
||||||
if npm audit --production 2>&1 | grep -q "found 0 vulnerabilities"; then
|
|
||||||
echo "No known vulnerabilities in npm dependencies"
|
|
||||||
else
|
|
||||||
echo "::warning::NPM vulnerabilities found"
|
|
||||||
echo "npm_vulnerable=true" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Notify on vulnerabilities
|
|
||||||
if: env.composer_vulnerable == 'true' || env.npm_vulnerable == 'true'
|
|
||||||
run: |
|
|
||||||
REPO="${{ github.event.repository.name }}"
|
|
||||||
curl -sS \
|
|
||||||
-H "Title: ${REPO} has vulnerable dependencies" \
|
|
||||||
-H "Tags: lock,warning" \
|
|
||||||
-H "Priority: high" \
|
|
||||||
-d "Security audit found vulnerabilities. Review dependency updates." \
|
|
||||||
"${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 }}
|
|
||||||
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
# 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
|
|
||||||
+8
-3
@@ -2,16 +2,16 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [01.02.00] --- 2026-06-21
|
## [01.04.00] --- 2026-06-23
|
||||||
|
|
||||||
|
|
||||||
<!-- VERSION: 01.02.00 -->
|
<!-- VERSION: 01.04.00 -->
|
||||||
|
|
||||||
All notable changes to MokoSuiteOpenGraph will be documented in this file.
|
All notable changes to MokoSuiteOpenGraph 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/).
|
||||||
|
|
||||||
## [01.02.00] --- 2026-06-21
|
## [01.04.00] --- 2026-06-23
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
- Fix JSON-LD XSS vulnerability via `</script>` injection in content data (#34)
|
- Fix JSON-LD XSS vulnerability via `</script>` injection in content data (#34)
|
||||||
@@ -20,6 +20,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||||||
- Fix multilingual data corruption in content plugin load/save (#41)
|
- Fix multilingual data corruption in content plugin load/save (#41)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- Fediverse/Mastodon `fediverse:creator` meta tag — first extension on any CMS to support this (#57)
|
||||||
|
- Live character count indicators on OG title, OG description, SEO title, meta description fields with color-coded warnings (#58)
|
||||||
|
- LinkedIn social preview card in article/menu editor alongside Facebook and Twitter/X previews (#61)
|
||||||
|
- `og:video` meta tag support with per-article video URL field, auto-detect MIME type for YouTube/Vimeo/direct files (#59)
|
||||||
|
- Pinterest rich pin tags: `article:tag` from Joomla content tags, `product:availability` from MokoSuiteShop stock (#60)
|
||||||
- Site-wide default OG title and description plugin parameters
|
- Site-wide default OG title and description plugin parameters
|
||||||
- Discord embed color via `theme-color` meta tag (color picker in plugin config)
|
- Discord embed color via `theme-color` meta tag (color picker in plugin config)
|
||||||
- LinkedIn article tags: `article:published_time`, `article:modified_time`, `article:author`
|
- LinkedIn article tags: `article:published_time`, `article:modified_time`, `article:author`
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
# Makefile for Joomla Extensions
|
|
||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# MokoJoomOpenGraph — Open Graph & social sharing meta tag management
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# CONFIGURATION - Customize these for your extension
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
# Extension Configuration
|
|
||||||
EXTENSION_NAME := mokoog
|
|
||||||
EXTENSION_TYPE := package
|
|
||||||
# Options: module, plugin, component, package, template
|
|
||||||
EXTENSION_VERSION := 1.0.0
|
|
||||||
|
|
||||||
# Module Configuration (for modules only)
|
|
||||||
MODULE_TYPE := site
|
|
||||||
# Options: site, admin
|
|
||||||
|
|
||||||
# Plugin Configuration (for plugins only)
|
|
||||||
PLUGIN_GROUP := system
|
|
||||||
# Options: system, content, user, authentication, etc.
|
|
||||||
|
|
||||||
# Directories
|
|
||||||
SRC_DIR := src
|
|
||||||
BUILD_DIR := build
|
|
||||||
DIST_DIR := dist
|
|
||||||
DOCS_DIR := docs
|
|
||||||
|
|
||||||
# Joomla Installation (for local testing - customize paths)
|
|
||||||
JOOMLA_ROOT := /var/www/html/joomla
|
|
||||||
JOOMLA_VERSION := 4
|
|
||||||
|
|
||||||
# Tools
|
|
||||||
PHP := php
|
|
||||||
COMPOSER := composer
|
|
||||||
NPM := npm
|
|
||||||
PHPCS := vendor/bin/phpcs
|
|
||||||
PHPCBF := vendor/bin/phpcbf
|
|
||||||
PHPUNIT := vendor/bin/phpunit
|
|
||||||
ZIP := zip
|
|
||||||
|
|
||||||
# Coding Standards
|
|
||||||
PHPCS_STANDARD := Joomla
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
COLOR_RESET := \033[0m
|
|
||||||
COLOR_GREEN := \033[32m
|
|
||||||
COLOR_YELLOW := \033[33m
|
|
||||||
COLOR_BLUE := \033[34m
|
|
||||||
COLOR_RED := \033[31m
|
|
||||||
|
|
||||||
# ==============================================================================
|
|
||||||
# TARGETS
|
|
||||||
# ==============================================================================
|
|
||||||
|
|
||||||
.PHONY: help
|
|
||||||
help: ## Show this help message
|
|
||||||
@echo "$(COLOR_BLUE)╔════════════════════════════════════════════════════════════╗$(COLOR_RESET)"
|
|
||||||
@echo "$(COLOR_BLUE)║ Joomla Extension Makefile ║$(COLOR_RESET)"
|
|
||||||
@echo "$(COLOR_BLUE)╚════════════════════════════════════════════════════════════╝$(COLOR_RESET)"
|
|
||||||
@echo ""
|
|
||||||
@echo "Extension: $(EXTENSION_NAME) ($(EXTENSION_TYPE)) v$(EXTENSION_VERSION)"
|
|
||||||
@echo ""
|
|
||||||
@echo "$(COLOR_GREEN)Available targets:$(COLOR_RESET)"
|
|
||||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " $(COLOR_BLUE)%-20s$(COLOR_RESET) %s\n", $$1, $$2}'
|
|
||||||
@echo ""
|
|
||||||
|
|
||||||
.PHONY: install-deps
|
|
||||||
install-deps: ## Install all dependencies (Composer + npm)
|
|
||||||
@echo "$(COLOR_BLUE)Installing dependencies...$(COLOR_RESET)"
|
|
||||||
@if [ -f "composer.json" ]; then \
|
|
||||||
$(COMPOSER) install; \
|
|
||||||
echo "$(COLOR_GREEN)✓ Composer dependencies installed$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: lint
|
|
||||||
lint: ## Run PHP linter (syntax check)
|
|
||||||
@echo "$(COLOR_BLUE)Running PHP linter...$(COLOR_RESET)"
|
|
||||||
@find . -name "*.php" ! -path "./vendor/*" ! -path "./node_modules/*" ! -path "./$(BUILD_DIR)/*" \
|
|
||||||
-exec $(PHP) -l {} \; | grep -v "No syntax errors" || true
|
|
||||||
@echo "$(COLOR_GREEN)✓ PHP linting complete$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: phpcs
|
|
||||||
phpcs: ## Run PHP CodeSniffer (Joomla standards)
|
|
||||||
@echo "$(COLOR_BLUE)Running PHP CodeSniffer...$(COLOR_RESET)"
|
|
||||||
@if [ -f "$(PHPCS)" ]; then \
|
|
||||||
$(PHPCS) --standard=$(PHPCS_STANDARD) --extensions=php --ignore=vendor,node_modules,$(BUILD_DIR) .; \
|
|
||||||
else \
|
|
||||||
echo "$(COLOR_YELLOW)⚠ PHP CodeSniffer not installed. Run: make install-deps$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: validate
|
|
||||||
validate: lint phpcs ## Run all validation checks
|
|
||||||
@echo "$(COLOR_GREEN)✓ All validation checks passed$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: clean
|
|
||||||
clean: ## Clean build artifacts
|
|
||||||
@echo "$(COLOR_BLUE)Cleaning build artifacts...$(COLOR_RESET)"
|
|
||||||
@rm -rf $(BUILD_DIR) $(DIST_DIR)
|
|
||||||
@echo "$(COLOR_GREEN)✓ Build artifacts cleaned$(COLOR_RESET)"
|
|
||||||
|
|
||||||
MOKO_PLATFORM ?= $(or $(wildcard ../moko-platform),$(wildcard $(HOME)/moko-platform),$(wildcard /opt/moko-platform))
|
|
||||||
MINIFY_SCRIPT := $(MOKO_PLATFORM)/build/minify.js
|
|
||||||
|
|
||||||
.PHONY: minify
|
|
||||||
minify: ## Minify CSS/JS assets
|
|
||||||
@echo "Minifying assets..."
|
|
||||||
@if [ -f "$(MINIFY_SCRIPT)" ]; then \
|
|
||||||
node "$(MINIFY_SCRIPT)" $(SRC_DIR); \
|
|
||||||
elif [ -f "scripts/minify.js" ]; then \
|
|
||||||
node scripts/minify.js; \
|
|
||||||
else \
|
|
||||||
echo "No minify script found"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: build
|
|
||||||
build: clean validate minify ## Build extension package
|
|
||||||
@echo "$(COLOR_BLUE)Building Joomla extension package...$(COLOR_RESET)"
|
|
||||||
@mkdir -p $(DIST_DIR) $(BUILD_DIR)
|
|
||||||
|
|
||||||
# Determine package prefix based on extension type
|
|
||||||
@case "$(EXTENSION_TYPE)" in \
|
|
||||||
module) \
|
|
||||||
PACKAGE_PREFIX="mod_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
plugin) \
|
|
||||||
PACKAGE_PREFIX="plg_$(PLUGIN_GROUP)_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
component) \
|
|
||||||
PACKAGE_PREFIX="com_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
package) \
|
|
||||||
PACKAGE_PREFIX="pkg_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
template) \
|
|
||||||
PACKAGE_PREFIX="tpl_$(EXTENSION_NAME)"; \
|
|
||||||
BUILD_TARGET="$(BUILD_DIR)/$$PACKAGE_PREFIX"; \
|
|
||||||
;; \
|
|
||||||
*) \
|
|
||||||
echo "$(COLOR_RED)✗ Unknown extension type: $(EXTENSION_TYPE)$(COLOR_RESET)"; \
|
|
||||||
exit 1; \
|
|
||||||
;; \
|
|
||||||
esac; \
|
|
||||||
\
|
|
||||||
mkdir -p "$$BUILD_TARGET"; \
|
|
||||||
\
|
|
||||||
echo "Building $$PACKAGE_PREFIX..."; \
|
|
||||||
\
|
|
||||||
rsync -av --progress \
|
|
||||||
--exclude='$(BUILD_DIR)' \
|
|
||||||
--exclude='$(DIST_DIR)' \
|
|
||||||
--exclude='.git*' \
|
|
||||||
--exclude='vendor/' \
|
|
||||||
--exclude='node_modules/' \
|
|
||||||
--exclude='tests/' \
|
|
||||||
--exclude='Makefile' \
|
|
||||||
--exclude='composer.json' \
|
|
||||||
--exclude='composer.lock' \
|
|
||||||
--exclude='package.json' \
|
|
||||||
--exclude='package-lock.json' \
|
|
||||||
--exclude='phpunit.xml' \
|
|
||||||
--exclude='*.md' \
|
|
||||||
--exclude='.editorconfig' \
|
|
||||||
. "$$BUILD_TARGET/"; \
|
|
||||||
\
|
|
||||||
cd $(BUILD_DIR) && $(ZIP) -r "../$(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip" "$${PACKAGE_PREFIX}"; \
|
|
||||||
\
|
|
||||||
echo "$(COLOR_GREEN)✓ Package created: $(DIST_DIR)/$${PACKAGE_PREFIX}-$(EXTENSION_VERSION).zip$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: package
|
|
||||||
package: build ## Alias for build
|
|
||||||
@echo "$(COLOR_GREEN)✓ Package ready for distribution$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: release
|
|
||||||
release: validate build ## Create a release (validate + build)
|
|
||||||
@echo "$(COLOR_GREEN)✓ Release package ready$(COLOR_RESET)"
|
|
||||||
|
|
||||||
.PHONY: version
|
|
||||||
version: ## Display version information
|
|
||||||
@echo "$(COLOR_BLUE)Extension Information:$(COLOR_RESET)"
|
|
||||||
@echo " Name: $(EXTENSION_NAME)"
|
|
||||||
@echo " Type: $(EXTENSION_TYPE)"
|
|
||||||
@echo " Version: $(EXTENSION_VERSION)"
|
|
||||||
|
|
||||||
.PHONY: security-check
|
|
||||||
security-check: ## Run security checks on dependencies
|
|
||||||
@echo "$(COLOR_BLUE)Running security checks...$(COLOR_RESET)"
|
|
||||||
@if [ -f "composer.json" ]; then \
|
|
||||||
$(COMPOSER) audit || echo "$(COLOR_YELLOW)⚠ Vulnerabilities found$(COLOR_RESET)"; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
all: install-deps validate build ## Run complete build pipeline
|
|
||||||
@echo "$(COLOR_GREEN)✓ Complete build pipeline finished$(COLOR_RESET)"
|
|
||||||
|
|
||||||
# Default target
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# MokoSuiteOpenGraph
|
# MokoSuiteOpenGraph
|
||||||
|
|
||||||
<!-- VERSION: 01.02.00 -->
|
<!-- VERSION: 01.04.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.
|
||||||
|
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -60,6 +60,14 @@
|
|||||||
<option value="product">Product</option>
|
<option value="product">Product</option>
|
||||||
<option value="profile">Profile</option>
|
<option value="profile">Profile</option>
|
||||||
</field>
|
</field>
|
||||||
|
<field
|
||||||
|
name="og_video"
|
||||||
|
type="url"
|
||||||
|
label="COM_MOKOOG_FIELD_OG_VIDEO"
|
||||||
|
description="COM_MOKOOG_FIELD_OG_VIDEO_DESC"
|
||||||
|
filter="url"
|
||||||
|
validate="url"
|
||||||
|
/>
|
||||||
<field
|
<field
|
||||||
name="published"
|
name="published"
|
||||||
type="list"
|
type="list"
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ COM_MOKOOG_FIELD_OG_IMAGE="OG Image"
|
|||||||
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
||||||
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
||||||
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
||||||
|
COM_MOKOOG_FIELD_OG_VIDEO="Video URL"
|
||||||
|
COM_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video for social sharing previews. Supports direct video URLs and YouTube/Vimeo links."
|
||||||
|
|
||||||
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
||||||
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ COM_MOKOOG_FIELD_OG_IMAGE="OG Image"
|
|||||||
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
COM_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing."
|
||||||
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
COM_MOKOOG_FIELD_OG_TYPE="OG Type"
|
||||||
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
COM_MOKOOG_FIELD_OG_TYPE_DESC="The Open Graph content type."
|
||||||
|
COM_MOKOOG_FIELD_OG_VIDEO="Video URL"
|
||||||
|
COM_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video for social sharing previews. Supports direct video URLs and YouTube/Vimeo links."
|
||||||
|
|
||||||
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
COM_MOKOOG_FILTER_SEARCH="Search OG titles"
|
||||||
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
COM_MOKOOG_FILTER_CONTENT_TYPE="Content Type"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="component" method="upgrade">
|
<extension type="component" method="upgrade">
|
||||||
<name>com_mokoog</name>
|
<name>com_mokoog</name>
|
||||||
<version>01.02.00</version>
|
<version>01.04.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>
|
||||||
|
|||||||
@@ -6,12 +6,13 @@
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `#__mokoog_tags` (
|
CREATE TABLE IF NOT EXISTS `#__mokoog_tags` (
|
||||||
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
`content_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'e.g. com_content, menu, com_virtuemart',
|
`content_type` VARCHAR(100) NOT NULL DEFAULT '' COMMENT 'e.g. com_content, menu, com_mokoshop',
|
||||||
`content_id` INT(11) UNSIGNED NOT NULL DEFAULT 0,
|
`content_id` INT(11) UNSIGNED NOT NULL DEFAULT 0,
|
||||||
`og_title` VARCHAR(255) NOT NULL DEFAULT '',
|
`og_title` VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
`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',
|
||||||
|
`og_video` VARCHAR(512) NOT NULL DEFAULT '',
|
||||||
`seo_title` VARCHAR(70) NOT NULL DEFAULT '',
|
`seo_title` VARCHAR(70) NOT NULL DEFAULT '',
|
||||||
`meta_description` VARCHAR(200) NOT NULL DEFAULT '',
|
`meta_description` VARCHAR(200) NOT NULL DEFAULT '',
|
||||||
`robots` VARCHAR(100) NOT NULL DEFAULT '',
|
`robots` VARCHAR(100) NOT NULL DEFAULT '',
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
--
|
||||||
|
-- MokoJoomOpenGraph 01.03.00 - Add og_video column
|
||||||
|
--
|
||||||
|
|
||||||
|
ALTER TABLE `#__mokoog_tags` ADD COLUMN `og_video` VARCHAR(512) NOT NULL DEFAULT '' AFTER `og_type`;
|
||||||
@@ -49,6 +49,14 @@
|
|||||||
<option value="music.song">Music</option>
|
<option value="music.song">Music</option>
|
||||||
<option value="video.other">Video</option>
|
<option value="video.other">Video</option>
|
||||||
</field>
|
</field>
|
||||||
|
<field
|
||||||
|
name="og_video"
|
||||||
|
type="url"
|
||||||
|
label="PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO"
|
||||||
|
description="PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO_DESC"
|
||||||
|
filter="url"
|
||||||
|
validate="url"
|
||||||
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="mokoog_seo" label="PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL"
|
<fieldset name="mokoog_seo" label="PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL"
|
||||||
description="PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC">
|
description="PLG_CONTENT_MOKOOG_FIELDSET_SEO_DESC">
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recomme
|
|||||||
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_FIELD_OG_VIDEO="Video URL"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video to embed in social sharing previews. Supports direct video URLs and YouTube/Vimeo links. Outputs og:video meta tags."
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags"
|
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_FIELDSET_SEO_DESC="Control search engine meta tags for this page."
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ PLG_CONTENT_MOKOOG_FIELD_OG_IMAGE_DESC="Custom image for social sharing. Recomme
|
|||||||
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_FIELD_OG_VIDEO="Video URL"
|
||||||
|
PLG_CONTENT_MOKOOG_FIELD_OG_VIDEO_DESC="URL of a video to embed in social sharing previews. Supports direct video URLs and YouTube/Vimeo links. Outputs og:video meta tags."
|
||||||
|
|
||||||
PLG_CONTENT_MOKOOG_FIELDSET_SEO_LABEL="SEO Meta Tags"
|
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_FIELDSET_SEO_DESC="Control search engine meta tags for this page."
|
||||||
|
|
||||||
|
|||||||
@@ -102,3 +102,46 @@
|
|||||||
color: #536471;
|
color: #536471;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* LinkedIn card */
|
||||||
|
.mokoog-card-li {
|
||||||
|
border: 1px solid #e0dfdc;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-li .mokoog-card-body {
|
||||||
|
border-top-color: #e0dfdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-li .mokoog-card-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-card-li .mokoog-card-domain {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.6);
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Character count indicators */
|
||||||
|
.mokoog-char-count {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-char-ok {
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-char-warn {
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mokoog-char-over {
|
||||||
|
color: #d32f2f;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,9 +15,44 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
ogDesc: document.getElementById('jform_mokoog_og_description'),
|
ogDesc: document.getElementById('jform_mokoog_og_description'),
|
||||||
ogImage: document.getElementById('jform_mokoog_og_image'),
|
ogImage: document.getElementById('jform_mokoog_og_image'),
|
||||||
articleTitle: document.getElementById('jform_title'),
|
articleTitle: document.getElementById('jform_title'),
|
||||||
metaDesc: document.getElementById('jform_metadesc')
|
metaDesc: document.getElementById('jform_metadesc'),
|
||||||
|
seoTitle: document.getElementById('jform_mokoog_seo_title'),
|
||||||
|
metaDescription: document.getElementById('jform_mokoog_meta_description')
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Character count indicators
|
||||||
|
var charLimits = [
|
||||||
|
{ field: fields.ogTitle, optimal: 60, max: 90 },
|
||||||
|
{ field: fields.ogDesc, optimal: 155, max: 200 },
|
||||||
|
{ field: fields.seoTitle, optimal: 60, max: 70 },
|
||||||
|
{ field: fields.metaDescription, optimal: 155, max: 160 }
|
||||||
|
];
|
||||||
|
|
||||||
|
charLimits.forEach(function (cfg) {
|
||||||
|
if (!cfg.field) return;
|
||||||
|
|
||||||
|
var counter = document.createElement('span');
|
||||||
|
counter.className = 'mokoog-char-count';
|
||||||
|
cfg.field.parentNode.appendChild(counter);
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
var len = cfg.field.value.length;
|
||||||
|
counter.textContent = len + '/' + cfg.optimal;
|
||||||
|
|
||||||
|
if (len > cfg.max) {
|
||||||
|
counter.className = 'mokoog-char-count mokoog-char-over';
|
||||||
|
} else if (len > cfg.optimal) {
|
||||||
|
counter.className = 'mokoog-char-count mokoog-char-warn';
|
||||||
|
} else {
|
||||||
|
counter.className = 'mokoog-char-count mokoog-char-ok';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.field.addEventListener('input', refresh);
|
||||||
|
cfg.field.addEventListener('change', refresh);
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
|
||||||
// Find the mokoog fieldset and insert preview after it
|
// Find the mokoog fieldset and insert preview after it
|
||||||
var fieldset = document.querySelector('[data-showon-id="mokoog"]') ||
|
var fieldset = document.querySelector('[data-showon-id="mokoog"]') ||
|
||||||
document.getElementById('attrib-mokoog') ||
|
document.getElementById('attrib-mokoog') ||
|
||||||
@@ -110,6 +145,36 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
twCard.appendChild(twBody);
|
twCard.appendChild(twBody);
|
||||||
wrapper.appendChild(twCard);
|
wrapper.appendChild(twCard);
|
||||||
|
|
||||||
|
// LinkedIn preview card
|
||||||
|
var liLabel = document.createElement('small');
|
||||||
|
liLabel.className = 'mokoog-platform-label';
|
||||||
|
liLabel.textContent = 'LinkedIn';
|
||||||
|
wrapper.appendChild(liLabel);
|
||||||
|
|
||||||
|
var liCard = document.createElement('div');
|
||||||
|
liCard.className = 'mokoog-card mokoog-card-li';
|
||||||
|
|
||||||
|
var liImg = document.createElement('div');
|
||||||
|
liImg.id = 'mokoog-li-img';
|
||||||
|
liImg.className = 'mokoog-card-img';
|
||||||
|
liCard.appendChild(liImg);
|
||||||
|
|
||||||
|
var liBody = document.createElement('div');
|
||||||
|
liBody.className = 'mokoog-card-body';
|
||||||
|
|
||||||
|
var liTitle = document.createElement('div');
|
||||||
|
liTitle.id = 'mokoog-li-title';
|
||||||
|
liTitle.className = 'mokoog-card-title';
|
||||||
|
liBody.appendChild(liTitle);
|
||||||
|
|
||||||
|
var liDomain = document.createElement('div');
|
||||||
|
liDomain.id = 'mokoog-li-domain';
|
||||||
|
liDomain.className = 'mokoog-card-domain';
|
||||||
|
liBody.appendChild(liDomain);
|
||||||
|
|
||||||
|
liCard.appendChild(liBody);
|
||||||
|
wrapper.appendChild(liCard);
|
||||||
|
|
||||||
preview.appendChild(wrapper);
|
preview.appendChild(wrapper);
|
||||||
fieldset.parentNode.insertBefore(preview, fieldset.nextSibling);
|
fieldset.parentNode.insertBefore(preview, fieldset.nextSibling);
|
||||||
|
|
||||||
@@ -152,6 +217,18 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
} else {
|
} else {
|
||||||
twImgEl.style.display = 'none';
|
twImgEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LinkedIn (shorter truncation: title 70, no description shown in card)
|
||||||
|
var liTitle = title.length > 70 ? title.substring(0, 67) + '...' : title;
|
||||||
|
document.getElementById('mokoog-li-title').textContent = liTitle;
|
||||||
|
document.getElementById('mokoog-li-domain').textContent = domain;
|
||||||
|
var liImgEl = document.getElementById('mokoog-li-img');
|
||||||
|
if (img) {
|
||||||
|
liImgEl.style.backgroundImage = 'url(' + encodeURI(img) + ')';
|
||||||
|
liImgEl.style.display = '';
|
||||||
|
} else {
|
||||||
|
liImgEl.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.values(fields).forEach(function (el) {
|
Object.values(fields).forEach(function (el) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="content" method="upgrade">
|
<extension type="plugin" group="content" method="upgrade">
|
||||||
<name>Content - MokoJoomOpenGraph</name>
|
<name>Content - MokoJoomOpenGraph</name>
|
||||||
<version>01.02.00</version>
|
<version>01.04.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>
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
|||||||
$db = Factory::getDbo();
|
$db = Factory::getDbo();
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->select($db->quoteName([
|
->select($db->quoteName([
|
||||||
'og_title', 'og_description', 'og_image', 'og_type',
|
'og_title', 'og_description', 'og_image', 'og_type', 'og_video',
|
||||||
'seo_title', 'meta_description', 'robots', 'canonical_url',
|
'seo_title', 'meta_description', 'robots', 'canonical_url',
|
||||||
]))
|
]))
|
||||||
->from($db->quoteName('#__mokoog_tags'))
|
->from($db->quoteName('#__mokoog_tags'))
|
||||||
@@ -249,6 +249,7 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
|||||||
'og_description' => trim($ogData['og_description'] ?? ''),
|
'og_description' => trim($ogData['og_description'] ?? ''),
|
||||||
'og_image' => trim($ogData['og_image'] ?? ''),
|
'og_image' => trim($ogData['og_image'] ?? ''),
|
||||||
'og_type' => trim($ogData['og_type'] ?? 'article'),
|
'og_type' => trim($ogData['og_type'] ?? 'article'),
|
||||||
|
'og_video' => $this->sanitizeUrl($ogData['og_video'] ?? ''),
|
||||||
'seo_title' => trim($ogData['seo_title'] ?? ''),
|
'seo_title' => trim($ogData['seo_title'] ?? ''),
|
||||||
'meta_description' => trim($ogData['meta_description'] ?? ''),
|
'meta_description' => trim($ogData['meta_description'] ?? ''),
|
||||||
'robots' => trim($robots),
|
'robots' => trim($robots),
|
||||||
@@ -266,6 +267,28 @@ final class MokoOGContent extends CMSPlugin implements SubscriberInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize a URL to only allow http/https schemes.
|
||||||
|
*
|
||||||
|
* @param string $url Raw URL value
|
||||||
|
*
|
||||||
|
* @return string Sanitized URL or empty string
|
||||||
|
*/
|
||||||
|
private function sanitizeUrl(string $url): string
|
||||||
|
{
|
||||||
|
$url = trim($url);
|
||||||
|
|
||||||
|
if ($url === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract the language tag from content data.
|
* Extract the language tag from content data.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID="Facebook App ID"
|
|||||||
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
|
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
|
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
|
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR="Fediverse Creator"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR_DESC="Your Fediverse/Mastodon handle (e.g. @user@mastodon.social). Outputs a fediverse:creator meta tag for author attribution on Mastodon and other Fediverse platforms."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID="Facebook App ID"
|
|||||||
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
|
PLG_SYSTEM_MOKOOG_FIELD_FB_APP_ID_DESC="Your Facebook App ID for fb:app_id meta tag."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
|
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR="Discord Embed Color"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
|
PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC="The color of the embed sidebar when shared on Discord. Sets the theme-color meta tag. Leave blank to use Discord defaults."
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR="Fediverse Creator"
|
||||||
|
PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR_DESC="Your Fediverse/Mastodon handle (e.g. @user@mastodon.social). Outputs a fediverse:creator meta tag for author attribution on Mastodon and other Fediverse platforms."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE="Auto-generate Tags"
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
PLG_SYSTEM_MOKOOG_FIELD_AUTO_GENERATE_DESC="Automatically generate OG tags from article content when no custom tags are set."
|
||||||
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
PLG_SYSTEM_MOKOOG_FIELD_STRIP_HTML="Strip HTML from Description"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="system" method="upgrade">
|
<extension type="plugin" group="system" method="upgrade">
|
||||||
<name>System - MokoJoomOpenGraph</name>
|
<name>System - MokoJoomOpenGraph</name>
|
||||||
<version>01.02.00</version>
|
<version>01.04.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>
|
||||||
@@ -106,6 +106,14 @@
|
|||||||
description="PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC"
|
description="PLG_SYSTEM_MOKOOG_FIELD_DISCORD_COLOR_DESC"
|
||||||
default=""
|
default=""
|
||||||
/>
|
/>
|
||||||
|
<field
|
||||||
|
name="fediverse_creator"
|
||||||
|
type="text"
|
||||||
|
label="PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR"
|
||||||
|
description="PLG_SYSTEM_MOKOOG_FIELD_FEDIVERSE_CREATOR_DESC"
|
||||||
|
default=""
|
||||||
|
filter="string"
|
||||||
|
/>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset name="advanced" label="PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED">
|
<fieldset name="advanced" label="PLG_SYSTEM_MOKOOG_FIELDSET_ADVANCED">
|
||||||
<field
|
<field
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
|
|
||||||
if ($catOg) {
|
if ($catOg) {
|
||||||
// Merge: category fills any gaps in the content-level data
|
// Merge: category fills any gaps in the content-level data
|
||||||
foreach (['og_title', 'og_description', 'og_image', 'og_type', 'seo_title', 'meta_description', 'robots', 'canonical_url'] as $field) {
|
foreach (['og_title', 'og_description', 'og_image', 'og_type', 'og_video', 'seo_title', 'meta_description', 'robots', 'canonical_url'] as $field) {
|
||||||
if (empty($ogData->$field) && !empty($catOg->$field)) {
|
if (empty($ogData->$field) && !empty($catOg->$field)) {
|
||||||
$ogData->$field = $catOg->$field;
|
$ogData->$field = $catOg->$field;
|
||||||
}
|
}
|
||||||
@@ -177,6 +177,38 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
$doc->setMetaData('theme-color', $discordColor);
|
$doc->setMetaData('theme-color', $discordColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fediverse/Mastodon creator attribution
|
||||||
|
$fediverseCreator = $this->params->get('fediverse_creator', '');
|
||||||
|
|
||||||
|
if ($fediverseCreator) {
|
||||||
|
$doc->setMetaData('fediverse:creator', $fediverseCreator);
|
||||||
|
}
|
||||||
|
|
||||||
|
// og:video tags
|
||||||
|
$videoUrl = $ogData->og_video ?? '';
|
||||||
|
|
||||||
|
if ($videoUrl) {
|
||||||
|
$doc->setMetaData('og:video', $videoUrl, 'property');
|
||||||
|
|
||||||
|
if (str_starts_with($videoUrl, 'https://')) {
|
||||||
|
$doc->setMetaData('og:video:secure_url', $videoUrl, 'property');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect video type from URL — embeds vs direct files
|
||||||
|
$isEmbed = str_contains($videoUrl, 'youtube.com') || str_contains($videoUrl, 'youtu.be')
|
||||||
|
|| str_contains($videoUrl, 'vimeo.com');
|
||||||
|
|
||||||
|
if ($isEmbed) {
|
||||||
|
$doc->setMetaData('og:video:type', 'text/html', 'property');
|
||||||
|
} else {
|
||||||
|
$ext = strtolower(pathinfo(parse_url($videoUrl, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION));
|
||||||
|
$mimeMap = ['mp4' => 'video/mp4', 'webm' => 'video/webm', 'ogg' => 'video/ogg'];
|
||||||
|
$doc->setMetaData('og:video:type', $mimeMap[$ext] ?? 'video/mp4', 'property');
|
||||||
|
$doc->setMetaData('og:video:width', '1280', 'property');
|
||||||
|
$doc->setMetaData('og:video:height', '720', 'property');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LinkedIn article tags
|
// LinkedIn article tags
|
||||||
if ($option === 'com_content' && $view === 'article' && $id > 0) {
|
if ($option === 'com_content' && $view === 'article' && $id > 0) {
|
||||||
$doc->setMetaData('article:published_time', $this->getArticleDate($id, 'publish_up'), 'property');
|
$doc->setMetaData('article:published_time', $this->getArticleDate($id, 'publish_up'), 'property');
|
||||||
@@ -189,13 +221,34 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MokoSuiteShop product meta tags
|
// MokoSuiteShop product meta tags (pricing + Pinterest availability)
|
||||||
if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) {
|
if ($option === 'com_mokoshop' && $view === 'product' && $id > 0) {
|
||||||
$productData = $this->loadShopProduct($id);
|
$productData = $this->loadShopProduct($id);
|
||||||
|
|
||||||
if ($productData) {
|
if ($productData) {
|
||||||
$doc->setMetaData('product:price:amount', number_format((float) $productData->price, 2, '.', ''), 'property');
|
$doc->setMetaData('product:price:amount', number_format((float) $productData->price, 2, '.', ''), 'property');
|
||||||
$doc->setMetaData('product:price:currency', $productData->currency ?: 'USD', 'property');
|
$doc->setMetaData('product:price:currency', $productData->currency ?: 'USD', 'property');
|
||||||
|
$availability = ((int) ($productData->stock_qty ?? 0) > 0) ? 'instock' : 'outofstock';
|
||||||
|
$doc->setMetaData('product:availability', $availability, 'property');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pinterest article:tag rich pins (from Joomla content tags)
|
||||||
|
if ($option === 'com_content' && $view === 'article' && $id > 0) {
|
||||||
|
$db = Factory::getDbo();
|
||||||
|
$tagQuery = $db->getQuery(true)
|
||||||
|
->select($db->quoteName('t.title'))
|
||||||
|
->from($db->quoteName('#__tags', 't'))
|
||||||
|
->join('INNER', $db->quoteName('#__contentitem_tag_map', 'm')
|
||||||
|
. ' ON ' . $db->quoteName('m.tag_id') . ' = ' . $db->quoteName('t.id'))
|
||||||
|
->where($db->quoteName('m.type_alias') . ' = ' . $db->quote('com_content.article'))
|
||||||
|
->where($db->quoteName('m.content_item_id') . ' = ' . $id)
|
||||||
|
->where($db->quoteName('t.published') . ' = 1');
|
||||||
|
$db->setQuery($tagQuery);
|
||||||
|
$tags = $db->loadColumn();
|
||||||
|
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
$doc->setMetaData('article:tag', $tag, 'property');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +352,7 @@ final class MokoOG extends CMSPlugin implements SubscriberInterface
|
|||||||
'og_description' => '',
|
'og_description' => '',
|
||||||
'og_image' => '',
|
'og_image' => '',
|
||||||
'og_type' => '',
|
'og_type' => '',
|
||||||
|
'og_video' => '',
|
||||||
'seo_title' => '',
|
'seo_title' => '',
|
||||||
'meta_description' => '',
|
'meta_description' => '',
|
||||||
'robots' => '',
|
'robots' => '',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="plugin" group="webservices" method="upgrade">
|
<extension type="plugin" group="webservices" method="upgrade">
|
||||||
<name>Web Services - MokoJoomOpenGraph</name>
|
<name>Web Services - MokoJoomOpenGraph</name>
|
||||||
<version>01.02.00</version>
|
<version>01.04.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>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>Package - MokoSuiteOpenGraph</name>
|
<name>Package - MokoSuiteOpenGraph</name>
|
||||||
<packagename>mokoog</packagename>
|
<packagename>mokoog</packagename>
|
||||||
<version>01.02.00</version>
|
<version>01.04.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>
|
||||||
@@ -30,6 +30,9 @@
|
|||||||
<language tag="en-GB">language/en-GB/pkg_mokoog.sys.ini</language>
|
<language tag="en-GB">language/en-GB/pkg_mokoog.sys.ini</language>
|
||||||
</languages>
|
</languages>
|
||||||
|
|
||||||
<dlid prefix="dlid=" suffix=""/>
|
<updateservers>
|
||||||
<blockChildUninstall>true</blockChildUninstall>
|
<server type="extension" name="MokoSuiteOpenGraph Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteOpenGraph/updates.xml</server>
|
||||||
|
</updateservers>
|
||||||
|
<dlid prefix="dlid=" suffix=""/>
|
||||||
|
<blockChildUninstall>true</blockChildUninstall>
|
||||||
</extension>
|
</extension>
|
||||||
|
|||||||
Reference in New Issue
Block a user