|
|
|
@@ -73,12 +73,12 @@ jobs:
|
|
|
|
|
|
|
|
|
|
steps:
|
|
|
|
|
- name: Checkout repository
|
|
|
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
|
|
|
uses: actions/checkout@v4
|
|
|
|
|
with:
|
|
|
|
|
token: ${{ secrets.GA_TOKEN }}
|
|
|
|
|
fetch-depth: 0
|
|
|
|
|
|
|
|
|
|
- name: Setup MokoStandards tools
|
|
|
|
|
- name: Setup moko-platform tools
|
|
|
|
|
env:
|
|
|
|
|
MOKO_CLONE_TOKEN: ${{ secrets.GA_TOKEN }}
|
|
|
|
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
|
|
|
@@ -87,11 +87,15 @@ jobs:
|
|
|
|
|
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
|
|
|
|
|
git clone --depth 1 --branch main --quiet \
|
|
|
|
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
|
|
|
|
/tmp/mokostandards-api 2>/dev/null || true
|
|
|
|
|
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
|
|
|
|
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
|
|
|
|
if [ -d "/tmp/moko-platform" ]; then
|
|
|
|
|
echo "moko-platform already available — skipping clone"
|
|
|
|
|
else
|
|
|
|
|
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
|
|
|
|
|
fi
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
- name: Generate updates.xml entry
|
|
|
|
@@ -100,14 +104,14 @@ jobs:
|
|
|
|
|
BRANCH="${{ github.ref_name }}"
|
|
|
|
|
REPO="${{ github.repository }}"
|
|
|
|
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
|
|
|
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
|
|
|
VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
|
|
|
|
|
|
|
|
|
# Auto-bump patch on all branches (dev, alpha, beta, rc)
|
|
|
|
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
|
|
|
git config --local user.name "gitea-actions[bot]"
|
|
|
|
|
BUMPED=$(php /tmp/mokostandards-api/cli/version_bump.php --path . 2>/dev/null || true)
|
|
|
|
|
BUMPED=$(php /tmp/moko-platform/cli/version_bump.php --path . 2>/dev/null || true)
|
|
|
|
|
if [ -n "$BUMPED" ]; then
|
|
|
|
|
VERSION=$(php /tmp/mokostandards-api/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
|
|
|
|
|
VERSION=$(php /tmp/moko-platform/cli/version_read.php --path . 2>/dev/null || echo "$VERSION")
|
|
|
|
|
git add -A
|
|
|
|
|
git commit -m "chore(version): auto-bump patch ${VERSION} [skip ci]" \
|
|
|
|
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>" 2>/dev/null || true
|
|
|
|
@@ -442,15 +446,190 @@ jobs:
|
|
|
|
|
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
|
|
|
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
|
|
|
|
php /tmp/mokostandards-api/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
|
|
|
elif [ -f "/tmp/mokostandards-api/deploy/deploy-sftp.php" ]; then
|
|
|
|
|
php /tmp/mokostandards-api/deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
|
|
|
PLATFORM=$(php /tmp/moko-platform/cli/platform_detect.php --path . 2>/dev/null || true)
|
|
|
|
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/moko-platform/deploy/deploy-joomla.php" ]; then
|
|
|
|
|
php /tmp/moko-platform/deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
|
|
|
|
elif [ -f "/tmp/moko-platform/deploy/deploy-sftp.php" ]; then
|
|
|
|
|
php /tmp/moko-platform/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: Validate updates.xml integrity
|
|
|
|
|
run: |
|
|
|
|
|
ERRORS=0
|
|
|
|
|
|
|
|
|
|
if [ ! -f "updates.xml" ]; then
|
|
|
|
|
echo "::error::updates.xml not found"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Well-formed XML
|
|
|
|
|
if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('updates.xml')" 2>/dev/null; then
|
|
|
|
|
echo "::error::updates.xml is not valid XML"
|
|
|
|
|
ERRORS=$((ERRORS+1))
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
python3 << 'PYEOF'
|
|
|
|
|
import xml.etree.ElementTree as ET, sys, re, os
|
|
|
|
|
|
|
|
|
|
tree = ET.parse("updates.xml")
|
|
|
|
|
root = tree.getroot()
|
|
|
|
|
updates = root.findall("update")
|
|
|
|
|
errors = 0
|
|
|
|
|
warnings = 0
|
|
|
|
|
seen_tags = set()
|
|
|
|
|
|
|
|
|
|
# All 5 channels MUST be present
|
|
|
|
|
REQUIRED_CHANNELS = {"stable", "rc", "beta", "alpha", "dev"}
|
|
|
|
|
VALID_TAGS = REQUIRED_CHANNELS | {"development"} # accept legacy alias
|
|
|
|
|
REPO = os.environ.get("GITEA_REPO", "")
|
|
|
|
|
ORG = os.environ.get("GITEA_ORG", "MokoConsulting")
|
|
|
|
|
REPO_BASE = f"https://git.mokoconsulting.tech/{ORG}/"
|
|
|
|
|
|
|
|
|
|
# Gitea release tag names per channel (Moko standard)
|
|
|
|
|
RELEASE_TAG_MAP = {
|
|
|
|
|
"stable": "stable",
|
|
|
|
|
"rc": "release-candidate",
|
|
|
|
|
"beta": "beta",
|
|
|
|
|
"alpha": "alpha",
|
|
|
|
|
"dev": "development",
|
|
|
|
|
"development": "development",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Joomla update XML required fields per
|
|
|
|
|
# https://docs.joomla.org/Deploying_an_Update_Server
|
|
|
|
|
REQUIRED_FIELDS = ["name", "element", "type", "version", "infourl"]
|
|
|
|
|
|
|
|
|
|
for i, u in enumerate(updates):
|
|
|
|
|
tag_el = u.find("tags/tag")
|
|
|
|
|
tag = tag_el.text.strip() if tag_el is not None and tag_el.text else None
|
|
|
|
|
label = f"Entry {i+1} (<tag>{tag or '?'}</tag>)"
|
|
|
|
|
|
|
|
|
|
# -- Required Joomla fields --
|
|
|
|
|
for field in REQUIRED_FIELDS:
|
|
|
|
|
el = u.find(field)
|
|
|
|
|
if el is None or not (el.text or "").strip():
|
|
|
|
|
print(f"::error::{label}: missing required <{field}>")
|
|
|
|
|
errors += 1
|
|
|
|
|
|
|
|
|
|
# -- <downloads><downloadurl> --
|
|
|
|
|
dl = u.find("downloads/downloadurl")
|
|
|
|
|
if dl is None or not (dl.text or "").strip():
|
|
|
|
|
print(f"::error::{label}: missing <downloads><downloadurl>")
|
|
|
|
|
errors += 1
|
|
|
|
|
else:
|
|
|
|
|
dl_url = dl.text.strip()
|
|
|
|
|
# Must point to org repo
|
|
|
|
|
if REPO_BASE not in dl_url:
|
|
|
|
|
print(f"::error::{label}: download URL not under {REPO_BASE}: {dl_url}")
|
|
|
|
|
errors += 1
|
|
|
|
|
# Must end in .zip
|
|
|
|
|
if not dl_url.endswith(".zip"):
|
|
|
|
|
print(f"::error::{label}: download URL must end in .zip: {dl_url}")
|
|
|
|
|
errors += 1
|
|
|
|
|
# Must use correct Gitea release tag in path
|
|
|
|
|
if tag and tag in RELEASE_TAG_MAP:
|
|
|
|
|
expected_tag = RELEASE_TAG_MAP[tag]
|
|
|
|
|
if f"/download/{expected_tag}/" not in dl_url:
|
|
|
|
|
print(f"::error::{label}: download URL should contain /download/{expected_tag}/ but got: {dl_url}")
|
|
|
|
|
errors += 1
|
|
|
|
|
|
|
|
|
|
# -- <client> (required for Joomla to match update) --
|
|
|
|
|
client = u.find("client")
|
|
|
|
|
if client is None or not (client.text or "").strip():
|
|
|
|
|
print(f"::error::{label}: missing <client> (required for Joomla update matching)")
|
|
|
|
|
errors += 1
|
|
|
|
|
|
|
|
|
|
# -- <targetplatform> --
|
|
|
|
|
tp = u.find("targetplatform")
|
|
|
|
|
if tp is None:
|
|
|
|
|
print(f"::error::{label}: missing <targetplatform>")
|
|
|
|
|
errors += 1
|
|
|
|
|
else:
|
|
|
|
|
tp_name = tp.get("name", "")
|
|
|
|
|
tp_ver = tp.get("version", "")
|
|
|
|
|
if tp_name != "joomla":
|
|
|
|
|
print(f"::error::{label}: targetplatform name should be 'joomla', got '{tp_name}'")
|
|
|
|
|
errors += 1
|
|
|
|
|
if not tp_ver:
|
|
|
|
|
print(f"::error::{label}: targetplatform missing version regex")
|
|
|
|
|
errors += 1
|
|
|
|
|
elif "5" not in tp_ver or "6" not in tp_ver:
|
|
|
|
|
print(f"::warning::{label}: targetplatform version may not cover Joomla 5+6: {tp_ver}")
|
|
|
|
|
warnings += 1
|
|
|
|
|
|
|
|
|
|
# -- <type> must be valid Joomla type --
|
|
|
|
|
type_el = u.find("type")
|
|
|
|
|
if type_el is not None and type_el.text:
|
|
|
|
|
valid_types = {"component", "module", "plugin", "template", "library", "package", "file"}
|
|
|
|
|
if type_el.text.strip() not in valid_types:
|
|
|
|
|
print(f"::error::{label}: invalid type '{type_el.text}' (expected: {valid_types})")
|
|
|
|
|
errors += 1
|
|
|
|
|
|
|
|
|
|
# -- <version> format (XX.YY.ZZ with optional suffix) --
|
|
|
|
|
ver_el = u.find("version")
|
|
|
|
|
if ver_el is not None and ver_el.text:
|
|
|
|
|
if not re.match(r"^\d{2}\.\d{2}\.\d{2}(-\w+)?$", ver_el.text.strip()):
|
|
|
|
|
print(f"::warning::{label}: version '{ver_el.text}' does not match XX.YY.ZZ format")
|
|
|
|
|
warnings += 1
|
|
|
|
|
|
|
|
|
|
# -- <maintainer> and <maintainerurl> --
|
|
|
|
|
for field in ["maintainer", "maintainerurl"]:
|
|
|
|
|
el = u.find(field)
|
|
|
|
|
if el is None or not (el.text or "").strip():
|
|
|
|
|
print(f"::warning::{label}: missing <{field}>")
|
|
|
|
|
warnings += 1
|
|
|
|
|
|
|
|
|
|
# -- Valid stability tag --
|
|
|
|
|
if tag is None:
|
|
|
|
|
print(f"::error::{label}: missing <tags><tag>")
|
|
|
|
|
errors += 1
|
|
|
|
|
elif tag not in VALID_TAGS:
|
|
|
|
|
print(f"::error::{label}: invalid tag '{tag}' (expected: {VALID_TAGS})")
|
|
|
|
|
errors += 1
|
|
|
|
|
|
|
|
|
|
# -- Duplicate tag check --
|
|
|
|
|
norm_tag = "dev" if tag == "development" else tag
|
|
|
|
|
if norm_tag in seen_tags:
|
|
|
|
|
print(f"::error::{label}: duplicate channel '{tag}'")
|
|
|
|
|
errors += 1
|
|
|
|
|
if norm_tag:
|
|
|
|
|
seen_tags.add(norm_tag)
|
|
|
|
|
|
|
|
|
|
# -- All 5 channels must exist --
|
|
|
|
|
missing = REQUIRED_CHANNELS - seen_tags
|
|
|
|
|
if missing:
|
|
|
|
|
print(f"::error::Missing required update channels: {', '.join(sorted(missing))}")
|
|
|
|
|
errors += 1
|
|
|
|
|
|
|
|
|
|
# -- Version ordering: higher stability must not exceed dev version --
|
|
|
|
|
channel_versions = {}
|
|
|
|
|
for u in updates:
|
|
|
|
|
tag_el = u.find("tags/tag")
|
|
|
|
|
ver_el = u.find("version")
|
|
|
|
|
if tag_el is not None and ver_el is not None and tag_el.text and ver_el.text:
|
|
|
|
|
norm = "dev" if tag_el.text.strip() == "development" else tag_el.text.strip()
|
|
|
|
|
# Strip suffix for comparison (01.00.18-dev -> 01.00.18)
|
|
|
|
|
base_ver = re.sub(r"-\w+$", "", ver_el.text.strip())
|
|
|
|
|
channel_versions[norm] = base_ver
|
|
|
|
|
|
|
|
|
|
# Cascade check: dev >= alpha >= beta >= rc >= stable
|
|
|
|
|
ORDER = ["dev", "alpha", "beta", "rc", "stable"]
|
|
|
|
|
for j in range(1, len(ORDER)):
|
|
|
|
|
current = ORDER[j]
|
|
|
|
|
previous = ORDER[j - 1]
|
|
|
|
|
if current in channel_versions and previous in channel_versions:
|
|
|
|
|
if channel_versions[current] > channel_versions[previous]:
|
|
|
|
|
print(f"::error::{current} version ({channel_versions[current]}) is ahead of {previous} ({channel_versions[previous]})")
|
|
|
|
|
errors += 1
|
|
|
|
|
|
|
|
|
|
# -- Summary --
|
|
|
|
|
print(f"\nupdates.xml validation: {len(updates)} entries, {errors} error(s), {warnings} warning(s)")
|
|
|
|
|
if errors > 0:
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
PYEOF
|
|
|
|
|
|
|
|
|
|
- name: Summary
|
|
|
|
|
if: always()
|
|
|
|
|
run: |
|
|
|
|
|